/* ── OCTOPONG · hazard tray ──────────────────────────────────
   The in-match rail that answers "what can I drop, and when?".

   Built out of theme.css's language rather than beside it: cut
   corners, a hard dark outline drawn with chained zero-blur drop
   shadows, chevron-clipped chips, one hover distance, the same
   focus ring. Nothing here re-declares a :root token — see the
   TRAP 3 note at the top of theme.css for why that matters.

   ── THE OUTLINE IS ON A CHILD, NOT ON .tray ──────────────────
   theme.css puts the drop-shadow chain on the panel itself. That
   is right for a static panel and wrong here: a `filter` pulls the
   whole subtree into one filtered layer, so the dial's conic sweep
   changing every frame would re-run five drop shadows over the
   entire 244x293 rail, sixty times a second, for a 92px repaint.
   `.tray-plate` is an empty element that carries the shape and the
   outline on its own; the content sits above it at z-index 1 and
   repaints on its own. Positive z-index, not the -1 that floods a
   clipped face (theme.css, "the outline").
   ─────────────────────────────────────────────────────────── */

/* ── the layer must not eat the arena ─────────────────────────
   ui.css has `#ui > * { pointer-events:auto }`, specificity (1,0,0).
   theme.css answers it with `.hud { pointer-events:none }` at
   (0,1,0) — which LOSES, so the HUD layer has been hit-testable
   across the whole viewport this entire time (measured:
   elementFromPoint over the arena returns the layer, not the
   canvas). It has not broken anything only because input.js hangs
   its pointer listeners off `window`, where nothing above the
   canvas can shadow them.

   This layer is not going to repeat it. The selector is qualified
   the same way ui.css qualifies its rule, so it wins on
   specificity instead of on load order. */
#ui > .screen.tray-layer,
.screen.tray-layer{
  display:block;
  pointer-events:none;
}
/* ── the layer is GONE when it is not on ─────────────────────
   `.screen` hides with opacity + visibility, and `visibility` is
   inherited — which a child can take back. `.tray.is-open` sets
   `visibility:visible`, so a rail whose layer was switched off
   still painted, straight through the results screen, exactly the
   double-exposure theme.css spent a review round on. Anything that
   can be shown by a descendant overriding an inherited property is
   not really hidden; `display:none` is. */
.screen.tray-layer:not(.on){ display:none; }

/* Portrait status tab. Hidden in landscape, where the full rail lives in
   genuinely empty gutter space. See the portrait block near the end for
   positioning and the drawer transition. */
.tray-tab{ display:none; }

.tray{
  --s: var(--op-ui-scale, 1);

  /* ── the gutter ────────────────────────────────────────────
     camera.js holds the arena at a fixed fraction of the SHORT
     screen axis, so on a landscape window the arena is about
     1.06 x viewport-height wide however many walls it has, and
     what is left over at the sides is dead space at every field
     size. Measured at 1280x720 the polygon's leftmost point is
     x=260 (6 players) / 273 (4) / 296 (8); the rail stops at 248. */
  --tr-gut: calc((100vw - 106vh) / 2);

  --tr-track:#0a1430;
  --tr-face-hi:#1b3268;
  --tr-face-lo:#0c1937;
  --tr-haz:#ff8b57;            /* the hazard orange from palette.js, lifted
                                  off 0xff5a2a far enough to read on the face */

  /* SABOTEUR by default; .is-ars swaps the whole ramp */
  --tr-acc:var(--aqua);
  --tr-acc-hi:var(--aqua-hi);
  --tr-acc-lo:var(--aqua-lo);
  --tr-on:#05221f;             /* ink that sits ON the accent — 10.7:1 */

  --f:0;                       /* 0..1 sweep, written by tray.js */

  position:absolute;
  /* NOT scaled by --s. The inset from the screen edge is a layout
     constant, and scaling it pushes the rail toward the arena: at
     --op-ui-scale 1.5 a 16 -> 24px left inset was enough to put the
     rail's right edge across the six-player hexagon's leftmost
     vertex. Measured, then fixed. */
  left:16px;
  top:50%;
  transform:translateY(-50%);
  /* 24px of slack rather than 20: at 1280x720 the tightest field
     (6 walls, leftmost vertex at x=260) then clears by 10px. */
  width:clamp(calc(104px * var(--s)), calc(var(--tr-gut) - 24px), calc(244px * var(--s)));

  container-type:inline-size;
  container-name:tray;

  display:flex; flex-direction:column; align-items:stretch;
  padding:0 calc(9px * var(--s)) calc(8px * var(--s));
  font-family:var(--ui);

  opacity:0; visibility:hidden;
  /* NOT pointer-events:auto here. `visibility` is stepped at the end of
     the fade, so for 200ms after localPlaceState() goes null the rail
     is invisible and still hit-testable — measured: elementFromPoint
     over the gutter returned .tray-plate in a CLASSIC match, i.e. an
     invisible panel swallowing clicks on the arena behind it. The
     pointer only ever belongs to the rail while the rail is open. */
  pointer-events:none;

  transition:opacity var(--dur-mid) var(--ease-out), visibility 0s linear var(--dur-mid);
}
.tray.is-open{
  opacity:1; visibility:visible;
  pointer-events:auto;
  transition:opacity var(--dur-mid) var(--ease-out), visibility 0s linear 0s;
}
/* ── the three-player triangle ────────────────────────────────
   Its base spans the whole viewport and its apex is 144px off the
   top, so the only free gutter is high. tray.js flips this on the
   frame the polygon drops to three walls.

   The safe band is not generous and it is worth writing down. The
   triangle's left edge projects to

       x = 4px + 0.753 * (0.974 * viewportHeight - y)

   so at 1280x720 with a 244px rail the rail's bottom-right corner
   has to stay above y = 369. The rail is 304px tall at scale 1 and
   starts at 50, which lands at 354. That margin does not survive
   the accessibility scale, so this is the one place --op-ui-scale
   is capped rather than honoured: a rail that has grown over the
   playfield is not an accessibility win. */
.tray.is-high{
  --s:min(var(--op-ui-scale, 1), 1);
  top:50px; bottom:auto; transform:none;
}

.tray.is-ars{
  --tr-acc:var(--gold);
  --tr-acc-hi:var(--gold-hi);
  --tr-acc-lo:var(--gold-lo);
  --tr-on:#2a1600;             /* 11.1:1 on the gold */
}

/* the face + the hard edge, on their own layer */
.tray-plate{
  position:absolute; inset:0; z-index:0; display:block;
  background:linear-gradient(180deg, var(--tr-face-hi), var(--tr-face-lo));
  clip-path:polygon(12px 0, 100% 0, 100% calc(100% - 12px),
                    calc(100% - 12px) 100%, 0 100%, 0 12px);
  --edge:var(--outline);
  filter:
    drop-shadow(2px 0 0 var(--edge))  drop-shadow(-2px 0 0 var(--edge))
    drop-shadow(0 2px 0 var(--edge))  drop-shadow(0 -2px 0 var(--edge))
    drop-shadow(0 5px 0 rgba(3,6,16,.55)) drop-shadow(0 12px 22px rgba(0,0,0,.45));
  transition:filter var(--dur-mid) var(--ease-out);
}
.tray > *:not(.tray-plate){ position:relative; z-index:1; }

/* ── the role plate ───────────────────────────────────────────
   Same shape and weight as theme.css's .panel-head, coloured by
   which economy you are actually in. This is the first thing that
   has to land: a dead player and a living one are playing two
   different games from here on. */
.tray-role{
  margin:0 calc(-9px * var(--s)) calc(8px * var(--s));
  padding:calc(7px * var(--s)) calc(11px * var(--s));
  /* theme.css's .panel-head runs accent -> accent-lo. Measured here
     that bottoms out at 4.94:1 on the aqua ramp, which clears AA by
     a hair and nothing else. The plate is the headline of this whole
     component, so it runs hi -> accent instead and holds 10.7:1
     across its whole height. */
  background:linear-gradient(180deg, var(--tr-acc-hi), var(--tr-acc));
  color:var(--tr-on);
  font:800 calc(12px * var(--s))/1 var(--display);
  letter-spacing:var(--ls-caps); text-transform:uppercase;
  text-align:center;
  clip-path:polygon(12px 0, 100% 0, 100% 100%, 0 100%, 0 12px);
}

/* ── the meter ────────────────────────────────────────────────
   Exactly one of these two is displayed, and they are deliberately
   NOT the same object. SABOTEUR is a clock you wait out; ARSENAL
   is ammo you buy with deflects. Same slot on screen, same reading
   position for the number, different shape and different colour. */
.tray-meter{
  height:calc(84px * var(--s));
  display:grid; place-items:center;
}
.tray .tray-dial,
.tray .tray-charge{ display:none; }
.tray.is-sab .tray-dial{ display:block; }
.tray.is-ars .tray-charge{ display:block; }

/* ── SABOTEUR — a dial, because it is a clock ─────────────── */
.tray-dial{
  position:relative;
  width:calc(84px * var(--s)); height:calc(84px * var(--s));
  border-radius:50%;
  box-shadow:0 0 0 3px var(--outline), 0 4px 0 rgba(3,6,16,.55);
  transition:box-shadow var(--dur-mid) var(--ease-out);
}
.tray-sweep{
  position:absolute; inset:0; border-radius:50%;
  background:conic-gradient(from -90deg,
    var(--tr-acc) 0 calc(var(--f) * 360deg), var(--tr-track) 0);
}
.tray-hole{
  position:absolute; inset:calc(11px * var(--s)); border-radius:50%;
  background:radial-gradient(circle at 50% 32%, #16295a, #0a1430 72%);
  box-shadow:inset 0 0 0 2px var(--outline);
  display:grid; place-items:center; align-content:center; gap:0;
}
.tray-num{
  display:block;
  font:900 calc(25px * var(--s))/1 var(--display);
  color:var(--ink); letter-spacing:-.01em;
  font-variant-numeric:tabular-nums;
  text-shadow:0 2px 0 rgba(0,0,0,.55);
}
.tray-unit{
  display:block; margin-top:calc(2px * var(--s));
  font:800 calc(9px * var(--s))/1 var(--ui);
  letter-spacing:.2em; color:var(--tr-acc-hi); font-style:normal;
}
/* the one-shot ring that makes READY an event rather than an arrival */
.tray-dial::after{
  content:""; position:absolute; inset:calc(-4px * var(--s));
  border-radius:50%; border:3px solid var(--tr-acc-hi);
  opacity:0; pointer-events:none;
}

/* ── ARSENAL — notches, because it is ammo ───────────────────
   The bar carries EXACTLY as many notches as the selected hazard
   costs, so switching from a 3-charge vortex to a 8-charge portal
   visibly lengthens the thing you are filling. That is the mode's
   whole decision, and a percentage bar would have hidden it. */
.tray-charge{ width:100%; text-align:center; }
.tray-chargenum{
  display:flex; align-items:baseline; justify-content:center; gap:calc(3px * var(--s));
  margin-bottom:calc(9px * var(--s));
}
.tray-num2{
  font:900 calc(38px * var(--s))/1 var(--display);
  color:var(--ink); font-variant-numeric:tabular-nums;
  text-shadow:0 3px 0 rgba(0,0,0,.55);
}
.tray-unit2{
  font:800 calc(15px * var(--s))/1 var(--display);
  color:var(--tr-acc-hi); font-style:normal; letter-spacing:.04em;
}
.tray-segs{ display:flex; gap:calc(3px * var(--s)); width:100%; }
.tray-segs i{
  flex:1 1 0; min-width:0; height:calc(17px * var(--s));
  background:var(--tr-track);
  box-shadow:inset 0 0 0 2px var(--outline);
  clip-path:polygon(3px 0, 100% 0, calc(100% - 3px) 100%, 0 100%);
}
.tray-segs i.on{
  background:linear-gradient(180deg, var(--tr-acc-hi), var(--tr-acc));
}

/* ── the word ─────────────────────────────────────────────── */
.tray-status{
  margin:calc(8px * var(--s)) 0 0; text-align:center;
}
.tray-word{
  display:inline-block;
  font:900 calc(15px * var(--s))/1 var(--display);
  letter-spacing:.1em; text-transform:uppercase;
  color:var(--ink-dim);
  text-shadow:0 2px 0 rgba(0,0,0,.5);
}
.tray.is-ready .tray-word{ color:var(--tr-acc-hi); }

/* ── the hand ─────────────────────────────────────────────── */
.tray-hand{
  display:flex; flex-direction:column; gap:calc(4px * var(--s));
  margin-top:calc(8px * var(--s));
}
.tray-slot{
  position:relative; border:0; cursor:pointer; width:100%;
  height:calc(38px * var(--s)); padding:0 calc(8px * var(--s)) 0 calc(5px * var(--s));
  display:grid; align-items:center;
  grid-template-columns:auto auto 1fr auto; gap:calc(7px * var(--s));
  background:linear-gradient(180deg,#1c3667,#101f42);
  color:var(--ink); text-align:left;
  clip-path:polygon(8px 0, 100% 0, 100% calc(100% - 8px),
                    calc(100% - 8px) 100%, 0 100%, 0 8px);
  --edge:var(--outline);
  filter:
    drop-shadow(1.5px 0 0 var(--edge)) drop-shadow(-1.5px 0 0 var(--edge))
    drop-shadow(0 1.5px 0 var(--edge)) drop-shadow(0 -1.5px 0 var(--edge))
    drop-shadow(0 3px 0 rgba(3,6,16,.55));
  transition:transform var(--dur-fast) var(--ease-pop),
             background var(--dur-fast), filter var(--dur-fast);
}
/* theme.css's rule: things in a stack slide on X, and hover is
   never movement alone */
.tray-slot:hover{
  transform:translateX(var(--lift-x));
  background:linear-gradient(180deg,#24438f,#152c5c);
}
.tray-slot:active{ transform:translateX(var(--lift-x)) translateY(2px); }

/* A clipped element clips its own outline, so `outline` draws no
   ring at all here — same trap theme.css documents. The ring is the
   same drop-shadow chain in aqua plus an inset highlight, and it is
   :focus rather than :focus-visible for the same reason. */
.tray-slot:focus{
  outline:none;
  --edge:var(--aqua);
  /* theme.css's ring is a single aqua inset, which works everywhere
     else in the shell because nothing else in the shell is aqua. Here
     the SABOTEUR accent IS aqua, so an aqua-on-aqua ring would make
     "focused" and "selected" the same picture. A white core keeps the
     house colour and still reads as focus on either ramp. */
  box-shadow:inset 0 0 0 2px var(--ink), inset 0 0 0 5px rgba(47,230,214,.95);
}

.tray-key{
  width:calc(18px * var(--s)); height:calc(18px * var(--s));
  display:grid; place-items:center;
  background:#0e1c3e; color:var(--ink-dim);
  font:800 calc(11px * var(--s))/1 var(--ui);
  box-shadow:inset 0 0 0 2px var(--outline);
  clip-path:polygon(3px 0,100% 0,100% calc(100% - 3px),calc(100% - 3px) 100%,0 100%,0 3px);
}
.tray-glyph{
  width:calc(23px * var(--s)); height:calc(23px * var(--s));
  display:grid; place-items:center; color:var(--tr-haz);
}
.tray-glyph svg{ width:100%; height:100%; display:block; }
.tray-name{
  font:800 calc(12px * var(--s))/1 var(--ui);
  letter-spacing:.07em; text-transform:uppercase;
  color:var(--ink);                       /* 11.8:1 on the slot face */
  text-shadow:0 1px 0 rgba(0,0,0,.5);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.tray-cost{
  display:flex; align-items:baseline; gap:1px;
  padding:calc(3px * var(--s)) calc(6px * var(--s));
  background:var(--tr-track); box-shadow:inset 0 0 0 2px var(--outline);
  clip-path:polygon(4px 0,100% 0,calc(100% - 4px) 100%,0 100%);
}
.tray-cost b{
  font:900 calc(12px * var(--s))/1 var(--ui);
  color:var(--gold);                      /* 11.6:1 on #0a1430 */
  font-variant-numeric:tabular-nums;
}
.tray-cost i{
  font:800 calc(9px * var(--s))/1 var(--ui);
  color:var(--ink-dim); font-style:normal;  /* 6.8:1 on #0a1430 */
}

/* ── selected ─────────────────────────────────────────────────
   theme.css settled on ONE "this one is picked" language: the rim
   goes to full weight in the accent, the face takes an accent wash,
   the cut corner is filled, and the thing sits forward. Same
   grammar as .modecard.on, at the scale a 38px row allows. */
.tray-slot.is-sel{
  --edge:var(--tr-acc);
  background:
    linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,0) 60%),
    linear-gradient(180deg,#27509b,#152c5c);
  transform:translateX(3px);
  filter:
    drop-shadow(2.5px 0 0 var(--edge)) drop-shadow(-2.5px 0 0 var(--edge))
    drop-shadow(0 2.5px 0 var(--edge)) drop-shadow(0 -2.5px 0 var(--edge))
    drop-shadow(0 3px 0 rgba(3,6,16,.55));
}
.tray-slot.is-sel::after{
  content:""; position:absolute; right:0; bottom:0;
  width:16px; height:16px; pointer-events:none;
  background:linear-gradient(135deg, transparent 49%, var(--tr-acc) 50%);
}
.tray-slot.is-sel .tray-key{ background:var(--tr-acc); color:var(--tr-on); }
.tray-slot.is-sel .tray-glyph{ color:var(--tr-acc-hi); }
.tray-slot.is-sel:hover{ transform:translateX(calc(var(--lift-x) + 3px)); }

/* ── cannot afford it ─────────────────────────────────────────
   Three signals, none of which touch the legibility of the name:
   the face goes flat and dark, the glyph loses its heat, and the
   price turns hot. The name stays at full --ink, because a slot
   you cannot read is not a slot you can plan around. */
.tray-slot.is-off{
  background:linear-gradient(180deg,#0f1c3c,#08122a);
}
.tray-slot.is-off .tray-glyph{ color:#7e5a4a; }
.tray-slot.is-off .tray-name{ color:var(--ink-dim); }   /* 7.4:1 on #08122a */
.tray-slot.is-off .tray-cost{ box-shadow:inset 0 0 0 2px var(--hot-lo); }
.tray-slot.is-off .tray-cost b{ color:var(--hot); }     /* 5.5:1 on #0a1430 */
.tray-slot.is-off:hover{ background:linear-gradient(180deg,#16264c,#0c1836); }

.tray-slot.is-empty{ opacity:.5; cursor:default; }
.tray-slot.is-empty:hover{ transform:none; }

/* ── the footnote ─────────────────────────────────────────── */
.tray-hint{
  margin:calc(7px * var(--s)) 0 0; text-align:center;
  font:800 calc(9.5px * var(--s))/1.3 var(--ui);
  letter-spacing:.13em; text-transform:uppercase;
  color:var(--ink-dim);                    /* 7.8:1 on the panel foot */
}

/* screen-reader lane. Written only when the selection, the hazard
   or the ready flag actually changes — an aria-live region driven
   at 60fps is a denial of service on a screen reader. */
.tray-sr{
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap;
}

/* ── READY is an event ────────────────────────────────────────
   A meter that merely finishes filling is something you have to be
   looking at. This has to land in peripheral vision while the
   player is tracking the ball, so the whole rail acknowledges it
   once — the edge flashes to the accent, the dial punches, a ring
   throws off it — and then holds a slow breath so a glance back
   confirms it without another flash. */
.tray.is-ready .tray-dial{
  box-shadow:0 0 0 3px var(--outline), 0 4px 0 rgba(3,6,16,.55),
             0 0 20px var(--tr-acc);
  animation:trayBreath 1.9s var(--ease-out) infinite;
}
.tray.is-ready .tray-segs i.on{
  box-shadow:inset 0 0 0 2px var(--outline), 0 0 10px var(--tr-acc);
}
.tray.is-ready .tray-charge{ animation:trayBreath 1.9s var(--ease-out) infinite; }

.tray.is-fire .tray-plate{
  animation:trayPlate .62s var(--ease-out) both;
}
.tray.is-fire .tray-dial{ animation:trayPunch .5s cubic-bezier(.16,1.4,.36,1) both; }
.tray.is-fire .tray-dial::after{ animation:trayRing .62s var(--ease-out) both; }
.tray.is-fire .tray-charge{ animation:trayPunch .5s cubic-bezier(.16,1.4,.36,1) both; }
.tray.is-fire .tray-word{ animation:trayWord .5s cubic-bezier(.16,1.4,.36,1) both; }

@keyframes trayPunch{
  0%   { transform:scale(1); }
  30%  { transform:scale(1.12); }
  100% { transform:scale(1); }
}
@keyframes trayRing{
  0%   { opacity:1; transform:scale(1); }
  100% { opacity:0; transform:scale(1.55); }
}
@keyframes trayWord{
  0%   { transform:scale(.7); opacity:.3; }
  40%  { transform:scale(1.14); opacity:1; }
  100% { transform:scale(1); opacity:1; }
}
@keyframes trayPlate{
  0%   { filter:
           drop-shadow(3px 0 0 var(--tr-acc-hi))  drop-shadow(-3px 0 0 var(--tr-acc-hi))
           drop-shadow(0 3px 0 var(--tr-acc-hi))  drop-shadow(0 -3px 0 var(--tr-acc-hi))
           drop-shadow(0 0 26px var(--tr-acc)); }
  100% { filter:
           drop-shadow(2px 0 0 var(--outline))  drop-shadow(-2px 0 0 var(--outline))
           drop-shadow(0 2px 0 var(--outline))  drop-shadow(0 -2px 0 var(--outline))
           drop-shadow(0 5px 0 rgba(3,6,16,.55)) drop-shadow(0 12px 22px rgba(0,0,0,.45)); }
}
@keyframes trayBreath{
  0%,100% { opacity:1; }
  50%     { opacity:.86; }
}

/* ── narrow gutters ───────────────────────────────────────────
   A 1280-wide window leaves 258px of gutter; a 1100-wide one
   leaves 168. The rail keeps its meter and its keys and drops the
   hazard names, which the glyph and the aria-label still carry. */
@container tray (max-width:178px){
  .tray-name{ display:none; }
  .tray-slot{ grid-template-columns:auto auto 1fr; }
  .tray-cost{ justify-self:end; }
  .tray-hint{ font-size:calc(8.5px * var(--s)); letter-spacing:.08em; }
  .tray-role{ font-size:calc(10.5px * var(--s)); letter-spacing:.08em; }
}
/* Narrower still (a 4:3 window, a very short one). Everything the
   player has to act on survives — the meter, the number, the key and
   the glyph — and the price becomes a badge on the slot rather than a
   column in it, because at 96px a row of key + glyph + chip runs out
   of width and clips the chip, which is worse than no chip at all. */
@container tray (max-width:132px){
  .tray-meter{ height:calc(66px * var(--s)); }
  .tray-dial{ width:calc(66px * var(--s)); height:calc(66px * var(--s)); }
  .tray-hole{ inset:calc(9px * var(--s)); }
  .tray-num{ font-size:calc(20px * var(--s)); }
  .tray-num2{ font-size:calc(30px * var(--s)); }
  .tray-word{ font-size:calc(12px * var(--s)); letter-spacing:.05em; }
  .tray-hint{ display:none; }
  /* key + glyph pack left; the right strip is reserved for the badge
     so the two can never sit on top of each other */
  .tray-slot{ grid-template-columns:auto auto; justify-content:start;
              gap:calc(4px * var(--s));
              padding:0 calc(36px * var(--s)) 0 calc(4px * var(--s)); }
  .tray-cost{ position:absolute; right:calc(3px * var(--s)); top:50%;
              transform:translateY(-50%); padding:1px calc(4px * var(--s));
              clip-path:none; }
  .tray-cost b{ font-size:calc(9.5px * var(--s)); }
  .tray-cost i{ display:none; }
}
/* ── PHONE / TALL WINDOW: THE HAZARD STRIP ────────────────────
   Below about 1.42:1 the arena is as wide as the window and the
   side gutters the desktop rail lives in close up entirely.

   WHAT THIS REPLACED, and why. The previous answer was a 218px-wide
   DRAWER pinned under the HUD with a 156px handle to open it. On a
   360px handset that drawer is 60% of the screen width, it covered
   the left third of the playfield while open, and the hand — the
   thing you are actually trying to press — was behind a tap. The
   report was "the saboteur overlay on mobile is way too large", and
   it was right.

   WHERE THE STRIP GOES, measured rather than guessed. The arena's
   projected silhouette (8 walls, the widest field, via the live
   camera) leaves these free bands in portrait:

       360x740   arena y 186..558   free top 186   free bottom 182
       375x667   arena y 130..546   free top 130   free bottom 121
       390x844   arena y 209..640   free top 209   free bottom 204

   Both bands are candidates, which is what made this a real choice.
   The BOTTOM wins on three counts: the HUD's top row already owns
   43px of the top band, leaving less clear space there than below;
   the bottom of a phone is where a thumb is; and the hand is the one
   part of this component you press under time pressure. 375x667 is
   the binding case at 121px, so the strip is built to fit inside it.

   WHAT IT SHOWS, which is the standing constraint on this component:
   the countdown to the next hazard, and every hazard you can place.
   Both are on screen at all times now — there is no open/closed
   state left, so the drawer handle is gone with it. What went is the
   role plate (the HUD names the mode), the keyboard 1/2/3 caps (a
   phone has no number row) and the "click the field to place" hint.
   ─────────────────────────────────────────────────────────── */
/* THE `max-width` IS NOT DECORATION — it is the iPad Pro 12.9.
   1366x1024 is 1.334:1, i.e. under the 1.42 gutter threshold, so it
   took the strip and the strip ran 65px into the bottom of the arena
   (arena y 73..992 in a 1024px window — there is no free band down
   there at all). But its SIDE gutters are still 192px wide, which is
   more than the rail's 132px, so that shape wants the rail.

   The honest condition is "is the gutter wider than the rail", i.e.
   100vw >= 1.06*100vh + 240px, and a media query cannot express a
   term in vw AND vh at once. `max-width:1299px` is the width cut that
   reproduces it across the whole device matrix — every portrait
   phone and tablet is under it and takes the strip; the 12.9" in
   landscape is over it and keeps the rail.

   KNOWN GAP, stated rather than hidden: a near-square LANDSCAPE
   window between about 1.0 and 1.4 that is narrower than 1300px has
   room for neither — the arena fills it corner to corner. No device
   in the matrix is that shape, and the previous code overlapped
   there too. */
@media (max-aspect-ratio:71/50) and (max-width:1299px){
  .screen.tray-layer{
    /* the two numbers worth tuning. --tr-strip-bottom clears the
       HUD's bottom row and the home indicator; the height falls out
       of the dial plus the status word. */
    --tr-strip-bottom:calc(env(safe-area-inset-bottom, 0px) + 14px);
    --tr-strip-side:8px;
  }

  /* no drawer, so no handle — and the dormant chip goes with it,
     see the note under .tray-dormant */
  .tray-layer .tray-tab{ display:none; }

  .tray,
  .tray.is-high{
    --s:min(var(--op-ui-scale, 1), 1);
    left:calc(env(safe-area-inset-left, 0px) + var(--tr-strip-side));
    right:calc(env(safe-area-inset-right, 0px) + var(--tr-strip-side));
    top:auto; bottom:var(--tr-strip-bottom);
    width:auto; max-height:none;
    transform:none;

    /* the clock and its word stack in column one; the hand takes
       everything left over in column two and lays out across it */
    display:grid;
    grid-template-columns:auto minmax(0,1fr);
    grid-template-rows:auto auto;
    align-items:center; gap:2px 10px;
    padding:6px 9px;
  }
  /* the drawer used to slide; a strip that is simply there does not */
  .tray-layer.is-mobile-open .tray.is-open,
  .tray-layer:not(.is-mobile-open) .tray.is-open{
    transform:none; opacity:1; visibility:visible; pointer-events:auto;
  }

  .tray-role{ display:none; }
  .tray-hint{ display:none; }
  .tray-key{ display:none; }

  .tray-meter{ grid-column:1; grid-row:1; height:auto; }
  .tray-status{ grid-column:1; grid-row:2; margin:0; }
  .tray-hand{
    grid-column:2; grid-row:1 / span 2;
    flex-direction:row; align-items:stretch;
    margin:0; gap:5px;
  }
  /* TWO LINES, not one. On the rail these three parts sit in a row
     because the rail is 218px wide; a third of a 375px strip is 89px,
     and one row there put the cooldown chip ("12.3 S") next to the
     name and squeezed VORTEX / MINE / PORTAL down to "V." / "M." /
     "P.". The name goes over the chip instead: the glyph spans both
     lines as the card's anchor, the name gets the full width, and the
     countdown keeps its own line — which is the pair the player has
     to read, so neither may ellipsize. */
  .tray-slot{
    flex:1 1 0; min-width:0; width:auto; height:auto; min-height:44px;
    padding:5px 6px;
    grid-template-columns:auto minmax(0,1fr);
    grid-template-rows:auto auto;
    align-items:center; gap:1px 6px;
  }
  .tray-slot .tray-glyph{ grid-column:1; grid-row:1 / span 2; }
  .tray-slot .tray-name{ grid-column:2; grid-row:1; }
  .tray-slot .tray-cost{ grid-column:2; grid-row:2; justify-self:start; }
  /* the stacked-rail hover slides on X; in a row that reads as the
     card bumping its neighbour, so it lifts instead */
  .tray-slot:hover,
  .tray-slot.is-sel:hover{ transform:translateY(-2px); }

  .tray-dial{ width:46px; height:46px; }
  .tray-hole{ inset:6px; }
  .tray-num{ font-size:16px; }
  .tray-unit{ font-size:8px; }
  .tray-charge{ width:92px; }
  .tray-chargenum{ margin-bottom:4px; }
  .tray-num2{ font-size:25px; }
  .tray-unit2{ font-size:11px; }
  .tray-segs i{ height:10px; }
  .tray-word{ font-size:10px; }
  .tray-glyph{ width:21px; height:21px; }
  .tray-name{ font-size:10px; letter-spacing:.04em; }

  /* THE DORMANT CHIP IS GONE ON A PHONE, and this is deliberate.
     It is a rule reminder — "your hand unlocks when your wall falls"
     — that already retires itself five seconds in (_armDormFade in
     tray.js). On a 360px screen it was a 210x70 card sitting in the
     bottom-left, which is now exactly where the strip lives, and the
     user explicitly offered to lose it. It stays on desktop and on
     landscape tablets, where it costs nothing and reads once. */
  .tray-dormant,
  .tray-dormant.is-on{ display:none; }
}



/* ── accessibility ────────────────────────────────────────── */
html.op-a11y-contrast .tray{
  --tr-face-hi:#0f1f42; --tr-face-lo:#060d22; --tr-track:#04091c;
}
html.op-a11y-contrast .tray-slot{ background:linear-gradient(180deg,#132648,#0a1430); }

html.op-a11y-still .tray,
html.op-a11y-still .tray *,
html.op-a11y-still .tray *::after{
  animation:none !important; transition:none !important;
}
html.op-a11y-still .tray-slot:hover,
html.op-a11y-still .tray-slot.is-sel{ transform:none; }

@media (prefers-reduced-motion:reduce){
  .tray, .tray *, .tray *::after{ animation:none !important; }
  .tray-slot{ transition:none; }
  .tray-slot:hover{ transform:none; }
  /* READY still has to be unmistakable without motion: the ring is
     full, the word is the accent, and the dial keeps its halo. */
  .tray.is-ready .tray-dial{
    box-shadow:0 0 0 3px var(--outline), 0 4px 0 rgba(3,6,16,.55),
               0 0 0 6px var(--tr-acc);
  }
}

/* ── the dormant chip ────────────────────────────────────────
   Shown while the tray itself has nothing to show: alive in a
   saboteur-source mode, where the hand only unlocks on elimination.
   Deliberately quiet — it is a rule reminder, not a control. */
.tray-dormant{
  position:absolute; left:16px; bottom:16px;
  display:none; flex-direction:column; gap:2px;
  padding:10px 14px; max-width:210px;
  background:linear-gradient(180deg,rgba(16,28,58,.92),rgba(9,16,36,.92));
  border:2px solid var(--outline,#050814); border-radius:10px;
  box-shadow:0 4px 0 var(--outline,#050814);
  pointer-events:none;
}
.tray-dormant.is-on{ display:flex; opacity:1; transition:opacity .65s ease; }
/* retires five seconds into the match — see _armDormFade in tray.js */
.tray-dormant.is-on.is-faded{ opacity:0; }
@media (prefers-reduced-motion:reduce){
  .tray-dormant.is-on{ transition:none; }
}
.td-word{
  font:800 12px/1 var(--display); letter-spacing:.08em;
  color:var(--hot,#ff4d9d);
}
.td-note{
  font:700 11px/1.45 var(--ui); letter-spacing:.02em;
  color:#a9c6e8;                       /* 10.6:1 on this plate */
  text-transform:uppercase;
}

/* ── touch targets ── see theme.css. The hazard cards were 33px tall
   in the phone drawer, which is the one control in the game you have
   to hit while something is chasing you. */
@media (pointer:coarse){
  .tray .tray-slot{ min-height:44px; }
}
