* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0b1420; }
body { font-family: 'Segoe UI', system-ui, sans-serif; color: #e8e2d0; }
/* explicit width/height: <canvas> is a replaced element with intrinsic size,
   so inset:0 alone won't stretch it to the viewport once we stop letting the
   renderer write inline px sizes. 100% keeps clientWidth/Height == viewport,
   which the camera + tap picking rely on. */
#game { position: fixed; inset: 0; width: 100%; height: 100%; display: block; touch-action: none; }
.hidden { display: none !important; }

/* ---------- overlays ---------- */
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(5, 10, 18, 0.65);
  display: flex; align-items: center; justify-content: center; z-index: 50;
}
.modal {
  background: #17222f; border: 1px solid #3a4a5c; border-radius: 12px;
  padding: 28px; width: 420px; max-width: 92vw; box-shadow: 0 12px 40px rgba(0,0,0,.6);
}
.join-modal h1 { font-size: 26px; margin-bottom: 6px; }
.subtitle { color: #9fb0c0; font-size: 13px; margin-bottom: 18px; }
.join-new { display: flex; gap: 8px; margin-bottom: 10px; }
.join-new input[type=text], #join-name {
  flex: 1; padding: 10px; border-radius: 8px; border: 1px solid #3a4a5c;
  background: #0e1721; color: #e8e2d0; font-size: 14px;
}
#join-color { width: 46px; height: 40px; border: none; background: none; cursor: pointer; }
button {
  background: #2a3949; color: #e8e2d0; border: 1px solid #46586c; border-radius: 8px;
  padding: 9px 14px; font-size: 13px; cursor: pointer;
}
button:hover { background: #37495c; }
button.primary { background: #b4552d; border-color: #d97742; font-weight: 600; }
button.primary:hover { background: #cc6636; }
button:disabled { opacity: .45; cursor: default; }
.error { color: #ff7a6e; font-size: 13px; min-height: 18px; margin-bottom: 8px; }
#join-existing { max-height: 220px; overflow-y: auto; margin-top: 6px; }
#join-existing .fac {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px; cursor: pointer;
}
#join-existing .fac:hover { background: #22303f; }
.dot { width: 14px; height: 14px; border-radius: 50%; display: inline-block; flex-shrink: 0; }

.join-modal-h1 { font-size: 22px; margin-bottom: 14px; }
.modal-backdrop.opaque { background: #0b1420; }

/* lobby support button (bottom-left ?) + contact form fields */
.lobby-help {
  position: fixed; left: 18px; bottom: 18px; z-index: 41;
  width: 34px; height: 34px; border-radius: 50%;
  background: rgba(179, 86, 15, .82); border: 1px solid rgba(217, 122, 42, .85); color: #fff;
  font-size: 19px; font-weight: 700; line-height: 1; padding: 0; cursor: pointer;
  box-shadow: 0 4px 14px rgba(0,0,0,.5);
}
.lobby-help:hover { background: rgba(201, 99, 26, .9); }
#support-modal input, #support-modal textarea {
  width: 100%; padding: 10px; border-radius: 8px; border: 1px solid #3a4a5c;
  background: #0e1721; color: #e8e2d0; font-size: 14px; font-family: inherit;
}
#support-modal textarea { resize: vertical; min-height: 96px; }
/* account name is locked to the signed-in identity — show it greyed + uneditable */
#support-modal input[readonly] {
  background: #0a121b; color: #7f8fa0; border-color: #2a3846; cursor: not-allowed;
}

/* ---------- patch notes ---------- */
.patch-modal { width: 560px; max-height: 82vh; display: flex; flex-direction: column; padding: 22px 24px; }
.patch-head { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.patch-head h1 { font-size: 22px; margin-right: auto; }
.patch-x { padding: 5px 10px; font-size: 14px; line-height: 1; }
.patch-body { overflow-y: auto; min-height: 0; display: flex; flex-direction: column; gap: 20px; }
.patch-rel { border-top: 1px solid #2c3a4a; padding-top: 14px; }
.patch-rel:first-child { border-top: none; padding-top: 0; }
.patch-rel-head { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; margin-bottom: 9px; }
.patch-ver {
  font-size: 11px; font-weight: 700; letter-spacing: .5px; color: #2a1d05;
  padding: 3px 7px; border-radius: 5px; background: #e2b760;
}
.patch-title { font-size: 15px; font-weight: 700; color: #f2e9d2; }
.patch-date { margin-left: auto; font-size: 12px; color: #7f8fa0; }
.patch-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
.patch-list li {
  display: flex; align-items: flex-start; gap: 9px;
  font-size: 13px; line-height: 1.5; color: #c8d2dc;
}
.patch-tag {
  flex: none; min-width: 58px; text-align: center; margin-top: 1px;
  font-size: 10px; font-weight: 700; letter-spacing: .5px;
  padding: 2px 6px; border-radius: 4px; border: 1px solid;
}
.patch-tag.new { color: #7fd8a0; border-color: rgba(127, 216, 160, .4); background: rgba(127, 216, 160, .12); }
.patch-tag.balance { color: #e2b760; border-color: rgba(226, 183, 96, .4); background: rgba(226, 183, 96, .12); }
.patch-tag.fix { color: #8fb8e0; border-color: rgba(143, 184, 224, .4); background: rgba(143, 184, 224, .12); }

/* ---------- full-screen pre-game screens (title screen + lobby browser) ---------- */
.screen {
  position: fixed; inset: 0; z-index: 48; background: #0b1420;
  display: flex; align-items: center; justify-content: center;
}
/* The login screen doubles as an in-app overlay (the guest "save your account"
   flow opens it over a live browser/lobby/game). All screens share z-index 48
   and #login sits earlier in the DOM, so at equal z-index a visible browser or
   lobby would paint on top of it; it also has to clear the in-game HUD (tops out
   at 70). Lift it above both, but below the guest-nag banner (900) so that stays
   visible as context. Harmless in the normal flow — other screens are display:none. */
#login { z-index: 120; }
/* background layer — battlefield screenshot under a dark scrim; drop a <video> in here later.
   Layer order is top-to-bottom: colour washes, then the scrim that keeps foreground text
   readable, then the photo itself. The scrim must stay translucent or it hides the image. */
.screen-bg { position: absolute; inset: 0; overflow: hidden; background: #0b1420; }
.screen-bg::before {
  content: ''; position: absolute; inset: 0;
  background: url('/assets/menu-bg.jpg') center / cover no-repeat;
}
/* scrim over the photo: a vertical ramp that deepens towards the bottom, so the lower
   half of the screen stays dark enough to carry foreground text and buttons */
.screen-bg::after {
  content: ''; position: absolute; inset: 0;
  background:
    radial-gradient(1100px 600px at 72% 18%, rgba(217, 119, 66, .14), transparent 60%),
    linear-gradient(180deg,
      rgba(9, 14, 20, .18) 0%,
      rgba(9, 14, 20, .34) 30%,
      rgba(8, 12, 18, .62) 62%,
      rgba(4, 7, 11, .97) 100%);
}
.screen-bg video { width: 100%; height: 100%; object-fit: cover; opacity: .5; }

/* title screen */
.title-inner { position: relative; text-align: center; max-width: 600px; padding: 24px; }
/* the title is the logo artwork, not text — the h1 carries no glyphs, so line-height
   is zeroed (an inline image otherwise reserves descender space under it) and the
   shadow moved from text-shadow to a drop-shadow, which follows the alpha rather
   than boxing the image. The intrinsic size is on the <img> so the layout doesn't
   jump while it loads. */
.title-inner h1 { margin-bottom: 18px; line-height: 0; }
.title-inner h1 img {
  width: min(100%, 440px); height: auto;
  filter: drop-shadow(0 4px 24px rgba(0, 0, 0, .6));
}
.title-inner p { color: #9fb0c0; font-size: 15px; line-height: 1.55; margin-bottom: 30px; }
#title-play { font-size: 18px; padding: 14px 52px; border-radius: 10px; }

/* lobby browser: big two-pane window (game list | selected-game details).
   NOTE: the waiting room (#lobby) reuses this base — keep it generic and put the
   room-browser redesign under #browser so the waiting room stays a centred window. */
.browser-panel {
  position: relative; display: flex; flex-direction: column;
  width: min(960px, 96vw); height: min(660px, 92vh);
  background: #121d2a; border: 1px solid #3a4a5c; border-radius: 14px;
  padding: 20px 22px; box-shadow: 0 16px 60px rgba(0, 0, 0, .6);
}

/* ===== room browser redesign (full-page 3D world, floating lobby window) ===== */
/* stop the .screen flex from centring — everything is positioned by hand */
#browser {
  display: block;
  /* the lobby window's width, shared by the panel and the banner's centring calc.
     The King is framed at LEFT_FRAC=0.30 of the full-screen world canvas (lobbyscene.js),
     and the banner is centred in the open area to the LEFT of this panel. For the
     banner to sit under the King, the open-area centre — (100vw − gap − w) / 2 —
     must equal 0.30·100vw, which holds when w = 40vw − gap. So on wide screens the
     panel GROWS to keep the banner aligned with the King; on normal/narrow screens
     it stays the existing min(600px, 54vw). (Below 700px the panel goes full-bleed
     and the banner/canvas are hidden, so this only governs ≥700px.) */
  --lobby-gap: 36px;
  --lobby-w: max(min(600px, 54vw), calc(40vw - var(--lobby-gap)));
}
/* ===== the living world behind the front-of-house screens ===================
   ONE canvas, one renderer (ui/lobbyscene.js), shared by the title screen, the
   sign-in screen and the lobby — so a player walks through all three without the
   world behind them changing. It is a SIBLING of the screens rather than a child
   of #browser, because a renderer cannot follow its canvas between three parents;
   that means it sits just UNDER .screen's z-index and the screens that want it
   have to hand back the opaque background .screen gives them.

   `body.world-bg` means "the world canvas is up" and is written by nothing but
   showWorldScene/hideWorldScene.

   ⚠️⚠️ THE WORLD IS ON A PHONE TOO, BUT ONLY BEHIND THE TITLE AND SIGN-IN
   SCREENS — and WHICH screens is decided in JS, not here. This whole block used
   to be scoped to `min-width: 701px`, so a phone fell back to the flat photo on
   all three: the front door of the game showed a screenshot of the battlefield
   where every desktop showed the living world. The title and sign-in screens
   are centred copy over a backdrop and have all the room in the world behind
   them, so they get it at every width.

   The LOBBY is the one that genuinely cannot: its panel goes full-bleed and
   OPAQUE on a phone (see the `max-width: 700px` block below), so the world there
   would be a whole WebGL scene drawn at 60fps into ground nobody can see. A body
   class cannot tell which screen is up, so showWorldScene takes a `phone` flag
   and the lobby simply does not pass it — see ui/lobbyscene.js. That leaves only
   #browser's own rule needing the breakpoint. */
#world-canvas { position: fixed; inset: 0; width: 100%; height: 100%; display: none; z-index: 47; }
body.world-bg #world-canvas { display: block; }
/* the screens the world backs let it through */
body.world-bg #title-screen,
body.world-bg #login.on-world { background: transparent; }
/* ...and their backdrop drops its PHOTO — the live world replaces it — while
   keeping the scrim that carries the copy over the top (see .screen-bg::after).
   ⚠️ #login is gated on .on-world as well: that screen doubles as an in-app
   overlay (the guest "save your account" flow), and an overlay opened over a
   running lobby must keep its own opaque backdrop rather than letting the
   panel underneath show through. */
body.world-bg #title-screen .screen-bg,
body.world-bg #login.on-world .screen-bg { background: transparent; }
body.world-bg #title-screen .screen-bg::before,
body.world-bg #login.on-world .screen-bg::before { display: none; }
/* ⚠️⚠️ THE COPY IS CARRIED BY THE TEXT, NOT BY A SCRIM OVER THE WORLD — and
   that is the whole lesson here. The photo's own scrim is a bottom-heavy ramp
   finishing at .97 (near black) plus a wash, which is right over a still and
   ruinous over a live scene: the lobby puts NOTHING between you and the world,
   so anything strong enough to carry 15px body copy makes this screen visibly
   murkier than the lobby two clicks later — reported as exactly that.
   A centred vignette is not the answer either. Tight enough to leave the
   corners alone, it reads as a dark OVAL against the pale flat mountains
   behind the title; wide enough to hide its own rim, it is the whole scene
   behind a veil again (at 620×320 the ellipse already reaches every corner of
   a 1542-wide window). So the ramp here is barely-there grounding at the two
   edges, and the title and paragraph get tight dark shadows that give them a
   real edge against pale rock without touching the scene at all.
   ⚠️ Judge this against the LOBBY, not on its own: they are the same camera on
   the same world, so any difference in brightness between them is this rule. */
body.world-bg #title-screen .screen-bg::after,
body.world-bg #login.on-world .screen-bg::after {
  background: linear-gradient(180deg,
    rgba(9, 14, 20, .10) 0%, rgba(9, 14, 20, 0) 32%,
    rgba(9, 14, 20, 0) 66%, rgba(5, 9, 14, .22) 100%);
}
/* the TIGHT shadow is the load-bearing one (a real edge against pale rock); the
   wide one is only depth. The paragraph's #9fb0c0 is chosen against a dark
   photo and greys out over a bright daytime sky, so it is lifted as well. */
body.world-bg #title-screen .title-inner h1 img {
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .85)) drop-shadow(0 6px 30px rgba(0, 0, 0, .7));
}
body.world-bg #title-screen .title-inner p {
  color: #e6edf4; text-shadow: 0 1px 3px rgba(0, 0, 0, .95), 0 3px 16px rgba(0, 0, 0, .8);
}
@media (min-width: 701px) {
  /* ⚠️ THE LOBBY ALONE STAYS GATED — see the note above. Its phone panel is
     opaque and full-bleed, so this must never come off the breakpoint. */
  body.world-bg #browser { background: transparent; }
}
/* THE MARK, TOP-LEFT OVER THE WORLD — the title screen's own logo artwork, at a
   size that reads as a mark rather than as a title (the screen's heading is
   "Lobby", inside the panel). It stands in the one part of this frame that is
   reliably empty: the King is framed at LEFT_FRAC 0.30 / VERT_FRAC 0.34, so the
   top-left corner is sky and distant rock at every aspect.
   ⚠️ `pointer-events: none` — it sits over the open area the Kingdom Designer
   banner is centred in, and a decorative image must never eat a press meant for
   the world or for anything that later lands beside it.
   ⚠️ Its shadow is the TITLE SCREEN'S OWN over-the-world pair (a tight edge plus
   a wide depth), not the flat one that artwork carries on the photo backdrop:
   the mountains behind it here are pale, and a single soft shadow leaves white
   artwork on light rock with no edge at all. */
#browser .lobby-logo {
  position: absolute; top: 26px; left: 30px; z-index: 2;
  width: min(170px, 13vw); height: auto; pointer-events: none;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .85)) drop-shadow(0 6px 30px rgba(0, 0, 0, .7));
}
/* Kingdom Designer banner floats over the scene, centred in the open area
   between the screen's left edge and the lobby window's left edge */
#browser .designer-banner {
  position: absolute; bottom: 44px; z-index: 2; margin: 0; width: min(400px, 40vw);
  left: calc((100vw - var(--lobby-gap) - var(--lobby-w)) / 2);
  transform: translateX(-50%);
}

/* the lobby window floats on the right — a frosted-glass pane so the world
   shows through behind it (drops to opaque on phones, where there's no scene) */
#browser .browser-panel {
  position: absolute; right: var(--lobby-gap); top: 44px; bottom: 44px; z-index: 2;
  width: var(--lobby-w); height: auto;
  background: rgba(16, 26, 38, .82); backdrop-filter: blur(9px); -webkit-backdrop-filter: blur(9px);
  border-color: rgba(90, 110, 132, .5);
}
#browser .browser-head { align-items: flex-start; padding-right: 44px; }
.browser-title { margin-right: auto; min-width: 0; }
.browser-title h1 { font-size: 22px; line-height: 1.15; }
#browser .browser-me { margin-top: 5px; }
/* keep My stats + Patch notes on the right, even when they wrap below the title */
#browser .browser-me-actions { flex: none; margin-left: auto; }
#browser .signout { color: #d99a7a; border-color: #55414a; }
#browser .signout:hover { background: #2a1c22; color: #f0b199; }
/* Sign out as a close ✕ pinned to the top-right corner of the lobby window */
#browser .panel-close {
  position: absolute; top: 14px; right: 14px; z-index: 3;
  width: 30px; height: 30px; padding: 0; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  font-size: 15px; line-height: 1;
}
.browser-head { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; flex-wrap: wrap; }
.browser-head h1 { font-size: 24px; margin-right: auto; }
.browser-me { display: flex; align-items: center; gap: 8px; min-width: 0; }
.browser-me .browser-lbl { color: #9fb0c0; font-size: 13px; flex: none; white-space: nowrap; }
.browser-me-actions { display: flex; align-items: center; gap: 8px; }
.browser-me button { flex: none; }
#browser-name {
  width: 180px; padding: 9px 10px; border-radius: 8px; border: 1px solid #3a4a5c;
  background: #0e1721; color: #e8e2d0; font-size: 14px;
}
#browser-color { width: 40px; height: 38px; border: none; background: none; cursor: pointer; flex: none; }
/* Kingdom Designer banner — full-width call-out above the search row */
.designer-banner {
  display: flex; align-items: center; gap: 14px; width: 100%; text-align: left;
  margin-bottom: 12px; padding: 14px 16px; border-radius: 12px; cursor: pointer;
  border: 1px solid rgba(226, 183, 96, .5);
  /* frosted panel (matches the lobby's brightness) with a golden wash on top */
  background:
    linear-gradient(90deg, rgba(226, 183, 96, .22), rgba(226, 183, 96, .08) 55%, rgba(226, 183, 96, .03)),
    rgba(26, 34, 46, .82);
  backdrop-filter: blur(9px); -webkit-backdrop-filter: blur(9px);
}
.designer-banner:hover {
  background:
    linear-gradient(90deg, rgba(226, 183, 96, .3), rgba(226, 183, 96, .12) 55%, rgba(226, 183, 96, .05)),
    rgba(30, 39, 52, .84);
  border-color: rgba(226, 183, 96, .7);
}
.designer-banner .db-icon {
  flex: none; width: 46px; height: 46px; border-radius: 10px; font-size: 24px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(226, 183, 96, .14); border: 1px solid rgba(226, 183, 96, .34);
}
.designer-banner .db-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.designer-banner .db-title {
  font-size: 17px; font-weight: 700; color: #f2e9d2;
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
}
.designer-banner .db-tag {
  font-size: 10px; font-weight: 700; letter-spacing: .6px; color: #2a1d05;
  padding: 3px 7px; border-radius: 5px; background: #e2b760;
}
.designer-banner .db-sub { font-size: 13px; color: #9fb0c0; }
.designer-banner .db-cta {
  flex: none; padding: 10px 16px; border-radius: 8px; font-size: 14px; font-weight: 700;
  color: #2a1d05; background: #e2b760; border: 1px solid #f0cd84;
}
.designer-banner:hover .db-cta { background: #f0cd84; }

/* Kingdom Designer banner: a persistent golden edge glow, plus a rainbow halo
   that sweeps around it once every 20s. */
.designer-banner {
  box-shadow: 0 0 16px rgba(226, 183, 96, .35), 0 0 44px rgba(226, 183, 96, .12);
  animation: db-golden-glow 3.2s ease-in-out infinite;
}
.designer-banner:hover {
  box-shadow: 0 0 22px rgba(240, 205, 132, .55), 0 0 58px rgba(240, 205, 132, .18);
}
@keyframes db-golden-glow {
  0%, 100% { box-shadow: 0 0 13px rgba(226, 183, 96, .3), 0 0 36px rgba(226, 183, 96, .1); }
  50%      { box-shadow: 0 0 24px rgba(240, 205, 132, .58), 0 0 52px rgba(240, 205, 132, .2); }
}
/* the rainbow halo — a blurred conic-gradient ring behind the banner that
   flares in, spins, and fades, dormant for the rest of the 20s cycle */
@property --db-rb-angle {
  syntax: '<angle>'; initial-value: 0deg; inherits: false;
}
.designer-banner::after {
  content: ''; position: absolute; inset: -5px; border-radius: 16px;
  z-index: -1; pointer-events: none; opacity: 0; padding: 5px;
  background: conic-gradient(from var(--db-rb-angle),
    #ff2d55, #ff9500, #ffe000, #34c759, #00c7be, #007aff, #5856d6, #af52de, #ff2d55);
  /* mask out the centre so only a rainbow ring hugs the banner's edge */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  filter: blur(5px) saturate(1.5);
  /* db-rainbow flares the ring in/out once every 20s; db-spin sweeps the colours */
  animation: db-rainbow 20s linear infinite, db-spin 2.4s linear infinite;
}
@keyframes db-rainbow {
  0%   { opacity: 0; transform: scale(1); }
  4%   { opacity: 1; transform: scale(1.02); }
  10%  { opacity: 1; transform: scale(1.05); }
  15%  { opacity: 0; transform: scale(1.06); }
  100% { opacity: 0; transform: scale(1.06); }
}
@keyframes db-spin {
  to { --db-rb-angle: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
  .designer-banner { animation: none; }
  .designer-banner::after { animation: none; opacity: 0; }
}

/* patch-notes header button: amber outline + unread dot */
.patch-btn {
  display: flex; align-items: center; gap: 7px;
  color: #e2b760; border-color: rgba(226, 183, 96, .45); background: rgba(226, 183, 96, .1);
}
.patch-btn:hover { background: rgba(226, 183, 96, .2); border-color: rgba(226, 183, 96, .7); }
.patch-btn .patch-dot { width: 7px; height: 7px; border-radius: 50%; background: #e2b760; display: none; }
.patch-btn.unread .patch-dot { display: block; }

/* the guided first game (see TUTORIAL.md): green, so it reads as an invitation
   rather than as one more of the lobby's utilities, and gently pulsing so a new
   player's eye lands on it. It is only ever on screen for an account that has
   not finished, so a permanent animation costs a returning player nothing.
   ⚠️ It stands in the FOOTER beside Create Game — the two things a new player
   might do next, side by side — rather than up among the header's account
   chips. `flex: none` is what makes Create Game close the gap again the moment
   the tutorial is finished and this button goes; the footer's own `flex: 1` on
   #browser-create does the rest, so nothing has to be told it has gone. */
.tutorial-btn {
  display: flex; align-items: center; justify-content: center; gap: 6px; min-width: 0;
  flex: 1 1 calc(25% - 2.5px); padding: 13px 14px; font-size: 15px; font-weight: 700; line-height: 1.2;
  color: #8fe0a4; border-color: rgba(143, 224, 164, .45); background: rgba(143, 224, 164, .12);
  animation: tut-pulse 3.4s ease-in-out infinite;
}
.tutorial-btn:hover { background: rgba(143, 224, 164, .24); border-color: rgba(143, 224, 164, .75); animation: none; }
/* ⚠️ The reward is set SMALLER than the label beside it, and the line-height
   above is what stops its emoji making this button taller than Create Game.
   At the footer's 15px it is a long tail — "· unlock 👀 Big Comedy Eyes" — and
   left at full size it made the tutorial the WIDER of the two buttons, which
   says the wrong thing about which is the main action here.
   ⚠️ At the footer's 1:3 split (see .browser-foot) there is no room for it at
   all — ~136px of button against ~113px of "▶ Tutorial" — so it is dropped
   there and refreshTutorialButton() puts it in the button's TOOLTIP instead.
   It comes back on a phone, where the button takes a full-width row of its own. */
.tutorial-btn .tut-reward { color: #cfe9d6; font-weight: 500; font-size: 13px; }
.browser-foot .tut-reward { display: none; }
.tutorial-btn .tut-reward b { color: #8fe0a4; font-weight: 700; }
@keyframes tut-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(143, 224, 164, 0); }
  50%      { box-shadow: 0 0 0 3px rgba(143, 224, 164, .16); }
}
@media (prefers-reduced-motion: reduce) { .tutorial-btn { animation: none; } }

.browser-controls { display: flex; gap: 8px; margin-bottom: 12px; }
/* search box with a leading magnifier glyph */
.browser-search {
  flex: 1; display: flex; align-items: center; gap: 9px; min-width: 0;
  padding: 9px 12px; border-radius: 8px; border: 1px solid #3a4a5c; background: #0e1721;
}
.browser-search svg { flex: none; color: #7f8fa0; }
.browser-search:focus-within { border-color: #46586c; }
#browser-search {
  flex: 1; min-width: 0; padding: 0; border: none; outline: none;
  background: transparent; color: #e8e2d0; font-size: 14px; font-family: inherit;
}

/* uppercase column header over the games list */
.browser-colhead {
  display: grid; grid-template-columns: 1fr auto 74px 92px; gap: 14px;
  padding: 0 14px 8px; font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: #7f8fa0;
}
.browser-colhead > :nth-child(3) { text-align: center; }
.browser-colhead > :nth-child(4) { text-align: right; }

/* base (waiting room reuses it): two panes side by side */
.browser-body { flex: 1; display: flex; gap: 14px; min-height: 0; }
/* room browser: single column, list fills it and the detail slides over */
#browser .browser-body { display: block; position: relative; }
#browser-list {
  position: absolute; inset: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: 8px; padding-right: 2px;
}
.browser-empty { margin: auto; color: #7f8fa0; font-size: 14px; text-align: center; padding: 26px 10px; }

/* a games-list row: Battlefield | Mode | Players | Status */
.room-row {
  position: relative; display: grid; grid-template-columns: 1fr auto 74px 92px;
  gap: 14px; align-items: center; padding: 12px 14px; cursor: pointer;
  border: 1px solid #2c3a4a; border-radius: 8px; background: #101b26;
}
.room-row:hover { background: #182636; border-color: #3a4a5c; }
.room-row.sel { border-color: #d97742; background: #1a2634; }
.room-row.sel::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: #d97742; border-radius: 8px 0 0 8px;
}
.room-row .room-main { min-width: 0; }
.room-row .room-name {
  font-size: 15px; color: #e8e2d0; font-weight: 600;
  display: flex; align-items: center; gap: 6px; min-width: 0;
}
.room-row .room-sub { font-size: 12px; color: #8598a8; margin-top: 2px; }
.room-row .room-mode { min-width: 0; }
.mode-chip {
  display: inline-block; font-size: 11px; color: #b9c6d2; white-space: nowrap;
  padding: 3px 9px; border: 1px solid #35465a; border-radius: 4px; background: #14212e;
}
.room-row .room-players { font-size: 15px; font-weight: 600; color: #e8e2d0; text-align: center; }
.room-row .room-status {
  font-size: 11px; letter-spacing: .05em; text-transform: uppercase; text-align: right; color: #8598a8;
}
.room-status .room-sched { font-size: 11px; letter-spacing: 0; text-transform: none; color: #e2b760; margin-top: 2px; }
.room-badge { font-size: 10px; padding: 2px 6px; border-radius: 10px; font-weight: 600; letter-spacing: .3px; }
.room-badge.waiting { background: #1f3a2a; color: #6fd39a; }
.room-badge.active { background: #3a2f1f; color: #e0b25a; }
.room-badge.mine { background: #1b3b2c; color: #55e08f; }

/* games this account already has a kingdom/seat in */
.room-row.mine {
  border-color: #3f7d5c; background: #12211a;
  box-shadow: inset 3px 0 0 #55e08f;
}
.room-row.mine:hover { background: #182c22; }
.room-row.mine.sel { border-color: #d97742; }
.room-row.mine.sel::before { background: #d97742; }
.detail-mine {
  font-size: 12px; color: #55e08f; background: #142218; border: 1px solid #2e5a43;
  border-radius: 6px; padding: 6px 8px; margin-bottom: 10px;
}
/* "My games" filter toggle */
.filter-btn {
  padding: 9px 14px; border-radius: 8px; border: 1px solid #3a4a5c; cursor: pointer;
  background: #0e1721; color: #8598a8; font-size: 13px; font-weight: 600; white-space: nowrap;
}
.filter-btn:hover { border-color: #3f7d5c; color: #55e08f; }
.filter-btn.on { background: #16291f; border-color: #55e08f; color: #7fedaf; }

/* waiting-room settings pane keeps the framed-card look */
#lobby-settings {
  flex: 1; display: flex; flex-direction: column; overflow-y: auto;
  border: 1px solid #2c3a4a; border-radius: 10px; background: #101b26; padding: 16px;
}

/* room-settings slide-in — covers the games list; a Back link returns to it */
#browser-detail {
  position: absolute; inset: 0; z-index: 3; overflow-y: auto;
  display: flex; flex-direction: column; padding: 4px 2px 2px;
  background: #121d2a; animation: browserSlideIn .24s ease;
}
@keyframes browserSlideIn { from { opacity: 0; transform: translateX(22px); } to { opacity: 1; transform: translateX(0); } }
.detail-back {
  align-self: flex-start; background: none; border: none; color: #9fb0c0;
  font-size: 13px; padding: 4px 2px; margin-bottom: 10px; cursor: pointer;
}
.detail-back:hover { background: none; color: #e8e2d0; }
.detail-head {
  display: flex; align-items: center; gap: 12px;
  padding-bottom: 14px; border-bottom: 1px solid #22303f; margin-bottom: 6px;
}
.detail-crest {
  flex: none; width: 46px; height: 46px; border-radius: 8px; font-size: 22px;
  display: flex; align-items: center; justify-content: center;
  background: #16232f; border: 1px solid #33465a;
}
#browser-detail .detail-name { font-size: 22px; margin-bottom: 2px; }
.detail-hostline { font-size: 13px; color: #8598a8; }
.detail-rows { display: flex; flex-direction: column; margin-bottom: 14px; }
#browser-detail #detail-join { margin-top: auto; }
/* waiting-room right column: settings pane + host edit affordances */
.lobby-right { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; }
#lobby-edit-form {
  overflow-y: auto; border: 1px solid #2c3a4a; border-radius: 10px;
  background: #101b26; padding: 16px;
}
.detail-empty { margin: auto; color: #7f8fa0; font-size: 14px; text-align: center; padding: 20px; }
.detail-name { font-size: 18px; font-weight: 600; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.detail-row {
  display: flex; justify-content: space-between; gap: 12px; font-size: 13px;
  padding: 7px 2px; border-bottom: 1px solid #1d2a38;
}
.detail-row .k { color: #8598a8; flex: none; }
.detail-row .v { text-align: right; }
#detail-join { margin-top: auto; width: 100%; padding: 12px; font-size: 15px; }

/* browser footer: full-width Create Game (+ a compact Kingdom Designer button
   that only appears on phones, where the left scene/banner is hidden) */
/* ⚠️ Create Game takes three quarters of the footer and the tutorial the last
   quarter, and the two bases are PERCENTAGES with the 10px gap already taken
   out of them (7.5 + 2.5) rather than the `flex: 3` / `flex: 1` this obviously
   wants to be. Grow ratios off a zero basis are NOT 3:1 here: each button's own
   padding and border sit outside the flex base and are added to both equally,
   so the measured split came out 2.62:1 (434/165 of a 609px row) — visibly not
   a quarter. With the bases summing to exactly the row less its gap there is no
   free space to distribute and the ratio is 3.000:1 to the pixel.
   The `flex-grow: 1` on each is what puts the row back when the tutorial button
   goes (finished, or a guest): Create Game is then the only item and grows into
   the whole width, so nothing has to be told it has gone. */
.browser-foot { display: flex; gap: 10px; margin-top: 12px; }
#browser-create { flex: 1 1 calc(75% - 7.5px); min-width: 0; padding: 13px; font-size: 15px; font-weight: 700; }

/* A selected room's ⚔️ Join button is pinned to the foot of the slide-in pane,
   which lands it a few pixels above this footer — two orange primaries almost
   touching, and the wrong one is the easy click. So while a room's settings are
   up, Create Game steps back to a quiet outline and the footer takes a rule and
   a little more air: the only filled button on screen is then the one that acts
   on the room you are actually looking at. Toggled by renderDetail(). */
.browser-panel.detail-open .browser-foot {
  margin-top: 16px; padding-top: 14px; border-top: 1px solid #22303f;
}
.browser-panel.detail-open #browser-create {
  background: none; border-color: #3a4a5c; color: #9fb0c0; font-weight: 600;
}
.browser-panel.detail-open #browser-create:hover {
  background: #22303f; border-color: #55677c; color: #e8e2d0;
}
/* ...and the tutorial button beside it stops pulsing and drops its fill for the
   same reason — a green button breathing away in the corner is exactly the sort
   of thing that pulls the eye off the room the player opened. It keeps its
   green ink, so it is still findable rather than hidden. */
.browser-panel.detail-open .tutorial-btn {
  background: none; border-color: rgba(143, 224, 164, .3); animation: none;
}
.browser-army-m {
  display: none; flex: none; padding: 13px 14px; font-size: 14px; font-weight: 600;
  color: #f2e9d2; background: rgba(226, 183, 96, .14); border: 1px solid rgba(226, 183, 96, .4);
}
.browser-army-m:hover { background: rgba(226, 183, 96, .24); }

.browser-me .browser-lbl { color: #9fb0c0; font-size: 13px; }
.browser-me b {
  color: #e8e2d0; font-size: 14px;
  max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
input.locked { opacity: .8; cursor: not-allowed; }
.field-hint { font-size: 12px; color: #8598a8; margin: 2px 0 10px; }

/* ---------- login / sign-in screen ---------- */
.auth-card {
  position: relative; width: min(400px, 94vw); max-height: 94vh; overflow-y: auto;
  background: #121d2a; border: 1px solid #3a4a5c; border-radius: 14px;
  padding: 26px 26px 20px; box-shadow: 0 16px 60px rgba(0, 0, 0, .6);
}
.auth-card h1 { font-size: 26px; margin-bottom: 6px; text-align: center; }
.auth-sub { color: #9fb0c0; font-size: 13px; line-height: 1.5; margin-bottom: 18px; text-align: center; }
.auth-form { display: flex; flex-direction: column; gap: 12px; }
.auth-lbl { display: flex; flex-direction: column; gap: 5px; font-size: 12px; color: #9fb0c0; }
.auth-lbl input, .auth-input {
  padding: 10px 11px; border-radius: 8px; border: 1px solid #3a4a5c;
  background: #0e1721; color: #e8e2d0; font-size: 14px; width: 100%;
}
.auth-input { margin-bottom: 8px; }
.auth-row { display: flex; align-items: center; justify-content: space-between; font-size: 12px; }
.auth-check { display: flex; align-items: center; gap: 6px; color: #9fb0c0; cursor: pointer; }
.auth-check input { width: auto; cursor: pointer; }
.auth-link { color: #d99a7a; text-decoration: none; }
.auth-link:hover { text-decoration: underline; }
.auth-submit { margin-top: 4px; width: 100%; padding: 11px; font-size: 15px; }
.auth-msg { font-size: 12px; color: #e0b25a; min-height: 16px; margin: 8px 0 0; text-align: center; }
.auth-signup { font-size: 12px; color: #9fb0c0; text-align: center; margin: 8px 0 0; }

.auth-divider {
  display: flex; align-items: center; text-align: center; color: #6f8093;
  font-size: 11px; margin: 16px 0 12px; gap: 10px;
}
.auth-divider::before, .auth-divider::after { content: ''; flex: 1; height: 1px; background: #2c3a4a; }
.auth-divider:empty { margin: 14px 0; }
.auth-divider:empty::before, .auth-divider:empty::after { margin: 0; }

.auth-providers { display: flex; flex-direction: column; gap: 8px; }
.auth-provider {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; padding: 10px; font-size: 14px; background: #1a2634; border-color: #3a4a5c;
}
.auth-provider:hover { background: #22303f; }
.auth-provider svg { flex: none; }

.auth-guest {
  display: block; width: 100%; margin-top: 8px; padding: 6px;
  background: none; border: none; color: #8598a8; font-size: 13px;
  text-decoration: underline; cursor: pointer;
}
.auth-guest:hover { background: none; color: #cdd7e0; }
.auth-back { width: 100%; margin-top: 8px; background: none; border-color: transparent; color: #8598a8; }
.auth-back:hover { background: #1a2634; color: #cdd7e0; }

@media (max-width: 700px) {
  /* the title screen's logo needs no phone rule — it is the logo image, and its own
     `width: min(100%, 440px)` already fits it to the narrowest screen */
  /* the floating card reads as a clipped window on a phone — go full-bleed,
     like a native page */
  .browser-panel {
    width: 100%; height: 100vh; height: 100dvh; border-radius: 0; border: none;
    box-shadow: none; padding: calc(10px + env(safe-area-inset-top)) 14px calc(10px + env(safe-area-inset-bottom));
  }
  /* no room for the 3D world beside a full-bleed panel — the floating banner goes
     (the Designer moves into the footer button) and the whole screen is reclaimed
     for an opaque panel.
     ⚠️ #world-canvas needs nothing here, but NOT for the reason it used to: the
     canvas is display:block at every width now (the title and sign-in screens
     stand in the world on a phone). What keeps it off THIS screen is that
     showBrowser passes no `phone` flag, so showWorldScene takes the world down
     rather than drawing it behind the opaque panel below. */
  #browser .designer-banner { display: none; }
  /* keep the panel POSITIONED (not static) so it paints above siblings, and
     opaque (no scene to show through) */
  #browser .browser-panel {
    position: relative; z-index: 1; inset: auto;
    width: 100%; height: 100vh; height: 100dvh;
    background: #121d2a; backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  .browser-army-m { display: inline-flex; align-items: center; }

  /* the lobby's logo goes with the scene it stands on — there is no world to
     stand over down here, and the panel is full-bleed. */
  #browser .lobby-logo { display: none; }

  /* the bottom-left ? overlaps the footer buttons on a phone — shrink it and
     tuck it beside the close ✕ in the top-right corner */
  .lobby-help {
    left: auto; right: 52px; top: 15px; bottom: auto;
    width: 28px; height: 28px; font-size: 17px;
    box-shadow: 0 2px 8px rgba(0,0,0,.5); z-index: 4;
  }
  /* ⚠️ The volume control's phone rule is NOT here — it lives with its own base
     rules further down the file, because this block comes BEFORE them and both
     are one id: at equal specificity source order decides, so an override here
     loses. See "master volume control". */

  /* the waiting room keeps its two-pane column stack */
  #lobby .browser-body { display: flex; flex-direction: column; }
  .lobby-left { flex: 1; }
  .lobby-right { flex: none; max-height: 46%; }

  /* header: title + name on the left, account chips wrap to their own row */
  .browser-head { gap: 8px; row-gap: 8px; margin-bottom: 10px; }
  .browser-head h1 { font-size: 20px; }
  .browser-title h1 { font-size: 19px; }
  #browser .browser-me-actions { flex: 1 1 100%; gap: 6px; }
  #browser .browser-me-actions button { flex: 1; padding: 7px 4px; font-size: 12px; justify-content: center; white-space: nowrap; }

  /* the footer already carries the phone-only Kingdom Designer button beside
     Create Game, so a third one alongside them would leave all three too narrow
     to read. The tutorial takes a row of its own underneath — which also gives
     its label room for the reward, the POINT of the button, at a width where
     the header could never have shown it. */
  #browser .browser-foot { flex-wrap: wrap; }
  #browser .tutorial-btn { flex: 1 0 100%; justify-content: center; }
  /* ⚠️ ...and Create Game goes back to taking whatever the Kingdom Designer
     leaves it. Its desktop basis is three quarters of the row, which alongside
     that button no longer fits — so it wrapped to a row of its own and the
     footer came out three deep. */
  #browser .browser-foot #browser-create { flex: 1 1 0; }
  /* a full-width row has room for the reward tail again */
  #browser .browser-foot .tut-reward { display: inline; }

  /* games grid drops the Mode chip so name / players / status still fit */
  .browser-colhead { grid-template-columns: 1fr 60px 84px; }
  .browser-colhead > :nth-child(2) { display: none; } /* MODE label */
  .browser-colhead > :nth-child(3) { grid-column: 2; }
  .browser-colhead > :nth-child(4) { grid-column: 3; }
  .room-row { grid-template-columns: 1fr 60px 84px; }
  .room-row .room-mode { display: none; }
}

/* create-game modal */
.cr-modal { width: 560px; max-width: 94vw; max-height: 92vh; padding: 0; display: flex; flex-direction: column; }
.cr-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 20px 24px 16px; border-bottom: 1px solid #22303f;
}
.cr-head h1 { font-size: 20px; }
.cr-x {
  font-size: 22px; line-height: 1; padding: 2px 9px; color: #8598a8;
  background: none; border: none;
}
.cr-x:hover { background: none; color: #e8e2d0; }
.cr-body { padding: 18px 24px; overflow-y: auto; }
.cr-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.cr-foot { display: flex; gap: 12px; padding: 14px 24px 20px; border-top: 1px solid #22303f; }

/* create-game form */
.cr-field { margin-bottom: 10px; }
.cr-field label { display: block; font-size: 12px; color: #9fb0c0; margin-bottom: 4px; }
.cr-field .cr-opt { color: #6f8093; }
.cr-field input, .cr-field select {
  width: 100%; padding: 9px 10px; border-radius: 8px; border: 1px solid #3a4a5c;
  background: #0e1721; color: #e8e2d0; font-size: 14px;
}
.cr-field .cr-hint { font-size: 11px; color: #6f8093; margin-top: 4px; line-height: 1.4; }
/* the native date/time picker glyph is near-black on our dark fields */
.cr-field input[type="datetime-local"]::-webkit-calendar-picker-indicator { filter: invert(0.75); cursor: pointer; }
.detail-sched { color: #8598a8; }

/* scheduled-start countdown banner above the ready/start buttons */
.lobby-countdown {
  background: #14202c; border: 1px solid #3a4a5c; border-radius: 8px;
  padding: 8px 10px; margin-bottom: 8px; font-size: 13px; color: #e8e2d0; text-align: center;
}
.lobby-countdown b { color: #f0c674; }
.lobby-countdown .sched-when, .lobby-countdown .sched-note {
  display: block; font-size: 11px; color: #8598a8; margin-top: 2px;
}

/* lobby / waiting room — two-pane layout (roster | settings), reuses .browser-panel */
.lobby-left { flex: 1.25; min-width: 0; display: flex; flex-direction: column; min-height: 0; }
.lobby-section { font-size: 12px; color: #9fb0c0; margin-bottom: 6px; }
#lobby-members { flex: 1; min-height: 60px; overflow-y: auto; margin-bottom: 8px; }
#lobby-members .fac .ready { margin-left: auto; font-size: 12px; }
#lobby-members .fac .ready.yes { color: #6fd39a; }
#lobby-members .fac .ready.no { color: #7f8fa0; }
#lobby-ready.on { background: #1f6d3e; border-color: #2e9457; color: #eafff1; }
#join-spec-note {
  background: #0e1721; border: 1px solid #3a4a5c; border-radius: 8px;
  padding: 10px 12px; margin-bottom: 14px; font-size: 13px; color: #9fb0c0;
}
#lobby-members .fac {
  display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: 8px;
}
#lobby-members .fac.mine { background: #22303f; }
#lobby-members .fac .crown { margin-left: auto; font-size: 12px; color: #f0c04a; }
#lobby-members .unnamed { color: #6f8093; font-style: italic; }
/* the faction a member is fielding, next to their name */
#lobby-members .mem-fac {
  font-size: 10.5px; color: #b9c6d4; background: #1b2634; border: 1px solid #2c3a4b;
  border-radius: 999px; padding: 1px 7px; white-space: nowrap;
}

/* Which SIDE a member is on (data/teams.js). A pip in the team's own colour
   rather than a swatch, because a faction keeps its own colours in a team game —
   two allies in red and green would otherwise read as opponents. `--tc` is set
   inline from the served TEAM_COLORS, so this rule never names a hex. */
#lobby-members .mem-team {
  font-size: 10px; font-weight: 700; letter-spacing: .3px;
  color: var(--tc); background: color-mix(in srgb, var(--tc) 16%, #141d28);
  border: 1px solid color-mix(in srgb, var(--tc) 45%, transparent);
  border-radius: 999px; padding: 1px 8px; white-space: nowrap;
}
/* the host's version on an AI row: the same pill, but it is a control */
#lobby-members .mem-team.btn { cursor: pointer; }
#lobby-members .mem-team.btn:hover { background: color-mix(in srgb, var(--tc) 30%, #141d28); }

/* ---- your own row: colour + faction pickers and the Designer ----------------
   A strip attached under your roster row, so the two things you most often want
   to change before readying up are settled where you already are. The pickers
   open INLINE (a block that wraps to its own line) rather than floating over the
   list — #lobby-members scrolls, so an absolutely positioned popover would be
   clipped by it. */
#lobby-members .fac.mine { border-radius: 8px 8px 0 0; }
#lobby-members .mem-opts {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  background: #22303f; border-radius: 0 0 8px 8px; padding: 0 8px 8px;
  margin-bottom: 4px;
}
#lobby-members .mem-opt {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 9px; font-size: 12px; line-height: 1.5; border-radius: 999px;
  background: #16202c; border: 1px solid #3a4a5c; color: #cdd8e4;
  max-width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
#lobby-members .mem-opt:hover { background: #1d2937; border-color: #5a6c80; color: #e8e2d0; }
#lobby-members .mem-opt.on { background: #1d2937; border-color: #6f8398; color: #e8e2d0; }
#lobby-members .mem-opt.warn { border-color: #6d5a2c; color: #f0c674; }
#lobby-members .mem-opt-dot {
  width: 12px; height: 12px; border-radius: 50%; flex: none;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.45);
}
#lobby-members .mem-caret { font-size: 9px; color: #8598a8; }
/* the Designer is the one thing here that leaves the screen, so it's the one
   thing here that's painted */
#lobby-members .mem-opt.design {
  margin-left: auto;
  background: linear-gradient(180deg, #ef8f37, #d06d18);
  border-color: #f6ab63; color: #fff5ea; font-weight: 600;
}
#lobby-members .mem-opt.design:hover {
  background: linear-gradient(180deg, #ff9d45, #e07a20); border-color: #ffc07a; color: #fff;
}
/* the faction crest, in that house's own accent (FACTION_LOOK in armybuilder.js) */
#lobby-members .mem-crest {
  width: 15px; height: 15px; flex: none; border-radius: 4px; border: 1px solid;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 9px; font-weight: 700;
}
#lobby-members .mem-crest.big { width: 26px; height: 26px; border-radius: 6px; font-size: 13px; }

/* an open picker: its own full-width line under the buttons */
#lobby-members .mem-pop {
  flex: 1 0 100%; background: #16202c; border: 1px solid #34445a; border-radius: 8px;
  padding: 8px 9px; margin-top: 2px;
}
#lobby-members .mem-pop-h {
  font-size: 11px; color: #9fb0c0; text-transform: uppercase; letter-spacing: .05em;
  margin-bottom: 7px;
}
#lobby-members .mem-pop-note { font-size: 10.5px; color: #6f8093; line-height: 1.45; margin-top: 6px; }
#lobby-members .mem-sw-row { display: flex; flex-wrap: wrap; gap: 7px; }
#lobby-members .mem-sw {
  width: 26px; height: 26px; border-radius: 50%; padding: 0; flex: none;
  border: 2px solid transparent; box-shadow: inset 0 0 0 1px rgba(0,0,0,.45);
  position: relative; cursor: pointer;
}
#lobby-members .mem-sw:hover { border-color: rgba(255,255,255,.45); }
#lobby-members .mem-sw.sel { border-color: #e8e2d0; }
/* the off-palette swatch wears the current colour on its rim and hides the
   native colour input behind it */
#lobby-members .mem-sw.custom {
  background: repeating-conic-gradient(#4a5a6c 0 25%, #354354 0 50%) 50%/10px 10px;
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: inset 0 0 0 4px currentColor, inset 0 0 0 5px rgba(0,0,0,.45);
  overflow: hidden;
}
#lobby-members .mem-sw.custom input { opacity: 0; width: 100%; height: 100%; cursor: pointer; border: none; padding: 0; }
#lobby-members .mem-fac-row {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: left;
  padding: 6px 8px; margin-bottom: 4px; border-radius: 7px;
  background: #1a2634; border: 1px solid #2c3a4b; color: #cdd8e4;
}
#lobby-members .mem-fac-row:hover { background: #22303f; border-color: #5a6c80; }
#lobby-members .mem-fac-row.sel { border-color: #6f8398; background: #24313f; }
#lobby-members .mem-fac-txt { display: flex; flex-direction: column; min-width: 0; }
#lobby-members .mem-fac-txt b { font-size: 12.5px; color: #e8e2d0; font-weight: 600; }
#lobby-members .mem-fac-txt i { font-size: 10.5px; color: #8598a8; font-style: normal; }
#lobby-members .mem-fac-check { margin-left: auto; font-size: 13px; }

/* AI opponent rows + the host's add-AI control */
#lobby-members .fac .bot-diff {
  font-size: 11px; color: #9fb0c0; background: #1a2634; border: 1px solid #3a4a5c;
  border-radius: 10px; padding: 1px 8px;
}
#lobby-members .fac .bot-remove {
  margin-left: 6px; padding: 1px 8px; font-size: 12px; line-height: 1.4;
  background: #2a1c1c; border-color: #5c3a3a; color: #ff9b8f;
}
/* the + row sits under the bottom player and opens the add-a-player popup */
#lobby-members .fac.addbot {
  width: 100%; justify-content: center; background: none; cursor: pointer;
  border: 1px dashed #3a4a5c; color: #9fb0c0; margin-top: 4px;
}
#lobby-members .fac.addbot:hover { background: #22303f; border-color: #5a6c80; color: #e8e2d0; }
#lobby-members .fac.addbot .addbot-plus { font-size: 18px; line-height: 1; }

/* ...and the two ways to fill that seat, in the popup it opens. Same shape as
   the roster's own faction/team picker rows (.mem-fac-row) so the waiting room
   reads as one surface — a title with the consequence under it, at a size a
   finger can hit. */
#add-bot .seat-opt {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  padding: 12px 13px; margin-bottom: 9px; border-radius: 9px;
  background: #1a2634; border: 1px solid #2c3a4b; color: #cdd8e4;
}
#add-bot .seat-opt:hover { background: #22303f; border-color: #d97742; }
#add-bot .seat-ico { font-size: 22px; line-height: 1; flex: none; }
#add-bot .seat-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
#add-bot .seat-txt b { font-size: 14px; color: #e8e2d0; font-weight: 600; }
#add-bot .seat-txt i { font-size: 11.5px; color: #8598a8; font-style: normal; }

/* ---------- master volume control (bottom-left, lobby screens) ----------
   It sits IMMEDIATELY RIGHT OF the support ? (.lobby-help), which owns the
   corner — the two are the only chrome on these screens that is about the app
   rather than about the game, so they read as one pair rather than as one
   control in each corner. `left` is the ?'s own 18px plus its 34px plus an 8px
   gap; keep the three in step if that button is ever resized. The top-left
   corner it used to hold is the lobby's logo now (see #browser .lobby-logo).
   ⚠️ IT GROWS RIGHTWARD on hover (the slider slides out of the button), which is
   why it takes the right-hand seat: anchored to the left edge there is open
   ground for the slider to open into at every width. */
#vol-ctl {
  position: fixed; bottom: 18px; left: 60px; z-index: 60;
  display: flex; align-items: center; gap: 0;
  border-radius: 999px; padding: 0; border: 1px solid transparent;
  transition: background .18s ease, border-color .18s ease, padding .18s ease;
}
#vol-ctl.hidden { display: none; }
/* on hover a dark glass pill fades in to hold the revealed slider */
#vol-ctl:hover {
  background: rgba(16, 26, 38, .82);
  border-color: rgba(255, 255, 255, .10);
  backdrop-filter: blur(9px); -webkit-backdrop-filter: blur(9px);
  padding-right: 8px;
}
/* the button matches the Support ? standing beside it: orange fill, white icon,
   same 34px disc — which is most of what makes the two read as one pair */
#vol-ctl-btn {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(179, 86, 15, .82); border: 1px solid rgba(217, 122, 42, .85); color: #fff;
  line-height: 0; padding: 0; cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .5);
  transition: transform .1s ease, background .15s ease;
}
#vol-ctl-btn svg { width: 20px; height: 20px; display: block; }
#vol-ctl-btn:hover { background: rgba(201, 99, 26, .9); }
#vol-ctl-btn:active { transform: scale(.92); }
#vol-ctl.muted #vol-ctl-btn { background: rgba(120, 95, 70, .8); border-color: rgba(150, 120, 95, .7); }
/* the slider stays tucked away and slides out only while the control is hovered */
#vol-ctl-slider {
  width: 0; opacity: 0; margin-left: 0; height: 4px; cursor: pointer;
  accent-color: #d98a4e;
  transition: width .18s ease, opacity .18s ease, margin .18s ease;
}
#vol-ctl:hover #vol-ctl-slider {
  width: 96px; opacity: 1; margin-left: 10px; margin-right: 4px;
}
#vol-ctl.muted:hover #vol-ctl-slider { opacity: .4; }
/* ⚠️⚠️ THE PHONE RULE HAS TO LIVE HERE, NOT IN THE `max-width: 700px` BLOCK
   HUNDREDS OF LINES ABOVE — that block comes FIRST in the file and both rules
   are one id, so at equal specificity source order decides and the base rule
   above would win. It does not merely fail to move the control: `bottom` is
   left standing while `top` is applied, so the box stretches between the two
   edges of the screen and the flex centring drops the button into the middle of
   the map (seen in game).
   On a phone the control keeps the top-LEFT seat it has always had, for the
   support ? 's own reason turned round: down here the bottom-left corner
   belongs to the footer (Kingdom Designer / Create Game), so the desktop
   pairing cannot come with it. Deliberately not tucked beside the ? either —
   this control is shown on the waiting room too, which has neither a ? nor a ✕
   to sit beside, and it GROWS RIGHTWARD, which that corner has no room for. */
@media (max-width: 700px) {
  #vol-ctl { top: 14px; left: 14px; bottom: auto; }
}

/* ================= THE IN-GAME UI WEARS YOUR HOUSE'S COLOURS =================
   Everything from here to the Kingdom Designer — the day box, the docks, the
   HUD, the cog menu and its window, the Build disc, the build and battle
   panels, the radial menu, the tooltips, the toasts, the Tab scoreboard — is
   painted out of ONE small set of custom properties. The values below are the
   game's original neutrals to the digit, so with no `data-faction` on <body>
   (the lobby, the login screen, a spectator) nothing whatever has changed.

   `main.js` stamps `body[data-faction]` while you hold a faction, and the four
   blocks under this one re-point the same properties to that house. So a
   Withering HUD is violet-black under purple rules, a Dwarf HUD warm earth
   under stone, and not one selector below had to learn about factions.

   ⚠️⚠️ EACH BLOCK IS ALSO KEYED ON `#ab-decrees[data-faction]`, and that is the
   Kingdom Designer's ⚜️ page rather than an afterthought. That page floats the
   hand's own decree cards over the stage, and a card is printed entirely out of
   the `--c-*` half of these blocks — but it is a LOBBY screen, where <body>
   carries no faction at all, so every card there would draw in the `:root`
   neutrals. Stamping the house on the page instead resolves from the nearest
   declaring ancestor and wins whatever the specificity (the same rule `.poor`
   and `.is-rally` rely on from one element lower). It is a SELECTOR on the same
   block rather than a second copy of the values, for the reason this file gives
   below: a house is ONE decision, and split across two places is how one of them
   gets changed and the other does not. It also gets the right answer when the
   page is browsing a rival's tab — the deck shown is the house being READ, not
   the one you happen to be playing.

   ⚠️ THE HOUSE SPEAKS THROUGH THE GROUND, THE RULES AND THE KEY — NEVER
   THROUGH THE SEMANTIC COLOURS. Gold stays gold (it is money), green stays
   green (health, income), red stays red (danger, an enemy, a surrender). Those
   literals are deliberately NOT in the table: a HUD that recoloured them would
   be a HUD you have to re-learn per house, and on the Radiant Knights it would
   quietly paint "danger" over half the screen.

   ⚠️ AND THE GROUNDS ARE RGB TRIPLES, not colours. Nearly every panel in here
   is translucent over a live 3D map at some alpha of its own (.6 / .88 / .92 /
   .95 / .97), so one triple behind `rgba(var(--ui-bg), …)` moves all of them
   together where a colour would need a variable per alpha. */
:root {
  --ui-bg: 14, 22, 32;        /* the panel ground, at whatever alpha the panel wants */
  --ui-bg-deep: 12, 19, 28;   /* ...and the darker one tooltips sit on */
  --ui-bg-up: 23, 34, 47;     /* ...and the lighter one a floating button sits on */
  --ui-panel: #17222f;        /* an OPAQUE panel */
  --ui-card: #1c2937;         /* a card, row or button inside one */
  --ui-raise: #22303f;        /* ...the same, hovered or open */
  --ui-btn-lg: #2f4256;       /* the Build disc: the one large button on the map */
  --ui-btn-lg-hi: #3d5570;
  --ui-line: #3a4a5c;         /* the ordinary rule */
  --ui-line-soft: #2c3a4a;    /* ...a divider inside a panel */
  --ui-line-hi: #46586c;      /* ...and a raised thing's brighter edge */
  --ui-key: #d97742;          /* LIVE / SELECTED / hovered — the one accent */

  /* THE THIRD DIMENSION, and it is three layers rather than a drop shadow.
     `bevel` is the light ON the surface: a lit top lip and a dark bottom one,
     which is what stops a panel reading as a flat rectangle of colour cut out
     of the map. `lift` is what it casts on the map behind it. `sheen` is a
     background LAYER (drop it in front of the ground colour) — a soft fall of
     light down the top half, so a panel is lit from above like everything else
     on screen. Cheap: no extra element, no filter, no repaint. */
  --ui-bevel: inset 0 1px 0 rgba(255, 255, 255, .085), inset 0 -1px 0 rgba(0, 0, 0, .45);
  --ui-lift: 0 5px 14px rgba(0, 0, 0, .42);
  --ui-lift-lg: 0 14px 34px rgba(0, 0, 0, .6);
  --ui-sheen: linear-gradient(180deg, rgba(255, 255, 255, .062), rgba(255, 255, 255, 0) 46%);

  /* ---- and THE DECK its crown holds (ui/hand.js) ----
     --c-bg1/2/3  the ground a card is printed on, top to bottom
     --c-frame    the metal its border is struck in, --c-frame2 the shadow side
     --c-edge/2   the card's own STOCK — the two solid steps under the face that
                  give it thickness (see --card-depth)
     --c-ink      the title, --c-cost the price, --c-glow (R,G,B) the halo a
                  castable card wears
     These sit in the SAME block as the HUD's, deliberately: a house is one
     decision, and split across two places is how one of them gets changed and
     the other does not. The card rules that read them are far below, under
     THE HAND. Below are the neutral defaults — a deal that lands before its
     snapshot does still has to draw as something. */
  --c-bg1: #1d2c42; --c-bg2: #162233; --c-bg3: #0e1725;
  --c-frame: #d8b45f; --c-frame2: #7d652c;
  --c-edge: #0a0f18; --c-edge2: #05080d;
  --c-ink: #ffe9b0; --c-cost: #f0c04a; --c-glow: 255, 205, 90;

  /* THE STOCK. Two solid steps and two soft shadows: the steps are the edge of
     the card itself and are what actually reads as thickness, the soft pair is
     the contact shadow it casts on the card behind it. Kept in a variable
     because box-shadow is one list and the animated card states (the cast
     pulse, the unread shimmer, the discard) rewrite the whole of it — without
     this they would flatten the card for the length of the animation. */
  --card-depth:
    0 1px 0 var(--c-edge),
    0 3px 0 var(--c-edge2),
    0 5px 9px rgba(0, 0, 0, .45),
    0 14px 26px rgba(0, 0, 0, .5);
  /* ...and the bevel ON the face: a lit top lip and the ground darkening away
     under it, which is what stops the printed area reading as flat paper */
  --card-face:
    inset 0 1px 0 rgba(255, 255, 255, .17),
    inset 0 0 0 1px rgba(0, 0, 0, .22),
    inset 0 -12px 20px -12px rgba(0, 0, 0, .7);
}
/* THE RADIANT KNIGHTS — crimson ground, gilt rules and a gold key. */
body[data-faction="radiant"],
#ab-decrees[data-faction="radiant"] {
  --ui-bg: 32, 18, 23; --ui-bg-deep: 25, 14, 18; --ui-bg-up: 44, 26, 31;
  --ui-panel: #201621; --ui-card: #291a22; --ui-raise: #34222b;
  --ui-btn-lg: #452830; --ui-btn-lg-hi: #58343c;
  --ui-line: #5c3f42; --ui-line-soft: #3f2c30; --ui-line-hi: #7c5a4c;
  --ui-key: #f4d06a;

  /* ...and its deck: gilt on crimson. */
  --c-bg1: #6e1a22; --c-bg2: #4a0f18; --c-bg3: #2a0710;
  --c-frame: #f4d06a; --c-frame2: #a67c2b;
  --c-edge: #260810; --c-edge2: #140409;
  --c-ink: #ffe8b4; --c-cost: #ffd76b; --c-glow: 255, 208, 106;
}
/* THE WOODLAND ELVES — forest ground, bough-brown rules and a bronze key.
   ⚠️ The key is BRONZE and not the house's green: green already means health
   and income everywhere in this HUD, and an accent that collides with a meaning
   the player already reads is worse than no accent. */
body[data-faction="sylvan"],
#ab-decrees[data-faction="sylvan"] {
  --ui-bg: 13, 27, 21; --ui-bg-deep: 10, 21, 16; --ui-bg-up: 23, 42, 32;
  --ui-panel: #112019; --ui-card: #16281e; --ui-raise: #1e372a;
  --ui-btn-lg: #274231; --ui-btn-lg-hi: #345540;
  --ui-line: #40583f; --ui-line-soft: #2c3e30; --ui-line-hi: #5a7c53;
  --ui-key: #d09a55;

  /* ...and its deck: brown boughs on forest green. */
  --c-bg1: #245231; --c-bg2: #163a22; --c-bg3: #0a2114;
  --c-frame: #b98146; --c-frame2: #6f4a25;
  --c-edge: #071a0f; --c-edge2: #041008;
  --c-ink: #e2f2c8; --c-cost: #ffd479; --c-glow: 214, 162, 94;
}
/* THE CURSED UNDEAD — violet-black ground, spectral rules and a purple key. */
body[data-faction="withering"],
#ab-decrees[data-faction="withering"] {
  --ui-bg: 19, 15, 30; --ui-bg-deep: 14, 11, 23; --ui-bg-up: 29, 23, 43;
  --ui-panel: #161124; --ui-card: #1b152b; --ui-raise: #251e3a;
  --ui-btn-lg: #2f2547; --ui-btn-lg-hi: #3d305c;
  --ui-line: #4a3a66; --ui-line-soft: #332949; --ui-line-hi: #614d85;
  --ui-key: #b878ee;

  /* ...and its deck: spectral purple on near-black. */
  --c-bg1: #201432; --c-bg2: #130c1e; --c-bg3: #07050c;
  --c-frame: #b878ee; --c-frame2: #6a3aa0;
  --c-edge: #060409; --c-edge2: #030205;
  --c-ink: #ead9ff; --c-cost: #ffd479; --c-glow: 184, 120, 238;
}
/* THE MOUNTAIN DWARFS — earth ground, dressed-stone rules and a stone key. */
body[data-faction="bastion"],
#ab-decrees[data-faction="bastion"] {
  --ui-bg: 29, 24, 18; --ui-bg-deep: 22, 18, 13; --ui-bg-up: 41, 34, 26;
  --ui-panel: #1e1811; --ui-card: #261f17; --ui-raise: #322a20;
  --ui-btn-lg: #423628; --ui-btn-lg-hi: #544632;
  --ui-line: #5a5044; --ui-line-soft: #3e372d; --ui-line-hi: #7c7264;
  --ui-key: #c3ccd6;

  /* ...and its deck: dressed grey stone on earth brown. */
  --c-bg1: #55391f; --c-bg2: #3a2612; --c-bg3: #20150a;
  --c-frame: #c3ccd6; --c-frame2: #79828d;
  --c-edge: #170e06; --c-edge2: #0c0803;
  /* ⚠️ the card glow is WARM where its frame is cold. It is every other
     house's own frame colour, but a grey halo on a grey card standing over grey
     mountain is no halo at all — seen in game, a castable dwarf card was
     indistinguishable from an unaffordable one. This is the one house whose
     "ready" has to be said in a colour its own border does not already use. */
  --c-ink: #f0e3c8; --c-cost: #ffd479; --c-glow: 244, 226, 178;
}

/* ---------- NOTHING OVER THE BATTLEFIELD IS TEXT YOU SELECT ----------
   ⚠️⚠️ Every overlay listed here sits over a map that is played with a finger,
   and the gestures that surface asks for — running along the card hand, dragging
   a card onto the ground, holding to grow a Rally Flag's muster ring — are the
   same press-and-slide a browser reads as "select this text". So a thumb across
   the hand came back with a blue highlight dragged over the names and prices.

   ⚠️ The hand's own rule (see #hand) is not enough on its own: `user-select`
   stops an element's text being TAKEN, and a drag that begins on some other
   overlay — a dock, a toast, the day box — and travels across the hand starts
   its selection there. Killing it for the whole in-game layer is what closes
   that off, and costs nothing: not one word of it is content anybody copies.

   ⚠️ SCOPED TO THE IN-GAME LAYER, deliberately not `body`. The pre-game screens
   (title screen, sign-in, the room browser, the waiting room, the Kingdom Designer)
   carry real text and real form fields, and a blanket rule would make every one
   of them unselectable to fix a problem none of them have.

   ⚠️⚠️ ...WITH ONE EXCEPTION, which is worth knowing before adding another: the
   Kingdom Designer's ⚜️ Decrees page carries a swipeable card carousel, so it
   has exactly the gesture this rule exists to protect and kills selection for
   itself (see .ab-decrees). The test is the GESTURE, not which screen it is on —
   any surface that asks for a press-and-slide needs this, wherever it lives.

   ⚠️ Every id here is one a JS module actually creates — world-labels,
   float-layer, entity-tip, unit-menu and buildbar-layer are all built in code
   rather than in index.html, and a selector naming the wrong one is dead
   silently. Check against the module before adding to this list. */
#topbar, #winner-banner, .dock, #eventfeed, #actionbar, #placement-hint,
#hand, #hand-arrow, #dawn-seq, #scoreboard, #world-labels,
#float-layer, #entity-tip, #unit-menu, #buildbar-layer {
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}

/* ---------- THE GOLD MARKER ----------
   Every price, payout and treasury figure in the game ends in a coin, and it
   used to be the 🪙 emoji — a picture drawn at whatever size, weight and hue the
   platform felt like, which beside the hand's own gilt read as a different
   object on every machine. It is one asset now (client/assets/coin.png, 87px,
   so it is still 3× sharp at the biggest place it lands — the end screen's
   24px award figures).

   ⚠️ IT IS A BACKGROUND ON AN EMPTY <i>, NOT AN <img>. Nearly every site that
   carries one is built as a template literal (a hand card's face, a HUD row, an
   upgrade row), so it has to be one short tag with no attributes to keep in
   step — and a background can never be dragged out of the page or show a broken
   -image glyph if the asset is missing.

   ⚠️ SIZED IN `em`, so it tracks whatever text it sits in with no per-site rule:
   9.5px on a phone's hand card up to 24px on the end screen, one declaration.
   `vertical-align` is what sits it on the text's baseline rather than hanging it
   off the line box; -.12em was measured against the digits either side of it.

   ⚠️ .95em RATHER THAN 1, and it is measured rather than picked. The asset is
   trimmed to the coin's own edges, where an emoji glyph sits INSIDE its em box
   with its own margin — so at a flat 1em this lands visibly chunkier than the
   marker it replaces and starts competing with the figure it is annotating
   (worst at the end screen's 24px). .95 puts its mass just under the digits',
   which keeps the NUMBER the thing being read.

   ⚠️ IT IS A FIXED-COLOUR IMAGE, which is the one thing it cannot do that the
   emoji could not either: `.poor` repaints a price RED and everything else on
   that card grey, and the coin stays gold through both. That is deliberate —
   a coin is a coin — but it means the red/grey distinction has to keep being
   carried by the FIGURE (see `.hc-cost`), never by the marker beside it. */
.coin {
  display: inline-block; width: .95em; height: .95em; vertical-align: -.12em;
  background: url('../assets/coin.png') center / contain no-repeat;
}

/* ---------- top bar ---------- */
/* --nag-h is the height of a fixed top banner (guest / verify nag) when one is
   showing, else 0 — the two nags set it between them (see guestnag.js). Every
   in-game HUD element pinned near the top offsets by it so the banner never
   covers the day timer, the docks, etc. */
#topbar {
  position: fixed; top: calc(10px + var(--nag-h, 0px)); left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 6px; z-index: 20; pointer-events: none;
}
#daybox {
  background: var(--ui-sheen), rgba(var(--ui-bg), .88);
  border: 1px solid var(--ui-line); border-radius: 20px;
  box-shadow: var(--ui-bevel), var(--ui-lift);
  padding: 6px 18px; font-size: 13px; white-space: nowrap;
}
#daybox b { color: #f0c04a; }

/* The day's world event: its own tab ATTACHED under the day box — the pill's own
   bottom border is the seam, so the two read as one object. (It used to be a
   third clause inside the pill with the detail on a hover tooltip, which touch
   can't reach at all.) Clicking opens #event-modal. The stack wrapper exists so
   the topbar's 6px gap falls between the day block and the battle bar rather
   than splitting the tab off the pill. */
#daystack { display: flex; flex-direction: column; align-items: center; }
#dayevent {
  pointer-events: auto; cursor: pointer;
  margin-top: -1px;                /* tuck up onto the pill's bottom border */
  display: flex; align-items: center; gap: 6px; max-width: 92vw;
  background: var(--ui-sheen), rgba(var(--ui-bg), .88);
  box-shadow: var(--ui-lift);
  border: 1px solid var(--ui-line); border-top: none;
  border-radius: 0 0 13px 13px;
  padding: 4px 15px 5px;
  font-size: 12px; line-height: 1.3; white-space: nowrap; color: #9fd0ff;
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
#dayevent:hover { background: rgba(26, 40, 56, .95); border-color: #5a7186; }
#dayevent .de-icon { font-size: 14px; line-height: 1; }
#dayevent .de-name { font-weight: 700; color: #cfe9ff; overflow: hidden; text-overflow: ellipsis; }
#dayevent .de-more { color: #6f8497; font-size: 11px; }
#dayevent:hover .de-more { color: #9fd0ff; }

/* ...and the event in full */
#event-backdrop { position: fixed; inset: 0; z-index: 48; background: rgba(5, 10, 18, .6); }
#event-modal {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 49;
  width: 460px; max-width: 92vw; text-align: center;
  background: var(--ui-panel); border: 1px solid rgba(150, 200, 245, .5); border-radius: 14px;
  padding: 26px 24px 18px;
  box-shadow: 0 18px 60px rgba(0, 0, 0, .6), 0 0 44px rgba(120, 175, 225, .18);
}
#event-modal .ev-close {
  position: absolute; top: 8px; right: 8px; background: none; border: none;
  color: #7f8fa0; font-size: 15px; line-height: 1; padding: 6px 8px; cursor: pointer;
}
#event-modal .ev-close:hover { background: none; color: #f4f7fb; }
#event-modal .ev-icon { font-size: 54px; line-height: 1; filter: drop-shadow(0 3px 10px rgba(0, 0, 0, .6)); }
#event-modal .ev-name {
  margin-top: 10px; font-size: 24px; font-weight: 800; letter-spacing: .4px; color: #dcefff;
  text-shadow: 0 0 20px rgba(140, 195, 240, .55);
}
#event-modal .ev-desc { margin: 10px 0 0; font-size: 14px; line-height: 1.6; color: #a8bed2; }
#event-modal .ev-foot {
  margin-top: 16px; padding-top: 12px; border-top: 1px solid #2a3746;
  font-size: 11.5px; color: #7f8fa0;
}
#battlebar {
  width: 440px; background: var(--ui-sheen), rgba(var(--ui-bg), .92); border: 1px solid #6c3a3a;
  box-shadow: var(--ui-bevel), var(--ui-lift);
  border-radius: 10px; padding: 6px 10px 9px; cursor: pointer;
  pointer-events: auto; /* topbar is click-through; re-enable clicks on the bar */
}
#battlebar:hover { border-color: #a85a5a; background: rgba(24, 32, 42, .95); }
#battlebar-title { font-size: 11px; text-align: center; color: #d0a0a0; margin-bottom: 4px; }
#battlebar-bar { display: flex; align-items: center; gap: 8px; }
#battlebar-bar .bhp {
  display: flex; flex: 1; height: 14px; border-radius: 7px; overflow: hidden;
}
#battlebar-bar .seg {
  display: flex; align-items: center; justify-content: center; min-width: 0;
  font-size: 10px; font-weight: 700; color: rgba(255,255,255,.92);
  text-shadow: 0 1px 2px rgba(0,0,0,.7); transition: width .5s; overflow: hidden;
}
#battlebar-bar .bcount {
  flex: none; width: 26px; height: 26px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700; color: #fff;
  text-shadow: 0 1px 2px rgba(0,0,0,.7);
  border: 2px solid rgba(255,255,255,.28); box-sizing: border-box;
}

#winner-banner {
  position: fixed; top: 90px; left: 50%; transform: translateX(-50%);
  background: rgba(30, 24, 6, .95); border: 2px solid #f0c04a; color: #f0c04a;
  border-radius: 12px; padding: 14px 30px; font-size: 20px; font-weight: 700; z-index: 30;
}

/* ---------- collapsible corner docks ---------- */
.dock { position: fixed; z-index: 20; display: flex; flex-direction: column; gap: 6px; }
.dock.tl { top: calc(8px + var(--nag-h, 0px)); left: 8px; align-items: flex-start; }
.dock.tr { top: calc(8px + var(--nag-h, 0px)); right: 8px; align-items: flex-end; }
.dock-tab {
  width: 42px; height: 42px; padding: 0; font-size: 19px; line-height: 1; flex: none;
  border-radius: 10px; background: var(--ui-sheen), rgba(var(--ui-bg), .92); border: 1px solid var(--ui-line);
  box-shadow: var(--ui-bevel), var(--ui-lift);
  display: flex; align-items: center; justify-content: center;
}
.dock-tab:hover { background: var(--ui-raise); }
.dock.open .dock-tab { border-color: var(--ui-key); background: var(--ui-raise); }
.dock-panel { display: none; max-width: 92vw; }
.dock.open .dock-panel { display: block; }

/* ⚠️ THE STATS PANEL IS PART OF THE SCREEN, NOT A DRAWER — AT EVERY WIDTH. Gold,
   income, muster and the army count are what a player reads between every
   decision, so they are simply always up and the tab that toggled them is gone
   from the markup: a button whose only job is to put back something that should
   never have left is a button only ever pressed by accident.
   ⚠️ A PHONE USED TO KEEP THAT TAB, because the panel was 60vw of a board that
   has to be played on — and what made it too tall was the breakdowns, all of
   them inlined under their rows at once. They open on a TAP now, one at a time
   (initHud in ui/hud.js), so the panel is the size of its own numbers and
   there is nothing left to collapse.
   ⚠️ Scoped with `>` so the Heroes roster nested inside this panel is untouched.
   dock.js still stamps `.open` on this dock at desktop width; it is inert, and
   ui/heroes.js measures whether the panel is DRAWN rather than reading it. */
#dock-stats > .dock-panel { display: block; }

/* Heroes bar: phones only. On desktop the roster stays inside the stats panel,
   so this dock is display:none there (the media block below reveals it, and
   .hidden still wins when the player has no Heroes yet).

   ⚠️ IT IS A BAR UNDER THE STATS PANEL, not a 42px 👑 square above it. As a
   square it was a crown sharing the top-left corner with the stats window with
   nothing to relate it to — and it had to sit ABOVE that window, which is why
   the panel carried a 48px top margin to get out of its way. Under it, at the
   column's own width, it reads as what it is: the lid on the list it opens.
   ⚠️ THE LABEL IS THE CROWN, not the word "Heroes". The bar is the width of the
   panel above it (see the phone block), and that panel's default size is now a
   92px square — a width the word does not fit in and never will. What carries
   the meaning instead is the position (it is the lid on the Heroes list), the
   count beside it, the button's own title, and the roster a press opens.
   ⚠️ Its `top` is set from ui/heroes.js (layoutPanel), because what it has to
   sit under is a panel whose height is three different things depending on
   which size the player has it in. */
#dock-heroes { display: none; left: 56px; }
#dock-heroes .dock-tab {
  position: relative; width: var(--roster-w, 156px); height: 26px; max-width: 92vw;
  justify-content: space-between; padding: 0 9px; gap: 6px;
  font-size: 12px; font-weight: 600; color: #c8d2dc; border-radius: 8px;
}
#dock-heroes .dock-tab.alert { border-color: #e0b64a; animation: hero-pulse 1s ease-in-out infinite; }
/* ⚠️ Three children under space-between, with the chevrons the same glyph and
   so the same width — which is what centres the label without a second layout
   rule to keep in step with it. */
#dock-heroes .dock-tab .h-mid { display: flex; align-items: center; gap: 5px; }
/* a point up on the bar's own 12px: the crown is the label now, and at the text
   size it replaced it read as a smudge rather than a crown. */
#dock-heroes .dock-tab .h-label { line-height: 1; font-size: 13px; }
/* ⚠️ THE FLIP IS A ROTATION, NOT A SWAPPED GLYPH — so nothing in JS has to
   remember to keep the two ends in step with the open state, and it animates.
   ▾ closed (press to reveal, below) → ▴ open (press to hide).
   ⚠️ Scoped through `.dock-tab`, NOT `#dock-heroes .h-chev`: the roster's own
   desktop collapse bar (#heroes-tab) has a `.h-chev` too and is a DESCENDANT of
   this dock while the box is in its phone home — it is display:none there, so
   the loose selector is harmless today and would stop being so the moment that
   bar was ever shown on a phone. */
#dock-heroes .dock-tab .h-chev { flex: none; font-size: 9px; color: #9fb0c0; line-height: 1; transition: transform .2s; }
#dock-heroes.open .dock-tab .h-chev { transform: rotate(180deg); }
/* the count, inline in the bar rather than a pip on a corner — there is a label
   to sit beside now. Collapsed by :empty, so a Hero-less bar is just "Heroes". */
#dock-heroes .dock-tab .h-badge {
  min-width: 16px; padding: 0 5px; border-radius: 8px;
  background: var(--ui-key); color: #0e1620;
  font-size: 10px; font-weight: 700; line-height: 15px; text-align: center;
}
#dock-heroes .dock-tab .h-badge:empty { display: none; }

/* Unread battles on the ⚙️ tab. The Battles button lives inside this menu now,
   so without a mark on the tab itself a new fight would announce itself to
   nobody until the player happened to open the cog. Same badge as the Heroes
   tab's, and empty text collapses it — updateReportsBadge (ui/panels.js) blanks
   rather than removes it. */
#dock-settings .dock-tab { position: relative; }
#dock-settings .dock-tab .cog-badge:not(:empty) {
  position: absolute; top: -4px; right: -4px; min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 8px; background: var(--ui-key); color: #0e1620;
  font-size: 10px; font-weight: 700; line-height: 16px; text-align: center;
}

/* ---------- Heroes roster (compact area docked under the stats panel) ---------- */
#heroes-box { margin-top: 6px; }
#heroes-box.hidden { display: none; }
#heroes-tab {
  width: 100%; text-align: left; padding: 7px 12px; cursor: pointer;
  font-size: 13px; font-weight: 700; color: #f0c04a;
  background: var(--ui-sheen), rgba(var(--ui-bg), .88);
  border: 1px solid var(--ui-line); border-radius: 10px;
  box-shadow: var(--ui-bevel), var(--ui-lift);
  display: flex; align-items: center; gap: 6px;
}
#heroes-tab:hover { border-color: var(--ui-key); }
#heroes-tab.alert { border-color: #e0b64a; animation: hero-pulse 1s ease-in-out infinite; }
#heroes-tab .h-count { color: #9fb0c0; font-weight: 600; font-size: 12px; }
#heroes-tab .h-chev { margin-left: auto; color: #9fb0c0; font-size: 10px; transition: transform .2s; }
#heroes-tab.collapsed .h-chev { transform: rotate(-90deg); }
@keyframes hero-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(240, 192, 74, .55); }
  50% { box-shadow: 0 0 0 6px rgba(240, 192, 74, 0); }
}
#heroes-panel {
  margin-top: 5px; max-height: 42vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: 5px;
}
#heroes-panel.collapsed { display: none; }
.hero-row {
  background: var(--ui-sheen), rgba(var(--ui-bg), .6);
  border: 1px solid var(--ui-line-soft); border-radius: 8px;
  box-shadow: var(--ui-bevel);
  padding: 6px 9px; cursor: pointer;
}
.hero-row:hover { border-color: var(--ui-key); background: var(--ui-card); }
.hero-row.fighting { border-color: #d0524a; box-shadow: inset 0 0 0 1px rgba(208, 82, 74, .5); }
.hero-top { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.hero-name { color: #ffd98a; font-weight: 600; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hero-lv { color: #9fb0c0; font-size: 11px; flex: none; }
.hero-bar { height: 6px; border-radius: 4px; background: #0f1922; overflow: hidden; margin: 4px 0 3px; }
.hero-bar i { display: block; height: 100%; transition: width .3s ease; }
.hero-hp { color: #8fa0b0; font-size: 10px; }
/* trait + retinue badges on a maxed Hero (see data/traits.js) */
.hero-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
.hero-trait, .hero-retinue {
  font-size: 10px; line-height: 1; padding: 3px 6px; border-radius: 999px;
  border: 1px solid var(--ui-line); background: rgba(24, 36, 50, .8); color: #b8c8d8; white-space: nowrap;
}
.hero-trait { border-color: #6b5330; background: rgba(64, 48, 24, .7); color: #ffd98a; }

/* ---------- THE TWO CORNER PANELS WEAR THE DECK'S BORDER ----------
   The stats window and the ⚙️ menu are the two panels a player READS while
   playing, so they are struck like a Royal Decree card: the same double rule in
   the house's own metal (--c-frame over --c-frame2), with the house's ornament
   in each corner. Same discipline as the Build disc — the colours come from the
   deck's custom properties on <body>, so a house repainted further up this file
   repaints these with nothing to keep in step.

   ⚠️ THE RULES ARE CSS AND THE ORNAMENT IS SVG, and the split is the whole
   trick: a panel is any size, so an SVG frame stretched to it would distort the
   ornament with it. The rules stretch (they are borders); the corners are fixed
   22px boxes pinned to the corners (ui/hand.js panelFrameSvg / dressPanel).

   ⚠️ ALL THREE LAYERS REGISTER OFF THE PADDING BOX. An absolutely positioned
   child's containing block is the padding box, so that is where the injected
   corners sit — and these two insets are measured from the same place, which is
   what makes the ornament's own rule (drawn at 3 units) land ON the gold line.
   The panel's own 1px border is then the outermost hairline, exactly as
   .hc-inner carries one outside the card's printed frame. */
#hud, #settings-menu { position: relative; border-color: var(--c-frame2); }
#hud::before, #settings-menu::before,
#hud::after, #settings-menu::after { content: ''; position: absolute; pointer-events: none; }
#hud::before, #settings-menu::before {
  inset: 3px; border: 1px solid var(--c-frame); opacity: .9; border-radius: 7px;
}
#hud::after, #settings-menu::after {
  inset: 6.6px; border: 1px solid var(--c-frame2); opacity: .8; border-radius: 4px;
}
/* ⚠️ The SPAN is pinned and the <svg> inside it keeps `.hc-frame` (absolute,
   inset 0) — so it fills the span and inherits every f-* colour rule from the
   card block below. Mirrored rather than rotated: a rotation would have to be
   about the panel's centre, which is not a known quantity here. */
.pf-corner { position: absolute; width: 22px; height: 22px; pointer-events: none; }
.pf-tl { top: 0; left: 0; }
.pf-tr { top: 0; right: 0; transform: scaleX(-1); }
.pf-bl { bottom: 0; left: 0; transform: scaleY(-1); }
.pf-br { bottom: 0; right: 0; transform: scale(-1, -1); }

/* ---------- HUD ---------- */
#hud {
  background: var(--ui-sheen), rgba(var(--ui-bg), .88);
  border: 1px solid var(--ui-line); border-radius: 10px;
  box-shadow: var(--ui-bevel), var(--ui-lift);
  /* ⚠️ THE PADDING IS THE CORNER ORNAMENT'S ROOM, not slack (it was 10px 14px
     before the frame went on). The ornament reaches ~20px into the panel, and
     at the old figure the house's rays were drawn straight through the faction
     name and the gold row. */
  padding: 15px 17px; font-size: 13px; min-width: 190px;
}
#hud .row { display: flex; justify-content: space-between; gap: 14px; padding: 1.5px 0; }
/* ⚠️ `:not(.mini)` because A MINI ROW HAS NO LABEL IN IT — it is icon + figure
   (see ui/hud.js), so this colour has nothing there to paint. It is scoped
   rather than left to sort itself out because the selector is (1,2,1) against
   .score/.gold/.inc at (1,1,0): the moment a mini row's first child IS the
   figure — an icon dropped, a fourth stat with no glyph — this out-specifies
   all three and silently paints the one thing telling those figures apart back
   to label grey. (Measured: that is exactly what it did before the icons went
   in.) `#hud` never carries `.mini` off a phone, so nothing else moves. */
#hud:not(.mini) .row span:first-child { color: #9fb0c0; }
#hud .gold { color: #f0c04a; font-weight: 700; }
/* the points score: the headline number of a match, so it reads brighter than
   an ordinary stat row without competing with gold's yellow */
#hud .score { color: #8fd2ff; font-weight: 700; }
#hud .inc { color: #7fc86c; }
#hud .row.help { cursor: help; }
#hud .row.help:hover span:first-child { color: #f0c04a; }
#hud .row.sub { margin-top: -3px; }
#hud .row.sub span { color: #7f8ea0 !important; font-size: 11px; }

/* HUD breakdowns (a child element rather than a `title`, so they survive the
   600ms innerHTML refresh). Opened by HOVER on a mouse and by a TAP on a touch
   screen — see the phone block far below, and initHud in ui/hud.js for the
   `openTip` key the tap flow remembers across those refreshes. */
#hud .hastip { position: relative; cursor: help; }
#hud .hastip:hover > span:first-child,
#hud .hastip.tip-open > span:first-child { color: #f0c04a; }
#hud .hud-tip {
  display: none; position: absolute; left: calc(100% + 12px); top: -8px;
  width: 230px; background: rgba(var(--ui-bg-deep), .98); border: 1px solid var(--ui-line-hi);
  border-radius: 8px; padding: 9px 11px; z-index: 60;
  box-shadow: 0 8px 24px rgba(0,0,0,.6); white-space: normal; cursor: default;
}
#hud .hastip:hover .hud-tip, #hud .hastip.tip-open .hud-tip { display: block; }
#hud .hud-tip .ht-h { color: #f0c04a; font-weight: 700; font-size: 12px; margin-bottom: 5px; }
#hud .hud-tip .ht-line { display: flex; justify-content: space-between; gap: 14px; font-size: 12px; padding: 1px 0; color: #c8d2dc; }
#hud .hud-tip .ht-line b { color: #7fc86c; font-weight: 600; }
/* the rank a raised spawner musters at, beside the unit name in the muster list */
#hud .hud-tip .ht-total { display: flex; justify-content: space-between; gap: 14px; font-size: 12px; margin-top: 5px; padding-top: 5px; border-top: 1px solid var(--ui-line-soft); font-weight: 700; }
#hud .hud-tip .ht-total b { color: #f0c04a; }
#hud .hud-tip .ht-foot { color: #7f8ea0; font-size: 10px; margin-top: 6px; line-height: 1.4; }

/* ---------- Settings menu (top-right cog) ---------- */
#settings-menu {
  background: var(--ui-sheen), rgba(var(--ui-bg), .95);
  border: 1px solid var(--ui-line); border-radius: 10px;
  box-shadow: var(--ui-bevel), var(--ui-lift-lg);
  /* as with #hud, the padding is the corner ornament's room — was 12px 14px */
  padding: 16px 17px; font-size: 13px; min-width: 210px;
}
#settings-menu .settings-h { font-weight: 700; font-size: 13px; color: #f0c04a; margin-bottom: 10px; }
#settings-menu .settings-sec { border-top: 1px solid var(--ui-line-soft); padding-top: 10px; margin-bottom: 12px; }
#settings-menu .settings-label { color: #c8d2dc; font-weight: 600; margin-bottom: 8px; }
#settings-menu .settings-note { color: #7f8ea0; font-size: 11px; margin-top: 6px; }
/* the neutral sibling of .settings-exit: same shape, but it opens a window
   rather than ending a game, so it must not wear the danger red */
#settings-menu .settings-btn {
  width: 100%; padding: 8px 10px; font-size: 13px; font-weight: 600; text-align: center;
  border-radius: 8px; border: 1px solid var(--ui-line);
  background: var(--ui-sheen), var(--ui-card); box-shadow: var(--ui-bevel);
  color: #c8d2dc; cursor: pointer;
}
#settings-menu .settings-btn:hover { background: var(--ui-raise); border-color: #4d637c; color: #e8e2d0; }
/* the menu's vertical rhythm comes ENTIRELY from adjacent-sibling rules, so
   every pairing that can occur needs one — btn+btn is Settings followed by
   Pause, and without it the two collapse into a single slab. */
#settings-menu .settings-btn + .settings-btn { margin-top: 7px; }
#settings-menu .settings-btn + .settings-exit { margin-top: 7px; }
#settings-menu .settings-exit {
  width: 100%; padding: 8px 10px; font-size: 13px; font-weight: 600;
  border-radius: 8px; border: 1px solid #6b3030; background: #3a1c1c; color: #f0b0a0; cursor: pointer;
}
#settings-menu .settings-exit:hover { background: #4d2323; border-color: #d0524a; color: #ffd0c4; }
#settings-menu .settings-exit + .settings-exit { margin-top: 7px; }

/* ---------- Settings window (the ⚙️ Settings button in that menu) ---------- */
/* EVERY MODAL OPENED DURING A GAME wears the house too — the Settings window,
   the exit dialog, the pause ask and its vote card, the world-event window.
   Scoping it on `body[data-faction]` rather than naming them one at a time is
   what makes that true of a dialog added later, and it leaves the lobby's own
   modals (no faction is set out there) on the neutral `.modal` above. */
body[data-faction] .modal {
  background: var(--ui-sheen), var(--ui-panel);
  border-color: var(--ui-line);
  box-shadow: var(--ui-bevel), 0 16px 46px rgba(0, 0, 0, .66);
}
.settings-modal { width: 460px; max-height: 82vh; display: flex; flex-direction: column; padding: 22px 24px; }
.set-tabs { display: flex; gap: 6px; border-bottom: 1px solid var(--ui-line-soft); margin-top: 4px; flex: none; }
.set-tab {
  padding: 8px 14px; font-size: 13px; font-weight: 600; cursor: pointer;
  background: none; border: none; border-bottom: 2px solid transparent;
  border-radius: 8px 8px 0 0; color: #9fb0c0;
}
.set-tab:hover { background: var(--ui-card); color: #e8e2d0; }
.set-tab.active { background: var(--ui-card); border-bottom-color: var(--ui-key); color: #f0c04a; }
.set-body { overflow-y: auto; overscroll-behavior: contain; min-height: 0; padding-top: 18px; }
.set-pane { display: none; }
.set-pane.active { display: block; }
#settings-page .settings-row {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  font-size: 13px; color: #c8d2dc;
}
#settings-page .settings-row + .settings-row { margin-top: 15px; }
#settings-page .settings-row input[type=range] { flex: 1; max-width: 220px; accent-color: var(--ui-key); }
/* the Graphics tab's dropdowns. Sized to the audio sliders' track so the two
   panes line up when you tab between them. */
.set-select {
  flex: 1; max-width: 220px; padding: 6px 8px; font: inherit; font-size: 13px;
  color: #e8e2d0; background: var(--ui-card); cursor: pointer;
  border: 1px solid var(--ui-line); border-radius: 6px;
}
.set-select:focus { outline: none; border-color: var(--ui-key); }
.set-note { margin: 18px 0 0; font-size: 12px; line-height: 1.5; color: #8b98a6; }

/* transient event toasts that scroll up through the middle of the screen */
#eventfeed {
  position: fixed; top: 28%; left: 50%; transform: translateX(-50%); z-index: 30;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  width: max-content; max-width: 70vw; pointer-events: none;
}
#eventfeed .ev-toast {
  background: var(--ui-sheen), rgba(var(--ui-bg), .88);
  border: 1px solid var(--ui-line); border-radius: 8px;
  box-shadow: var(--ui-bevel), var(--ui-lift);
  padding: 7px 15px; font-size: 14px; color: #e8e2d0; text-align: center;
  box-shadow: 0 3px 14px rgba(0, 0, 0, .45); white-space: nowrap;
  animation: ev-toast 6.5s ease-out forwards;
}
@keyframes ev-toast {
  0%   { opacity: 0; transform: translateY(24px) scale(.96); }
  8%   { opacity: 1; transform: translateY(0) scale(1); }
  75%  { opacity: 1; transform: translateY(-28px); }
  100% { opacity: 0; transform: translateY(-52px); }
}

/* invite code, sharable at any time */
#roomcode {
  width: 200px; box-sizing: border-box; text-align: center; cursor: pointer; margin-bottom: 6px;
  background: var(--ui-panel); border: 1px solid var(--ui-line); border-radius: 8px;
  padding: 6px 8px; font-size: 12px; color: #9fb0c0; user-select: none;
}
#roomcode:hover { background: var(--ui-raise); }
#roomcode b { color: #e8e2d0; font-family: monospace; font-size: 15px; letter-spacing: 3px; }

/* Build sits in the bottom-RIGHT corner as ONE large disc. The bottom CENTRE is
   the card hand (ui/hand.js) — where the Royal Decrees, the Rally Flag AND every
   buildable thing are committed from — so the two surfaces never contend for the
   same strip of screen; the ⚜️ button that used to live here is gone with the
   window it opened, and Battles moved into the ⚙️ cog menu. What is left is the
   one thing you DO on the map, so it is sized like it: the disc treatment used
   to be a phone-only compromise for two cramped buttons and is the desktop look
   now too.

   IT IS A SWITCH, NOT A DOOR. It stopped opening a build window when the build
   menu became a deck in the hand; pressing it swaps which deck is dealt, and it
   wears the icon of the deck it would give you — a keep while the decrees are in
   your hand, a crown while the buildings are. Both icons are in the markup and
   `.showing-build` (written by ui/hand.js, and only by it) picks between them,
   so the button can never be showing one thing while the hand holds the other. */
#actionbar {
  position: fixed; bottom: 14px; right: 14px; z-index: 20;
  display: flex; gap: 8px;
}
#actionbar button {
  position: relative;
  width: 128px; height: 128px; padding: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: radial-gradient(120% 120% at 34% 22%, rgba(255, 255, 255, .16), rgba(255, 255, 255, 0) 58%), var(--ui-btn-lg);
  /* the card's own outermost edge: 1px of the shadow metal, exactly what
     .hc-inner carries under its printed frame — see the ring below */
  border-color: var(--c-frame2); color: #e8e2d0;
  /* ⚠️ NO CAST SHADOW. Every other large surface here lifts off the map with
     `--ui-lift`, and this one deliberately does not: the disc sits at the
     bottom-right corner over open ground, so a solid step plus a wide blur under
     it read as a smudge on the grass rather than as the button standing proud —
     the ring and the dome's own lighting are what give it its depth. Only the
     two INSETS are left: the lit top lip and the shading down its lower face. */
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, .16), inset 0 -6px 12px rgba(0, 0, 0, .45);
  transition: background 140ms ease, transform 140ms ease;
}
#actionbar button:hover { background: var(--ui-btn-lg-hi); transform: translateY(-2px); }
#actionbar button:active { transform: translateY(0); }
/* THE DISC WEARS THE DECK'S BORDER (ui/hand.js roundFrameSvg): the same double
   rule struck in the same house metal as a Royal Decree card, with the house's
   own ornament round it. It carries `.hc-frame` as well, so every colour,
   weight and opacity comes from the card rules further down this file rather
   than from a second copy here — there is nothing to keep in step. */
#actionbar button .ab-frame { border-radius: 50%; }
/* ⚠️ THE ICONS HAVE TO BE POSITIONED TOO. The ring is absolutely positioned and
   written first, and a positioned element paints above static in-flow content
   whatever the DOM order — so without this the frame's stacking layer sits over
   the icon rather than behind it. (They do not overlap today; this is what stops
   a larger icon or a fatter stud quietly hiding one.) */
#actionbar button .ab-ico { position: relative; }
/* the rules come up to full strength under the pointer, the way a hovered card's
   border re-lights */
#actionbar button:hover .ab-frame .f-rule,
#actionbar button:hover .ab-frame .f-thin { opacity: 1; }
/* the icon is an inline SVG, so it takes the button's own colour and stays
   crisp at this size — see the markup note in index.html. Two of them ride the
   button and exactly one is ever drawn. */
#actionbar button .ab-ico { display: flex; line-height: 0; }
#actionbar button .ab-ico-crown,
#actionbar button.showing-build .ab-ico-build { display: none; }
#actionbar button.showing-build .ab-ico-crown { display: flex; }

/* ---------- ↻ rotate (touch only) ---------- */
/* The R key's counterpart on a phone — see the markup note in index.html for why
   it exists and why it is on the left. Deliberately NOT styled as a second Build
   disc: it is a transient control that appears mid-placement, so it is small,
   flat and plainly a button rather than a second permanent fixture competing
   with the one in the other corner.
   ⚠️ `display: none` OUTSIDE a coarse-pointer screen, so a mouse can never see
   it however the frame loop's `.hidden` happens to be set — R is the answer
   there and a duplicate control is one more thing over the map.
   ⚠️⚠️ ABOVE THE HAND (z-index 27 vs 26), which is the Build disc's own lesson
   from the other corner: it stands clear of the RESTING cards, but an ARMED card
   lifts `--pop + 26` out of the row — and this button is only ever on screen
   while something IS armed, so a card near the fan's left-hand end would be drawn
   straight over it. The one control that turns the thing you are placing must
   win its own 62px whatever is standing over it. */
#rotatebar { display: none; }
@media (pointer: coarse) {
  #rotatebar {
    display: block;
    position: fixed; z-index: 27;
    left: max(12px, env(safe-area-inset-left));
    bottom: calc(var(--hand-peek) + 30px + env(safe-area-inset-bottom));
  }
  #rotatebar.hidden { display: none; }
  #rotatebar button {
    width: 62px; height: 62px; padding: 0; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--ui-sheen), rgba(var(--ui-bg-up), .95);
    border: 1px solid var(--ui-line-hi); color: #e8e2d0;
    box-shadow: var(--ui-bevel), var(--ui-lift);
  }
  #rotatebar button:active { transform: translateY(1px); }
}

/* clear of the resting hand; ui/hand.js steps it higher (body.hand-up) while the
   hand is raised or a card is aiming */
/* ⚠️ `bottom` has to clear the RESTING hand, which is centred on the same axis
   and drawn above this (z-index 26) — and the hand shows its cards WHOLE now, so
   that is a full card tall (~160px) rather than the 84px title-peek this used to
   be set against. It steps higher still while the hand is raised — see
   body.hand-up further down. */
#placement-hint {
  position: fixed; bottom: 176px; left: 50%; transform: translateX(-50%); z-index: 25;
  transition: bottom 200ms ease;
  background: rgba(30, 24, 6, .95); border: 1px solid #f0c04a; border-radius: 8px;
  padding: 8px 16px; font-size: 13px; color: #f0c04a;
  cursor: pointer; max-width: 94vw;
}
/* tapping the pill cancels placement (wired in initPanels) — show a ✕ so it reads as a button */
#placement-hint::after { content: '✕'; margin-left: 10px; color: #ffe9b0; font-weight: 700; }
/* (There is no #place-confirm pill here any more. A tap on the map places or
   casts where it lands on every pointer — see the note above rotatePlacement in
   client/js/ui/panels.js for what the centre-lock ✓ flow was and why it went.) */
#entity-tip {
  position: fixed; z-index: 45; pointer-events: none;
  background: rgba(var(--ui-bg), .96); border: 1px solid var(--ui-line-hi); border-radius: 8px;
  padding: 9px 12px; max-width: 260px; font-size: 12px; line-height: 1.45;
  box-shadow: 0 6px 20px rgba(0,0,0,.5);
}
#entity-tip .et-title { font-weight: 700; font-size: 13px; margin-bottom: 2px; }
#entity-tip .et-sub { color: #f0c04a; font-size: 11px; margin-bottom: 4px; }
#entity-tip .et-own { font-size: 11px; font-weight: 600; margin-bottom: 3px; }
#entity-tip .et-body { color: #c8d2dc; }
#entity-tip .et-hp { color: #9fb0c0; }
#entity-tip .et-warn { color: #ff9a6e; font-weight: 600; }
#entity-tip b { color: #7fc86c; }
#entity-tip .et-stat { display: flex; justify-content: space-between; gap: 18px; padding: 1px 0; }
#entity-tip .et-stat span { color: #9fb0c0; }
#entity-tip .et-stat b { color: #e8e2d0; font-weight: 600; }
#entity-tip .et-dim { color: #7f8ea0; font-weight: 400; font-size: 10px; }
#entity-tip .et-foot { color: #7f8ea0; font-size: 10px; margin-top: 5px; line-height: 1.35; }
#entity-tip .et-up { color: #7fc86c; font-size: 10px; }
/* ...and its opposite number. A live decree can pull a figure DOWN (a chill, a
   Cave-in's broken ground, Entangle holding a soldier at a dead stop), so the
   card marks both directions — see the `up()` helper in ui/entitytip.js.
   Deliberately RED and not a house colour: this is the danger/loss semantic the
   HUD keeps out of the faction palette on purpose. */
#entity-tip .et-down { color: #e07a6a; font-size: 10px; }
/* the "in effect now" rows: a name and, under it, the effect in its own words.
   Two lines rather than an .et-stat flex row — these are sentences, and a flex
   row squashes the name to nothing in a 260px box. Green whatever the sign,
   because the row says WHAT is happening and the ▲/▼ on the stat above it
   carries whether that is good news. */
#entity-tip .et-fx { padding: 1px 0 2px; }
#entity-tip .et-fx-name { display: block; color: #9fe08a; font-weight: 600; font-size: 11px; }
#entity-tip .et-fx-note { display: block; color: #8fa0b0; font-size: 10px; line-height: 1.3; }

/* floating combat numbers */
/* construction progress bars floating above sites under build */
#buildbar-layer { position: fixed; inset: 0; pointer-events: none; z-index: 14; overflow: hidden; }
.buildbar {
  position: absolute; transform: translate(-50%, -50%);
  will-change: left, top;
}
.bb-track {
  width: 46px; height: 6px; border-radius: 4px; overflow: hidden;
  background: rgba(8, 14, 20, .8); box-shadow: 0 0 0 1px rgba(0,0,0,.55), 0 1px 3px rgba(0,0,0,.6);
}
.bb-track i { display: block; height: 100%; border-radius: 4px; transition: width .25s ease, background .25s ease; }
/* A Building Buster's RECHARGE bar (ui/buildbar.js). ⚠️ No width transition: a gun
   firing empties its bar in one frame, and easing that over a quarter of a
   second reads as the bar lagging behind the shot rather than causing it. The
   colour still eases, which is what makes "loaded" arrive as a glow rather than
   as a flick. */
.bb-track i.bb-snap { transition: background .2s ease; }

#float-layer { position: fixed; inset: 0; pointer-events: none; z-index: 15; overflow: hidden; }
.dmgnum {
  position: absolute; font-weight: 800; font-size: 13px;
  text-shadow: 0 1px 3px rgba(0,0,0,.9), 0 0 6px rgba(0,0,0,.5);
  animation: dmgfloat 0.85s ease-out forwards;
  transform: translate(-50%, 0);
  will-change: transform, opacity;
}
.dmgnum.dmg { color: #ffd27a; }
.dmgnum.heal { color: #7fe28c; }
.dmgnum.poison { color: #9bd94a; }
/* gold picked up off the ground (a Coin Goblins purse). Brighter and warmer than
   .dmg, which is close enough in hue to be mistaken for one at a glance — and it
   carries a 🪙 in the text besides, since the two mean opposite things. */
.dmgnum.gold { color: #ffd257; text-shadow: 0 1px 3px rgba(0,0,0,.9), 0 0 10px rgba(255,200,60,.55); }
@keyframes dmgfloat {
  0%   { transform: translate(-50%, 0) scale(1.15); opacity: 1; }
  20%  { transform: translate(-50%, -9px) scale(1); }
  100% { transform: translate(-50%, -44px) scale(0.85); opacity: 0; }
}

/* battle cry: red, bold, shakes like it's being shouted, gone after 2s */
.shout {
  position: absolute; font-weight: 900; font-size: 16px; color: #ff5a3c;
  text-shadow: 0 1px 3px rgba(0,0,0,.95), 0 0 8px rgba(255,60,30,.6);
  transform: translate(-50%, 0);
  animation: shout-shake 0.12s linear infinite, shout-fade 2s ease-out forwards;
  will-change: transform, opacity;
}
@keyframes shout-shake {
  0%   { transform: translate(-50%, 0) rotate(-4deg); }
  25%  { transform: translate(-52%, -1px) rotate(3deg); }
  50%  { transform: translate(-50%, 1px) rotate(-3deg); }
  75%  { transform: translate(-48%, -1px) rotate(4deg); }
  100% { transform: translate(-50%, 0) rotate(-4deg); }
}
@keyframes shout-fade {
  0%   { opacity: 0; }
  10%  { opacity: 1; }
  80%  { opacity: 1; }
  100% { opacity: 0; }
}

/* --- daybreak: light washing in from the screen edges. The glow is a vignette
   (radial gradient + inset shadows) that breathes like shimmering light; peace
   days swap the gold for a soft dawn white, the world event for a cold slate,
   and a Royal Decree cast for the caster's own colour.

   The banner beneath is now only the decree-cast flare — the dawn's own words
   belong to the ceremony in ui/dawn.js, which borrows just the glow (washGlow)
   and sets its length per beat via --db-dur. --- */
#daybreak-glow {
  position: fixed; inset: 0; pointer-events: none; z-index: 44; opacity: 0;
  background: radial-gradient(ellipse at 50% 50%,
    rgba(255, 190, 80, 0) 55%, rgba(255, 178, 60, .18) 78%, rgba(255, 214, 120, .5) 100%);
  box-shadow: inset 0 0 18vmin 2vmin rgba(255, 196, 84, .8),
              inset 0 0 5vmin 0 rgba(255, 236, 170, .6);
  will-change: opacity;
}
#daybreak-glow.peace {
  background: radial-gradient(ellipse at 50% 50%,
    rgba(255, 232, 185, 0) 55%, rgba(255, 230, 180, .16) 78%, rgba(255, 245, 210, .45) 100%);
  box-shadow: inset 0 0 18vmin 2vmin rgba(255, 236, 180, .75),
              inset 0 0 5vmin 0 rgba(255, 250, 228, .55);
}
/* a Royal Decree flare: same wash of light, tinted the casting player's colour
   (--db-c, set inline by ui/daybreak.js) */
#daybreak-glow.decree {
  background: radial-gradient(ellipse at 50% 50%,
    transparent 55%,
    color-mix(in srgb, var(--db-c) 20%, transparent) 78%,
    color-mix(in srgb, var(--db-c) 55%, transparent) 100%);
  box-shadow: inset 0 0 18vmin 2vmin color-mix(in srgb, var(--db-c) 80%, transparent),
              inset 0 0 5vmin 0 color-mix(in srgb, color-mix(in srgb, var(--db-c) 55%, #fff) 60%, transparent);
}
/* the day's world event: a cold slate wash, deliberately unlike the warm dawn
   and unlike any faction colour — this one is the world, not a player */
#daybreak-glow.event {
  background: radial-gradient(ellipse at 50% 50%,
    transparent 55%, rgba(120, 170, 220, .18) 78%, rgba(150, 200, 245, .5) 100%);
  box-shadow: inset 0 0 18vmin 2vmin rgba(120, 175, 225, .7),
              inset 0 0 5vmin 0 rgba(200, 230, 255, .5);
}
/* the Dragon falling (ui/dragonfall.js): a hot ember red, the beast's own
   colour rather than a faction's — the whole map is being told about this, so it
   must not read as one player's flare */
#daybreak-glow.dragon {
  background: radial-gradient(ellipse at 50% 50%,
    transparent 50%, rgba(255, 122, 46, .22) 76%, rgba(255, 90, 30, .58) 100%);
  box-shadow: inset 0 0 20vmin 2vmin rgba(224, 74, 24, .82),
              inset 0 0 6vmin 0 rgba(255, 196, 106, .55);
}
/* the Angel of Judgement (server/data/angel.js): a hard white-gold, the
   brightest wash in the game by a distance — it is used exactly twice in a
   match, when she comes down on the last morning and when she ends it, and both
   times the whole table is being told at once. Deliberately paler and harsher
   than .decree's faction-tinted gold, which it would otherwise be mistaken for. */
#daybreak-glow.angel {
  background: radial-gradient(ellipse at 50% 50%,
    transparent 44%, rgba(255, 236, 170, .3) 72%, rgba(255, 250, 225, .74) 100%);
  box-shadow: inset 0 0 26vmin 3vmin rgba(255, 214, 110, .9),
              inset 0 0 8vmin 0 rgba(255, 253, 240, .72);
}
#daybreak-glow.show { animation: daybreak-glow var(--db-dur, 4.6s) ease-in-out forwards; }
@keyframes daybreak-glow {
  0%   { opacity: 0; }
  14%  { opacity: 1; }
  36%  { opacity: .72; }
  58%  { opacity: .95; }
  100% { opacity: 0; }
}

#daybreak-banner {
  position: fixed; top: 13%; left: 50%; transform: translateX(-50%);
  z-index: 44; pointer-events: none; opacity: 0; text-align: center;
}
#daybreak-banner .db-title {
  font-size: 32px; font-weight: 800; letter-spacing: 1px; color: #ffe9b0;
  text-shadow: 0 0 22px rgba(255, 180, 60, .95), 0 0 6px rgba(255, 220, 140, .8), 0 2px 6px rgba(0, 0, 0, .55);
}
#daybreak-banner .db-sub {
  margin-top: 4px; font-size: 14px; font-weight: 600; color: #ffdf9a;
  text-shadow: 0 0 10px rgba(255, 180, 60, .7), 0 1px 4px rgba(0, 0, 0, .6);
}
#daybreak-banner.peace .db-title { color: #fff6dc; text-shadow: 0 0 22px rgba(255, 228, 165, .9), 0 0 6px rgba(255, 244, 205, .8), 0 2px 6px rgba(0, 0, 0, .55); }
#daybreak-banner.peace .db-sub { color: #ffedc4; text-shadow: 0 0 10px rgba(255, 225, 160, .7), 0 1px 4px rgba(0, 0, 0, .6); }
#daybreak-banner.decree .db-title {
  color: color-mix(in srgb, var(--db-c) 45%, #fff);
  text-shadow: 0 0 22px var(--db-c), 0 0 6px color-mix(in srgb, var(--db-c) 60%, #fff), 0 2px 6px rgba(0, 0, 0, .55);
}
#daybreak-banner.decree .db-sub {
  color: color-mix(in srgb, var(--db-c) 30%, #fff);
  text-shadow: 0 0 10px color-mix(in srgb, var(--db-c) 70%, transparent), 0 1px 4px rgba(0, 0, 0, .6);
}
#daybreak-banner.show { animation: daybreak-banner 4.6s ease-out forwards; }
@keyframes daybreak-banner {
  0%   { opacity: 0; transform: translateX(-50%) translateY(14px) scale(.96); }
  12%  { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
  72%  { opacity: 1; }
  100% { opacity: 0; transform: translateX(-50%) translateY(-10px) scale(1.02); }
}

#toast {
  /* above the pre-game screens (48) and modals (50): it carries global status
     like "connection lost" that must stay visible everywhere */
  position: fixed; bottom: 110px; left: 50%; transform: translateX(-50%); z-index: 55;
  background: rgba(60, 18, 12, .95); border: 1px solid #c0392b; border-radius: 8px;
  padding: 8px 16px; font-size: 13px;
}
#toast.good { background: rgba(14, 40, 16, .95); border-color: #4a9a44; }

/* ---------- F1 screenshot mode: hide every overlay, leave the battlefield ----------
   Every in-game widget (docks, action bar, panels, and the DOM overlay layers the
   worldlabels/floaters/buildbar modules append) is a direct child of <body>, so one
   rule covers them all. Deliberate exceptions: the canvas itself, the pre-game
   .screen / .modal-backdrop pages (F1 only toggles in-game, but this keeps a screen
   opened while hidden — exit to lobby — from vanishing), and #toast, which carries
   the "press F1 to bring it back" hint and global status like "connection lost". */
body.ui-hidden > *:not(#game):not(.screen):not(.modal-backdrop):not(#toast) { display: none !important; }

/* ---------- panels ---------- */
/* clear of the resting card hand along the bottom edge (ui/hand.js) */
.panel {
  position: fixed; bottom: 72px; left: 50%; transform: translateX(-50%); z-index: 30;
  background: var(--ui-sheen), var(--ui-panel);
  border: 1px solid var(--ui-line); border-radius: 12px;
  padding: 16px; width: 500px; max-width: 94vw; max-height: 70vh; overflow-y: auto;
  box-shadow: 0 12px 40px rgba(0,0,0,.6);
}
.panel.wide { width: 860px; }
.panel h2 { font-size: 16px; margin-bottom: 12px; display: flex; justify-content: space-between; }
.panel .panel-sub { font-size: 12px; color: #9fb0c0; margin: -8px 0 12px; }
.panel .close { cursor: pointer; color: #9fb0c0; font-size: 14px; }
.panel .close:hover { color: #fff; }
.help-body p { font-size: 13px; color: #c8d2dc; margin-bottom: 10px; line-height: 1.45; }

/* (There is no build-menu block here any more. Every buildable thing is a
   CARD IN THE HAND along the bottom edge — see THE CARD HAND further down,
   and .hc-model there for the tile of geometry a building card wears in
   place of a decree's emoji. The window it replaced was `#panel-build`, a
   grid of model buttons fitted to its own height; the disc in the corner is
   a switch between the two decks now rather than a way of opening it.) */

/* ---------- battle reports ---------- */
.report-list { display: flex; flex-direction: column; gap: 8px; }
.report-row {
  display: flex; align-items: center; gap: 10px; padding: 9px 11px; cursor: pointer;
  border: 1px solid var(--ui-line-soft); border-radius: 8px;
  background: var(--ui-sheen), var(--ui-card); box-shadow: var(--ui-bevel);
}
.report-row:hover { background: var(--ui-raise); border-color: #4d76a0; }
.report-row .rr-icon { font-size: 20px; flex: none; }
.report-row .rr-text { flex: 1; min-width: 0; }
.report-row .rr-open { color: #9fb0c0; font-size: 14px; flex: none; }
.rc-title { font-size: 13px; font-weight: 600; color: #e8e2d0; }
.rc-meta { font-size: 11px; color: #9fb0c0; margin-top: 2px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.rc-meta .dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; flex: none; }
.rc-locate {
  padding: 2px 8px; font-size: 10px; border-radius: 6px; cursor: pointer;
  background: #23384c; border: 1px solid #35506a; color: #cfe0ef;
}
.rc-locate:hover { background: #2e4a63; border-color: #4d76a0; }
.rc-badge { font-size: 10px; color: #cdd7e0; }

/* ---------- pop-out battle report window ---------- */
#report-backdrop { position: fixed; inset: 0; z-index: 46; background: rgba(5, 10, 18, .6); }
#report-modal {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 47;
  width: 760px; max-width: 96vw; max-height: 88vh; overflow-y: auto;
  background: var(--ui-panel); border: 1px solid var(--ui-line); border-radius: 12px;
  padding: 16px 18px; box-shadow: 0 18px 60px rgba(0, 0, 0, .6);
}
.brm-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; margin-bottom: 12px; }
.brm-title { font-size: 17px; font-weight: 700; color: #e8e2d0; line-height: 1.3; }
.brm-head .close { cursor: pointer; color: #9fb0c0; font-size: 15px; padding: 2px 4px; }
.brm-head .close:hover { color: #fff; }

/* victory / defeat banner cards */
.brm-sides { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-bottom: 14px; }
.brm-side { border: 1px solid var(--ui-line-soft); border-radius: 10px; padding: 10px 12px; text-align: center; }
.brm-side.won { border-color: #4a9a44; background: linear-gradient(180deg, rgba(50, 110, 45, .3), rgba(40, 90, 40, .1)); }
.brm-side.lost { border-color: #7a3a34; background: linear-gradient(180deg, rgba(110, 45, 40, .28), rgba(90, 40, 40, .08)); }
.brm-result { font-size: 14px; font-weight: 800; letter-spacing: 1px; margin-bottom: 4px; }
.brm-side.won .brm-result { color: #a8e8a0; }
.brm-side.lost .brm-result { color: #f0a89e; }
.brm-side-name { display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
.brm-side-name .dot { width: 11px; height: 11px; border-radius: 50%; flex: none; }
.brm-side-stats { display: flex; flex-wrap: wrap; justify-content: center; gap: 3px 12px; font-size: 11px; color: #c8d2dc; }
.brm-side-stats b { color: #e8e2d0; }

/* comparison graphs */
.brm-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
.brm-chart { border: 1px solid var(--ui-line-soft); border-radius: 8px; background: var(--ui-card); padding: 9px 11px; }
.brm-chart h3 { font-size: 12px; color: #f0c04a; margin-bottom: 7px; }
.brm-bar-row { display: flex; align-items: center; gap: 7px; margin-bottom: 5px; font-size: 11px; }
.brm-bar-name { width: 76px; flex: none; color: #c8d2dc; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; }
.brm-bar-track { flex: 1; display: flex; height: 13px; background: #131c27; border-radius: 4px; overflow: hidden; }
.brm-bar-track.slim { height: 7px; }
.brm-bar { height: 100%; border-radius: 4px 0 0 4px; min-width: 0; transition: width .4s ease; }
.brm-bar.dead { background: repeating-linear-gradient(45deg, #6e2f28, #6e2f28 4px, #4c211c 4px, #4c211c 8px); border-radius: 0; }
.brm-bar-val { width: 46px; flex: none; color: #e8e2d0; font-weight: 600; }

/* damage-share donut */
.brm-donut-wrap { display: flex; align-items: center; gap: 14px; }
.brm-donut {
  width: 74px; height: 74px; border-radius: 50%; flex: none;
  display: flex; align-items: center; justify-content: center;
}
.brm-donut span {
  width: 46px; height: 46px; border-radius: 50%; background: var(--ui-card);
  display: flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 700; color: #e8e2d0; text-align: center; line-height: 1.2;
}
.brm-legend { display: flex; flex-direction: column; gap: 4px; font-size: 11px; color: #c8d2dc; min-width: 0; }
.brm-leg-row { display: flex; align-items: center; gap: 6px; }
.brm-leg-row .dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.brm-leg-row b { margin-left: auto; color: #e8e2d0; }

/* per-unit-type breakdown */
.brm-h3 { font-size: 13px; color: #f0c04a; margin-bottom: 8px; }
.brm-breakdown { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.brm-side-types { border: 1px solid var(--ui-line-soft); border-radius: 8px; padding: 9px 11px; }
.brm-side-types.won { border-color: rgba(74, 154, 68, .55); }
.brm-side-types.lost { border-color: rgba(122, 58, 52, .55); }
.brm-side-types .brm-side-name { justify-content: flex-start; margin-bottom: 8px; }
.brm-type { margin-bottom: 7px; }
.brm-type-head { display: flex; justify-content: space-between; gap: 8px; font-size: 11px; color: #c8d2dc; margin-bottom: 2px; }
.brm-type-stats { color: #9fb0c0; white-space: nowrap; }

@media (max-width: 700px) {
  .brm-charts, .brm-breakdown { grid-template-columns: 1fr; }
  #report-modal { padding: 12px; }
}

/* leaving a game against AI alone: surrender, or leave the world running. */
#exit-backdrop { position: fixed; inset: 0; z-index: 48; background: rgba(5, 10, 18, .6); }
#exit-modal {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 49;
  width: 420px; max-width: 92vw; text-align: center;
  background: var(--ui-panel); border: 1px solid var(--ui-line); border-radius: 12px;
  padding: 20px 20px 14px; box-shadow: 0 18px 60px rgba(0, 0, 0, .6);
}
#exit-modal .exit-title { font-size: 17px; font-weight: 700; color: #e8e2d0; margin-bottom: 8px; }
#exit-modal .exit-body { font-size: 13px; color: #9fb0c0; line-height: 1.5; margin-bottom: 16px; }
#exit-modal .exit-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
#exit-modal .exit-actions button { padding: 10px 16px; font-size: 13px; }
#exit-modal .danger { background: #5c2b28; border-color: #8c4038; }
#exit-modal .danger:hover { background: #74352f; }
#exit-modal .exit-stay {
  background: none; border: none; color: #7f8fa0; font-size: 12px; padding: 10px 4px 2px;
}
#exit-modal .exit-stay:hover { background: none; color: #c8d2dc; text-decoration: underline; }

/* ---------- mobile / small screens ---------- */
@media (max-width: 820px) {
  /* header: keep it small and centred so it never crowds the corner tabs */
  #topbar { top: max(6px, env(safe-area-inset-top)); max-width: 100vw; }
  .dock.tl { top: max(8px, env(safe-area-inset-top)); }
  .dock.tr { top: max(8px, env(safe-area-inset-top)); }
  /* ⚠️⚠️ THE STATS PANEL IS ON SCREEN HERE TOO. It used to be a drawer at this
     width — its own 📊 tab, collapsed by default — and what that bought was a
     board with nothing on it: gold, income, muster and the army count are what a
     player reads between every decision, and on a phone they were behind a
     button that also had to be pressed again to get the map back. The panel
     simply falls through to the desktop rule above (tab hidden, panel shown);
     the two lines that used to put the drawer back are gone rather than
     overridden, so there is one answer and it is the desktop's.
     ⚠️ dock.js still stamps `.open` on this dock at desktop width only. That is
     inert either way now — nothing here reads it — and it is what the Heroes
     roster stopped keying on (see layoutPanel in ui/heroes.js, which measures
     whether the panel is VISIBLE rather than whether it is `.open`). */
  #daybox { font-size: 11px; padding: 5px 12px; max-width: 64vw; overflow: hidden; text-overflow: ellipsis; }
  .db-win { display: none; }              /* drop the win-condition blurb on narrow screens */
  /* the event tab is its own line, so the name still fits — just trim it down */
  #dayevent { font-size: 10.5px; padding: 3px 11px 4px; max-width: 64vw; }
  #dayevent .de-icon { font-size: 12px; }
  #event-modal { padding: 22px 18px 14px; }
  #event-modal .ev-icon { font-size: 44px; }
  #event-modal .ev-name { font-size: 20px; }
  #event-modal .ev-desc { font-size: 13px; }
  /* the full-size bar landed in the corner-tab band and ran the whole width —
     drop it clear of the 📊/👑/⚙️ buttons and trim it down to suit a phone */
  #battlebar { width: 84vw; margin-top: 20px; padding: 4px 8px 6px; border-radius: 8px; }
  #battlebar-title { font-size: 10px; margin-bottom: 3px; }
  #battlebar-bar { gap: 6px; }
  #battlebar-bar .bhp { height: 11px; border-radius: 6px; }
  #battlebar-bar .seg { font-size: 9px; }
  #battlebar-bar .bcount { width: 21px; height: 21px; font-size: 10px; }

  /* the disc tucks further into the corner and comes down a size; the bottom
     centre belongs to the hand. 128px was a quarter of a portrait tablet's
     width, and every pixel of it is ground the fan then has to be shifted off
     centre to clear — so `--hand-reserve` comes down with it, to 126px (96
     diameter + its 10px margin + the 20px .hand-zone carries beyond the cards
     themselves).
     ⚠️⚠️ THAT RESERVE IS SET DOWN IN THE HAND SECTION, NOT HERE, and it has to
     be: a media query adds no specificity, so `#hand { --hand-reserve }` in this
     block would be beaten on SOURCE ORDER alone by the base declaration a couple
     of thousand lines below — silently, leaving the disc shrunk and the fan
     still shoved aside to clear a button that is no longer there. The two are
     one decision all the same; change either and change the other.
     ⚠️ The phone block below goes further and takes the disc out of the row's
     band altogether. */
  #actionbar { bottom: 10px; right: 10px; }
  #actionbar button { width: 96px; height: 96px; }
  #actionbar button .ab-ico svg { width: 44px; height: 44px; }

  #eventfeed { max-width: 88vw; }
  #eventfeed .ev-toast { font-size: 12px; padding: 6px 12px; white-space: normal; }

  /* ⚠️⚠️ ONE NUMBER FOR THE WHOLE LEFT-HAND COLUMN. The stats panel and the
     Heroes bar under it are one object read top to bottom, and the day pill has
     to leave room for both — three places that must never disagree about how
     wide this column is, so the width is declared ONCE here and read from there.
     It is set on `body` (rather than on the panel) precisely so the pill, which
     is nowhere near this dock in the tree, can read it too.
     ⚠️ Inside the phone block only: on a desktop nothing reads it and the panel
     keeps the width its own rule gives it. */
  body { --stats-w: 208px; }
  body:has(#hud.compact) { --stats-w: 156px; }
  body:has(#hud.mini) { --stats-w: 92px; }

  /* the panel is only bounded by .dock-panel's 92vw, so its content used to
     stretch it nearly edge-to-edge over the map. Cap it to the same width the
     Heroes roster below it uses so the left column reads as one stack.

     ⚠️⚠️ THE TOP MARGIN IS THE **DAY BAR'S** ROOM, and that is the whole reason
     it exists — not the 👑 tab that used to stand here, which is a bar under
     this panel now (see #dock-heroes). Dropping the margin along with that tab
     put this panel's top-right corner straight through the day pill (measured
     at 500px: 21px of horizontal overlap, 25px of vertical).
     ⚠️ THERE IS NO HORIZONTAL ESCAPE FROM IT AT THESE TWO SIZES, so don't reach
     for a narrower panel instead. #daystack is CENTRED and ~208px wide at this
     font size; on a 360px phone that is x=76..284, against a left column
     starting at x=8. Only the `mini` square is small enough to sit beside it,
     and even that one needs the pill nudged out of the corner — see the
     `:has(#hud.mini)` pair below, which is the ONE case that drops this margin.
     ⚠️ 52px = the stack's own worst-case bottom (54px: the day pill plus the
     world-event tab hanging under it) less this dock's 8px top, plus the 6px
     gap the rest of this column is spaced on. #daybox ellipsises rather than
     wrapping at this width, so that 54px is a fixed number and not a guess. */
  #dock-stats .dock-panel { width: var(--stats-w); max-width: 60vw; margin-top: 52px; }
  #hud { min-width: 0; font-size: 12px; padding: 8px 11px; }
  #hud .row { gap: 8px; }
  #hud .row span:first-child { min-width: 0; overflow-wrap: anywhere; }

  /* ⚠️ THE PANEL IS THREE NUMBERS UNTIL IT IS ASKED FOR MORE (`#hud.compact`,
     driven by refreshHud in ui/hud.js — see THE STATS PANEL IS COMPACT ON A
     PHONE there). A phone's screen IS the board and this panel sits over the
     corner of it all game; nine rows is a lot of world a player cannot see for
     numbers most of which they are not reading right now.

     ⚠️ IT IS NARROWER AS WELL AS SHORTER, and the width is a FIXED figure rather
     than `max-content`. Measured over a whole match's worth of numbers, shrinking
     to content lands anywhere between 135px (Gold 1000) and 153px (Gold 98765) —
     so the right-hand edge would step outward every time a figure gained a digit,
     and back in again every time the player spent down past one. A panel that
     resizes itself while it is being read is worse than one a few px wider than
     it strictly needs to be. 156px is the widest that content ever wants, plus a
     hair; a genuinely long faction name wraps in the header, as it already did.

     ⚠️ Opened out it goes back to the roster's own 208px, so the two read as one
     left-hand column exactly as before — it is only the compact panel, which is
     three short rows, that has no business being that wide.
     ⚠️ The width itself is `--stats-w`, above. */
  #hud.compact { cursor: pointer; }

  /* ⚠️ AND ONE STEP SMALLER AGAIN: `mini` is the same three figures with the
     words taken off — an icon apiece instead, which is what actually says which
     is which. Colour did that job alone for a while and is not enough on its
     own: it only reads for someone who has just come from the labelled panel,
     and this square is where a player LIVES once they have found it.
     ⚠️ Icon left, figure right — which is `.row`'s own space-between, so the
     rule that forced the single number to the right is gone rather than
     overridden. A column of figures is still read down its right edge.
     ⚠️ IT IS THE DEFAULT NOW (ui/hud.js), which is what earns it the corner
     below — and what earns it a chevron of its own, since it is the first thing
     a phone player sees rather than somewhere they arrived by pressing twice. */
  #hud.mini { cursor: pointer; padding: 7px 9px 17px; }
  /* ⚠️⚠️ THE ONE PANEL THAT SITS IN THE CORNER ITSELF. Every other size clears
     the centred day pill by dropping 52px below it (see the margin above); this
     one is 92px, so it can stand beside the pill instead and the top-left corner
     stops being 52px of dead map.
     ⚠️⚠️ AND THE PILL STAYS CENTRED ON THE SCREEN — it is the thing every player
     looks at, and a pill nudged off centre reads as the layout having slipped
     rather than as room being made. It does not need moving here: the pill is
     ~208px (216 on a peace day, its widest), so centred its left edge sits at
     (100vw - 216) / 2, which clears this panel's own right edge at 100px from
     428px of viewport up. Everything from a 430px phone to the 820px top of this
     block is therefore untouched, and only the genuinely cramped widths below
     that get the nested rule underneath. */
  #dock-stats .dock-panel:has(#hud.mini) { margin-top: 0; }
  /* ⚠️ Belt-and-braces for the ONE part of the stack whose width is not fixed:
     the world-event tab under the pill carries whatever today's event is called.
     At 430px this clamp is 218px against a 208px pill, so it never touches the
     pill itself — it is only there to stop a long event name reaching the square.
     28px = the two 14px insets (each this dock's 8px plus a 6px gap). */
  body:has(#hud.mini) #daybox,
  body:has(#hud.mini) #dayevent { max-width: calc(100vw - 28px - 2 * var(--stats-w)); }

  /* ⚠️⚠️ ...AND BELOW THAT THE PILL DOES HAVE TO MOVE, because centred it would
     be drawn over the square: at 360px it reaches x=76 against the panel's
     x=8..100. It is RE-CENTRED rather than pushed — it centres in what is left
     between this column and the ⚙️ dock, so it stays balanced in the space it
     actually has. 14px = this dock's 8px inset + a 6px gap; 56px = the cog's
     42px + its own 8px inset + the same gap.
     ⚠️ AND IT IS CLAMPED TO THAT STRIP, not merely centred in it: #topbar is a
     flex COLUMN, so its children take their content width and are centred rather
     than stretched — an over-wide pill would simply overflow both ends and put
     its left edge back over the square. Clamped, it ellipsises its tail instead,
     which is what #daybox's overflow rule already exists for. Measured, it keeps
     every character at 375px and up and loses only its seconds below that.
     ⚠️ Nested inside the phone block on purpose: it is the same decision as the
     corner dock above and belongs beside it, not a few hundred lines away. */
  @media (max-width: 429px) {
    body:has(#hud.mini) #topbar {
      left: calc(14px + var(--stats-w)); right: 56px; transform: none;
    }
    body:has(#hud.mini) #daybox,
    body:has(#hud.mini) #dayevent { max-width: calc(100vw - 70px - var(--stats-w)); }
  }
  /* ⚠️ Hung in the bottom padding (which is deepened for it) rather than added
     as a row, so it costs the square almost nothing and lands clear of the
     figures, which are all right-aligned. It says what the NEXT press does,
     exactly as the header's "▾ more" does at the two bigger sizes.
     ⚠️ IT SITS INSIDE THE INNER GOLD RULE, which is drawn at inset 6.6px — hung
     any lower it is bisected by that line and reads as a mark on the border
     rather than as a control (seen in game at bottom: 2px). 7px clears it by a
     hair and the 17px padding is what leaves the glyph its own band.
     ⚠️⚠️ A REAL ELEMENT AND NOT `#hud.mini::after` — that pseudo is already
     taken, and taken by something load-bearing: `::before`/`::after` on #hud are
     the panel's own double rule (see the frame block above), so a `content:` here
     would rub out that gold line on the one size that has no corner ornaments
     left to carry the house's metal either. */
  #hud.mini .hud-peek {
    position: absolute; left: 0; right: 0; bottom: 7px;
    text-align: center; font-size: 9px; line-height: 1; color: #9fb0c0;
  }
  /* ⚠️ Sized DOWN from the figure it labels, and dimmed. The number is what is
     being read; the icon only has to be recognised, and at full weight three of
     them stacked out-shout the three numbers they are there to serve. */
  #hud.mini .hud-ic { font-size: 11px; line-height: 1; opacity: .85; }
  /* ⚠️ THE CORNER ORNAMENTS COME OFF. They are fixed 22px boxes pinned to the
     padding box's corners (.pf-corner, injected by dressPanel in ui/hand.js) —
     room the panel's own padding is sized to give them at the other two sizes.
     On a ~56×51px padding box the four of them very nearly meet in the middle,
     and the three figures this square exists to show are read through a lattice
     of stonework. The two rules keep the house's metal without the mass. */
  #hud.mini .pf-corner { display: none; }
  /* the chevron IS the affordance, so it sits on the header in both states and
     never moves out from under the finger reaching for it */
  #hud .hud-head { cursor: pointer; }
  /* ⚠️ THE MARGIN IS THE CORNER ORNAMENT'S ROOM, not slack — the same rule
     #hud's own padding is set by. This row sits at the top of the panel and the
     ornament is a 22px box pinned to the padding box's corner, so a chevron
     flush to the right edge has its last third drawn straight through the
     house's stonework (seen in game: the `e` of "more" inside the blocks).
     11px is the measured overlap; the extra px is breathing room. */
  #hud .hud-more { font-size: 10px; color: #7f93a8; font-weight: 600; white-space: nowrap; margin-right: 12px; }
  #settings-menu { max-width: 60vw; }

  /* Heroes live in their own bar here. That bar IS the header, so the roster's
     own collapse bar is redundant — drop it and let the list fill.
     ⚠️ IT MOVES TO THE EDGE, because it is the ONLY tab in this corner now: the
     56px inset was the width of the 📊 tab it used to sit beside, and that tab
     is gone (see above). Left there it would be a button floating over the
     middle of the stats panel.
     ⚠️⚠️ THE BAR IS THE WIDTH OF THE PANEL ABOVE IT, WHATEVER SIZE THAT IS IN —
     `--stats-w`, declared once at the top of this block. The two are one object
     read top to bottom, so a bar that stayed 156px while the panel shrank to a
     92px square would read as two unrelated things stacked in the corner.
     ⚠️ THE LIST IT OPENS IS NOT ALLOWED DOWN THERE WITH IT (`--roster-w` is its
     floor): a Hero row is a name, a health bar, a trait badge and a retinue
     badge, and 92px is not a width any of that can be read at. So the bar
     follows the panel and the list holds at 156px or the panel's width,
     whichever is wider — a small crown button opening a proper list, which is an
     ordinary enough shape for a drawer. */
  #dock-heroes { left: max(8px, env(safe-area-inset-left)); --roster-w: 156px; }
  #dock-heroes .dock-tab { width: var(--stats-w, 156px); }
  #dock-heroes:not(.hidden) { display: flex; }
  /* the roster runs down the left edge (lined up with the stats panel) instead
     of hanging off its own tab; `top` is measured in ui/heroes.js so it can sit
     under the stats panel when that's open too */
  /* ⚠️ 156px IS THIS LIST'S FLOOR, and it was picked as the compact stats
     panel's width — this roster is the other thing standing in the left-hand
     column over the board, and it was 208px, sized back when the panel above it
     was 208px too and the two read as one column.
     ⚠️ It costs no height, which is the trade that had to be checked: at 156px
     the trait and retinue badges on a maxed Hero wrap onto a second line and
     the row grows 22px — see the badge rule below, which is what buys it back.
     ⚠️ Nothing clips at this width. The Hero's name already ellipsises, and the
     widest fixed thing in a row is the trait badge (measured: the longest of
     the eight, "Standard-Bearer", clears it).
     ⚠️ It matches the panel above whenever that panel is wider, and holds at its
     own floor when it is narrower — see the bar's own rule above for why the two
     are allowed to differ in exactly that one direction. */
  #dock-heroes .dock-panel {
    position: fixed; left: max(8px, env(safe-area-inset-left));
    width: max(var(--stats-w, 156px), var(--roster-w)); max-width: 60vw;
  }
  /* ⚠️ A HAIR TIGHTER SO THE BADGES STAY ON ONE LINE. Measured at 156px: every
     trait in the game bar one fits beside a retinue badge as it stands, and
     "Standard-Bearer" — the longest — is the single case that wraps. A point of
     font size and 1px of padding either side is the whole difference, and it
     keeps a maxed Hero's row the same height it was at 208px.
     ⚠️ Scoped to #dock-heroes, which is the roster's PHONE home: on a desktop
     the same box lives inside the stats panel (placeBox in ui/heroes.js), where
     there is room and these badges are untouched. */
  #dock-heroes .hero-trait,
  #dock-heroes .hero-retinue { font-size: 9px; padding: 2px 5px; }
  #dock-heroes #heroes-box { margin-top: 0; }
  #dock-heroes #heroes-tab { display: none; }
  #dock-heroes #heroes-panel { margin-top: 0; max-height: 62vh; }

  /* ⚠️⚠️ A FINGER CANNOT HOVER, SO A BREAKDOWN IS OPENED BY TAPPING ITS ROW —
     `.tip-open`, one row at a time, driven by initHud in ui/hud.js. It used to
     be shown for EVERY row at once, inlined under each as an indented sub-list,
     which made the panel two to three times as tall as the numbers themselves
     and put most of a phone's board behind it. Tapped open, one at a time, the
     panel stays the size of its rows and the breakdown is still one press away.

     It drops INLINE rather than flying out to the side the way the desktop
     tooltip does: at 60vw there is no side to fly out to, and a 230px panel
     hanging off a 208px one would run straight off the screen.
     ⚠️ The footnotes stay HIDDEN. They are a sentence of explanation apiece,
     written for a screen with room for them, and the point of tapping a row is
     to see the numbers behind a figure rather than to read an essay. */
  /* ⚠️ No `cursor` override: the tap flow is gated on a COARSE pointer (initHud),
     where there is no cursor to dress — and a narrow desktop window lands in
     this block too, where the row is still a hover and `.hastip`'s own
     `cursor: help` is the honest answer. */
  #hud .row.hastip { flex-wrap: wrap; }
  #hud .hud-tip {
    position: static; flex-basis: 100%; width: auto;
    margin: 2px 0 3px 6px; padding: 3px 0 3px 8px;
    background: none; border: none; border-left: 2px solid #33465a;
    border-radius: 0; box-shadow: 0 0 0 transparent;
  }
  #hud .hud-tip .ht-h { margin-bottom: 2px; font-size: 11px; }
  #hud .hud-tip .ht-line,
  #hud .hud-tip .ht-total { font-size: 11px; gap: 8px; }
  #hud .hud-tip .ht-foot { display: none; }

  /* panels take most of the screen */
  .panel, .panel.wide { width: 96vw; bottom: 66px; max-height: 74vh; }
  /* the build grid deliberately gets NO column override here: fitBuildGrid
     costs the arrangement against this panel's real width and height and picks
     the columns itself, so a hard 1fr would drop a nine-button menu into one
     tall column — the exact scrolling this layout exists to avoid. */

  /* title / join / lobby modals: shrink and let the input rows wrap */
  .modal { padding: 20px 16px; }
  .join-modal h1 { font-size: 22px; }
  .join-new { flex-wrap: wrap; }
  .join-new input[type=text], #join-name, #lobby-name { min-width: 60%; }
  #title-join, #join-btn { flex: 1 1 100%; }
}

/* ---------- phones (portrait): the Build disc sits ABOVE the hand ------------
   ⚠️⚠️ ON A PHONE THE DISC LEAVES THE BOTTOM ROW ALTOGETHER, AND THAT IS THE
   ONLY WAY THE HAND IS EVER CENTRED. The disc is cleared by `--hand-reserve`,
   which layout() (ui/hand.js) takes off BOTH ends so the row stays centred —
   and on a narrow screen doubling it leaves the cards no room to exist in, so
   layout() falls back to reserving it once and sliding the whole row left. At
   the 112px disc this used to be, that was 134px of reserve out of a 390px
   phone: the row sat 67px left of centre AND each of ten build cards showed
   only 18px of itself, which is not enough to tell one from another.

   Sitting above the cards instead, the reserve is nothing: the fan takes the
   full width, dead centre, and a card shows 33–49px depending on the deck. The
   disc comes down to 72px with it — a comfortable touch target, and a third of
   the area it covered before.

   ⚠️ Its `bottom` is DERIVED from --hand-peek (:root, written per deal by
   build()) rather than written out, because the peek is the card height and the
   cards are squeezed to fit the viewport — a constant would be a gap on a small
   hand and an overlap on a full one. The +30 clears .hand-zone, which stands
   `--hand-peek + 22` tall at rest; below that the disc's bottom edge would sit
   inside the hand's own hit area and quietly stop taking presses.
   ⚠️⚠️ The disc is LIFTED ABOVE THE HAND here (z-index 27) — see the rule itself
   below for the two ways the hand was eating its press, both measured in game.
   It used to be left at 20, under the hand, on the argument that a tapped card's
   detail plate opens upward into exactly this band and a disc drawn over the
   thing the tap was for is worse than one covered while a finger is down. The
   plate closes on the release now, so that is the only case left — and being
   unable to reach the one button that goes back to the other deck is worse.
   ⚠️⚠️ --hand-reserve: 0 IS SET DOWN IN THE HAND SECTION, not here — see the
   tablet block above for why (source order beats it at equal specificity). */
@media (max-width: 600px) {
  /* ⚠️⚠️ THE DISC SITS ABOVE THE HAND HERE (27 against #hand's 26), and it has to.
     An ARMED card lifts `--pop + 26` clear of the row — that lift is the only
     thing left saying which card is live, now that the plate closes on release —
     and at this width the row runs the full screen, so a card armed near the
     right-hand end is drawn straight over the disc. It is `pointer-events: auto`
     at z-index 60 inside #hand, so it ATE the press: measured at 500×703, an
     armed Watchtower spanned y 439–575 over a disc at 460–532 and pressing the
     disc's centre hit the card's own <img>. The disc is the ONLY way back to the
     other deck, so it must win its own 72px whatever is standing over it.
     ⚠️ The z-index note this replaces argued the disc should stay UNDER the hand
     so a tapped card's detail plate was never covered. That was written when the
     plate persisted after a tap; it closes on release now (see THE READ ENDS
     WHEN THE FINGER COMES OFF), so the only thing it can cover is a plate the
     player is actively holding open — which is the case that note called
     acceptable — and the trade has flipped. */
  #actionbar {
    right: max(10px, env(safe-area-inset-right));
    bottom: calc(var(--hand-peek) + 30px + env(safe-area-inset-bottom));
    z-index: 27;
  }
  #actionbar button { width: 72px; height: 72px; }
  #actionbar button .ab-ico svg { width: 34px; height: 34px; }

  /* clear the hand AND the disc standing over it — both derived from the peek,
     so a squeezed hand doesn't leave either of them floating. The panel clears
     the disc whole (30 + 72 of button, plus a margin); the hint only has to
     clear the cards, since it is drawn ABOVE the disc (z-index 25 vs 20) and
     is a pill that is gone the moment the placement is made. */
  .panel, .panel.wide {
    bottom: calc(var(--hand-peek) + 114px + env(safe-area-inset-bottom));
    max-height: 56vh; max-height: 56dvh;
  }
  #placement-hint { bottom: calc(var(--hand-peek) + 16px + env(safe-area-inset-bottom)); }
}

/* ---------- named-unit tag inside tooltips / info card ---------- */
#entity-tip .et-name { color: #ffd98a; }

/* ---------- in-world name tags + emote floaters ---------- */
#world-labels { position: fixed; inset: 0; pointer-events: none; z-index: 16; overflow: hidden; }
.wl-name {
  position: absolute; transform: translate(-50%, -100%);
  font-size: 12px; font-weight: 700; white-space: nowrap; padding: 0 2px;
  color: #fff; text-shadow: 0 0 3px #000, 0 1px 2px #000, 0 0 6px rgba(0,0,0,.85);
}
.wl-emote {
  position: absolute; font-size: 26px; line-height: 1;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,.6));
  animation: wl-emote-pop 3.2s ease-out forwards;
}
@keyframes wl-emote-pop {
  0%   { opacity: 0; transform: translate(-50%, -60%)  scale(.4); }
  12%  { opacity: 1; transform: translate(-50%, -100%) scale(1.15); }
  22%  { opacity: 1; transform: translate(-50%, -105%) scale(1); }
  80%  { opacity: 1; transform: translate(-50%, -125%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -155%) scale(1); }
}
.wl-say {
  position: absolute; transform: translate(-50%, -140%);
  font-size: 11px; font-weight: 600; white-space: nowrap; line-height: 1.35;
  color: #f2f4f8; background: rgba(14, 17, 26, .84);
  border: 1px solid rgba(255, 255, 255, .16); border-radius: 9px;
  padding: 2px 8px;
  animation: wl-say-pop 4.2s ease-out forwards;
}
@keyframes wl-say-pop {
  0%   { opacity: 0; transform: translate(-50%, -112%); }
  7%   { opacity: 1; transform: translate(-50%, -140%); }
  84%  { opacity: 1; transform: translate(-50%, -145%); }
  100% { opacity: 0; transform: translate(-50%, -165%); }
}

/* An ORDER cry (ui/worldlabels.js ORDER_SAYS — the Rally Flag so far): the
   answer to something the player just did, not the ambient chatter of an
   autonomous army. It wears the flag's own banner-red and holds ~6.5s, so a
   muster setting off reads as a reply at a glance instead of blending into
   "working the farm 🌾". */
.wl-say.wl-order {
  color: #fff1ea;
  background: rgba(122, 30, 20, .92);
  border-color: rgba(255, 160, 130, .55);
  box-shadow: 0 0 12px rgba(209, 82, 63, .45);
  animation: wl-order-pop 6.5s ease-out forwards;
}
@keyframes wl-order-pop {
  0%   { opacity: 0; transform: translate(-50%, -108%) scale(.85); }
  5%   { opacity: 1; transform: translate(-50%, -144%) scale(1.06); }
  10%  { opacity: 1; transform: translate(-50%, -140%) scale(1); }
  88%  { opacity: 1; transform: translate(-50%, -148%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -172%) scale(1); }
}

/* ---------- radial unit menu ---------- */
#unit-menu { position: fixed; z-index: 47; width: 0; height: 0; }
#unit-menu.hidden { display: none; }
.um-ring { position: absolute; width: 0; height: 0; }
.um-btn {
  /* base look only; the ring buttons add absolute polar positioning below.
     pop buttons (emoji grid, save) must stay in normal flow so they don't
     stack on top of each other. */
  position: relative;
  width: 42px; height: 42px; border-radius: 50%; padding: 0;
  background: radial-gradient(120% 120% at 34% 24%, rgba(255, 255, 255, .15), rgba(255, 255, 255, 0) 60%), rgba(var(--ui-bg-up), .96);
  border: 1px solid var(--ui-line-hi); color: #e8e2d0;
  font-size: 19px; line-height: 1; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 3px 12px rgba(0,0,0,.55);
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  animation: um-pop .16s ease-out;
}
.um-btn:hover { background: #37495c; border-color: var(--ui-key); }
.um-btn:active { transform: scale(.9); }
.um-ic { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
@keyframes um-pop { from { opacity: 0; } to { opacity: 1; } }

/* Ring action buttons: polar placement (--a angle, --r radius) so they can
   spiral outward from the unit when the menu opens. */
.um-ring-btn {
  position: absolute;
  transform: translate(-50%, -50%) rotate(var(--a, 0deg)) translate(0, calc(-1 * var(--r, 0px)));
  animation: um-spiral .38s cubic-bezier(.2, 1.1, .4, 1) backwards;
}
.um-ring-btn:active {
  transform: translate(-50%, -50%) rotate(var(--a, 0deg)) translate(0, calc(-1 * var(--r, 0px)));
  filter: brightness(1.25);
}
/* keep the glyph upright while the button sits on its rotated polar arm */
.um-ring-btn .um-ic { transform: rotate(calc(-1 * var(--a, 0deg))); }
@keyframes um-spiral {
  from {
    opacity: 0;
    transform: translate(-50%, -50%) rotate(calc(var(--a, 0deg) - 300deg)) translate(0, 0) scale(.2);
  }
  to {
    opacity: 1;
    transform: translate(-50%, -50%) rotate(var(--a, 0deg)) translate(0, calc(-1 * var(--r, 0px))) scale(1);
  }
}

#um-pop {
  position: absolute; left: 0; top: 66px; transform: translateX(-50%);
  background: var(--ui-panel); border: 1px solid var(--ui-line); border-radius: 10px;
  box-shadow: 0 10px 30px rgba(0,0,0,.6); pointer-events: auto;
}
#um-pop.hidden { display: none; }
.um-card { width: 232px; max-width: 76vw; padding: 10px 12px; font-size: 12px; color: #c8d2dc; }
/* A clicked building carries its upgrade rows in the SAME card (ui/unitmenu.js),
   and those rows need the room a name, its worth so far and a price want
   without wrapping — so the one panel takes the wider measure rather than an
   info card and an upgrade menu having two widths between them. */
.um-card.um-card-wide { width: 268px; max-width: 84vw; }
.um-card .et-title { font-weight: 700; font-size: 13px; margin-bottom: 2px; }
.um-card .et-sub { color: #f0c04a; font-size: 11px; margin-bottom: 4px; }
.um-card .et-own { font-size: 11px; font-weight: 600; margin-bottom: 3px; }
.um-card .et-stat { display: flex; justify-content: space-between; gap: 18px; padding: 1px 0; }
.um-card .et-stat span { color: #9fb0c0; }
.um-card .et-stat b { color: #e8e2d0; font-weight: 600; }
.um-card b { color: #7fc86c; }
.um-card .et-foot { color: #7f8ea0; font-size: 10px; margin-top: 5px; line-height: 1.35; }
.um-card .et-up { color: #7fc86c; font-size: 10px; }
.um-card .et-down { color: #e07a6a; font-size: 10px; }
.um-card .et-fx { padding: 1px 0 2px; }
.um-card .et-fx-name { display: block; color: #9fe08a; font-weight: 600; font-size: 11px; }
.um-card .et-fx-note { display: block; color: #8fa0b0; font-size: 10px; line-height: 1.3; }
.um-card .et-dim { color: #7f8ea0; font-weight: 400; font-size: 10px; }
.um-card .et-name { color: #ffd98a; }
/* the building card's one-line rail of live figures (HP · Range · Build range) */
.um-card .et-hp { color: #9fb0c0; }
.um-card .et-warn { color: #ff9a6e; font-weight: 600; }

.um-form { display: flex; gap: 6px; padding: 8px; align-items: center; }
.um-form input {
  background: #0f1922; border: 1px solid var(--ui-line-hi); border-radius: 6px; color: #e8e2d0;
  padding: 7px 9px; font-size: 13px; width: 168px;
}
.um-save {
  width: 34px; height: 34px; border-radius: 8px; padding: 0; font-size: 16px;
  background: rgba(20, 74, 30, .96); border-color: #4a9a44; color: #cdeec6;
}
.um-emotes { display: grid; grid-template-columns: repeat(6, 1fr); gap: 5px; padding: 8px; width: 252px; max-width: 82vw; }
/* fill the grid cell instead of the fixed .um-btn size, so they never spill */
.um-emoji { width: 100%; height: auto; aspect-ratio: 1; min-width: 0; padding: 0; font-size: 18px; line-height: 1; }

/* the Kill ring button stands out red so it's hard to confuse with the others */
.um-kill { background: rgba(120, 26, 26, .96); border-color: #d0524a; color: #ffdad5; }
.um-kill:hover { background: #a5322c; border-color: #ff7565; }
/* Upgrade (raise a unit-spawner a level) — gold, matching its rank pips */
/* (`.um-upgrade` — the gold ⬆️ ring button that opened the found-a-city confirm
   — went with that confirm. A village's offers are rows on its own panel now.) */
/* royal court-power ladder inside the unit-info popup */
.et-power { margin: 3px 0; padding-left: 2px; }
.et-power.locked { opacity: 0.55; }
.et-power-name { font-size: 12px; font-weight: 700; color: #e0c36b; }
.et-power-desc { font-size: 11px; color: #9fb0c0; line-height: 1.35; }

.um-confirm { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; width: 178px; max-width: 70vw; align-items: stretch; text-align: center; }
.um-confirm-msg { color: #e8dcd0; font-size: 12px; line-height: 1.35; }
.um-kill-confirm {
  background: rgba(150, 30, 30, .96); border: 1px solid #d0524a; color: #ffe2dd;
  border-radius: 8px; padding: 8px 10px; font-size: 13px; font-weight: 600; cursor: pointer;
}
.um-kill-confirm:hover { background: #b23029; border-color: #ff7565; }
/* (`.um-warn` — the "you have N, this costs M" line under the found-a-city
   confirm — went with it too. An unaffordable ROW says the same thing by going
   `.poor` and showing its price in the header's purse.) */
/* (There was a gold `.um-settle` / `.um-found-confirm` confirm here, for founding
   a city on one of your villages. A village has TWO futures now — the city and a
   trade route — and a choice is not offered by two confirms on two ring buttons,
   so both are ROWS on the village's own panel and they reuse the upgrade-row
   styling below. See the block at the top of ui/unitmenu.js.) */

/* ---------- the spawner upgrade rows (ui/unitmenu.js) ----------
   One row per option, sitting UNDER the building's own card in the single panel
   a click opens — they were a separate ⚒️ menu until the two were merged, which
   is why the header is now one line: the building's name is the card's title
   above, so all this bar has left to say is what the rows are and what is in
   the purse. Ruled off from the card so the readable half and the buyable half
   are still visibly two things. */
.um-ups {
  display: flex; flex-direction: column; gap: 4px; text-align: left;
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--ui-line);
}
.um-up-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 1px; }
.um-up-title { font-size: 11.5px; font-weight: 700; color: #f5eede; }
.um-up-gold { font-size: 12px; font-weight: 700; color: #f0c04a; }
.um-up-row {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  padding: 6px 8px; border-radius: 7px; cursor: pointer;
  background: rgba(126, 94, 20, .28); border: 1px solid #6d5a2a; color: #f0e6d6;
}
.um-up-row:hover:not(:disabled) { background: rgba(150, 112, 24, .5); border-color: #ffd257; }
.um-up-row:disabled { cursor: default; }
.um-up-row.poor { opacity: .5; }
.um-up-row.maxed { background: rgba(74, 154, 68, .22); border-color: #3f6f3c; opacity: .8; }
.um-up-ico { font-size: 16px; line-height: 1; flex: none; }
.um-up-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.um-up-name { font-size: 12px; font-weight: 600; display: flex; gap: 6px; align-items: baseline; }
.um-up-rank { font-size: 10px; font-weight: 700; color: #f0c04a; }
.um-up-desc { font-size: 10.5px; color: #b6c2d0; line-height: 1.25; }
.um-up-cost { font-size: 11.5px; font-weight: 700; color: #f0c04a; flex: none; }
.um-up-row.maxed .um-up-cost { color: #a8e8a0; }
/* THE ELITE UPGRADE — the one row here that is not one of the five options, but
   what buying all of them unlocks. It appears only on a trainer whose every row
   below it already says ⭐, so it is deliberately the loudest thing in the panel:
   a brighter gold with a rule under it separating it from the shelf it replaces.
   ⚠️ It must out-rank `.maxed` in source order — a maxed row's green would
   otherwise win the tie on equal specificity and the one buyable thing on a
   fully upgraded trainer would read as spent. */
.um-up-row.um-up-elite {
  background: linear-gradient(180deg, rgba(196, 150, 34, .42), rgba(126, 94, 20, .34));
  border-color: #d0a63c; color: #fff4dd;
  margin-bottom: 3px; padding-bottom: 8px;
  border-bottom: 1px solid rgba(255, 210, 87, .45);
}
.um-up-row.um-up-elite:hover:not(:disabled) { background: rgba(196, 150, 34, .6); border-color: #ffe9a8; }
.um-up-row.um-up-elite .um-up-name { color: #ffe9a8; }
.um-up-row.um-up-elite .um-up-cost { color: #ffe9a8; }

/* Surrender on the ⚙️ menu (it used to live in the Diplomacy panel). Sits above
   Exit to lobby and is deliberately the louder of the two reds. */
#settings-menu .settings-surrender { background: #5a2626; border-color: #7c3434; color: #ffdcd6; }
#settings-menu .settings-surrender:hover { background: #713030; border-color: #b25046; color: #fff0ec; }

/* hover label describing a ring button (Show Info / Emoji / Kill Unit …) */
#um-tip {
  position: fixed; z-index: 48; pointer-events: none;
  background: rgba(var(--ui-bg-deep), .97); border: 1px solid var(--ui-line-hi); border-radius: 6px;
  color: #e8e2d0; font-size: 12px; font-weight: 600; line-height: 1;
  padding: 5px 8px; white-space: nowrap; box-shadow: 0 3px 12px rgba(0,0,0,.55);
}
#um-tip.hidden { display: none; }

/* ---------------- Kingdom Designer — "Tactile Dock" layout ----------------
   Full-bleed 3D stage with floating glass controls; cosmetics dock at the
   bottom. Markup in index.html (#armybuilder), wired by ui/armybuilder.js.
   Glass recipe: rgba(13,20,32,0.72) + 1px rgba(255,255,255,0.1) + blur(10px). */
#armybuilder { padding: 0; }
.ab-frame {
  position: relative; width: 100%; height: 100%; overflow: hidden;
  color: #f4f7fb; font-family: 'Segoe UI', system-ui, sans-serif;
}
.ab-bg {
  position: absolute; inset: 0;
  background: radial-gradient(130% 100% at 50% 0%, #243654 0%, #111a2c 45%, #0a0f1a 100%);
}
/* soft floor glow, tinted to the active royalty accent (set inline by JS) */
.ab-floor {
  position: absolute; left: 50%; top: 62%; transform: translateX(-50%);
  width: 52%; height: 24%; border-radius: 50%; filter: blur(4px);
  background: radial-gradient(circle, rgba(90,120,90,0.18), transparent 68%);
  pointer-events: none;
}
/* full-bleed by default; JS pins the bottom to the top of the dock so the model
   centres in the space above it. Do NOT set width/height:100% here — they'd
   override the inset and stop the bottom from shrinking the stage. */
#ab-canvas { position: absolute; inset: 0; display: block; }
/* darkens the edges of the 3D backdrop, framing the model in a pool of focus.
   Sits above the canvas but below every control (scrim is z-index 5). */
.ab-vignette {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: radial-gradient(120% 92% at 50% 44%, transparent 32%, rgba(6, 10, 18, 0.62) 100%);
}
.ab-scrim { position: absolute; inset: 0; z-index: 5; }
.ab-scrim.hidden { display: none; }
.ab-msg { min-height: 16px; font-size: 12.5px; color: #7fd18f; margin-top: 8px; text-align: center; }

/* generic glass icon button (close ✕, and cart.js reuses .ab-close) */
.ab-iconbtn, .ab-close {
  flex: none; width: 44px; height: 44px; padding: 0; border-radius: 12px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(13, 20, 32, 0.72); border: 1px solid rgba(255, 255, 255, 0.12);
  backdrop-filter: blur(10px); color: #93a1b5; font-size: 17px; line-height: 1; cursor: pointer;
}
.ab-iconbtn:hover, .ab-close:hover { background: rgba(255, 255, 255, 0.06); color: #f4f7fb; }

/* ---- top-left: royalty unit selector ---- */
.ab-royal { position: absolute; top: 24px; left: 26px; z-index: 6; }
.ab-royal-badge {
  display: flex; align-items: center; gap: 12px; padding: 12px 16px 12px 12px;
  border-radius: 15px; background: rgba(13, 20, 32, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px);
  cursor: pointer; color: inherit; text-align: left;
}
.ab-royal-badge:hover { border-color: rgba(255, 255, 255, 0.22); }
/* the crest box IS the ⓘ decree toggle: faction-tinted, shows an info glyph */
.ab-crest {
  width: 46px; height: 46px; flex: none; padding: 0; border-radius: 12px;
  display: grid; place-items: center; font-size: 22px; font-weight: 700; cursor: pointer;
  background: linear-gradient(150deg, #263042, #141a26); border: 1px solid rgba(255, 255, 255, 0.14);
}
.ab-crest:hover { filter: brightness(1.15); box-shadow: 0 0 0 2px rgba(230, 178, 76, 0.25); }
.ab-royal-text { display: flex; flex-direction: column; }
.ab-royal-eyebrow {
  font-size: 12px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; color: #8b98ac;
}
.ab-royal-name { font-size: 18px; font-weight: 600; color: #f4f7fb; }
/* the faction is the pick; its King or Queen is named under it as the leader */
.ab-royal-led { font-size: 11px; color: #93a1b5; margin-top: 1px; }
.ab-royal-chev {
  margin-left: 6px; width: 30px; height: 30px; flex: none; display: grid; place-items: center;
  border-radius: 9px; border: 1px solid rgba(255, 255, 255, 0.14); color: #93a1b5; font-size: 14px;
}
.ab-royal-menu {
  margin-top: 10px; width: 290px; padding: 8px; border-radius: 16px;
  background: rgba(13, 20, 32, 0.94); border: 1px solid rgba(255, 255, 255, 0.12);
  backdrop-filter: blur(14px); box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}
.ab-royal-menu.hidden { display: none; }
.ab-royal-menu-head {
  padding: 8px 10px 10px; font-size: 11px; font-weight: 600; letter-spacing: .16em;
  text-transform: uppercase; color: #7e8ba0;
}
.ab-royal-row {
  display: flex; align-items: center; gap: 12px; padding: 10px; border-radius: 12px;
  cursor: pointer; border: 1px solid transparent;
}
.ab-royal-row:hover { background: rgba(255, 255, 255, 0.05); }
.ab-royal-row.sel { border-color: rgba(255, 255, 255, 0.14); }
.ab-royal-row-thumb { width: 44px; height: 50px; flex: none; border-radius: 9px; object-fit: contain; }
.ab-royal-row-name { font-size: 15px; font-weight: 600; color: #dbe3ee; }
.ab-royal-row.sel .ab-royal-row-name { color: #f4f7fb; }
.ab-royal-row-sub { font-size: 12px; color: #8b98ac; }
.ab-royal-row-check { margin-left: auto; font-size: 15px; font-weight: 700; }

/* ---- top-right control cluster ---- */
.ab-topright { position: absolute; top: 24px; right: 26px; z-index: 6; display: flex; align-items: center; gap: 12px; }
.ab-basket {
  position: relative; flex: none; width: 44px; height: 44px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 12px; background: #e8792b; border: 1px solid rgba(255, 255, 255, 0.15);
  color: #fff; cursor: pointer;
}
.ab-basket:hover { background: #f2883a; }
.ab-basket svg { display: block; }
#ab-basket-count {
  position: absolute; top: -7px; right: -7px; min-width: 21px; height: 21px; padding: 0 5px;
  border-radius: 11px; background: #fff; color: #0a0f18; font-size: 12px; font-weight: 700;
  display: grid; place-items: center; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
}
#ab-basket-count.hidden { display: none; }
.ab-modes {
  flex: none; display: flex; gap: 0; padding: 4px; border-radius: 12px;
  background: rgba(13, 20, 32, 0.72); border: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px);
}
.ab-mode {
  min-width: 74px; padding: 8px 18px; text-align: center; border: 0; border-radius: 9px;
  background: transparent; color: #8b98ac; font-size: 14px; font-weight: 600; cursor: pointer;
}
.ab-mode:hover { color: #dbe3ee; }
.ab-mode.sel { background: linear-gradient(180deg, #f0c976, #e6b24c); color: #0a0f18; }
/* icon glyphs on the mode / action buttons show only on mobile (see media query) */
.ab-mode-ico, .ab-btn-ico { display: none; }
/* mobile mode icons are white line-art; on the gold "selected" chip they flip
   dark so they stay legible (matches the pill's text-colour behaviour) */
.ab-mode-ico { color: #fff; }
.ab-mode.sel .ab-mode-ico { color: #0a0f18; }
.ab-mode-ico svg { display: block; }

/* ---- top-centre: roster carousel ---- */
/* the Units toggle + roster carousel share a centred column at the stage top */
.ab-roster-wrap {
  position: absolute; top: 16px; left: 50%; transform: translateX(-50%); z-index: 5;
  display: flex; flex-direction: column; align-items: center; gap: 8px; max-width: 88vw;
}
/* toggle that collapses/expands the carousel — "UNITS" with a caret beneath */
.ab-roster-toggle {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 5px 16px; border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(13, 20, 32, 0.72); backdrop-filter: blur(10px);
  color: #93a1b5; font-size: 11px; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; cursor: pointer; line-height: 1;
}
.ab-roster-toggle:hover { color: #f4f7fb; border-color: rgba(255, 255, 255, 0.22); }
.ab-rt-caret { font-size: 11px; line-height: .8; }
/* collapsed: hide the carousel, leave the toggle centred at the top */
.ab-frame.roster-collapsed .ab-roster { display: none; }

.ab-roster {
  z-index: 4;
  display: flex; align-items: center; gap: 6px; padding: 7px 10px; max-width: min(560px, 88vw);
  border-radius: 14px; background: rgba(13, 20, 32, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px);
}
/* floating unit-name tip that tracks the hovered roster tile */
.ab-roster-tip {
  position: absolute; z-index: 8; transform: translate(-50%, -100%); pointer-events: none;
  padding: 4px 10px; border-radius: 8px; white-space: nowrap;
  background: rgba(13, 20, 32, 0.95); border: 1px solid rgba(255, 255, 255, 0.14);
  color: #f4f7fb; font-size: 12px; font-weight: 600; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.ab-roster-tip.hidden { display: none; }
.ab-rchev {
  flex: none; width: 30px; height: 30px; display: grid; place-items: center; border-radius: 9px;
  border: 0; background: transparent; color: #93a1b5; font-size: 18px; cursor: pointer;
}
.ab-rchev:hover { background: rgba(255, 255, 255, 0.06); color: #f4f7fb; }
.ab-roster-track {
  display: flex; align-items: center; gap: 8px; overflow-x: auto;
  padding: 2px; scrollbar-width: none; touch-action: pan-x;
}
.ab-roster-track::-webkit-scrollbar { display: none; }
.ab-rtile {
  position: relative; flex: none; width: 46px; height: 46px; border-radius: 10px;
  background: #131d2c; border: 1px solid rgba(255, 255, 255, 0.06);
  display: grid; place-items: center; cursor: pointer; overflow: visible;
}
.ab-rtile:hover { opacity: 0.85; }
/* pointer-events:none + user-drag:none: a swipe/click passes through to the
   scrollable rail instead of starting a native image-drag ghost */
.ab-rtile img {
  width: 100%; height: 100%; object-fit: contain; display: block;
  pointer-events: none; -webkit-user-drag: none; user-select: none;
}
.ab-rtile .ab-rtile-label {
  font-size: 9px; font-weight: 600; color: #93a1b5; text-align: center; padding: 0 2px; line-height: 1.1;
}
.ab-rtile.sel {
  border: 1.5px solid #e6b24c; box-shadow: 0 0 0 3px rgba(230, 178, 76, 0.14);
  background: rgba(230, 178, 76, 0.08);
}
.ab-rtile.royal { width: 52px; height: 52px; }
.ab-rtile.royal::before {
  content: '♛'; position: absolute; top: -11px; left: 50%; transform: translateX(-50%);
  font-size: 13px; color: #e6b24c;
}
/* ● = this unit has its own custom loadout override */
.ab-rtile.custom::after {
  content: ''; position: absolute; bottom: 4px; right: 4px; width: 7px; height: 7px;
  border-radius: 50%; background: #6fcf94; box-shadow: 0 0 0 2px rgba(10, 15, 24, 0.7);
}

/* ---- title block under the roster ---- */
.ab-titleblock {
  position: absolute; top: 130px; left: 50%; transform: translateX(-50%); z-index: 4;
  text-align: center; display: flex; flex-direction: column; align-items: center; gap: 3px;
}
.ab-title-row { display: flex; align-items: center; gap: 10px; }
.ab-title { font-size: 20px; font-weight: 600; color: #f4f7fb; }
.ab-title .crown { color: #e6b24c; }
.ab-reset {
  padding: 9px 15px; border-radius: 11px; border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.04); color: #c7d2e0; font-size: 14px; font-weight: 500;
  cursor: pointer; white-space: nowrap;
}
.ab-reset:hover { border-color: rgba(255, 255, 255, 0.28); color: #f4f7fb; }
.ab-reset.hidden { display: none; }
.ab-worn { font-size: 13px; font-weight: 500; color: #93a1b5; }
.ab-worn.hidden { display: none; }
/* flash red + shake when the player tries to wear more than the cap */
.ab-worn.ab-worn-full { color: #ff5a5a; font-weight: 700; animation: ab-worn-shake 0.4s ease-in-out; }
@keyframes ab-worn-shake {
  0%, 100% { transform: translateX(0); }
  15% { transform: translateX(-4px); }
  30% { transform: translateX(4px); }
  45% { transform: translateX(-3px); }
  60% { transform: translateX(3px); }
  75% { transform: translateX(-2px); }
  90% { transform: translateX(2px); }
}
/* "N equipped, not owned" chip + its hover tooltip */
.ab-notowned { position: relative; margin-top: 6px; }
.ab-notowned.hidden { display: none; }
.ab-notowned-chip {
  display: inline-flex; align-items: center; gap: 7px; padding: 6px 12px; border-radius: 10px;
  background: rgba(232, 121, 43, 0.16); border: 1px solid rgba(232, 121, 43, 0.5);
  color: #f0a56a; font-size: 13px; font-weight: 600; cursor: default;
}
.ab-notowned-tip {
  position: absolute; top: calc(100% + 10px); left: 50%; transform: translateX(-50%); display: none;
  width: 250px; padding: 13px; border-radius: 12px; text-align: left; z-index: 20;
  background: rgba(13, 20, 32, 0.97); border: 1px solid rgba(255, 255, 255, 0.14);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
}
/* invisible bridge across the 10px gap so the mouse can travel from the chip
   down to the tip (and its ✕ buttons) without dropping the hover */
.ab-notowned-tip::before {
  content: ''; position: absolute; left: 0; right: 0; top: -12px; height: 12px;
}
.ab-notowned:hover .ab-notowned-tip { display: block; }
.ab-notowned-tip h4 {
  font-size: 11px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
  color: #f0a56a; margin: 0 0 9px;
}
.ab-notowned-tip .row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 5px 0;
  font-size: 13px; color: #dbe3ee;
}
.ab-notowned-tip .row b { font-size: 12px; font-weight: 700; color: #e6b24c; margin-left: auto; }
.ab-notowned-x {
  flex: none; width: 20px; height: 20px; padding: 0; border-radius: 6px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; font-size: 11px; line-height: 1;
  background: rgba(232, 74, 74, 0.16); border: 1px solid rgba(232, 74, 74, 0.5); color: #f08a8a;
  transition: background .12s, color .12s;
}
.ab-notowned-x:hover { background: rgba(232, 74, 74, 0.32); color: #fff; }
.ab-notowned-tip .foot {
  margin-top: 8px; padding-top: 9px; border-top: 1px solid rgba(255, 255, 255, 0.08);
  font-size: 12px; color: #8b98ac;
}

/* ---- right: team-colour rail ---- */
.ab-teamrail {
  position: absolute; right: 26px; top: 50%; transform: translateY(-50%); z-index: 4;
  display: flex; flex-direction: column; gap: 9px; padding: 12px; border-radius: 16px;
  background: rgba(13, 20, 32, 0.72); border: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px);
}
.ab-teamrail.hidden { display: none; }
.ab-teamrail-eyebrow {
  display: flex; align-items: center; justify-content: center; gap: 5px;
  padding: 0; border: 0; background: none; cursor: default;
  font-size: 10px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase;
  font-family: inherit; color: #8b98ac; text-align: center; margin-bottom: 2px;
}
.ab-team-ico, .ab-team-caret { display: none; } /* rail toggle affordance: mobile only */
.ab-swatches { display: flex; flex-direction: column; gap: 9px; }
.ab-swatch {
  width: 34px; height: 34px; border-radius: 9px; cursor: pointer; border: 0;
  transition: transform .1s ease;
}
.ab-swatch:hover { transform: scale(1.1); }
.ab-swatch.sel { box-shadow: 0 0 0 2px #0d1420, 0 0 0 4px currentColor; transform: none; }
.ab-swatch-custom {
  position: relative; display: grid; place-items: center; color: #93a1b5; font-size: 15px;
  border: 1px dashed rgba(255, 255, 255, 0.25); overflow: hidden;
}
.ab-swatch-custom input[type="color"] {
  position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; padding: 0; border: 0;
}

/* ---- ⚜️ Decrees page (third pill in the top-right pillbox) ----
   A full-stage reference sheet for a house's whole decree pool. z-index 6 with
   .ab-topright (also 6) LATER in the DOM, so the pillbox paints over the page
   and the tab row is never trapped underneath its own content. The 84px top
   padding is what keeps the header out from under that floating cluster. */
.ab-decrees {
  position: absolute; inset: 0; z-index: 6; overflow: hidden;
  /* ⚠️ NO GROUND OF ITS OWN. This page used to be an opaque flat gradient laid
     over the stage, which made the designer's third page a different place
     entirely; it is a view ONTO the same world now (the 3D canvas is behind it,
     still rendering — see frame() in ui/armybuilder.js), so anything painted
     here would be painting the world out. */
  background: none; padding: 0;
}
.ab-decrees.hidden { display: none; }

/* ⚠️⚠️ NOTHING ON THIS PAGE IS TEXT YOU SELECT, for the reason the in-game
   overlays give far above (see NOTHING OVER THE BATTLEFIELD IS TEXT YOU SELECT).
   That rule deliberately spares the pre-game screens — the Kingdom Designer
   among them — on the grounds that they carry real text and real form fields and
   have no press-and-slide gesture to protect. The ⚜️ page broke that assumption
   the moment it became a carousel: browsing it IS a drag across a rack of cards,
   which is exactly what a browser reads as "select this text", so a swipe came
   back with a blue highlight dragged over the card names and the info panel.
   ⚠️ THE WHOLE PAGE, not just the carousel: `user-select` stops an element's
   text being TAKEN, and a swipe that starts on the rack and travels across the
   panel or the house tabs would otherwise select those instead.
   ⚠️ Scoped to #ab-decrees and no wider — the rest of the designer keeps its
   colour input, its name fields and its selectable text. */
.ab-decrees {
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
/* ...and a drag that begins on a decree's mark must not start a NATIVE IMAGE
   DRAG either — the icons are <img> (ui/decreeicon.js), so without this a swipe
   off a card peels a ghost of the artwork away under the cursor. The roster
   tiles solve the same problem the same way; pointer-events sends the press
   through to the slot, which is what owns the gesture. */
.ab-decrees img {
  pointer-events: none; -webkit-user-drag: none; user-select: none;
}

/* ⚠️⚠️ THE DESIGNER'S OWN FURNITURE COMES DOWN ON THIS PAGE, AND IT HAS TO BE
   SAID OUT LOUD NOW. While the ⚜️ page was an opaque sheet over the stage, all
   of this was merely COVERED — nothing was hidden, it simply had a wall in front
   of it. The page is transparent now (it is a view onto the same world), so
   every one of them shows through: the cosmetics dock across the bottom, the
   unit title, the team-colour rail, the guest note and the roster tab.
   None of them belongs to a reference sheet — there is nothing here to dress —
   and the card is the subject, so it gets the stage to itself. */
.ab-frame.decrees-open .ab-dock,
.ab-frame.decrees-open .ab-titleblock,
.ab-frame.decrees-open .ab-teamrail,
.ab-frame.decrees-open .ab-guestnote,
.ab-frame.decrees-open .ab-roster-wrap,
.ab-frame.decrees-open .ab-msg { display: none; }
/* the stage runs full height with the dock gone, so the vignette should too */
.ab-frame.decrees-open .ab-floor { display: none; }
.abd-wrap { max-width: 1180px; margin: 0 auto; padding: 84px 26px 30px; }
.abd-empty { text-align: center; padding: 60px 0; color: #93a1b5; font-size: 15px; }
.abd-stage { position: absolute; inset: 0; }

/* ⚠️⚠️ THE FACTION BADGE STANDS OVER THIS PAGE, and needs lifting to do it.
   #ab-royal and #ab-decrees are both z-6 and the PAGE is later in the DOM, so at
   equal z-index the page paints over the badge and the one control this screen
   is about would be buried. The scrim comes up with it: it is what a click
   outside the open dropdown lands on, and left at 5 the press would hit the
   carousel instead — starting a drag and never closing the menu.
   ⚠️ The badge must out-rank its own scrim, or the dropdown is unclickable. */
.ab-frame.decrees-open #ab-scrim { z-index: 7; }
.ab-frame.decrees-open .ab-royal { z-index: 8; }

/* ⚠️ THE SAME CONTROL, IN THE SAME PLACE, AS THE OTHER TWO PAGES. On a unit or
   building page the top-centre tab reads "Units"/"Buildings" and opens the
   selector; here it reads "Decree List" and opens the grid. It borrows
   .ab-roster-toggle outright rather than restyling a second button, so the three
   pages cannot drift apart on the one control they share. */
/* ⚠️ 32px rather than the roster toggle's 16px, which is the SAME LINE as the
   title and the pillbox either side of it rather than a different place. The
   guest banner is an overlay across the top ~30px of the viewport, and at 16 it
   swallowed the label — leaving a bare caret as the only thing marking the one
   control this page is really about. */
.abd-listbtn { position: absolute; top: 32px; left: 50%; transform: translateX(-50%); z-index: 4; }

/* ---- the carousel ---- */
.abd-carousel {
  position: absolute; inset: 0; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  /* the drag is ours: never let it scroll or pan the page underneath */
  touch-action: none; -webkit-tap-highlight-color: transparent;
  /* the card is the subject, so it sits a little above the middle of the stage
     and leaves the hint line and the meadow under it */
  padding-bottom: 56px;
}
/* ⚠️⚠️ THE RACK IS BUILT AT THE HAND'S OWN CARD SIZE AND THEN SCALED UP WHOLE,
   which is what makes this literally the card from your hand rather than a
   bigger card that resembles it. Sized up directly, every measurement inside the
   face stays where the hand put it — a 30px icon and 11.5px type on a card two
   and a half times the width — and the thing reads sparse and badly balanced
   (seen in the designer). Scaling the container instead carries the icon, the
   title, the price, the frame's rules and its ornament up together, in the
   proportions they were drawn in.
   ⚠️ `--k` is written by fitDecreeCard() in ui/armybuilder.js, because CSS
   cannot derive a unitless ratio from a length — there is no way to say "as big
   as this stage will take" in a scale() without doing the division in JS. */
.abd-track {
  position: relative;
  --cw: 106px;                    /* CARD_W in ui/hand.js */
  --ch: calc(var(--cw) * 1.42);   /* CARD_RATIO in ui/hand.js */
  width: var(--cw); height: var(--ch);
  transform: scale(var(--k, 2));
  /* the depth the rack is seen through. Deep on purpose, unlike the hand's 640px
     lean: here the neighbours are pushed a long way BACK, and a shallow
     perspective turns that recession into a smear.
     ⚠️ Expressed against the card rather than in flat px, so it survives --k. */
  perspective: calc(var(--cw) * 6);
}
/* Every slot is laid on top of every other at the centre and pushed out by its
   own `--o` (signed offset from the centred card) and `--a` (its size). Both are
   written by paintCarousel() and NOTHING else is touched on a step, which is
   what lets the transition animate rather than the rack being rebuilt. */
.abd-slot {
  position: absolute; inset: 0;
  transform-style: preserve-3d; cursor: pointer;
  /* ⚠️ Both offsets are CARD-RELATIVE (a % of the slot's own width, and a
     multiple of --cw) so the rack holds its shape at any --k.
     ⚠️ THE `scale` IS ON TOP OF WHAT THE PERSPECTIVE ALREADY DOES. translateZ
     shrinks a card by p/(p+d) on its own — about 0.91 / 0.83 / 0.77 at the three
     depths — which is a true recession but a gentle one, and the rack read as
     four cards of much the same size. The extra step is what makes the centred
     card plainly the biggest thing on the stage. */
  transform:
    translateX(calc(var(--o, 0) * var(--spread, 0.54) * 100%))
    translateZ(calc(var(--a, 0) * -0.6 * var(--cw)))
    rotateY(calc(var(--o, 0) * -16deg))
    scale(calc(1 - var(--a, 0) * 0.07));
  transition: transform .42s cubic-bezier(.22, .9, .28, 1);
}
.abd-slot.mid { cursor: default; }
/* past the wing there is nothing to see — see DECREE_WING in ui/armybuilder.js */
.abd-slot.gone { display: none; }

/* ⚠️ THE BOB IS ON ITS OWN ELEMENT. The slot's transform is the carousel
   geometry and is rewritten on every step; an animation on the same property
   would fight it and the card would snap back to centre each time the rack
   moved.
   ⚠️⚠️ ...AND SO IS THE COLOUR DRAIN, which is what says "further away" now that
   the rack no longer fades out. The neighbours used to lose OPACITY with depth,
   which reads as the cards going transparent — you see the mountains through
   them — rather than as distance. They keep their substance and lose their
   COLOUR instead: the house's crimson or spectral purple washes toward grey and
   dims a little, the way anything does across a stretch of air. Paired with the
   size ramp on the slot, the eye reads depth from two cues at once and the
   centred card is unmistakably the subject.
   ⚠️ A `filter` rather than the palette swap `.poor` and `.is-rally` use, and
   the distinction is worth keeping straight: those two are STATES, where the
   whole point is that the price stays red while everything round it greys, so a
   filter would flatten the one thing they exist to say. This carries no meaning
   at all — it is depth — so washing the card out whole, ornament and price
   together, is exactly right. It is also continuous in `--a`, where a palette
   swap would need a discrete set of variables per rank of the rack. */
.abd-float {
  position: absolute; inset: 0;
  /* ⚠️⚠️ IT HAZES TOWARD THE SKY, NOT TOWARD BLACK, and that is the difference
     between "further away" and "switched off". Draining saturation while also
     DARKENING (the obvious first cut, at brightness 0.76 by the third rank) took
     the far cards to near-charcoal — and a dark slab against this pale blue
     backdrop has MORE contrast against it, not less, so they read as burnt-out
     rather than receded, and the house's crimson was gone entirely. Lifting the
     brightness and dropping the contrast instead is what distance actually does
     to a colour: it washes toward the air in front of it. The saturation floor
     is set so the furthest card still carries a trace of its house. */
  filter:
    saturate(calc(1 - var(--a, 0) * 0.22))
    contrast(calc(1 - var(--a, 0) * 0.09))
    brightness(calc(1 + var(--a, 0) * 0.06));
  transition: filter .42s ease;
}
.abd-slot.mid .abd-float { animation: abd-bob 4.6s ease-in-out infinite; }
@keyframes abd-bob {
  0%, 100% { transform: translateY(-5px); }
  50%      { transform: translateY(5px); }
}

/* THE CARD IS THE HAND'S OWN (ui/hand.js decreeCardFace), so everything that
   prints it — the ground, the stock, the frame's metal, the foot — is already
   styled under THE HAND far below. All that is needed here is to undo the fan
   geometry `.hand-card` carries for the hand: it is absolutely placed against
   the bottom of the screen there, and fills its slot here. */
.abd-float .hand-card {
  inset: 0; width: 100%; height: 100%;
  transform: none; transition: none;
  --tiltx: 0deg;   /* upright: the carousel's own rotateY is the depth here */
  cursor: inherit;
}

/* ⚠️ ABOVE THE RACK, not under it. paintCarousel() gives each slot a z-index of
   40 minus its depth, so at the old z-2 the chevrons were behind every card —
   invisible the moment the rack grew wide enough to reach them, which is exactly
   what widening the carousel does. */
.abd-arrow {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 45;
  width: 46px; height: 46px; border-radius: 50%;
  background: rgba(13, 20, 32, 0.72); border: 1px solid rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(10px);
  color: #c7d2e0; font-size: 24px; line-height: 1; cursor: pointer;
}
.abd-arrow:hover { color: #f4f7fb; border-color: rgba(255, 255, 255, 0.28); }
.abd-prev { left: 26px; }
.abd-next { right: 26px; }

.abd-hint {
  position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 3;
  font-size: 12.5px; color: #cfe0ee; text-align: center; max-width: 78vw;
  padding: 6px 13px; border-radius: 10px;
  background: rgba(13, 20, 32, 0.6); border: 1px solid rgba(255, 255, 255, 0.08);
  backdrop-filter: blur(8px);
}

/* ---- the info panel: the hand's hover plate, standing open ---- */
.abd-info {
  position: absolute; top: 92px; right: 26px; z-index: 3;
  width: min(330px, 32vw); max-height: calc(100% - 160px); overflow-y: auto;
  padding: 15px 17px; border-radius: 16px;
  background: rgba(13, 20, 32, 0.78); border: 1px solid rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(10px);
}
.abd-i-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.abd-i-icon { font-size: 27px; line-height: 1; filter: drop-shadow(0 2px 5px rgba(0, 0, 0, .55)); }
.abd-i-name { font-size: 16px; font-weight: 700; color: #ffe9b0; }
.abd-i-price { font-size: 14px; font-weight: 700; color: #ffd479; margin-bottom: 9px; }
.abd-i-climb { font-size: 11.5px; font-weight: 600; color: #9fb0c0; margin-left: 7px; }
.abd-i-body { font-size: 13px; color: #cbd6e2; line-height: 1.55; }
.abd-i-aim { margin-top: 9px; }
.abd-i-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 11px; }

/* ---- the Decree List: every card in the pool, laid out flat ---- */
.abd-list {
  position: absolute; inset: 0; z-index: 8; overflow-y: auto;
  background: rgba(8, 13, 22, 0.9); backdrop-filter: blur(12px);
  /* ⚠️ 84px of top padding is the floating top-right cluster's room, not slack.
     The pillbox and basket sit at 24px in a LATER sibling at the same z-index,
     so they paint over this page whatever it does — and the list's own heading
     and its ✕ both land under them at any smaller inset. */
  padding: 84px 26px 28px;
}
.abd-list.hidden { display: none; }
.abd-list-head {
  display: flex; align-items: flex-start; gap: 16px;
  max-width: 1180px; margin: 0 auto 16px;
}
.abd-list-title { font-size: 20px; font-weight: 700; color: #f4f7fb; }
.abd-list-sub { font-size: 12.5px; color: #9fb0c0; line-height: 1.5; margin-top: 3px; max-width: 84ch; }
.abd-list-close {
  margin-left: auto; flex: none; white-space: nowrap;
  padding: 8px 14px; border-radius: 11px; cursor: pointer;
  border: 1px solid rgba(255, 255, 255, 0.14); background: rgba(255, 255, 255, 0.04);
  color: #c7d2e0; font-size: 13px; font-weight: 600;
}
.abd-list-close:hover { border-color: rgba(255, 255, 255, 0.28); color: #f4f7fb; }
.abd-tiles {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 10px;
  max-width: 1180px; margin: 0 auto;
}
.abd-tile {
  display: grid; grid-template-columns: auto 1fr auto; gap: 4px 10px; text-align: left;
  background: rgba(13, 20, 32, 0.7); border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: 13px; padding: 11px 13px; cursor: pointer;
}
.abd-tile:hover { border-color: var(--abd-accent, #e6b24c); }
/* the card the carousel is holding, marked so the grid says where you are */
.abd-tile.sel { border-color: var(--abd-accent, #e6b24c); background: rgba(255, 255, 255, 0.06); }
.abd-t-icon { font-size: 23px; line-height: 1; grid-row: span 2; align-self: start; }
.abd-t-name { font-size: 14px; font-weight: 700; color: #ffe9b0; align-self: center; }
.abd-t-cost { font-size: 12.5px; font-weight: 700; color: #ffd479; align-self: center; }
.abd-t-desc {
  grid-column: 2 / span 2; font-size: 12px; color: #b6c2cf; line-height: 1.45;
}

/* (The house-switcher strip that used to run along the bottom is gone — the
   faction badge top-left is the one faction control in the designer now, on this
   page as on the unit designer. See .ab-frame.decrees-open above.) */

/* the one rule that explains the whole mechanic */
.abd-chip {
  font-size: 10.5px; font-weight: 600; letter-spacing: .03em; padding: 2px 7px; border-radius: 5px;
  color: #9fb0c0; border: 1px solid rgba(255, 255, 255, 0.13);
}

/* guest "previewing only" note (floats bottom-left, above the dock) */
.ab-guestnote {
  position: absolute; left: 26px; bottom: 220px; z-index: 5; max-width: 300px;
  padding: 12px 14px; border-radius: 12px; font-size: 12.5px; color: #c7d2e0; line-height: 1.5;
  background: rgba(13, 20, 32, 0.9); border: 1px solid rgba(255, 255, 255, 0.12); backdrop-filter: blur(10px);
}
.ab-guestnote.hidden { display: none; }
.ab-guestnote b { color: #f4f7fb; }

/* ---- bottom cosmetics dock ---- */
.ab-dock {
  position: absolute; left: 20px; right: 20px; bottom: 16px; z-index: 4;
  border-radius: 18px; background: rgba(13, 20, 32, 0.9); border: 1px solid rgba(255, 255, 255, 0.09);
  backdrop-filter: blur(14px); padding: 12px 20px 10px; box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.35);
}
.ab-dock-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-bottom: 10px; }
/* tabs + the ↩ Army default reset share the left group; reset sits after Special */
.ab-tab-group { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ab-tabs { display: flex; gap: 8px; flex-wrap: wrap; }
/* the collapsed-tabs button is a mobile-only affordance (see the media query) */
.ab-slot-toggle { display: none; }
.ab-tab {
  padding: 9px 17px; border-radius: 11px; border: 1px solid rgba(255, 255, 255, 0.08);
  background: transparent; color: #93a1b5; font-size: 15px; font-weight: 500; cursor: pointer;
}
.ab-tab:hover:not(:disabled) { color: #dbe3ee; border-color: rgba(255, 255, 255, 0.18); }
.ab-tab.sel {
  background: rgba(230, 178, 76, 0.12); border-color: rgba(230, 178, 76, 0.4); color: #f3cd80; font-weight: 600;
}
.ab-tab:disabled { color: #5a6577; border-color: rgba(255, 255, 255, 0.06); cursor: default; }
.ab-dock-actions { display: flex; align-items: center; gap: 12px; flex: none; }
.ab-owned {
  display: flex; align-items: center; gap: 9px; cursor: pointer; padding: 10px 14px;
  border-radius: 11px; border: 1px solid rgba(255, 255, 255, 0.1); user-select: none;
  font-size: 14px; font-weight: 600; color: #c7d2e0;
}
.ab-owned:hover { border-color: rgba(255, 255, 255, 0.25); }
.ab-owned input { width: 20px; height: 20px; accent-color: #e6b24c; cursor: pointer; margin: 0; }
.ab-randomise {
  padding: 11px 16px; border-radius: 11px; border: 1px solid rgba(230, 178, 76, 0.4);
  background: rgba(230, 178, 76, 0.12); font-size: 14px; font-weight: 600; color: #f3cd80; cursor: pointer;
}
.ab-randomise:hover { background: rgba(230, 178, 76, 0.2); }
.ab-randomise.hidden { display: none; }
.ab-save {
  padding: 11px 34px; border-radius: 11px; background: linear-gradient(180deg, #f0c976, #e6b24c);
  font-size: 15px; font-weight: 700; color: #0a0f18; cursor: pointer; border: 0;
  box-shadow: 0 8px 20px rgba(230, 178, 76, 0.25);
}
.ab-save:hover { filter: brightness(1.05); }

/* card rail: horizontal, wheel + click-drag scroll (handled in JS) */
.ab-rail {
  display: flex; gap: 12px; overflow-x: auto; padding-bottom: 2px; min-height: 104px;
  cursor: grab; user-select: none; touch-action: pan-x;
}
.ab-rail.dragging { cursor: grabbing; }
.ab-rail::-webkit-scrollbar { height: 8px; }
.ab-rail::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.12); border-radius: 8px; }
.ab-none { align-self: center; font-size: 13px; color: #6f8093; font-style: italic; padding: 0 8px; }

/* a cosmetic card in the rail */
.ab-item {
  position: relative; flex: none; width: 128px; border-radius: 12px; padding: 9px; text-align: center;
  cursor: pointer; user-select: none; background: #0e1522; border: 1px solid rgba(255, 255, 255, 0.08);
}
.ab-item:hover { border-color: rgba(255, 255, 255, 0.18); }
/* pointer-events:none + user-drag:none: a swipe/click passes through to the
   scrollable rail instead of grabbing the item image */
.ab-item img {
  width: 100%; height: 60px; border-radius: 9px; margin-bottom: 6px; object-fit: contain; display: block;
  pointer-events: none; -webkit-user-drag: none; user-select: none;
}
.ab-item-name { font-size: 13px; font-weight: 500; color: #c7d2e0; line-height: 1.2; }
.ab-item.sel .ab-item-name { font-weight: 600; color: #f3cd80; }
/* "you could earn this instead" — only ever on the ONE item the tutorial
   grants, and only while the account has not finished it (see TUTORIAL.md).
   Green so it reads against the tile's gold price chip rather than competing
   with it: the two are different offers, not two prices.
   ⚠️ ONE line, and the tile is only 110px of usable width — so the wording is
   as short as it can be while still naming the route, and `nowrap` keeps it
   there. Left to wrap it runs a second line straight under the 🖌️ paint
   button, which is absolutely positioned at bottom right. */
.ab-item-tut {
  font-size: 10.5px; font-weight: 600; color: #8fe0a4; line-height: 1.25;
  margin-top: 2px; white-space: nowrap;
}
/* ⚠️⚠️ ...and the 🖌️ button has to get out of its way, which is fiddlier than
   it looks. It is anchored to the tile's BOTTOM, so by default it lands on the
   hint; lifting it one row only moves the collision onto the item's NAME (both
   were seen in the Designer). The answer is a padding BAND: the tile grows a
   bottom pad that the absolutely-positioned button sits inside, and the flow
   content — image, name, hint — stops above it, so nothing can share a line
   with it. The whole rail grows by ~19px, which keeps the tiles uniform.
   (`:has` is already used a few rules up, for the cart overlay.) */
.ab-item:has(.ab-item-tut) { padding-bottom: 28px; }
.ab-item.sel {
  border: 1.5px solid #e6b24c; background: rgba(230, 178, 76, 0.06); box-shadow: 0 0 0 3px rgba(230, 178, 76, 0.12);
}
.ab-item.locked img { opacity: 0.82; }
.ab-item.sel.locked { border-color: #e8792b; background: rgba(232, 121, 43, 0.08); box-shadow: 0 0 0 3px rgba(232, 121, 43, 0.16); }
.ab-item.sel.locked .ab-item-name { color: #f0a56a; }
/* padlock badge, top-left — only when the card is equipped but unowned */
.ab-lockbadge {
  position: absolute; top: 6px; left: 6px; z-index: 2; width: 23px; height: 23px; border-radius: 7px;
  display: grid; place-items: center; color: #e6b24c;
  background: rgba(10, 15, 24, 0.8); border: 1px solid rgba(230, 178, 76, 0.55);
}
.ab-lockbadge svg { display: block; }
/* price chip, top-right — on unowned items; hover swaps 🔒price → 🛒 buy overlay */
.ab-lock {
  position: absolute; top: 6px; right: 6px; z-index: 2; display: flex; align-items: center; gap: 4px;
  padding: 4px 8px; border-radius: 8px; background: #e6b24c; color: #141414;
  font-size: 12px; font-weight: 700; cursor: pointer; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.ab-lock svg { display: block; }
.ab-lock:hover, .ab-item:has(.ab-cart-btn:hover) .ab-lock { opacity: 0; }
/* the orange buy/trolley overlay sits over the price chip's spot */
.ab-cart-btn {
  position: absolute; top: 6px; right: 6px; z-index: 3; display: none; align-items: center; gap: 4px;
  padding: 4px 8px; border-radius: 8px; background: #e8792b; border: 0; color: #fff;
  font-size: 12px; font-weight: 700; cursor: pointer; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.ab-lock:hover + .ab-cart-btn, .ab-cart-btn:hover, .ab-cart-btn.queued { display: flex; }
.ab-cart-btn svg { display: block; }
.ab-cart-btn:not(.queued):hover { background: #f2883a; }
/* queued: basket icon by default, red ✕ on hover to signal removal */
.ab-cart-btn.queued { background: #2c7a3f; }
.ab-cart-btn.queued:hover { background: #c0392b; }
.ab-cart-face { display: flex; align-items: center; gap: 4px; }
.ab-cart-btn.queued .ab-cart-remove { display: none; }
.ab-cart-btn.queued:hover .ab-cart-in { display: none; }
.ab-cart-btn.queued:hover .ab-cart-remove { display: flex; }
/* 🖌️ customise, bottom-right — preserves per-item colour editing */
.ab-paint-btn {
  position: absolute; right: 6px; bottom: 6px; z-index: 2; padding: 2px 5px; font-size: 12px; line-height: 1;
  background: rgba(10, 15, 24, 0.82); border: 1px solid rgba(255, 255, 255, 0.14); border-radius: 7px; cursor: pointer;
}
.ab-paint-btn:hover { border-color: #e6b24c; }

/* ---- floating colour editor popover ---- */
.ab-paint {
  position: absolute; right: 26px; bottom: 204px; z-index: 7; width: 236px;
  display: flex; flex-direction: column; gap: 8px; padding: 13px 14px; border-radius: 14px;
  background: rgba(13, 20, 32, 0.96); border: 1px solid rgba(255, 255, 255, 0.14);
  backdrop-filter: blur(12px); box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
}
.ab-paint.hidden { display: none; }
.ab-paint-head { display: flex; align-items: center; justify-content: space-between; font-size: 13px; color: #dbe3ee; }
.ab-paint-head b { font-weight: 700; }
.ab-paint-head button { background: none; border: none; color: #93a1b5; font-size: 14px; cursor: pointer; padding: 0 2px; }
.ab-paint-head button:hover { color: #f4f7fb; }
.ab-paint-row { display: flex; align-items: center; gap: 8px; }
.ab-paint-row.hidden { display: none; }
.ab-paint-row input[type="color"] { flex: 1; height: 32px; padding: 2px; cursor: pointer; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.14); background: transparent; }
.ab-paint-row button {
  background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.14); color: #dbe3ee;
  border-radius: 8px; font-size: 12px; padding: 5px 11px; cursor: pointer;
}
.ab-paint-row button:hover { border-color: rgba(255, 255, 255, 0.28); }
.ab-check { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #dbe3ee; cursor: pointer; }
.ab-check input { accent-color: #e6b24c; cursor: pointer; }
.ab-paint-hint { font-size: 11px; color: #6f8093; line-height: 1.4; }

/* narrow screens: shrink the floating controls so they don't collide */
@media (max-width: 720px) {
  .ab-royal { top: 14px; left: 14px; }
  .ab-topright { top: 14px; right: 14px; gap: 8px; }
  /* the UNITS toggle stays pinned at the very top, but the wide gap drops the
     opened unit list lower so it clears the top-corner badge and buttons
     (fine for the list to cover the unit-name titleblock beneath it) */
  .ab-roster-wrap { top: 12px; gap: 42px; }
  .ab-roster { padding: 8px; }
  .ab-titleblock { top: 118px; }
  /* icon parks just above the dock; column-reverse + bottom anchor makes the
     swatches expand vertically UPWARD out of it while the 🎨 icon stays put */
  .ab-teamrail {
    right: 12px; top: auto; bottom: 250px; transform: none; padding: 6px; gap: 5px;
    flex-direction: column-reverse;
  }
  .ab-dock { left: 12px; right: 12px; bottom: 12px; padding: 14px; }
  /* one compact row: Skin + Default on the left, Owned/Randomise/Save pinned
     right (margin-left:auto). nowrap + tightened sizes keep all five on a
     single row down to ~390px; below that the reset text drops (see ≤400px) */
  .ab-dock-head { flex-wrap: nowrap; gap: 6px; }

  /* collapse the Skin/Hat/Face/Back/Body/Special tabs into one button; the tabs
     reveal as a popover above it so they never inflate the dock height */
  .ab-tab-group { position: relative; flex: 0 1 auto; gap: 6px; }
  .ab-dock-actions { margin-left: auto; gap: 6px; }
  .ab-owned { padding: 7px 9px; font-size: 12px; gap: 6px; }
  .ab-owned input { width: 16px; height: 16px; }
  .ab-slot-toggle {
    display: inline-flex; align-items: center; gap: 6px; padding: 7px 10px;
    border-radius: 11px; border: 1px solid rgba(230, 178, 76, 0.4);
    background: rgba(230, 178, 76, 0.12); color: #f3cd80; font-size: 13px; font-weight: 600; cursor: pointer;
  }
  .ab-reset { padding: 7px 9px; font-size: 12px; }
  .ab-slot-caret { font-size: 12px; opacity: 0.85; transition: transform 0.15s; }
  .ab-tab-group.tabs-open .ab-slot-caret { transform: rotate(180deg); }
  .ab-tabs {
    display: none; position: absolute; left: 0; bottom: 100%; margin-bottom: 8px; z-index: 7;
    flex-direction: column; gap: 6px; padding: 8px; min-width: 160px;
    background: rgba(13, 20, 32, 0.97); border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 12px; box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45); backdrop-filter: blur(14px);
  }
  .ab-tab-group.tabs-open .ab-tabs { display: flex; }
  .ab-tabs .ab-tab { text-align: left; }
  .ab-guestnote { display: none; }
  .ab-paint { right: 12px; }

  /* collapse the faction badge to just its crest + chevron */
  /* smaller crest, but keep the desktop-style "FACTION" label + name. The name
     is capped so the badge stays left of the centred UNITS toggle, and the
     "Led by …" leader line is dropped — no room, and the faction is the pick. */
  .ab-royal-badge { position: relative; padding: 6px 10px 6px 6px; gap: 7px; }
  .ab-crest { width: 30px; height: 30px; font-size: 15px; border-radius: 9px; }
  .ab-royal-text { display: flex; }
  .ab-royal-eyebrow { font-size: 8px; letter-spacing: .08em; }
  .ab-royal-name { font-size: 13px; white-space: nowrap; max-width: 92px; overflow: hidden; text-overflow: ellipsis; }
  .ab-royal-led { display: none; }
  /* small down-caret centred beneath the badge, signalling the dropdown */
  .ab-royal-chev {
    display: grid; position: absolute; left: 50%; bottom: -13px; transform: translateX(-50%);
    width: auto; height: auto; margin: 0; padding: 0; border: 0; background: none;
    color: #93a1b5; font-size: 13px; line-height: 1; pointer-events: none;
  }
  /* while open: hide the caret (the menu is showing) and reveal the full name */
  .ab-royal.menu-open .ab-royal-chev { display: none; }
  .ab-royal.menu-open .ab-royal-name { max-width: none; }

  /* mode + action buttons become icon-only to save width */
  .ab-mode-txt, .ab-btn-txt { display: none; }
  .ab-btn-ico { display: inline; }
  .ab-mode-ico { display: inline-flex; align-items: center; }
  .ab-mode { min-width: 0; padding: 8px 12px; font-size: 16px; }
  .ab-randomise { padding: 8px 10px; font-size: 15px; }
  /* Save keeps its text label on mobile (Randomise stays icon-only) */
  .ab-save { padding: 8px 14px; font-size: 13px; }
  .ab-save .ab-btn-txt { display: inline; }
  .ab-save .ab-btn-ico { display: none; }

  /* Unit/Building toggle leaves the top-right group and parks centred, riding
     just above the dock — attached to the bottom bar but not inside it.
     position:fixed escapes .ab-topright and anchors to the (full-screen) frame.
     The dock is a single compact row (see .ab-dock-head above), ~213px tall +
     its 12px offset, so bottom = ~225px + a hairline gap clears it. */
  .ab-modes {
    position: fixed; left: 50%; bottom: 234px; transform: translateX(-50%);
    z-index: 6;
  }
  /* ...but the ⚜️ page has no dock to ride above: the pills drop to the bottom
     edge rather than floating over the middle of the stage. */
  .ab-frame.decrees-open .ab-modes { bottom: 16px; }
  .abd-wrap { padding: 78px 16px 84px; }

  /* ⚠️⚠️ ON A PHONE THE WHOLE STAGE IS A COLUMN: badge, card, then the reading.
     A 330px info panel pinned right is most of the screen standing over the very
     thing it describes, and the carousel then has nowhere to be.
     ⚠️ the card is capped against the WIDTH here rather than the height, since
     what runs out on a phone is sideways room — three cards of a rack abreast */
  .abd-carousel { padding-top: 82px; padding-bottom: 232px; }
  /* the chevrons would sit on top of the neighbouring cards at this width, and
     the swipe is the gesture a phone reaches for anyway */
  .abd-arrow { display: none; }
  /* ⚠️ AND THE HINT LINE GOES. The bottom edge here carries the mode pills
     (fixed, bottom 16px — the rule above) with the info panel stacked over them,
     and there is no third band left; the panel is the half worth keeping, and a
     carousel on a touch screen is swiped without being told to. */
  .abd-hint { display: none; }
  /* ⚠️ THE LIST BUTTON DROPS BELOW THE BADGE. The faction badge holds the top
     line on this page as on every other, and at 375px the two cannot share it —
     the badge alone is ~250px wide. */
  .abd-listbtn { top: 74px; }
  .abd-info {
    top: auto; right: 12px; left: 12px; bottom: 72px;
    width: auto; max-height: 148px;
  }

  .abd-list { padding: 78px 14px 84px; }
  .abd-tiles { grid-template-columns: 1fr; }

  /* colour rail collapses to its palette-icon header; tap to reveal the swatches */
  .ab-team-caret { display: inline; }
  .ab-team-ico { display: inline-flex; align-items: center; }
  .ab-team-ico svg { display: block; }
  .ab-team-label { display: none; }
  .ab-teamrail-eyebrow { cursor: pointer; font-size: 15px; margin-bottom: 0; }
  .ab-teamrail:not(.rail-open) { gap: 0; }
  .ab-teamrail:not(.rail-open) .ab-swatches,
  .ab-teamrail:not(.rail-open) .ab-swatch-custom { display: none; }
  /* open swatches as a compact grid so the rail stays short (clears the dock) */
  .ab-teamrail.rail-open .ab-swatches { display: grid; grid-template-columns: repeat(3, 24px); gap: 5px; }
  .ab-teamrail.rail-open .ab-swatch { width: 24px; height: 24px; }
  .ab-teamrail.rail-open .ab-swatch-custom { width: 24px; height: 24px; }
}
/* narrowest phones: drop the reset button's "Default" word (keep the ↩ icon +
   its tooltip) so the five-control row still fits on one line */
@media (max-width: 400px) {
  .ab-reset-txt { display: none; }
}
/* the cart overlay: covers the whole designer panel, the draft stays beneath */
.ab-cart {
  position: absolute; inset: 0; z-index: 6; display: flex; flex-direction: column;
  background: #121d2a; border-radius: 14px; padding: 20px 22px;
}
.cart-head { display: flex; align-items: center; gap: 14px; margin-bottom: 12px; }
.cart-head h2 { font-size: 20px; margin-right: auto; }
.cart-rows { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
.cart-row {
  display: flex; align-items: center; gap: 12px;
  background: #14202f; border: 1px solid #2a3a4e; border-radius: 10px; padding: 8px 12px;
}
.cart-row img { width: 56px; height: 56px; flex: none; }
.cart-name { flex: 1; font-size: 14px; color: #dfe7ef; }
.cart-price { font-size: 14px; font-weight: 600; color: #ffd479; }
.cart-remove {
  flex: none; width: 28px; height: 28px; padding: 0; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  background: #2a1c1a; border: 1px solid #6b3a34; color: #ff9a8e; font-size: 13px; cursor: pointer;
}
.cart-remove:hover { background: #3d2521; }
.cart-empty { color: #9fb0c0; text-align: center; padding: 40px 0; font-size: 15px; line-height: 2; }
.cart-empty span { font-size: 13px; color: #6f8296; }
.cart-foot { border-top: 1px solid #2a3a4e; margin-top: 12px; padding-top: 12px; }
.cart-total {
  display: flex; justify-content: space-between; font-size: 16px; margin-bottom: 10px;
}
.cart-total b { color: #ffd479; font-size: 18px; }
.cart-pay { display: flex; gap: 10px; flex-wrap: wrap; }
.cart-pay button { flex: 1; min-width: 180px; padding: 12px; font-size: 14px; font-weight: 600; }
.cart-stripe { background: #e67e22; border-color: #f39c4a; color: #fff; }
.cart-stripe:hover:not(:disabled) { background: #f08a2e; }
.cart-msg { min-height: 18px; margin-top: 8px; font-size: 12.5px; color: #9fb0c0; }
/* the designer's own confirm dialog (remove-from-basket / locked-items-on-save),
   laid out like #exit-modal but never shared with the in-game exit flow */
#ab-modal-backdrop { position: fixed; inset: 0; z-index: 58; background: rgba(5, 10, 18, .6); }
#ab-modal {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 59;
  width: 440px; max-width: 92vw; text-align: center;
  background: #17222f; border: 1px solid #3a4a5c; border-radius: 12px;
  padding: 20px 20px 14px; box-shadow: 0 18px 60px rgba(0, 0, 0, .6);
}
#ab-modal .exit-title { font-size: 17px; font-weight: 700; color: #e8e2d0; margin-bottom: 8px; }
#ab-modal .exit-body { font-size: 13px; color: #9fb0c0; line-height: 1.5; margin-bottom: 16px; }
#ab-modal .exit-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
#ab-modal .exit-actions button { padding: 10px 16px; font-size: 13px; }
#ab-modal .danger { background: #5c2b28; border-color: #8c4038; }
#ab-modal .danger:hover { background: #74352f; }
/* the "purchase successful" dialog's unlocked-item list (#exit-modal, main.js) */
.purchase-items {
  list-style: none; margin: 0 0 16px; padding: 0;
  display: flex; flex-direction: column; gap: 4px;
}
.purchase-items li {
  background: #14202f; border: 1px solid #2a3a4e; border-radius: 8px;
  padding: 7px 10px; font-size: 13px; color: #dfe7ef; text-align: left;
}
.purchase-items li::before { content: '•'; color: #ffd479; margin-right: 8px; }

/* the paintbrush's colour editor */
.ab-paint {
  background: #0f1824; border: 1px solid #2a3a4e; border-radius: 8px;
  padding: 9px 11px; margin-bottom: 8px; display: flex; flex-direction: column; gap: 7px;
}
.ab-paint.hidden { display: none; }
.ab-paint-head { display: flex; align-items: center; justify-content: space-between; font-size: 13px; color: #dfe7ef; }
.ab-paint-head button {
  background: none; border: none; color: #9fb0c0; font-size: 13px; cursor: pointer; padding: 0 2px;
}
.ab-paint-head button:hover { color: #dfe7ef; }
.ab-paint-row { display: flex; align-items: center; gap: 8px; }
.ab-paint-row.hidden { display: none; }
.ab-paint-row input[type="color"] { flex: 1; height: 30px; padding: 2px; cursor: pointer; }
.ab-paint-row button {
  background: #14202f; border: 1px solid #2a3a4e; color: #dfe7ef; border-radius: 6px;
  font-size: 12px; padding: 4px 10px; cursor: pointer;
}
.ab-paint-row button:hover { border-color: #3d557a; }
.ab-paint-hint { font-size: 11px; color: #6f8093; }

/* ---------- end-of-game screen (ui/endscreen.js) ---------- */
#endscreen { position: fixed; inset: 0; z-index: 60; }
#endscreen.hidden { display: none; }

/* while the end screen is up, mute the in-game chrome that could bleed
   through or float above it (join picker, toasts) */
body.es-open #join, body.es-open #winner-banner, body.es-open #eventfeed,
body.es-open #entity-tip,
body.es-open #world-labels, body.es-open #float-layer { display: none !important; }

/* screen-edge glow in the winner's faction colour (--wc set by endscreen.js) */
.es-glow {
  position: absolute; inset: 0; pointer-events: none;
  box-shadow: inset 0 0 130px 26px var(--wc, #f0c04a);
  animation: es-glow-pulse 2.4s ease-in-out infinite;
}
.es-glow2 { box-shadow: inset 0 0 60px 6px var(--wc, #f0c04a); animation-delay: -1.2s; opacity: .6; }
.es-glow-soft { box-shadow: inset 0 0 90px 10px var(--wc, #f0c04a); opacity: .38; animation: none; }
@keyframes es-glow-pulse {
  0%, 100% { opacity: .55; }
  50% { opacity: 1; }
}

/* --- phase 1: victory splash --- */
.es-intro {
  position: absolute; inset: 0; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; text-align: center; padding: 24px;
  background: radial-gradient(ellipse at center, rgba(5, 10, 18, .72) 0%, rgba(5, 10, 18, .45) 70%, rgba(5, 10, 18, .15) 100%);
}
.es-crown { line-height: 0; color: #f0c04a; animation: es-drop .9s cubic-bezier(.2, 1.7, .4, 1) both; filter: drop-shadow(0 6px 18px rgba(0, 0, 0, .6)); }
.es-crown svg { width: 74px; height: 74px; }
.es-vtitle {
  font-size: 19px; font-weight: 700; letter-spacing: 11px; text-indent: 11px; color: #f0c04a;
  animation: es-fade .7s .35s both;
}
.es-vname {
  font-size: clamp(34px, 7vw, 64px); font-weight: 800; color: #fff; line-height: 1.1;
  text-shadow: 0 0 26px var(--wc, #f0c04a), 0 0 70px var(--wc, #f0c04a), 0 3px 10px rgba(0, 0, 0, .7);
  animation: es-slam .65s .15s cubic-bezier(.2, 1.4, .4, 1) both;
  max-width: 92vw; overflow-wrap: anywhere;
}
.es-vname::after {
  content: ''; display: block; height: 4px; border-radius: 2px; margin: 14px auto 0;
  width: min(320px, 60%); background: var(--wc, #f0c04a);
  box-shadow: 0 0 18px var(--wc, #f0c04a);
}
.es-vreason { font-size: 16px; color: #d5dee8; animation: es-fade .7s .8s both; max-width: 640px; }
.es-continue { margin-top: 26px; font-size: 15px; padding: 12px 34px; border-radius: 10px; animation: es-fade .7s 1.3s both; }
@keyframes es-drop {
  from { transform: translateY(-90px) scale(.6); opacity: 0; }
  to { transform: translateY(0) scale(1); opacity: 1; }
}
@keyframes es-slam {
  from { transform: scale(2.4); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}
@keyframes es-fade { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* --- phase 2: the match report --- */
.es-stats {
  position: absolute; inset: 0; overflow-y: auto; overscroll-behavior: contain;
  background: rgba(9, 15, 23, .97);
  padding: 24px min(5vw, 46px) 46px;
}
.es-head { display: flex; align-items: flex-start; gap: 14px; flex-wrap: wrap; margin-bottom: 18px; }
.es-head-l { margin-right: auto; min-width: 0; }
.es-h1 { font-size: 30px; font-weight: 800; overflow-wrap: anywhere; display: flex; align-items: center; gap: 10px; }
.es-h1-ico { line-height: 0; color: #f0c04a; flex: none; }
.es-h1-ico svg { width: 28px; height: 28px; }
.es-hsub { color: #9fb0c0; font-size: 14px; margin-top: 4px; }
/* the factions that make up a winning SIDE, under the team's name. Quieter than
   the headline on purpose: the team is what won, the names are who it was. */
.es-hteam { color: #c8b47e; font-size: 13px; margin-top: 2px; letter-spacing: .3px; }
.es-vteam {
  color: #d9c48c; font-size: clamp(15px, 2.4vw, 22px); margin-top: 6px;
  letter-spacing: 1px; text-align: center;
}
/* which side a faction fought on, on its standings card */
.es-stand-team {
  margin-top: 4px; font-size: 10px; font-weight: 700; letter-spacing: .4px;
  color: #93a6b8; text-transform: uppercase;
}
.es-head-r { display: flex; gap: 8px; flex: none; }
.es-exit { font-size: 14px; display: inline-flex; align-items: center; gap: 7px; }
.es-exit svg { width: 16px; height: 16px; }
.es-exit-big { font-size: 16px; padding: 13px 40px; border-radius: 10px; gap: 9px; }
.es-exit-big svg { width: 18px; height: 18px; }
.es-watch { color: #9fb0c0; }
/* a small inline gold coin (the old 🪙 gold marker) */
.es-coin { display: inline-flex; align-items: center; color: #f0c04a; line-height: 0; margin-left: 4px; vertical-align: -1px; }
.es-coin svg { width: 1em; height: 1em; }
.es-note { color: #9fb0c0; margin: 12px 0; }
/* a lead line sitting under a section title, above that section's cards */
.es-note-sub { font-size: 12.5px; line-height: 1.5; margin: -2px 0 10px; max-width: 70ch; }
.es-foot { display: flex; justify-content: center; margin-top: 26px; }

.es-section { font-size: 16px; font-weight: 700; color: #e8e2d0; margin: 26px 0 10px; }

/* standings strip */
.es-standings { display: flex; gap: 10px; flex-wrap: wrap; }
.es-stand {
  display: flex; align-items: center; gap: 10px; padding: 10px 14px;
  background: #121d2a; border: 1px solid #3a4a5c; border-radius: 10px;
}
.es-stand-win { border-color: #f0c04a; box-shadow: 0 0 14px rgba(240, 192, 74, .25); }
.es-stand-dead { opacity: .62; }
.es-stand-medal { min-width: 28px; display: flex; justify-content: center; align-items: center; color: #9fb0c0; font-weight: 700; font-size: 15px; }
.es-stand-medal svg { width: 22px; height: 22px; }
.es-stand-win .es-stand-medal { color: #f0c04a; }
.es-stand-dead .es-stand-medal { color: #e08585; }
.es-stand-name { display: flex; align-items: center; gap: 7px; font-weight: 700; }
.es-stand-sub { color: #9fb0c0; font-size: 12px; }

/* awards (stat tiles) */
.es-awards { display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); }
.es-award {
  background: #121d2a; border: 1px solid #3a4a5c; border-radius: 10px;
  padding: 12px 13px; display: flex; flex-direction: column; gap: 3px;
}
.es-award-icon { line-height: 0; color: #f0c04a; }
.es-award-icon svg { width: 24px; height: 24px; }
.es-award-val { font-size: 24px; font-weight: 650; color: #e8e2d0; display: flex; align-items: center; }
.es-award-name { font-size: 13px; font-weight: 700; color: #f0c04a; }
.es-award-who { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: #e8e2d0; min-width: 0; }
.es-award-who span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.es-award-blurb { font-size: 11px; color: #6f8093; }

/* chart cards */
.es-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); }
.es-card { background: #121d2a; border: 1px solid #3a4a5c; border-radius: 12px; padding: 12px 14px 10px; min-width: 0; }
.es-card-title { font-size: 13.5px; font-weight: 700; color: #e8e2d0; margin-bottom: 8px; }
.es-svg { display: block; width: 100%; height: auto; }
.es-svg .es-tick { font-variant-numeric: tabular-nums; }
.es-svg .es-bar:hover { filter: brightness(1.18); }
.es-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 6px; }
.es-legend-item { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: #9fb0c0; }
.es-legend-key { display: inline-block; width: 14px; height: 3px; border-radius: 2px; flex: none; }
.es-dot { display: inline-block; width: 11px; height: 11px; border-radius: 50%; flex: none; border: 2px solid #121d2a; box-shadow: 0 0 0 1px rgba(255,255,255,.18); }

/* crosshair / bar tooltip (fixed, viewport coords) */
.es-tip {
  position: fixed; z-index: 70; pointer-events: none;
  background: #17222f; border: 1px solid #3a4a5c; border-radius: 8px;
  padding: 8px 11px; font-size: 12.5px; box-shadow: 0 8px 26px rgba(0, 0, 0, .5);
  max-width: 260px;
}
.es-tip-title { color: #9fb0c0; font-size: 11.5px; margin-bottom: 5px; }
.es-tip-row { display: flex; align-items: center; gap: 7px; padding: 1.5px 0; }
.es-tip-row b { color: #e8e2d0; font-variant-numeric: tabular-nums; }
.es-tip-name { color: #9fb0c0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.es-tip-foot { color: #9fb0c0; font-size: 11.5px; margin-top: 5px; padding-top: 5px; border-top: 1px solid #2a3846; }

/* the full ledger */
.es-table-wrap { overflow-x: auto; background: #121d2a; border: 1px solid #3a4a5c; border-radius: 12px; }
.es-table { border-collapse: collapse; width: 100%; font-size: 13px; }
.es-table th, .es-table td { padding: 7px 14px; text-align: right; white-space: nowrap; }
.es-table thead th { border-bottom: 1px solid #3a4a5c; position: sticky; top: 0; background: #121d2a; }
.es-th-stat, .es-td-stat { text-align: left !important; color: #9fb0c0; }
/* a component of the row above it (the score sources under Final score) */
.es-td-sub { padding-left: 26px !important; }
.es-th-p { display: inline-flex; align-items: center; gap: 7px; font-weight: 700; color: #e8e2d0; }
.es-th-medal { display: inline-flex; align-items: center; color: #f0c04a; line-height: 0; }
.es-th-medal svg { width: 15px; height: 15px; }
.es-td-val { color: #e8e2d0; font-variant-numeric: tabular-nums; }
.es-td-best { color: #f0c04a; font-weight: 700; }
.es-tr-section { text-align: left !important; font-weight: 700; color: #e8e2d0; padding-top: 14px !important; border-top: 1px solid rgba(255,255,255,.06); }
.es-tr-sec { display: inline-flex; align-items: center; gap: 8px; }
.es-tr-sec-ico { display: inline-flex; align-items: center; color: #f0c04a; line-height: 0; }
.es-tr-sec-ico svg { width: 17px; height: 17px; }
.es-table tbody tr:hover td { background: rgba(255, 255, 255, .03); }

/* the gold winner banner doubles as the report-reopen button */
#winner-banner { cursor: pointer; }
#winner-banner:hover { background: rgba(44, 36, 10, .97); }

@media (max-width: 700px) {
  .es-head-r { width: 100%; }
  .es-exit { flex: 1; }
  .es-grid { grid-template-columns: 1fr; }
}

/* ---- player stats page (ui/stats.js) -------------------------------------
   The match-report half reuses the end screen's .es-* card/chart classes, so
   only the page chrome, the lifetime tiles and the match rows live here. */
.stats-modal { width: 980px; max-height: 88vh; display: flex; flex-direction: column; padding: 22px 24px; }
.stats-body { overflow-y: auto; overscroll-behavior: contain; min-height: 0; }
.stats-h2 { font-size: 16px; font-weight: 700; color: #e8e2d0; margin: 22px 0 10px; }
.stats-h2:first-child { margin-top: 4px; }
.stats-note { color: #9fb0c0; margin: 10px 0; }
.stats-note.error { color: #e08585; }

/* lifetime totals */
.stats-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: 10px; }
.stats-tile {
  background: #121d2a; border: 1px solid #3a4a5c; border-radius: 12px;
  padding: 12px 10px; text-align: center;
}
.stats-tile-icon { line-height: 0; color: #f0c04a; }
.stats-tile-icon svg { width: 22px; height: 22px; }
.stats-tile-val { font-size: 22px; font-weight: 800; color: #e8e2d0; margin: 6px 0 2px; }
/* a tile whose value is a name, not a number — must not overflow the tile */
.stats-tile-val.text { font-size: 15px; line-height: 1.25; margin-top: 8px; overflow-wrap: anywhere; }
.stats-tile-lbl { font-size: 11.5px; color: #9fb0c0; text-transform: uppercase; letter-spacing: .04em; }

/* damage split by unit */
.stats-dmg { display: flex; flex-direction: column; gap: 6px; }
.stats-dmg-row { display: flex; align-items: center; gap: 10px; }
.stats-dmg-name { flex: 0 0 118px; font-size: 13px; color: #e8e2d0; }
.stats-dmg-track { flex: 1; height: 14px; background: #121d2a; border: 1px solid #3a4a5c; border-radius: 7px; overflow: hidden; }
.stats-dmg-fill { height: 100%; background: linear-gradient(90deg, #6b4a2a, #f0c04a); }
.stats-dmg-val { flex: 0 0 64px; text-align: right; font-size: 12.5px; color: #9fb0c0; font-variant-numeric: tabular-nums; }
/* same bar list, games-per-Royal colouring (see barList in ui/stats.js) */
.stats-bars-royal .stats-dmg-fill { background: linear-gradient(90deg, #3a4f7a, #7fc8f0); }
/* royal names are longer than unit names — give the label column room */
.stats-bars-royal .stats-dmg-name { flex-basis: 150px; }

/* match history rows */
.stats-games { display: flex; flex-direction: column; gap: 8px; }
.stats-game { background: #121d2a; border: 1px solid #3a4a5c; border-radius: 12px; overflow: hidden; }
.stats-game-win { border-color: #6b5a2a; }
.stats-game-head {
  width: 100%; display: flex; align-items: center; gap: 12px; padding: 12px 14px;
  background: none; border: 0; cursor: pointer; text-align: left; color: inherit; font: inherit;
}
.stats-game-head:hover { background: rgba(255, 255, 255, .04); }
.stats-game-flag { flex: none; line-height: 0; }
.stats-game-flag svg { width: 20px; height: 20px; }
.stats-game-flag.win { color: #f0c04a; }
.stats-game-flag.loss { color: #e08585; }
.stats-game-mid { display: flex; flex-direction: column; gap: 3px; margin-right: auto; min-width: 0; }
.stats-game-name { font-size: 14.5px; font-weight: 700; color: #e8e2d0; overflow-wrap: anywhere; }
.stats-game-sub { font-size: 12px; color: #9fb0c0; }
.stats-game-verdict {
  flex: none; font-size: 11.5px; font-weight: 800; letter-spacing: .06em;
  padding: 4px 9px; border-radius: 6px;
}
.stats-game-verdict.win { background: rgba(240, 192, 74, .16); color: #f0c04a; }
.stats-game-verdict.loss { background: rgba(224, 133, 133, .13); color: #e08585; }
.stats-game-caret { flex: none; color: #9fb0c0; transition: transform .15s; }
.stats-game-open .stats-game-caret { transform: rotate(180deg); }
.stats-game-report { padding: 4px 16px 20px; border-top: 1px solid #3a4a5c; }
/* the report's own section headings sit tighter inside a history row */
.stats-game-report .es-section:first-child { margin-top: 14px; }

@media (max-width: 700px) {
  .stats-game-verdict { display: none; }
  .stats-dmg-name { flex-basis: 84px; }
}

/* guest notice on the stats page (guests are not tracked server-side) */
.stats-guest { text-align: center; padding: 34px 24px 40px; }
.stats-guest-icon { line-height: 0; color: #9fb0c0; }
.stats-guest-icon svg { width: 44px; height: 44px; }
.stats-guest-h { font-size: 19px; font-weight: 700; color: #e8e2d0; margin: 12px 0 8px; }
.stats-guest-p { color: #9fb0c0; font-size: 14px; max-width: 460px; margin: 0 auto; line-height: 1.55; }
.stats-guest-note { color: #7d8d9c; font-size: 12.5px; max-width: 460px; margin: 12px auto 0; }

/* ---- friends (ui/friends.js) ----------------------------------------------
   A roster panel, not a chat app: one row per person, presence on the left, and
   the actions that person affords on the right. Deliberately borrows the stats
   page's chrome (.stats-h2, the ✕) — it opens from the same header and reading
   as a second kind of window would be noise. */
.friends-modal { width: 560px; max-height: 82vh; display: flex; flex-direction: column; padding: 22px 24px; }
.fr-body { overflow-y: auto; overscroll-behavior: contain; min-height: 0; margin-top: 12px; }

/* the lobby header's button: a count of who's on right now, plus a dot when
   somebody is waiting on an answer */
#browser-friends { position: relative; display: inline-flex; align-items: center; gap: 6px; }
.fr-badge {
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px;
  background: #2e7d4f; color: #d6ffe6; font-size: 11px; font-weight: 800; line-height: 18px;
  text-align: center;
}
.fr-reqdot {
  position: absolute; top: -3px; right: -3px; width: 9px; height: 9px; border-radius: 50%;
  background: #e2b760; border: 1px solid #17222f;
}

/* the header's own corner: the person+ that opens the search, then the ✕.
   There are no tabs — requests live in the list itself, so adding somebody is
   the only other thing this window does and one icon is the whole of it. */
.fr-head-acts { display: flex; align-items: center; gap: 10px; }
/* orange, like every other "do the thing" button on this screen (.primary, the
   invite action, the support ?) — adding somebody is the only action this
   header carries, so it may as well look like one. `.on` = the search drawer is
   open and the icon now means Close, so it sinks to a darker, pressed orange. */
.fr-iconbtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0; border-radius: 8px;
  background: #b4552d; border: 1px solid #d97742; color: #ffeede; cursor: pointer;
}
.fr-iconbtn:hover { background: #cc6636; border-color: #e88b57; color: #fff; }
.fr-iconbtn.on { background: #6d3316; border-color: #a84f2a; color: #f0b48c; }

/* one line of feedback for the last action, in the panel rather than a toast —
   the answer belongs beside the button that asked the question */
.fr-note {
  margin-top: 10px; padding: 8px 10px; border-radius: 8px; font-size: 13px;
  background: rgba(224, 133, 133, .1); border: 1px solid #6d3b3b; color: #f0a6a6;
}
.fr-note.good { background: #142218; border-color: #2e5a43; color: #8fe3a0; }

.fr-list { display: flex; flex-direction: column; gap: 6px; }
.fr-sub {
  font-size: 11.5px; color: #9fb0c0; text-transform: uppercase; letter-spacing: .04em;
  margin: 14px 0 6px;
}
.fr-sub:first-child { margin-top: 2px; }
.fr-row {
  display: flex; align-items: center; gap: 10px; padding: 9px 11px;
  background: #121d2a; border: 1px solid #3a4a5c; border-radius: 10px;
}
.fr-dot {
  flex: none; width: 9px; height: 9px; border-radius: 50%;
  background: #46586c; box-shadow: none;
}
.fr-dot.on { background: #55e08f; box-shadow: 0 0 7px rgba(85, 224, 143, .7); }
/* a request waiting on an answer, sitting in the same list as the friends it
   wants to join — the warm edge is what marks it as needing something */
.fr-row.pending { border-color: #5c4a2c; background: #1a1a17; }
/* the name + presence line doubles as the button onto that friend's record */
.fr-who {
  flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; text-align: left;
  background: none; border: 0; padding: 0; color: inherit; font: inherit; cursor: pointer;
}
.fr-who.static { cursor: default; }
.fr-who:not(.static):hover .fr-name { color: #ffd766; }
.fr-name { font-size: 14px; font-weight: 700; color: #e8e2d0; overflow-wrap: anywhere; }
.fr-where { font-size: 12px; color: #9fb0c0; overflow-wrap: anywhere; }
.fr-acts { flex: none; display: flex; align-items: center; gap: 6px; }
.fr-act { padding: 6px 11px; font-size: 12.5px; font-weight: 600; }
.fr-act.primary { background: #2e7d4f; border-color: #45a06a; }
.fr-act.primary:hover { background: #369460; }
.fr-act.invite { background: #b4552d; border-color: #d97742; }
.fr-act.invite:hover { background: #cc6636; }
.fr-act.danger { background: #6d3b3b; border-color: #8c4a4a; color: #f0c8c8; }
.fr-x {
  padding: 4px 8px; font-size: 12px; color: #8598a8; background: none; border: 1px solid transparent;
}
.fr-x:hover { background: #2a1c22; border-color: #55414a; color: #f0b199; }
.fr-confirm { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #9fb0c0; }
.fr-status { font-size: 12px; color: #7d8d9c; }
.fr-empty { color: #9fb0c0; font-size: 13.5px; line-height: 1.6; padding: 18px 4px; }
.fr-empty.small { padding: 6px 4px 2px; font-size: 12.5px; }
.fr-hint { color: #7d8d9c; font-size: 12px; line-height: 1.55; margin-top: 12px; }
/* the search drawer the person+ icon opens: it sits ABOVE the roster rather
   than replacing it, so the list you're adding to stays in view */
.fr-add {
  padding: 12px; margin-bottom: 14px; border-radius: 10px;
  background: #0d1620; border: 1px solid #2c3a4a;
}
.fr-add .fr-hint { margin-top: 10px; }
.fr-add .fr-list { margin-top: 10px; }
.fr-search { display: flex; gap: 8px; }
.fr-search input {
  flex: 1; padding: 10px; border-radius: 8px; border: 1px solid #3a4a5c;
  background: #0e1721; color: #e8e2d0; font-size: 14px;
}
.fr-guest { text-align: center; padding: 26px 18px 30px; }
.fr-guest-h { font-size: 18px; font-weight: 700; color: #e8e2d0; margin-bottom: 8px; }
.fr-guest-p { color: #9fb0c0; font-size: 13.5px; max-width: 420px; margin: 0 auto; line-height: 1.6; }

/* An incoming invite: a small bottom-right card, never a modal, so it can't
   swallow a click meant for whatever is behind it.
   ⚠️ It must sit ABOVE the modal layer (50), not below. At 49 it was still
   VISIBLE through a translucent `.modal-backdrop` but every click on it landed
   on that backdrop instead — the friends panel closed and the invite did
   nothing. A card you can see but can't press is worse than either extreme; at
   51 it stays pressable, and being a 260px corner box it never covers a
   centred modal's own controls. The end screen (60) and toast (55) still win. */
/* `bottom` clears the in-game action bar, which moved into this very corner
   when the bottom centre became the card hand — landing the invite on top of
   the Build disc would make both unpressable while it is up. It is measured off
   that disc (64px tall, 14px off the bottom edge) plus a gap, so it has to move
   whenever the disc's size does. */
#friend-invite {
  position: fixed; right: 18px; bottom: 90px; z-index: 51; width: 260px;
  background: #17222f; border: 1px solid #d97742; border-radius: 12px; padding: 13px 15px;
  box-shadow: 0 10px 32px rgba(0, 0, 0, .6);
  animation: fiIn .22s ease;
}
@keyframes fiIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
.fi-head { font-size: 13.5px; color: #e8e2d0; }
.fi-head b { color: #ffd766; }
.fi-room { font-size: 12.5px; color: #9fb0c0; margin: 4px 0 11px; overflow-wrap: anywhere; }
.fi-acts { display: flex; gap: 7px; }
.fi-acts button { flex: 1; padding: 7px 6px; font-size: 12.5px; }

@media (max-width: 700px) {
  .friends-modal { width: 100%; max-height: 88vh; padding: 18px 15px; }
  .settings-modal { width: 100%; max-height: 88vh; padding: 18px 15px; }
  #settings-page .settings-row input[type=range] { max-width: 150px; }
  /* ⚠️ the graphics labels are words rather than a slider track, so they need
     more of the row than a slider does — "Performance — smoothest" must not be
     truncated on the one screen size these settings exist for */
  .set-select { max-width: 172px; }
  .fr-row { flex-wrap: wrap; row-gap: 8px; }
  .fr-acts { margin-left: auto; }
  /* on a phone the corner stack is a column of discs, so the card clears both */
  #friend-invite { right: 10px; left: 10px; bottom: calc(116px + env(safe-area-inset-bottom)); width: auto; }
}

/* ---------- THE HAND (ui/hand.js) ----------------------------------------
   This crown's decrees and its Rally Flag as playing cards along the bottom
   edge. It replaced the ⚜️ Royal Decree window entirely, so everything that
   window owned — the seal banner, the decree grid, the Rally Flag strip and the
   action-bar button that opened it — lives here now instead.

   The whole thing is one transform per card driven by custom properties, so a
   state change (hand raised, card hovered, card dragged) is a class swap and
   nothing is laid out twice:

     --x    horizontal offset from the row's centre     (per card, from JS)
     --lift how far the whole hand is out of hiding     (shared, by state)
     --pop  extra lift for the one card being hovered   (shared, by state)

   THE ROW IS LEVEL. It used to bow and tilt like a hand somebody is holding
   (--arc / --rot per card, flattened on hover by --arcf / --rotf multipliers
   rather than overrides, since the two were set inline and no state rule could
   have won against them on specificity). It read beautifully and was the wrong
   shape for the job: every card sat at a different height and angle, which is
   exactly what makes a row hard to compare across, and the bow spent vertical
   room the peek then had to pay for. All six properties are gone rather than
   pinned to zero — what keeps the row from reading as a flat strip of UI is the
   cards' own lean (.hc-inner's rotateX) and the sheen walking along it. */

/* ⚠️⚠️ --hand-peek IS DECLARED ON :root, NOT ON #hand, BECAUSE THINGS OUTSIDE
   THE HAND HAVE TO MEASURE IT. How tall the resting hand stands is the one
   number every other bottom-edge surface is positioned against — the placement
   hint, the panels, and on a phone the Build disc itself, which sits ABOVE the
   cards rather than beside them (see the phone block). ui/hand.js's build()
   writes the live value onto document.documentElement per deal; a declaration
   on #hand would sit NEARER the cards than that and shadow it outright, since
   a custom property resolves from the nearest ancestor that declares it
   whatever the specificity — the same rule the deck's own --c-* colours rely on
   from the other side.
   ⚠️ THE VALUE HERE IS ONLY THE FALLBACK BEFORE THE FIRST DEAL: build() writes
   the card height it just worked out plus FULL_CARD_GAP, so the whole card
   including its price is on screen at rest. It was a title's worth (84px,
   derived from the face's own metrics) back when the hand bowed and you raised
   it to read the rest — but the half a card is chosen on is the half at the
   FOOT, and a peek that cut that off hid the one number that decides whether
   you can play it. */
:root { --hand-peek: 150px; }
#hand {
  position: fixed; left: 0; right: 0; bottom: 0; height: 0; z-index: 26;
  pointer-events: none;
  /* ⚠️⚠️ A FINGER RUNNING ALONG THE ROW READS THE CARDS — IT DOES NOT SELECT
     THEM. A card is a small block of text (a name, a price, a plate of prose)
     under a gesture that presses and slides, which on a touch screen is also
     precisely how text is selected: browsing the hand came out as a blue
     highlight dragged across the names and prices, with the selection then
     stealing the gesture from move() below. grab()'s preventDefault on
     pointerdown does not cover it — the UA starts a selection from its own
     long-press, and iOS raises the callout on top of that. Nothing in here is
     text anybody would ever want to copy, so it is switched off for the whole
     surface rather than card by card. */
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  /* How much of a card shows while the hand is down — WHICH IS ALL OF IT.
     ⚠️ --hand-peek itself is declared on :root and written there by build();
     see the note above the selector for why it may not be declared here. */
  --lift: var(--hand-peek);
  --pop: 76px;         /* the hovered card's extra rise */
  --cw: 106px;
  --ch: 150px;
  --hw: 800px;
  /* ground at EACH END the row must not spread into, so it stays CENTRED on the
     screen while still clearing the Build disc in the bottom-right corner (see
     layout() in ui/hand.js, which reads this — and falls back to reserving it
     once and sliding left where doubling it would leave the cards no room, i.e.
     on a phone).
     ⚠️ 170px = the disc's 128 diameter + its 14px margin + the 20px of padding
     .hand-zone carries beyond the cards themselves. That last term is the one
     that bites: it is the HIT AREA that has to clear the disc, not the cards,
     and a strip of the disc that stops taking presses is a player unable to get
     back to the other deck.
     ⚠️ IT APPLIES TO BOTH DECKS. It used to be zero for the decrees on a desktop,
     on the grounds that a seven-card fan never reaches the corner — which is
     true down to about 1150px and false below it: at 1100 the resting decree
     hand's zone crossed the disc's left edge by 10px. That was survivable while
     the disc merely opened a window; it is not now that it is the only way to
     the buildings. Above ~1320px this costs the decrees nothing at all, because
     HAND_MAX binds first.
     ⚠️ IT COMES DOWN WITH THE DISC AT EVERY BREAKPOINT, and to ZERO on a phone,
     where the disc leaves the row's band entirely — see the action-bar blocks.
     The reserve and the disc's diameter are ONE decision and are always written
     together; a reserve left behind a shrunken disc is a fan needlessly shoved
     off centre, and one left behind a grown disc is a button that has stopped
     taking presses. */
  --hand-reserve: 170px;
}
#hand.hidden { display: none; }
/* ⚠️ THE RAISE IS DERIVED FROM THE PEEK, not a fixed 172px. The peek is now a
   whole card and the card's height moves with the viewport, so a constant would
   be a rise of almost nothing on a tall card — or a DROP. Derived, the travel is
   the same 26px whatever size the cards came out at, and that travel is the
   affordance: what the raise buys is room for the detail plate above the card,
   not a first look at the card itself. */
#hand.up { --lift: calc(var(--hand-peek) + 26px); }

/* The hit area IS the cards' positioning parent, deliberately. pointerleave
   fires when the pointer leaves an element *and its descendants*, so cards
   parented anywhere else would make reaching for one read as leaving the hand
   and drop it out from under the reach. It is only as wide as the fan, so the
   bottom corners of the map stay clickable, and only as tall as the peek until
   the hand is actually up — an invisible 250px strip across the bottom of a
   strategy map is a lot of ground to make unclickable for a hover affordance. */
.hand-zone {
  position: absolute; bottom: 0; left: 50%;
  /* --hand-shift is 0 in the ordinary case: the row is centred and the reserve
     is taken off BOTH ends instead. ui/hand.js's layout() writes it, because
     whether the centred width still fits the cards depends on how many there
     are — which the stylesheet cannot know. */
  transform: translateX(calc(-50% - var(--hand-shift, 0px)));
  /* just the row, plus the Rally Flag's --raise standing proud of it. It used to
     carry 46px of slack for the bow's middle cards; with the row level there is
     nothing above the peek but that one card, and an invisible band across the
     bottom of a strategy map is ground made unclickable for a hover affordance */
  width: var(--hw); height: calc(var(--hand-peek) + 22px);
  pointer-events: auto;
  transition: height 180ms ease;
}
#hand.up .hand-zone, #hand.busy .hand-zone { height: 300px; }
.hand-cards { position: absolute; inset: 0; }

/* ---------- A ROW WIDER THAN THE SCREEN, SWIPED SIDEWAYS ----------
   The building deck on a phone: nine cards at a comfortable width run off both
   ends rather than squeezing into 390px, and a sideways slide moves the whole
   row 1:1 with the finger (ui/hand.js's browseMove). `--hand-scroll` is written
   by setScroll and clamped so neither end can be dragged past the screen edge.

   ⚠️ THE TRANSFORM IS CLASS-GATED RATHER THAN ALWAYS-ON-AT-ZERO, because a
   transform on .hand-cards opens a STACKING CONTEXT and traps every card's
   z-index inside it — anything else absolutely positioned in #hand (the ⚰️
   fallen-crown banner is the one today) then draws over the cards rather than
   under them. Gating it costs nothing and keeps a row that does not scroll
   stacking exactly as it always did.
   ⚠️ No transition: the row must track the finger exactly. A rebuild only ever
   re-CLAMPS the value, so there is nothing to ease. */
#hand.swipe .hand-cards { transform: translateX(var(--hand-scroll, 0px)); }

/* --raise is an extra lift on top of the fan's own bow. Only the Rally Flag has
   one: it stands in the middle of the hand and needs to read as a different
   KIND of card from the six proclamations around it, not merely a differently
   coloured one. Deliberately absent from the .hot / .lifting / .aiming
   transforms below — every card should lift to the same height when it is the
   one being read or cast, so the raise says "this sits apart" without also
   saying "this behaves differently". */
.hand-card {
  position: absolute; bottom: 0; left: 50%;
  width: var(--cw); height: var(--ch);
  transform:
    translate(calc(-50% + var(--x)), calc(100% - var(--lift) - var(--raise, 0px)))
    scale(var(--sc, 1));
  transform-origin: 50% 100%;
  /* the depth the face's rotateX is seen through. Shallow on purpose: at a
     card's 150px this is a lean, not a diorama — enough that the top edge
     visibly recedes and the ornament along it foreshortens, and no more. */
  perspective: 640px;
  transition: transform 220ms cubic-bezier(.2, .9, .25, 1.05), filter 180ms ease;
  pointer-events: auto; cursor: pointer;
  touch-action: none;              /* the drag is ours; don't let it scroll or pan */
  -webkit-tap-highlight-color: transparent;
  will-change: transform;
}

/* ---------- THE FOUR HOUSES ----------
   A DECREE CARD IS PRINTED IN ITS HOUSE'S COLOURS, and so is everything else on
   the screen. ⚠️ THE VALUES ARE NOT HERE — they live in the one block per house
   at the top of the file (THE IN-GAME UI WEARS YOUR HOUSE'S COLOURS), alongside
   that house's HUD colours, because a house is ONE decision and split across
   two places is how one of them gets changed and the other does not. What the
   `--c-*` properties mean is documented there too.

   ⚠️ THEY ARE DECLARED ON <body>, NOT ON #hand, and that is load-bearing rather
   than tidiness. A custom property is resolved from the NEAREST ancestor that
   declares it, whatever the specificity — so a neutral fallback left on #hand
   would sit between the card and the house's values on <body> and win every
   time, and the whole deck would draw grey-blue for every faction. (The two
   states that DO re-declare them — `.poor` and `.is-rally` — rely on exactly
   that rule from the other side: they sit nearer the card than <body> does.)

   ⚠️ The dawn ceremony reads the same properties for the same reason: its fan
   flies down onto the hand's own slots and each card is swapped for the real
   one at the seam, so a ceremony on a different palette would make that swap
   visible on the one morning it is hardest to miss. */

/* the face. ⚠️ The rotateX is the 3D: the cards LEAN BACK in the hand at rest
   and stand upright when one is picked out (--tiltx, dropped to 0 by every
   state that lifts a card). It is on .hc-inner rather than on .hand-card
   deliberately — the card's own transform is the fan geometry (slot, bow, tilt,
   drag) and five rules rewrite it, so a perspective term added there would have
   to be repeated in every one of them and would be silently lost from whichever
   was missed. */
.hc-inner {
  position: absolute; inset: 0; overflow: hidden;
  display: flex; flex-direction: column; align-items: center;
  /* ⚠️ THE BOTTOM PADDING IS THE FOOT ORNAMENT'S ROOM, not slack. The frame
     (ui/hand.js FRAMES) prints a motif on the bottom rule, and at the 8px this
     was the price was struck straight through it. (The top padding used to be
     load-bearing too, back when --hand-peek was derived from the face's own
     metrics; it is derived from the whole card height now, so both ends of this
     are just the frame's room.) */
  padding: 11px 7px 13px; box-sizing: border-box;
  border-radius: 11px;
  /* A pool of the house's own light behind the icon, over the printed ground.
     ⚠️ Its centre follows .hc-icon's margin-top — the icon was dropped a little
     off the top rule (see below) and a glow left at 28px would have sat above
     the thing it is meant to be lighting. A building card has a model rather
     than an emoji there, and at this radius the six pixels are invisible on it. */
  background:
    radial-gradient(56px 44px at 50% 34px, rgba(var(--c-glow), .17), rgba(0, 0, 0, 0) 72%),
    linear-gradient(168deg, var(--c-bg1) 0%, var(--c-bg2) 46%, var(--c-bg3) 100%);
  border: 1px solid var(--c-frame2);
  box-shadow: var(--card-depth), var(--card-face);
  transform: rotateX(var(--tiltx, 9deg));
  transform-origin: 50% 100%;
  transition: transform 240ms cubic-bezier(.2, .9, .25, 1.05);
}

/* THE HOUSE ORNAMENT (ui/hand.js FRAMES). Colours live here so a card greying
   out, being sealed or being the Rally Flag re-strikes its border by swapping
   two custom properties rather than by knowing anything about the shapes. */
.hc-frame {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none;
}
.hc-frame .f-rule { fill: none; stroke: var(--c-frame); stroke-width: 1.3; opacity: .9; }
.hc-frame .f-thin { fill: none; stroke: var(--c-frame2); stroke-width: .8; opacity: .8; }
.hc-frame .f-orn {
  fill: none; stroke: var(--c-frame); stroke-width: 1.15; opacity: .88;
  stroke-linecap: round; stroke-linejoin: round;
}
.hc-frame .f-fill { fill: var(--c-frame); opacity: .95; }
.hc-frame .f-soft { fill: var(--c-frame); opacity: .3; }
/* dressed stone: filled enough to read as a BLOCK rather than as a little
   frame drawn in the corner of a bigger one */
.hc-frame .f-block {
  fill: var(--c-frame); fill-opacity: .3;
  stroke: var(--c-frame); stroke-width: 1; opacity: .9;
}
/* struck OUT of the ornament in the card's own ground: the break in the top
   rule the crest stands in, a leaf's midrib, a skull's sockets, a snow line */
.hc-frame .f-deep { fill: var(--c-bg1); }
.hc-frame .f-cutf { fill: var(--c-bg1); }
.hc-frame .f-cut {
  fill: none; stroke: var(--c-bg1); stroke-width: 1.2;
  stroke-linecap: round; stroke-linejoin: round;
}

/* THE SHEEN — a highlight lying across the face, slid along the fan by the
   card's slot (--sheen, set in ui/hand.js) so the whole hand is lit from one
   place. It is a wide fixed gradient that is MOVED rather than a gradient whose
   stops change, because gradient stops cannot be transitioned and the sweep on
   hover is half of what the highlight is for. */
.hc-gloss {
  position: absolute; top: 0; bottom: 0; left: -55%; right: -55%;
  pointer-events: none;
  /* ⚠️ WIDE AND FAINT. At .13 over a 12% band it read as a hard diagonal stripe
     painted on the card rather than as light falling across it — the giveaway
     being that it had a visible edge. Twice the width and two thirds the
     strength is the same amount of light with nowhere for an edge to be. */
  background: linear-gradient(102deg,
    rgba(255, 255, 255, 0) 28%, rgba(255, 255, 255, .085) 50%,
    rgba(255, 255, 255, .03) 60%, rgba(255, 255, 255, 0) 76%);
  transform: translateX(calc(var(--sheen, 0) * 5.5%));
  transition: transform 300ms cubic-bezier(.2, .9, .25, 1.05);
}
.hand-card.hot .hc-gloss { transform: translateX(16%); }

/* ⚠️ IT SITS OFF THE TOP RULE, not against it. At 4px the emoji was crowding the
   frame's crest — the one ornament that BREAKS the top rule and therefore reaches
   furthest into the face — and the card read top-heavy, with everything jammed
   under the border and a slab of empty ground above the price. Ten pixels is
   enough to give the crest its own air. (The glow pool in .hc-inner is centred on
   this number; move them together.) */
.hc-icon {
  font-size: 30px; line-height: 1; margin-top: 10px;
  filter: drop-shadow(0 3px 5px rgba(0, 0, 0, .8)) drop-shadow(0 0 9px rgba(var(--c-glow), .35));
  /* ⚠️⚠️ THE MARK IS WHAT GIVES WAY ON A SQUEEZED CARD, NOT THE TITLE.
     layout() in ui/hand.js shrinks a card to as little as ~97px on a short
     viewport, which is well under the height this face is designed at — so the
     column overflows and something has to lose. With an emoji that was decided
     by accident (a glyph is exactly its font-size, so the icon stayed 24px and
     the title kept a clipped line); the drawn mark is 1.7em, so left alone it
     WON the fight and the title collapsed to nothing — a card showing a picture,
     a price and no name (measured at 606×683: name 11px → 0px).
     An explicit height plus `min-height: 0` is what lets the box shrink at all —
     a flex item's automatic minimum is its content, and the content here is a
     replaced element — and the shrink factor is what makes it shrink FIRST.
     3 restores the emoji's own numbers exactly (icon 24px, title 11px). */
  height: 1.7em; min-height: 0; flex-shrink: 3;
}
.hc-icon .dicon { width: 100%; height: 100%; }

/* ---------- A DECREE'S DRAWN MARK ----------
   The artwork that replaced the emoji on every surface a decree names itself on
   (ui/decreeicon.js): the hand card, its hover plate, the dawn ceremony's card,
   the cast banner, the Kingdom Designer's reference sheet and the aiming pill.

   ⚠️⚠️ IT IS SIZED IN `em`, WHICH IS THE WHOLE REASON THE SWAP IS ONE CLASS.
   Every slot it lands in already sets a font-size for the emoji it replaced —
   .hc-icon 30px, .dseq-c-icon 34px, .abd-icon 25px, plus the phone and
   short-viewport overrides of the first two — so an em-sized mark inherits all
   of that and not one of those rules had to be touched. Give it a pixel size and
   the icon stops shrinking with the card on a narrow screen, silently.

   ⚠️ 1.7em is NOT the emoji's height, and is not a guess: the art is drawn with
   its own margin inside a square (the ink is ~69% of the box), so a box the size
   of the glyph lands visibly SMALLER than what it replaced — measured in game at
   1.45em, where the mark read as lost on the card. This puts the visible ink at
   about 1.17em, which sits level with the weight the emoji had.

   ⚠️ It is left free to SHRINK as a flex item (no `flex: none`), and `object-fit`
   is what makes that safe: on a squeezed card the mark letterboxes down a little
   rather than distorting or pushing the title out of the bottom of the card.

   ⚠️ No filter of its own: every one of those slots already carries the drop
   shadow and the house glow, and a filter on the parent applies to this too. */
.dicon {
  display: block; margin: 0 auto;
  width: 1.7em; height: 1.7em;
  object-fit: contain;
}
/* the two INLINE slots — a title and a pill, where the mark sits before words on
   the same line rather than centred over them (`dicon-in`, and the pill's own
   node, which has no room for a class of its own). ⚠️ `middle` rather than a
   hand-tuned baseline offset: the mark lands beside text at four different font
   sizes here, and only `middle` stays right at all of them. */
.dicon-in, #placement-hint .dicon {
  display: inline-block; margin: 0; vertical-align: middle;
}
/* the emoji standing in for a mark whose artwork has not been drawn yet — see
   the error listener in ui/decreeicon.js. Inline, since a fallback lands in both
   kinds of slot and a centred block is the safer of the two to get wrong. */
.dicon-glyph { display: inline-block; line-height: 1; }

/* THE DAY'S WORLD EVENT wears the same kind of mark (ui/eventicon.js), in the
   three slots that announce it: the tab under the day box, the window it opens,
   and the dawn ceremony's third beat. Sized in `em` for the decree mark's reason
   — each of those slots already sets a font-size, and two of them override it on
   a phone, so one class rides all five rules with no edit.

   ⚠️ 1.3em, not the decree mark's 1.7em, and the difference is measured rather
   than a taste call: this art is painted out to ~89% of its own square where the
   line-art marks fill ~69% of theirs, so the same visible weight (about 1.17em of
   ink) needs a smaller box. Match the INK, not the box, if either set is redrawn.

   ⚠️ No filter of its own — the two big slots already carry a drop shadow, and a
   filter on the parent applies to this too. */
.eicon {
  display: block; margin: 0 auto;
  width: 1.3em; height: 1.3em;
  object-fit: contain;
}
/* the one INLINE slot — the Rally Flag banner's title (ui/daybreak.js), where
   the mark sits before words on the same line rather than centred over them.
   `.dicon-in`'s twin, and `middle` for its reason: the mark lands beside text
   that is 32px here and would be a hand-tuned baseline offset at any other. */
.eicon-in { display: inline-block; margin: 0; vertical-align: middle; }
.eicon-glyph { display: inline-block; line-height: 1; }
/* A BUILDING CARD WEARS THE ACTUAL MODEL where a decree wears an emoji — the
   real geometry, rendered offscreen in this faction's own colour and skin
   (ui/hand.js paintModel → render/thumbs.js). Building is the one choice a
   player then has to recognise from above, at a distance, on a busy board, so
   it is worth showing them the thing itself; it also means a building added
   later gets its card with no artwork to draw.

   ⚠️ IT TAKES WHATEVER THE NAME AND THE PRICE LEAVE IT, rather than the fixed
   42px box a decree's emoji sits in. That box existed because --hand-peek used
   to be derived from the face's own metrics, so a picture that pushed the title
   down would cut it off at the bottom of the screen. The hand shows its cards
   WHOLE now (ui/hand.js build() writes --hand-peek from the card height), so
   there is no peek line to stay above — and the dead slate between the title and
   the price was simply wasted on the one card whose picture IS the choice. */
/* ⚠️⚠️ `flex: 1 1 0`, NOT `1 1 auto`. An auto basis is the item's own content
   height, and the content here is a 128px thumbnail — so the box asked the flex
   line for 128px against a ~108px card, the line overflowed, and every
   SHRINKABLE item on it gave way proportionally. The one that gave was the
   title: "Archery Range" came out with its second line sliced in half (seen in
   game, and measured — a 29.9px name box shrunk to 22.3px). A zero basis makes
   this the item that GROWS into whatever is left instead of the one that
   demands, and the name and foot below are pinned `flex: none` so they can
   never be the thing that gives again. */
.hc-model {
  flex: 1 1 0; min-height: 0; width: 100%; margin-top: 2px;
  display: flex; align-items: center; justify-content: center;
}
.hc-model img {
  max-width: 100%; max-height: 100%;
  filter: drop-shadow(0 3px 5px rgba(0, 0, 0, .75)) drop-shadow(0 0 9px rgba(var(--c-glow), .3));
}
/* ⚠️ TWO LINES' WORTH WHETHER OR NOT THE NAME NEEDS THEM. The model is elastic
   now, so a one-line name would hand its card a bigger picture than the
   "Archery Range" beside it — and a row of pictures at two sizes stops reading
   as a row. (The old build menu's .bc-name fixed its height for exactly this.) */
/* ⚠️ 2.6em, not 2.4: .hc-name's line-height is 1.25, so two lines need 2.5em
   before the descenders — at 2.4 the second line of "Archery Range" was sliced
   off along the bottom (seen in game). Keep this ahead of the line-height if
   that ever moves. */
.hand-card.is-build .hc-name {
  flex: none; margin-top: 4px; height: 2.6em; -webkit-line-clamp: 2;
}
.hand-card.is-build .hc-foot { flex: none; }
/* an unaffordable or already-built card drains with the rest of the face (see
   .poor below), and the model is the one part no custom property reaches */
.hand-card.poor .hc-model img,
.hand-card.spent .hc-model img { filter: grayscale(1) brightness(.8) drop-shadow(0 3px 5px rgba(0, 0, 0, .7)); }
.hc-name {
  margin-top: 8px; font-size: 11.5px; font-weight: 700; line-height: 1.25;
  color: var(--c-ink); text-align: center; overflow: hidden;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .75);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
}
.hc-foot { margin-top: auto; padding-top: 6px; }

/* THE TITLE SITS IN THE MIDDLE OF THE CARD, on the two decks that have an emoji
   rather than a model. The face is icon / name / price down a flex column with
   the price pinned to the foot, so with the name tucked straight under the icon
   every decree card was signed along its top edge with a bare slab of printed
   ground between the words and the gold — the empty half of the card was the
   half the eye rests on.
   ⚠️ TWO AUTO MARGINS ON THE NAME, and the foot's own dropped to zero. All the
   free space between the icon and the price is then split equally above and
   below the name, which centres it exactly; leaving `.hc-foot { margin-top:
   auto }` in place would make it a THIRD auto margin sharing that space and the
   title would settle a third of the way down instead of halfway.
   A building card is deliberately untouched — its picture is elastic (.hc-model
   grows into whatever the name and price leave), so there is no slack there to
   centre anything in. */
.hand-card:not(.is-build) .hc-name { margin-top: auto; margin-bottom: auto; }
.hand-card:not(.is-build) .hc-foot { margin-top: 0; }
.hc-cost {
  font-size: 11px; font-weight: 700; color: var(--c-cost); white-space: nowrap;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .7);
}
/* What a card would PAY BACK, on the face beside what it costs — present only on
   the decrees whose worth is a live number (Bonepickers, ten gold a body fallen
   today). Green so it reads as the opposite of the price it sits under, and on
   its own line because at 74px of card width the two will not share one. */
.hc-yield {
  display: block; margin-top: 2px;
  font-size: 10.5px; font-weight: 700; color: #7fd18a; white-space: nowrap;
}
.hc-yield.none { color: #8a8f96; } /* nothing to strip: the cast will be refused */

/* ---------- A BUILDING CARD IS NEUTRAL ----------
   A Barracks is a Barracks whoever is raising it. Dressed in the house's gilt
   and heraldry the whole rack said the one thing every card on it has in
   common, and the ornament then competed for the eye with the MODEL — which is
   the thing you are actually choosing between. So the building deck re-declares
   the same `--c-*` properties one element lower (the Rally Flag's trick, and it
   works for the same reason: a custom property resolves from the nearest
   ancestor that declares it, whatever the specificity) in brushed slate and
   steel, and is struck with the neutral architectural `build` frame from
   ui/hand.js's FRAMES rather than its owner's.

   ⚠️ --c-cost STAYS GOLD. It is money, and money is gold everywhere else in
   this UI — the same #f0c04a the old build menu's price line used. It is also
   the one part `.poor` repaints RED, which is how "you can't afford this" says
   WHY rather than merely going quiet.

   ⚠️⚠️ THIS BLOCK MUST STAY ABOVE `.hand-card.poor`. Both are (0,2,0) — one
   class on `.hand-card` — so nothing but source order decides which set of
   values a card that is BOTH gets, and a build card that could not be afforded
   would keep its slate rather than draining to grey. (`.is-rally` sits below
   `.poor` and gets away with it only because a Rally Flag has no gold price and
   so is never `.poor`.) */
.hand-card.is-build {
  --c-bg1: #3b4553; --c-bg2: #2a323e; --c-bg3: #1a202a;
  --c-frame: #9fb0c0; --c-frame2: #556373;
  --c-edge: #121821; --c-edge2: #090d13;
  --c-ink: #e8ecf2; --c-cost: #f0c04a; --c-glow: 159, 176, 192;
}

/* the states. A castable card is the only one that lights up — with a whole
   hand on screen the old single "something is ready" glow on the ⚜️ button
   becomes one signal per card, which is strictly more than it ever said. */
.hand-card.ready .hc-inner {
  border-color: var(--c-frame);
  box-shadow: var(--card-depth), var(--card-face), 0 0 18px rgba(var(--c-glow), .42);
}
/* ⚠️ CAN'T AFFORD IT ⇒ THE CARD GOES GREY. It used to merely dim (saturate .55
   / brightness .82), which at a glance is indistinguishable from a card that is
   simply not the lit one — so with a full hand and a thin purse the player had
   to read six prices to find out which of them was actually castable. Draining
   the colour says it in one look.
   ⚠️ It is a PALETTE SWAP rather than a `filter` on .hc-inner, because a filter
   applies to the whole subtree and would take the red price down with it. The
   price is the one thing that must keep its colour: grey says "not now" and red
   says WHY, and a card greyed by an empty purse and one sealed by a fallen
   crown are otherwise the same picture. Re-declaring the house's own custom
   properties one element lower drains the ground, the stock, the ornament and
   the detail plate together while leaving --c-cost red — and it needs no rule
   per part, so a part added later greys with the rest instead of being missed.
   (The emoji is the one thing no property reaches, hence its own line.) */
.hand-card.poor {
  --c-bg1: #2a2f36; --c-bg2: #1f2329; --c-bg3: #14171c;
  --c-frame: #6c7076; --c-frame2: #474b51;
  --c-edge: #0c0e11; --c-edge2: #06080a;
  --c-ink: #9096a0; --c-cost: #e08a8a; --c-glow: 130, 138, 148;
}
.hand-card.poor .hc-icon { filter: grayscale(1) brightness(.78) drop-shadow(0 3px 5px rgba(0, 0, 0, .7)); }
.hand-card.poor .hc-gloss { opacity: .4; }
/* ⚠️ Sealed and spent are the two states still worked as a FILTER — they wash
   the whole card at once, ornament and emoji together, which is the point. The
   numbers had to come UP from .62/.7 when the houses did: a filter compounds
   with the veil below it, and against grounds that are already near-black
   (the Cursed Undead's especially) the old pair took a sealed card past
   unreadable and into a black rectangle. */
.hand-card.sealed .hc-inner { filter: grayscale(.9) brightness(.86); }
.hand-card.spent .hc-inner { filter: grayscale(.75) brightness(.84); }

.hc-veil { position: absolute; inset: 0; pointer-events: none; }
.hand-card.sealed .hc-veil { background: rgba(10, 6, 14, .38); }
.hand-card.poor .hc-veil { background: rgba(12, 12, 16, .28); }

/* THE RALLY FLAG IS NOT A DECREE AND MUST NOT BE PRINTED LIKE ONE: banner red
   whatever house is dealing, never sealed by a fallen crown, and — see FRAMES
   in ui/hand.js — struck with a plain campaign border carrying no house
   ornament at all. Its palette is declared on the CARD, so it overrides the
   house's on #hand for everything below it by inheritance rather than by
   specificity. ⚠️ It has to be redder and cooler than the Radiant Knights'
   crimson, which is the one house whose ground it sits against. */
.hand-card.is-rally {
  --c-bg1: #5a2118; --c-bg2: #38150f; --c-bg3: #1a0908;
  --c-frame: #e88a5a; --c-frame2: #93472f;
  --c-edge: #1a0705; --c-edge2: #0d0302;
  --c-ink: #ffd0b8; --c-cost: #ff9d78; --c-glow: 232, 138, 90;
}

/* hovered: clear of its neighbours, tilt flattened, PICKED UPRIGHT out of the
   lean the rest of the hand keeps (--tiltx), sheen swept across, plate open */
.hand-card.hot {
  --sc: 1.09; --tiltx: 0deg;
  transform:
    translate(calc(-50% + var(--x)), calc(100% - var(--lift) - var(--pop)))
    scale(var(--sc));
  z-index: 40 !important;
}
.hand-card.hot .hc-inner {
  border-color: var(--c-frame);
  box-shadow: var(--card-depth), var(--card-face), 0 0 24px rgba(var(--c-glow), .34);
}

/* the detail plate — the description, the price and, when it can't be cast,
   why. pointer-events: none is load-bearing: it hangs ABOVE the hit area, so a
   plate that took the pointer would read as leaving the hand and shut itself. */
/* --px is the on-screen nudge ui/hand.js measures for the cards at the ends of
   the fan, whose plates would otherwise hang off the side of the viewport */
.hc-plate {
  position: absolute; left: 50%; bottom: calc(100% + 12px);
  transform: translateX(calc(-50% + var(--px, 0px))) translateY(6px);
  width: 236px; max-width: 78vw; box-sizing: border-box;
  padding: 11px 13px; border-radius: 11px; text-align: left;
  /* the plate is the card's own stock, dressed: the house's ground darkened
     until the body text sits on something near-black, over its own frame metal */
  background:
    linear-gradient(rgba(6, 9, 14, .93), rgba(6, 9, 14, .93)),
    linear-gradient(168deg, var(--c-bg1), var(--c-bg3));
  border: 1px solid var(--c-frame2);
  box-shadow: 0 12px 34px rgba(0, 0, 0, .7), inset 0 1px 0 rgba(255, 255, 255, .07);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 150ms ease, transform 190ms cubic-bezier(.2, .9, .25, 1.05), visibility 0s linear 150ms;
}
.hand-card.hot .hc-plate {
  opacity: 1; visibility: visible;
  transform: translateX(calc(-50% + var(--px, 0px))) translateY(0);
  transition: opacity 150ms ease, transform 190ms cubic-bezier(.2, .9, .25, 1.05), visibility 0s;
}
/* the plate is dressed in the same house colours the card is, so it reads as
   that card's own note rather than as a shared tooltip that happened to open */
.hp-title { font-size: 13.5px; font-weight: 700; color: var(--c-ink); margin-bottom: 5px; }
.hp-body { font-size: 12px; line-height: 1.5; color: #b6c2cf; }
.hp-body b { color: #ffd0c0; }

/* ---- CARD TEXT: the four marks every decree description is written in -------
   ui/cardtext.js turns `*…*` `^…^` `~…~` and the `|` split into these. Deliberately
   NOT scoped to the plate: the Kingdom Designer's ⚜️ page renders the same
   strings through the same function — in its info panel (.abd-i-body) and on
   every tile of its Decree List (.abd-t-desc) — and one set of colours is what
   stops the two surfaces reading as two different games.
   ⚠️ The value gold has to beat the green/red it sits INSIDE — it does, because
   the number is a child element with a colour of its own rather than a rule
   competing at the same specificity.
   ⚠️⚠️ ...but it ALSO has to beat `.hp-body b` two rules above, which is (0,1,1)
   where a bare `.ct-v` is (0,1,0) — so the plate quietly painted every value in
   that rule's cream instead of gold (seen in a render). `b.ct-v` ties it and
   wins on source order, which is why this block sits below that one. */
b.ct-v { color: #ffd27a; font-weight: 700; }
.ct-good { color: #86dfa0; }
.ct-bad { color: #f08a7a; }
/* the duration and the limits, on their own line at the foot — the one thing a
   player looks for without re-reading the sentence, so it is always in the same
   place and always dimmer than the effect above it */
.ct-note {
  display: block; margin-top: 6px; padding-top: 5px;
  border-top: 1px solid rgba(255, 255, 255, .09);
  font-size: .93em; color: #8fa2b5;
}
.hc-aim {
  display: block; margin-top: 7px; font-size: 11px; font-style: italic;
  color: #7f93a8; letter-spacing: .2px;
}
.hp-why {
  margin-top: 8px; padding-top: 7px; border-top: 1px solid #2c3a4a;
  font-size: 11.5px; font-weight: 600; color: #e08a8a;
}

/* dragging. An UNTARGETED card rides the cursor, because the gesture is the
   whole cast and there is nothing on the map to point at. A TARGETED one lifts
   a fixed amount and stops: from there the ARROW does the pointing, and a card
   chasing the cursor would sit on top of the ground being aimed at. */
.hand-card.dragging { transition: none; z-index: 60 !important; }
.hand-card.lifting {
  --tiltx: 0deg;
  transform:
    translate(calc(-50% + var(--x) + var(--dx, 0px)), calc(100% - var(--lift) + var(--dy, 0px)))
    scale(1.06);
}
.hand-card.lifting.armed { filter: drop-shadow(0 0 16px rgba(255, 205, 90, .75)); }
.hand-card.lifting.armed .hc-inner { border-color: #ffd766; }
.hand-card.aiming {
  --tiltx: 0deg; z-index: 60;
  transform:
    translate(calc(-50% + var(--x)), calc(100% - var(--lift) - var(--pop) - 26px))
    scale(1.04);
  transition: transform 200ms cubic-bezier(.2, .9, .25, 1.05);
}
/* armed gold is deliberately the SAME for every house — it is not the card's
   identity speaking any more, it is "let go and this fires" */
.hand-card.aiming .hc-inner {
  border-color: #ffd766;
  box-shadow: var(--card-depth), var(--card-face), 0 0 26px rgba(255, 205, 90, .45);
}
.hand-card.aiming .hc-plate { display: none; } /* the arrow is the instruction now */

/* the cursor has come back over the hand, so letting go now PUTS THE CARD AWAY
   rather than casting (ui/hand.js release()). The glow goes out and the card
   settles a little — the arrow has already gone — so "nothing will be spent" is
   visible before the button is up rather than discovered after it. */
.hand-card.dropping { filter: brightness(.86) saturate(.72); }
.hand-card.dropping .hc-inner { border-color: var(--c-frame2); box-shadow: var(--card-depth), var(--card-face); }

/* pressed while it cannot be cast: a refusal you can feel, so the press never
   just does nothing */
.hand-card.nope { animation: hand-nope 300ms ease; }
@keyframes hand-nope {
  0%, 100% { margin-left: 0; }
  25% { margin-left: -7px; }
  75% { margin-left: 7px; }
}

/* ...and clicked when it needs DRAGGING: a hop upward, saying which way to go.
   A press that appears to do nothing reads as a broken button. */
.hand-card.nudge { animation: hand-nudge 420ms cubic-bezier(.3, .9, .3, 1); }
@keyframes hand-nudge {
  0%, 100% { margin-bottom: 0; }
  35% { margin-bottom: 20px; }
}

/* proclaimed — the SENT half. The cast is on its way to the server and nothing
   is settled yet, so the card drops back into the fan and pulses gold. */
.hand-card.just-cast .hc-inner { animation: hand-cast 520ms ease-out; }
/* ⚠️ every stop carries --card-depth, or the card would flatten onto the table
   for half a second in the middle of its own cast */
@keyframes hand-cast {
  0%   { box-shadow: var(--card-depth), var(--card-face); border-color: #ffe9b0; }
  18%  { box-shadow: var(--card-depth), var(--card-face), 0 0 34px 8px rgba(255, 226, 150, .95); border-color: #fff3d0; }
  100% { box-shadow: var(--card-depth), var(--card-face); border-color: var(--c-frame2); }
}

/* ...and SPENT: the server has discarded the card, so it really does leave. It
   is the .hc-inner that flies, not the card — the card's own transform is the
   fan geometry (slot, bow, tilt) and animating it would fight that — and the
   fan stays open behind it until it lands, ui/hand.js holding the rebuild for
   exactly DISCARD_MS. Turning up and away rather than straight up, so it reads
   as a card thrown out of a hand instead of a UI element sliding off. */
.hand-card.discarding { pointer-events: none; z-index: 45 !important; }
.hand-card.discarding .hc-inner {
  animation: hand-discard 460ms cubic-bezier(.35, 0, .7, .4) forwards;
}
@keyframes hand-discard {
  0%   { transform: translate(0, 0) rotate(0deg) scale(1); opacity: 1;
         box-shadow: var(--card-depth), 0 0 26px 6px rgba(255, 226, 150, .8); border-color: #ffe9b0; }
  20%  { transform: translate(2px, -30px) rotate(-3deg) scale(1.06); opacity: 1; }
  100% { transform: translate(34px, -230px) rotate(-16deg) scale(.66); opacity: 0; }
}

/* a fresh deal that hasn't been looked at — the ⚜️ button's 📜 badge became
   this, since with the hand permanently on screen there is no button to badge.
   ⚠️ Held off the BUILDING deck: `fresh` means "today's decrees have not been
   read", which is true whichever deck happens to be in the hand, so ui/hand.js
   keeps setting it — but shimmering a row of buildings would be the right
   signal pointed at the wrong cards. */
#hand.fresh:not(.mode-build) .hand-card:not(.is-rally) .hc-inner { animation: hand-fresh 2.1s ease-in-out infinite; }
@keyframes hand-fresh {
  0%, 100% { box-shadow: var(--card-depth), var(--card-face), 0 0 7px rgba(var(--c-glow), .22); }
  50% { box-shadow: var(--card-depth), var(--card-face), 0 0 24px rgba(var(--c-glow), .62); }
}

/* THE DECK CHANGING HANDS. The switch is pressed in the bottom-RIGHT corner and
   what it changes is at the bottom CENTRE, so without a beat of motion the swap
   happens outside where the eye is and reads as nothing having happened. The
   cards rise and settle once, staggered off their own slot (--sheen is that
   slot, already set per card), so the new deck comes in as a hand being laid
   down rather than as one frame replacing another.
   ⚠️ Deliberately NOT a forced raise of the whole hand: that would park a 300px
   overlay over the map until the pointer happened to visit it and leave, since
   the hand only lowers on pointerleave and the press never entered it. */
/* ⚠️ The +80ms is what keeps the delay POSITIVE: --sheen is the slot measured
   from the MIDDLE of the fan, so it is negative for every card left of centre,
   and a bare multiple of it would start those cards partway into their own
   animation. The offset moves the whole sweep past zero.
   ⚠️ It also animates margin rather than transform — the card's own transform is
   the fan geometry (slot, bow, tilt) and rewriting it here would fight that. The
   .nudge hop next door works the same way and for the same reason. */
#hand.swapping .hand-card {
  animation: hand-swap 420ms cubic-bezier(.2, .9, .25, 1.05) both;
  animation-delay: calc(var(--sheen, 0) * 16ms + 80ms);
}
@keyframes hand-swap {
  0%   { opacity: 0; margin-bottom: -26px; }
  55%  { opacity: 1; margin-bottom: 7px; }
  100% { opacity: 1; margin-bottom: 0; }
}

/* the fallen-crown notice: the cards are all still there, merely unusable, and
   without saying so that reads as a bug */
.hand-banner {
  position: absolute; left: 50%; transform: translateX(-50%) translateY(8px);
  bottom: calc(var(--lift) + 12px);
  padding: 7px 15px; border-radius: 9px; white-space: nowrap; max-width: 94vw;
  background: rgba(46, 20, 26, .95); border: 1px solid #7a3a3a;
  font-size: 12.5px; color: #f0a6a6;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .55);
  opacity: 0; visibility: hidden;
  transition: opacity 260ms ease, transform 260ms ease, visibility 0s linear 260ms, bottom 220ms ease;
}
.hand-banner b { color: #ffc9c9; }
.hand-banner.on {
  opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0);
  transition: opacity 260ms ease, transform 260ms ease, visibility 0s, bottom 220ms ease;
}

/* (There is no pager here any more. Both decks are ONE ROW on a phone, sized to
   show ROW_CARDS of themselves and swiped sideways for the rest — see the note
   above ROW_CARDS in ui/hand.js for why one gesture beat one rule per deck.) */

/* the aim arrow (ui/hand.js). Its own overlay because it is drawn between a
   card at the bottom of the screen and a point on the map, and belongs to
   neither. Above the hand so it is never buried by a raised card. */
#hand-arrow {
  position: fixed; inset: 0; width: 100%; height: 100%;
  z-index: 27; pointer-events: none;
}
#hand-arrow.hidden { display: none; }
#hand-arrow .ha-line {
  fill: none; stroke: #6fdc7a; stroke-width: 5; stroke-linecap: round;
  filter: drop-shadow(0 0 7px rgba(111, 220, 122, .85));
  opacity: .92;
}
#hand-arrow .ha-head {
  fill: #6fdc7a;
  filter: drop-shadow(0 0 7px rgba(111, 220, 122, .85));
}
/* red is how the rest of the aim preview says "you will be refused here", so the
   arrow says it the same way rather than inventing a second vocabulary */
#hand-arrow.bad .ha-line { stroke: #e2564c; filter: drop-shadow(0 0 7px rgba(226, 86, 76, .8)); }
#hand-arrow.bad .ha-head { fill: #e2564c; filter: drop-shadow(0 0 7px rgba(226, 86, 76, .8)); }

/* the placement pill sits just above the resting hand, and steps up out of the
   way while the hand is raised or a card is being aimed — those are exactly the
   moments a lifted card would otherwise cover it.
   (The base offset above already clears the RESTING hand for both decks, since
   they are the same height — this is only the extra step for a raised one.) */
body.hand-up #placement-hint { bottom: 304px; }

/* ---------- the Rally Flag's press-and-hold cue (ui/rally.js) --------------
   The beat between planting the flag and growing the muster ring waits on a
   gesture the player has no reason to expect — a HOLD, not a click. The pill
   says so in words, but the eye is on the army being sent, not on the bottom of
   the screen. So the cursor grows rings on a loop, in the flag's own banner red:
   a small silent rehearsal of what the hold is about to do.

   z-index 28 — over the hand (26) and its aim arrow (27), under the confirm
   pills (40); pointer-events: none, so it can never eat the very press it is
   asking for. JS parks it with a transform, so no layout runs per frame. */
#rally-cue {
  position: fixed; left: 0; top: 0; width: 0; height: 0;
  z-index: 28; pointer-events: none;
}
#rally-cue.hidden { display: none; }
#rally-cue i {
  position: absolute; left: -13px; top: -13px; width: 26px; height: 26px;
  border-radius: 50%; border: 2px solid #ff9d78;
  box-shadow: 0 0 10px rgba(217, 119, 66, .65);
  animation: rally-cue 1.5s cubic-bezier(.2, .6, .35, 1) infinite;
}
#rally-cue i:nth-child(2) { animation-delay: .75s; } /* the second ring rides the gap */
#rally-cue b {
  position: absolute; left: -3.5px; top: -3.5px; width: 7px; height: 7px;
  border-radius: 50%; background: #ff9d78;
  box-shadow: 0 0 9px rgba(255, 157, 120, .95);
  animation: rally-cue-dot 1.5s ease-in-out infinite;
}
@keyframes rally-cue {
  0%   { transform: scale(.42); opacity: 0; }
  18%  { opacity: .95; }
  100% { transform: scale(3.4); opacity: 0; }
}
@keyframes rally-cue-dot {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.55); }
}
/* ⚠️ Reduced motion gets a STATIC cue rather than none: the animation is the
   whole message here, so removing it would leave the beat with no affordance at
   all — the one case in this file where silence is worse than movement. */
@media (prefers-reduced-motion: reduce) {
  #rally-cue i { animation: none; transform: scale(1.9); opacity: .8; }
  #rally-cue i:nth-child(2) { transform: scale(1); opacity: .5; }
  #rally-cue b { animation: none; }
}

/* short viewports: a full-height card plus its plate does not fit above a
   1000×600 window, so the cards shrink rather than the plate being clipped */
@media (max-height: 700px) {
  /* the smaller face puts the title's first line at 48..61px, so the shared
     74px peek still clears it — only the RISE has to be brought back in line */
  #hand { --ch: 128px; --pop: 58px; }
  .hc-icon { font-size: 25px; }
  .hc-name { font-size: 10.5px; -webkit-line-clamp: 2; }
  body.hand-up #placement-hint { bottom: 258px; }
}

/* ⚠️⚠️ THE TWO NARROW-SCREEN RESERVES LIVE HERE, BELOW THE BASE DECLARATION,
   BECAUSE A MEDIA QUERY ADDS NO SPECIFICITY. `#hand { --hand-reserve }` is
   (0,1,0,0) whether or not it is wrapped in one, so what decides is SOURCE
   ORDER — and the action-bar blocks these belong with are ~2,000 lines ABOVE
   the base 170px. Written up there they lose, silently, leaving a shrunken disc
   and a fan still shoved aside to clear a button that is no longer beside it.
   (Measured in a running game: the phone rule read back as 170px.) The ≤820
   rule must likewise precede the ≤600 one for the same reason.
   Each number is the disc's own diameter at that width plus its margin plus the
   20px .hand-zone carries beyond the cards — see the action-bar blocks, which
   is where the diameters are set and where the argument for each is written. */
@media (max-width: 820px) { #hand { --hand-reserve: 126px; } }
@media (max-width: 600px) { #hand { --hand-reserve: 0px; } }

/* Phones. The hand takes the bottom edge WHOLE — the Build disc stands above it
   rather than beside it, so there is no reserve and the fan is dead centre (see
   the action-bar block above, which positions the disc and is where that
   argument lives). A finger cannot hover, so the plate opens on the FIRST tap
   and it is the second that commits — which means the plate has to be readable
   at this size. */
@media (max-width: 600px) {
  /* the peek shows the title here too — phone metrics put .hc-name's first line
     at 38..50px, so it is sized off those the same way the desktop one is */
  /* ⚠️ no --hand-peek here: ui/hand.js writes it per deal from the card height,
     which is already squeezed at this width, and --lift is derived from it */
  #hand { --pop: 66px; --ch: 124px; }
  .hc-icon { font-size: 23px; margin-top: 2px; }
  .hc-name { font-size: 9.5px; margin-top: 5px; -webkit-line-clamp: 2; }
  .hand-card.is-build .hc-name { margin-top: 2px; }
  .hc-cost { font-size: 9.5px; }
  /* the bottom padding is still the foot ornament's room, scaled with the rest */
  .hc-inner { padding: 8px 5px 10px; border-radius: 9px; }
  .hc-plate { width: 250px; max-width: 88vw; padding: 10px 12px; }
  .hp-body { font-size: 11.5px; }
  .hand-banner { font-size: 11px; padding: 6px 11px; white-space: normal; text-align: center; }
  /* the raised hand plus room for the plate that opens over it — derived from
     the peek like the resting figure above, since the card height moves with
     the viewport */
  body.hand-up #placement-hint { bottom: calc(var(--hand-peek) + 133px); }
}

/* ---------- A CARD IN THE AIR OWNS THE SCREEN (touch only) ----------
   ⚠️⚠️ ON A PHONE A DRAGGED CARD IS BEING AIMED AT GROUND THE HAND IS COVERING.
   The fan takes the full width of the bottom edge and shows its cards WHOLE, the
   detail plate is a 236px slab above them, and the finger is somewhere in the
   middle of the lot — so a player aiming a decree was doing it through a rack of
   six other cards and a paragraph of prose, on a board maybe 400px wide. While
   one card is in flight the rest of the deck goes, its plate goes, and the pager
   goes with them.

   ⚠️ THE ONE BEING AIMED DROPS BACK TO THE BOTTOM rather than standing 92px
   clear of the row (.aiming, above): there is nothing left beside it for it to
   need to stand clear OF, and every pixel it climbs is map it is covering while
   the arrow points past it. An UNTARGETED card is deliberately untouched — it
   rides the finger, and that ride IS the cast.

   ⚠️ Coarse pointers only. On a mouse the plate is under the cursor and closes
   itself, the hand is a small part of a wide screen, and the neighbouring cards
   are what a player drags ACROSS on the way to changing their mind. */
@media (pointer: coarse) {
  /* ⚠️⚠️ THE HIT AREA NEVER GROWS ON A TOUCH SCREEN, and this was a real bug
     rather than tidiness. `.hand-zone` inflates to 300px while the hand is up or
     a card is in the air so it can keep following a HOVERING pointer — and a
     touch screen has none, so all it does is take the bottom 300px of the board
     out of play: it is `pointer-events: auto`, it sits over the canvas, and on a
     phone the Build disc stands inside that band. Measured before the fix, at
     500×703 with the hand merely RAISED (which on touch it is after any card
     press, since only a press OUTSIDE #hand lowers it — and a press on the disc
     was landing inside): the zone spanned y 403–703, the disc 460–532, and
     pressing the disc's own centre hit `DIV.hand-cards`. The player was left
     unable to swap back to their decrees at all, with the map's bottom third
     dead as well. Nothing on touch reads the extra height — `overHand` measures
     the peek, and a drag holds pointer capture. */
  #hand.up .hand-zone, #hand.busy .hand-zone { height: calc(var(--hand-peek) + 22px); }
  #hand.busy .hand-card:not(.dragging) { opacity: 0; pointer-events: none; }
  #hand.busy .hc-plate { display: none; }
  /* ...and the placement pill with them. A DECREE drag is already quiet (see
     startDrag), but a BUILDING one writes its own hint, and on a phone that pill
     stands in exactly the band the card and its arrow are crossing — with a ✕
     that cannot be pressed while the finger is still down on the card anyway. It
     comes back on the release, which is when a `pair` decree's second-point
     prompt is actually needed. */
  body.hand-dragging #placement-hint { display: none; }
  /* ⚠️ (0,3,0) — one class more specific than `.hand-card.hot`, which a dragged
     card still carries, and later in the file besides. `--raise` is left out for
     the same reason .aiming leaves it out: the Rally Flag stands proud of the
     row it sits IN, not of a screen it is alone on. */
  .hand-card.dragging.aiming {
    transform: translate(calc(-50% + var(--x)), calc(100% - var(--lift))) scale(1.02);
  }
}

/* the fan is decoration; a reduced-motion preference gets the states without
   the glide. */
@media (prefers-reduced-motion: reduce) {
  /* the card's LEAN and its sheen are kept — they are a static look rather than
     motion, and dropping them would take the whole third dimension out of the
     hand. Only the glide between states goes. */
  .hand-card, .hc-inner, .hc-gloss, .hc-plate, .hand-zone, .hand-banner { transition-duration: 1ms; }
  #hand.fresh .hand-card .hc-inner { animation: none; }
  /* the shake and the hop are the ONLY feedback those two presses give, so they
     are shortened rather than removed — silence would read as a broken card.
     The discard is shortened too rather than dropped: without it a card would
     vanish out of the middle of the hand with nothing to say it had been spent,
     and ui/hand.js holds the fan open for DISCARD_MS either way. */
  .hand-card.nope, .hand-card.nudge, .hand-card.just-cast .hc-inner { animation-duration: 120ms; }
  .hand-card.discarding .hc-inner { animation-duration: 160ms; }
  /* the deck swap is shortened rather than dropped for the discard's reason: it
     is the only thing that says the hand now holds something else, and the
     button that changed it is in the far corner of the screen */
  #hand.swapping .hand-card { animation-duration: 140ms; animation-delay: 0s; }
}
/* ---------- the dawn sequence (ui/dawn.js) ----------
   One choreographed ceremony each daybreak — the day, then the world event, then
   the decree hand dealing out of the ⚜️ button — so the three can never fight
   over the same strip of screen.

   z-index 46: above the daybreak edge-wash (44) and the damage floaters (45),
   but BELOW every interactive modal (47+), so the end screen can never
   be trapped behind a ceremony. The root never takes pointer events — the game
   stays playable underneath, and a click anywhere skips (handled in JS, which
   lets the click fall through to whatever it hit). */
#dawn-seq { position: fixed; inset: 0; z-index: 46; pointer-events: none; }
#dawn-seq.hidden { display: none; }
.dseq-stage { position: absolute; inset: 0; }

/* Both stage pieces ride the same transform, driven by --ty/--sc/--op per beat,
   so a beat change is one class swap and the pieces glide between slots. */
.dseq-head, .dseq-event {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%) translateY(var(--ty, 0)) scale(var(--sc, 1));
  opacity: var(--op, 0);
  transition: transform 820ms cubic-bezier(.2, .85, .25, 1), opacity 620ms ease;
  will-change: transform, opacity;
  text-align: center;
}

.dseq-title {
  font-size: 44px; font-weight: 800; letter-spacing: 1px; color: #ffe9b0; white-space: nowrap;
  text-shadow: 0 0 26px rgba(255, 180, 60, .95), 0 0 8px rgba(255, 220, 140, .8), 0 2px 8px rgba(0, 0, 0, .6);
}
.dseq-sub {
  margin-top: 6px; font-size: 15px; font-weight: 600; color: #ffdf9a; white-space: nowrap;
  text-shadow: 0 0 12px rgba(255, 180, 60, .7), 0 1px 5px rgba(0, 0, 0, .65);
  transition: opacity 400ms ease;
}

/* the world event: NOT A WINDOW. It is the world speaking, not a panel the
   player opened, so it is announced ON the map rather than in a box laid over
   it — the words sit in a soft pool of dark that fades out to nothing with no
   edge anywhere. A border drew a rectangle around exactly the thing this
   ceremony is trying to blend into the day, and the weather it just brought in
   (render/weather.js) is half the announcement. Still LOCAL rather than a
   full-screen scrim, for that same reason: the sky stays visible around it. */
.dseq-event {
  width: min(540px, 88vw); padding: 10px 24px 8px;
}
/* The pool itself, on a pseudo-element so it can reach well past the words and
   have room to die away before it stops. Two things make the edge soft:
   `closest-side`, because the default (farthest-corner) is still part-opaque
   where it meets the mid-edges and cuts there — which is the hard rectangle we
   are getting rid of — and percentage insets, so the fade keeps its proportions
   on any viewport instead of pinching in on a phone. */
.dseq-event::before {
  content: ''; position: absolute; inset: -52% -24%; z-index: -1;
  /* MANY stops, easing out the whole way. A short ramp between two stops draws
     its own edge — the pool reads as a black oval laid on the map, which is the
     window again by another name — so the alpha falls away gently from about a
     third of the way out and only the middle is ever properly dark. */
  background: radial-gradient(ellipse closest-side at 50% 50%,
    rgba(4, 9, 15, .88) 0%, rgba(4, 9, 15, .86) 22%, rgba(4, 9, 15, .8) 36%,
    rgba(4, 9, 15, .69) 48%, rgba(4, 9, 15, .55) 59%, rgba(4, 9, 15, .4) 69%,
    rgba(4, 9, 15, .26) 79%, rgba(4, 9, 15, .14) 87%, rgba(4, 9, 15, .05) 94%,
    rgba(4, 9, 15, 0) 100%);
  transition: transform 480ms ease, opacity 320ms ease;
}
.dseq-ev-icon { font-size: 52px; line-height: 1; filter: drop-shadow(0 3px 10px rgba(0, 0, 0, .6)); }
.dseq-ev-name {
  margin-top: 8px; font-size: 27px; font-weight: 800; letter-spacing: .5px; color: #dcefff;
  text-shadow: 0 0 20px rgba(140, 195, 240, .8), 0 2px 6px rgba(0, 0, 0, .6);
}
/* With no panel under it, every line has to stand on its own: the description is
   the widest thing here, so its ends reach out to where the pool has already
   given most of itself up — and a bright map (the meteor sky, a snowfield) shows
   straight through. It is the one piece of the announcement that had no shadow,
   because it never needed one inside a box. */
.dseq-ev-desc {
  margin-top: 7px; font-size: 14.5px; line-height: 1.55; color: #b7cde1;
  text-shadow: 0 1px 5px rgba(0, 0, 0, .85), 0 0 16px rgba(0, 0, 0, .65);
  transition: opacity 400ms ease, max-height 500ms ease, margin-top 500ms ease;
  max-height: 120px; overflow: hidden;
}

/* ⚠️⚠️ THREE BEATS, ONE AFTER ANOTHER — the day, then the hand, then the event —
   and NOTHING shares the stage with anything else. The hand and the event used
   to come up together, and six freshly dealt cards across the middle of the map
   simply out-shouted a line of text above them: players read their new hand and
   missed the event, which is the one thing on this screen that changes how the
   day should be played. Both pieces ride the same --ty/--sc/--op transform, so
   each beat is one class swap and they glide between slots. */

/* beat 1 — the day has the screen to itself */
#dawn-seq.s-day .dseq-head { --ty: -6vh; --sc: 1; --op: 1; }
#dawn-seq.s-day .dseq-event { --ty: 10vh; --sc: .92; --op: 0; }

/* ⚠️ THE DAY EYEBROW PARKS ONCE AND STAYS PUT for beats 2 and 3 — it is a
   caption on the whole ceremony, not on either half of it, and a title that
   shuffled up a little as the hand gave way to the event would be movement
   drawing the eye to the one thing that has not changed.
   Its slot is a squeeze: the game's own day box sits above it and the fan's
   "⚜️ Today's Royal Decrees" caption below (dawn.js measures a real card to
   place that one, so it lands wherever the tallest card reaches). At the -28vh
   this started from, the two printed straight through each other. */
#dawn-seq.s-hand .dseq-head,
#dawn-seq.s-event .dseq-head { --ty: -32vh; --sc: .54; --op: .58; }

/* beat 2 — the decree fan comes up with nothing else on the stage. ⚠️ The event
   panel MUST be held down and hidden here: its content is only written when
   beat 3 opens, so without this it surfaces as an empty slate over the cards. */
#dawn-seq.s-hand .dseq-event { --ty: 6vh; --sc: .92; --op: 0; }

/* beat 3 — the cards fly home and the world event rises into the space they
   leave, at full size and near the middle of the screen where the eye rests.
   It is the only thing on the stage for four seconds. */
#dawn-seq.s-event .dseq-event { --ty: -6vh; --sc: 1; --op: 1; }

/* the day's small print can't survive being scaled down, so it leaves rather
   than shrinking into an illegible smudge */
#dawn-seq.s-hand .dseq-sub, #dawn-seq.s-event .dseq-sub { opacity: 0; }

#dawn-seq.s-out .dseq-head, #dawn-seq.s-out .dseq-event { --op: 0; --ty: -38vh; }

/* the hand: cards are dealt UP OUT OF the player's own hand along the bottom
   edge and, when the dwell is over, fall back down into it. JS sets
   --ox/--oy/--orot/--osc (the resting slot in ui/hand.js's fan) and
   --x/--y/--rot (the card's slot on the ceremony's stage). */

/* ...with a caption, because the event card is now sharing the stage and the
   two halves of the reveal have to be told apart. JS measures a real card to
   place it just clear of the fan's top edge. */
.dseq-hand-label {
  position: absolute; left: 50%; transform: translate(-50%, -100%);
  font-size: 12px; font-weight: 700; letter-spacing: 1.4px; text-transform: uppercase;
  color: rgba(255, 226, 165, .82); white-space: nowrap;
  text-shadow: 0 0 14px rgba(255, 180, 60, .55), 0 1px 4px rgba(0, 0, 0, .75);
  opacity: 0; transition: opacity 500ms ease 380ms;
}
.dseq-hand-label.on { opacity: 1; }
/* the caption names the fan, so it leaves when the fan does — on the tick beat 3
   opens, rather than lingering over the world event that has taken its place */
#dawn-seq.s-event .dseq-hand-label,
#dawn-seq.s-out .dseq-hand-label { opacity: 0; transition: opacity 260ms ease; }

/* ⚠️ IT IS THE HAND'S OWN CARD, printed the same way: the house palette above,
   the same stock and bevel, the same ornament (ui/hand.js frameSvg). The flight
   ends ON the card it becomes, so anything the two do not share shows as a flip
   at the seam. Its HEIGHT is therefore derived from --cw at the hand's card
   ratio rather than left to the content — the frame is cut at that ratio, and a
   content-height card would both letterbox the ornament and land on a slot of a
   different shape. */
.dseq-card {
  position: absolute; left: 0; top: 0;
  width: var(--cw, 150px); height: calc(var(--cw, 150px) * 1.42); box-sizing: border-box;
  display: flex; flex-direction: column; align-items: center; text-align: center;
  /* ⚠️ padding IN PROPORTION TO --cw, because this card is sized off --cw while
     its ornament is cut at a fixed ratio: at a flat 16px the crest on the top
     rule printed straight through the icon on a wide deal, and the price through
     the foot motif on a narrow one. */
  padding: calc(var(--cw, 150px) * .125) 10px calc(var(--cw, 150px) * .105);
  border-radius: 12px; overflow: hidden;
  background:
    radial-gradient(38% 26% at 50% 22%, rgba(var(--c-glow), .17), rgba(0, 0, 0, 0) 72%),
    linear-gradient(168deg, var(--c-bg1) 0%, var(--c-bg2) 46%, var(--c-bg3) 100%);
  border: 1px solid var(--c-frame);
  box-shadow: var(--card-depth), var(--card-face), 0 0 24px rgba(var(--c-glow), .3);
  /* --ox/--oy/--orot/--osc are the card's resting SLOT IN THE HAND, measured by
     ui/hand.js. Every card is dealt up out of the slot it will settle into and
     falls back onto exactly that slot when the dwell is over, so the flourish is
     literally the hand being drawn and then put down again. --osc lands it at
     the hand's own card size: the flight ends ON the card it becomes, and the
     two have to match at the seam or the swap is visible. */
  transform: translate(-50%, -50%) translate(var(--ox), var(--oy)) scale(var(--osc, .18)) rotate(var(--orot, 0deg));
  opacity: 0;
  transition: transform 560ms cubic-bezier(.2, .9, .25, 1.05), opacity 300ms ease;
  will-change: transform, opacity;
}
.dseq-card.dealt {
  transform: translate(-50%, -50%) translate(var(--x), var(--y)) scale(1) rotate(var(--rot));
  opacity: 1;
}
/* the flight down. It fades only over the LAST stretch (the 380ms delay), so the
   card is still solid while it travels and goes out just as the real hand takes
   over underneath it on the same slot. */
.dseq-card.tuck {
  transform: translate(-50%, -50%) translate(var(--ox), var(--oy)) scale(var(--osc, .14)) rotate(var(--orot, 0deg));
  opacity: 0;
  transition: transform 620ms cubic-bezier(.45, .05, .55, 1), opacity 200ms ease 380ms;
}
/* ⚠️⚠️ THE ICON SLOT IS IN PROPORTION TO --cw, for the same reason the padding
   above it is — and it has to be, because this card is SWAPPED FOR THE HAND'S at
   the seam of the flight down, so anything the two do not share reads as a flip
   on the one morning it is hardest to miss.
   .hc-icon is sized in fixed px against a card the hand draws at ~106 wide,
   which measures 0.461 of the card's width for the box and 0.238 down from its
   top edge; those two fractions ARE this rule. A fixed size cannot match them at
   both ends: at 34px the mark was 0.385 of the width on a 150px-wide deal (too
   small, and sitting too high at 0.132) and 0.545 on the 106px card it lands on
   — i.e. it visibly SHRANK at the very moment of the swap. Reported in play as
   "the icons are too small and slightly higher than the ones in my hand".
   min-height/flex-shrink mirror .hc-icon so a squeezed card loses the mark
   rather than its title; font-size is left for the fallback glyph alone. */
.dseq-c-icon {
  font-size: 34px; line-height: 1;
  height: calc(var(--cw, 150px) * .461);
  margin-top: calc(var(--cw, 150px) * .106);   /* + the .125 padding above = .238 */
  min-height: 0; flex-shrink: 3;
  filter: drop-shadow(0 3px 6px rgba(0, 0, 0, .75)) drop-shadow(0 0 10px rgba(var(--c-glow), .35));
}
.dseq-c-icon .dicon { width: 100%; height: 100%; }
/* ⚠️ CENTRED BETWEEN THE ICON AND THE PRICE, exactly as .hc-name is on the card
   this becomes (two auto margins here, none on the cost below — see the note
   there). The ceremony's card is swapped for the hand's at the seam of the
   flight down, so anything the two do NOT share shows as a flip on the one
   morning it is hardest to miss. */
.dseq-c-name {
  margin: auto 0; font-size: 13px; font-weight: 700; color: var(--c-ink); line-height: 1.3;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .75);
}
/* pinned to the foot the way the hand card's price is, so the two line up when
   one becomes the other (the name's own bottom auto margin does the pushing) */
.dseq-c-cost {
  padding-top: 7px; font-size: 11.5px; font-weight: 700;
  color: var(--c-cost); text-shadow: 0 1px 2px rgba(0, 0, 0, .7);
}

/* the opening beat's hint — a click there skips AHEAD to the reveal, never past
   it, so it can promise that without costing the player anything */
.dseq-skip {
  position: absolute; left: 50%; bottom: 76px; transform: translateX(-50%);
  font-size: 11.5px; letter-spacing: .5px; color: rgba(220, 232, 245, .45);
  text-shadow: 0 1px 4px rgba(0, 0, 0, .7); white-space: nowrap;
  opacity: 0; transition: opacity 500ms ease 900ms;
}
#dawn-seq.s-day .dseq-skip { opacity: 1; }

/* THERE IS NO CONTINUE BUTTON, AND NOTHING HOLDS THE REVEAL OPEN. The fan dwells
   a second and flies its cards down into the hand on its own, then the event
   holds the stage four — so the ceremony takes no pointer events AT ALL, and the
   whole root stays pointer-events: none with no exception carved out of it.

   A beat used to stop while the pointer rested on it, with a `.s-held` state
   that made the pause visible. Both are gone: a ceremony that stayed up for as
   long as the cursor sat still read as the game having frozen, and nothing on
   this stage is the last chance to read anything — the hand lives along the
   bottom edge all day and the event keeps its tab under the day box. */

/* short viewports: pull the day eyebrow in so it can't ride off the top, and
   keep the event card smaller so its description still clears the hand */
@media (max-height: 680px) {
  /* ⚠️⚠️ THE DAY EYEBROW GIVES UP ITS SLOT IN BEAT 2 HERE, because there is no
     slot: measured at 523px tall, the game's own day box ends at y=101 and the
     top of the fan is at y=121, so a title of any size prints over the cards
     (it did, at every value tried). It has had a whole beat of its own by then;
     the fan takes beat 2 outright and the eyebrow fades back in for the event,
     already parked so it fades rather than sliding into place. */
  #dawn-seq.s-hand .dseq-head { --ty: -22vh; --sc: .46; --op: 0; }
  #dawn-seq.s-event .dseq-head { --ty: -22vh; --sc: .46; --op: .55; }
  #dawn-seq.s-event .dseq-event { --ty: -5vh; --sc: .82; }
}

@media (max-width: 700px) {
  .dseq-title { font-size: 32px; }
  .dseq-sub { font-size: 13px; }
  .dseq-event { padding: 8px 14px 6px; }
  /* the pool is pulled in at the sides: at the desktop spread it would run off
     a phone's edges, and a fade the screen cuts short is a hard edge again.
     Deeper top and bottom to pay for it — the description wraps to more lines
     here, and a taller pool is what keeps them over the dark middle of it. */
  .dseq-event::before { inset: -42% -12%; }
  .dseq-ev-icon { font-size: 42px; }
  .dseq-ev-name { font-size: 21px; }
  .dseq-ev-desc { font-size: 13px; }
  /* ⚠️ only the SIDES are flattened here — the top and bottom stay proportional
     to --cw, because that is what keeps the crest off the icon and the price off
     the foot motif at whatever width the deal comes out at */
  .dseq-card {
    padding: calc(var(--cw, 150px) * .125) 7px calc(var(--cw, 150px) * .105);
    border-radius: 10px;
  }
  .dseq-c-icon { font-size: 26px; }
  .dseq-c-name { margin-top: 6px; font-size: 11px; }
  .dseq-c-cost { margin-top: 5px; font-size: 10px; }
  .dseq-skip { bottom: 96px; }
  .dseq-hand-label { font-size: 10.5px; letter-spacing: 1px; }
}

/* the ceremony is decoration: honour a reduced-motion preference by letting the
   beats snap between slots rather than gliding. The DWELL is untouched by this —
   it is how long the reveal is readable for, not an animation. */
@media (prefers-reduced-motion: reduce) {
  .dseq-head, .dseq-event, .dseq-card, .dseq-sub, .dseq-ev-desc, .dseq-skip,
  .dseq-hand-label { transition-duration: 1ms; }
}

/* ================= 🐉 THE DRAGON'S FALL (ui/dragonfall.js) ===================
   The one kill every player in the room is told about. It lands unasked-for, in
   the middle of whatever fight the player is steering, so:

   * pointer-events: none THROUGHOUT — nothing here is interactive and it must
     never eat a click. Same rule as the dawn ceremony.
   * z-index 46, level with that ceremony and below every interactive modal
     (47+), so a pause countdown, the exit dialog and the end screen all stay on
     top of it. The two can genuinely collide (a dawn can break while a dragon is
     being brought down); they occupy different bands of the screen — the
     ceremony works from the middle upward, this sits on the upper third — and
     whichever was appended later wins the overlap, which is fine when neither
     takes a click.
   * NO full-screen scrim. The kill is happening on the map right now, with its
     own death throes going up (render/effects.js) — a black wash over the world
     would hide the very thing being announced. The words carry their own pool of
     dark instead, exactly as the world-event announcement does. */
#dragonfall { position: fixed; inset: 0; z-index: 46; pointer-events: none; }
#dragonfall.hidden { display: none; }
.df-stage {
  position: absolute; left: 50%; top: 26%; transform: translate(-50%, -50%);
  width: min(620px, 92vw); padding: 18px 26px 16px; text-align: center;
}
/* the pool: many stops easing the whole way out, so it dies away with no edge
   anywhere rather than reading as a black panel laid over the battle */
.df-stage::before {
  content: ''; position: absolute; inset: -46% -20%; z-index: -1;
  background: radial-gradient(ellipse closest-side at 50% 50%,
    rgba(9, 4, 3, .9) 0%, rgba(9, 4, 3, .86) 24%, rgba(9, 4, 3, .78) 38%,
    rgba(9, 4, 3, .64) 50%, rgba(9, 4, 3, .48) 62%, rgba(9, 4, 3, .3) 74%,
    rgba(9, 4, 3, .15) 85%, rgba(9, 4, 3, .05) 93%, rgba(9, 4, 3, 0) 100%);
}

.df-whose {
  font-size: 12px; font-weight: 700; letter-spacing: 3.5px; text-transform: uppercase;
  color: #d09a72; text-shadow: 0 2px 8px rgba(0, 0, 0, .85);
}
.df-crest {
  font-size: 62px; line-height: 1.1; margin-top: 2px;
  filter: drop-shadow(0 0 18px rgba(255, 110, 40, .75)) drop-shadow(0 3px 10px rgba(0, 0, 0, .7));
}
/* ⚠️ nowrap, and every horizontal measure in EM. The title spreads its letters
   as it lands (df-title), and at a fixed 16px spread "THE DRAGON IS SLAIN" is
   wider than the stage — so it wrapped to two lines for the first fifth of a
   second and then snapped back to one, which reads as a layout bug rather than a
   flourish. In em the spread scales with the clamped font size, and nowrap means
   the letters simply reach out past the pool (which fades well beyond the words)
   instead of ever finding a second line. */
.df-title {
  font-size: clamp(21px, 4.4vw, 42px); font-weight: 800; letter-spacing: .1em;
  white-space: nowrap; color: #ffd9a8;
  text-shadow: 0 0 30px rgba(255, 110, 30, .95), 0 0 10px rgba(255, 190, 120, .8),
               0 3px 10px rgba(0, 0, 0, .7);
}
.df-slayer {
  margin-top: 10px; font-size: 17px; font-weight: 600; color: #f0e2d2;
  display: flex; align-items: center; justify-content: center; gap: 9px;
  text-shadow: 0 2px 8px rgba(0, 0, 0, .85);
}
/* the slayer's faction colour, which is the fastest read on the whole banner —
   most players know their rivals by colour long before they read the name */
.df-slayer .df-dot {
  width: 13px; height: 13px; border-radius: 50%; flex: none;
  box-shadow: 0 0 12px var(--df-c, #ff7a2e), 0 0 3px rgba(0, 0, 0, .6);
}
.df-slayer b { color: color-mix(in srgb, var(--df-c, #ff7a2e) 42%, #fff); font-weight: 800; }
#dragonfall.df-nobody .df-dot { display: none; }
.df-fate {
  margin-top: 7px; font-size: 14px; color: #c2a998; font-style: italic;
  text-shadow: 0 1px 6px rgba(0, 0, 0, .9);
}
/* your own kill: the one line worth shouting rather than reporting */
#dragonfall.df-yours .df-fate { color: #ffd9a8; font-style: normal; font-weight: 700; }

/* One timeline, staggered by per-element delays: the crest slams in first, the
   title drops onto it, and the two lines beneath arrive after. Everything is on
   the root's `show` class, so a second fall replays it after one reflow. */
#dragonfall.show .df-whose  { animation: df-line 5.2s ease-out both; animation-delay: .10s; }
#dragonfall.show .df-crest  { animation: df-slam 5.2s ease-out both; }
#dragonfall.show .df-title  { animation: df-title 5.2s ease-out both; animation-delay: .16s; }
#dragonfall.show .df-slayer { animation: df-line 5.2s ease-out both; animation-delay: .46s; }
#dragonfall.show .df-fate   { animation: df-line 5.2s ease-out both; animation-delay: .66s; }
#dragonfall.show .df-stage::before { animation: df-pool 5.2s ease-out both; }

/* the crest arrives big and settles — the beast coming down, not fading in */
@keyframes df-slam {
  0%   { opacity: 0; transform: scale(1.9) translateY(-26px); }
  9%   { opacity: 1; transform: scale(1) translateY(0); }
  13%  { transform: scale(1.06); }
  18%  { transform: scale(1); }
  86%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.04) translateY(-14px); }
}
/* the title spreads its letters as it lands, so it reads as a proclamation */
@keyframes df-title {
  0%   { opacity: 0; letter-spacing: .38em; transform: scale(.96); }
  12%  { opacity: 1; letter-spacing: .1em; transform: scale(1); }
  86%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(-12px); }
}
@keyframes df-line {
  0%   { opacity: 0; transform: translateY(12px); }
  14%  { opacity: 1; transform: translateY(0); }
  86%  { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-8px); }
}
@keyframes df-pool {
  0%   { opacity: 0; }
  10%  { opacity: 1; }
  86%  { opacity: 1; }
  100% { opacity: 0; }
}

/* the title needs no entry here — it is clamped against the viewport and spaced
   in em, so it shrinks with the screen and its spread shrinks with it */
@media (max-width: 700px) {
  .df-stage { top: 24%; padding: 14px 14px 12px; }
  .df-stage::before { inset: -38% -12%; }
  .df-whose { font-size: 10.5px; letter-spacing: 2.5px; }
  .df-crest { font-size: 46px; }
  .df-slayer { font-size: 14px; }
  .df-fate { font-size: 12.5px; }
}

/* the flourish is decoration; the words are not. A reduced-motion preference
   drops the movement and leaves the banner sitting there for its full length. */
@media (prefers-reduced-motion: reduce) {
  #dragonfall.show .df-whose, #dragonfall.show .df-crest, #dragonfall.show .df-title,
  #dragonfall.show .df-slayer, #dragonfall.show .df-fate,
  #dragonfall.show .df-stage::before { animation: df-pool 5.2s ease-out both; animation-delay: 0s; }
}

/* ================= ⏸️ PAUSING (ui/pause.js) =================================
   Three surfaces for one feature: the ask, the vote card, and the countdown.
   They share a look with the exit dialog and the day box so a pause reads as
   part of the game's furniture rather than a browser interruption. */

/* ---- the ask: "this will be put to a vote" ---- */
#pause-backdrop { position: fixed; inset: 0; z-index: 48; background: rgba(5, 10, 18, .6); }
#pause-ask {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 49;
  width: 430px; max-width: 92vw; text-align: center;
  background: #17222f; border: 1px solid #3a4a5c; border-radius: 12px;
  padding: 20px 20px 14px; box-shadow: 0 18px 60px rgba(0, 0, 0, .6);
}
#pause-ask .pz-ask-title { font-size: 17px; font-weight: 700; color: #e8e2d0; margin-bottom: 8px; }
#pause-ask .pz-ask-body { font-size: 13px; color: #9fb0c0; line-height: 1.5; margin-bottom: 16px; }
#pause-ask .pz-ask-label { font-size: 12px; font-weight: 600; color: #c8d2dc; margin-bottom: 8px; }
#pause-ask .pz-picks { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; margin-bottom: 10px; }
#pause-ask .pz-pick { padding: 6px 12px; font-size: 12px; }
#pause-ask .pz-ask-row { display: flex; gap: 8px; align-items: center; justify-content: center; }
#pause-ask .pz-ask-row input {
  width: 88px; text-align: center; font-size: 15px; font-weight: 700; padding: 8px 6px;
  background: #101a26; color: #e8e2d0; border: 1px solid #46586c; border-radius: 8px;
}
#pause-ask .pz-ask-unit { font-size: 13px; color: #9fb0c0; }
#pause-ask .pz-ask-note { font-size: 11px; color: #7f8ea0; margin-top: 7px; }
#pause-ask .pz-ask-actions { display: flex; gap: 10px; justify-content: center; margin-top: 16px; flex-wrap: wrap; }
#pause-ask .pz-ask-actions button { padding: 10px 16px; font-size: 13px; }

/* ---- the vote card: bottom-left, clear of the hand and the corner docks ---- */
/* same layer as the countdown, and appended after it in ui/pause.js so it wins
   the tie — a resume vote runs DURING a pause, so the two share the screen */
#pause-vote {
  position: fixed; left: 12px; bottom: 12px; z-index: 47; width: 300px; max-width: 92vw;
  background: rgba(14, 22, 32, .96); border: 1px solid #d9a742; border-radius: 12px;
  padding: 14px 15px 12px; box-shadow: 0 14px 44px rgba(0, 0, 0, .55);
}
#pause-vote .pz-v-title { font-size: 14px; font-weight: 700; color: #f0c04a; margin-bottom: 6px; }
#pause-vote .pz-v-body { font-size: 12px; color: #9fb0c0; line-height: 1.45; margin: 0 0 10px; }
#pause-vote .pz-voters { display: flex; flex-direction: column; gap: 4px; }
#pause-vote .pz-voter { display: flex; align-items: center; gap: 7px; font-size: 12px; color: #c8d2dc; }
#pause-vote .pz-voter.pz-waiting { color: #7f8ea0; }
#pause-vote .pz-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
#pause-vote .pz-vname { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#pause-vote .pz-mark { flex: none; font-size: 11px; }
#pause-vote .pz-v-foot { font-size: 11px; color: #7f8ea0; margin-top: 9px; }
#pause-vote .pz-v-actions { display: flex; gap: 8px; margin-top: 11px; }
#pause-vote .pz-v-actions button { flex: 1; padding: 9px 10px; font-size: 13px; font-weight: 600; }
/* once you've answered it stops asking and just reports */
#pause-vote.pz-compact { width: 236px; padding: 12px 13px 10px; }
#pause-vote.pz-compact .pz-v-title { font-size: 13px; }

/* ---- the countdown: the whole point of the feature ----
   pointer-events: none throughout — the world is stopped, but the docks, the
   ⚙️ menu and the map underneath must all stay reachable (a player may want to
   leave, or just look around, while they wait). */
/* ⚠️ 47, ABOVE the dawn ceremony (46). A pause can land mid-dawn — the ceremony
   is on client-side timers and carries on regardless — and at an equal z-index
   the later-appended #dawn-seq painted its decree fan straight over the
   countdown AND over the one control on it, leaving the Resume button
   unreachable behind a card. Still below the exit dialog (48/49) and the
   Settings window (50), which are things the player opened on purpose. */
#pause-overlay {
  position: fixed; inset: 0; z-index: 47; pointer-events: none;
  display: flex; align-items: center; justify-content: center;
  background: rgba(5, 10, 18, .34);
}
#pause-overlay .pz-box { text-align: center; padding: 0 20px; }
#pause-overlay .pz-label {
  font-size: 15px; font-weight: 700; letter-spacing: 3px; text-transform: uppercase;
  color: #f0c04a; text-shadow: 0 2px 12px rgba(0, 0, 0, .8);
}
#pause-overlay .pz-clock {
  font-size: 104px; font-weight: 800; line-height: 1.05; color: #e8e2d0;
  font-variant-numeric: tabular-nums; /* so the digits don't jitter as they count */
  text-shadow: 0 4px 26px rgba(0, 0, 0, .85);
}
#pause-overlay .pz-sub {
  font-size: 14px; color: #c8d2dc; margin-top: 6px; text-shadow: 0 2px 10px rgba(0, 0, 0, .8);
}
/* the ONE thing on the overlay that takes clicks — the root stays
   pointer-events: none so the map and the corner docks are still reachable */
#pause-overlay .pz-resume {
  pointer-events: auto; margin-top: 20px; padding: 11px 20px; font-size: 14px; font-weight: 600;
  background: #2a3949; border: 1px solid #5c7288; color: #e8e2d0;
  border-radius: 9px; box-shadow: 0 8px 26px rgba(0, 0, 0, .5);
}
#pause-overlay .pz-resume:hover { background: #37495c; border-color: #7d95ad; }

@media (max-width: 820px) {
  /* the hand and the bottom-right action discs own the bottom of a phone
     screen, so the vote card moves up out of both */
  #pause-vote { left: 8px; right: 8px; bottom: auto; top: 96px; width: auto; }
  #pause-vote.pz-compact { width: auto; }
  #pause-overlay .pz-clock { font-size: 64px; }
  #pause-overlay .pz-label { font-size: 13px; letter-spacing: 2px; }
  #pause-overlay .pz-sub { font-size: 12.5px; }
}

/* ---------- THE TAB SCOREBOARD (ui/scoreboard.js) -------------------------
   Held open while Tab is down. It sits ABOVE the docks and the hand but BELOW
   everything the player opened on purpose (the dawn ceremony at 46, the pause
   countdown at 47, the exit dialog at 48/49, the Settings window at 50), and it
   takes no clicks at all — a key-held glance must never eat a press meant for
   the map underneath it. */
#scoreboard {
  position: fixed; inset: 0; z-index: 45; pointer-events: none;
  display: flex; align-items: center; justify-content: center;
  background: rgba(6, 10, 16, .55);
}
#scoreboard.hidden { display: none; }
.sb-panel {
  width: min(94vw, 900px); max-height: 84vh; overflow: auto;
  padding: 16px 18px 14px; border-radius: 14px;
  background: var(--ui-sheen), rgba(var(--ui-bg-deep), .97); border: 1px solid var(--ui-line);
  box-shadow: var(--ui-bevel), 0 24px 70px rgba(0, 0, 0, .75);
}
.sb-title { font-size: 16px; font-weight: 700; color: #ffe9b0; margin-bottom: 12px; }
.sb-title span { font-weight: 500; font-size: 13px; color: #9fb0c0; }

/* One grid, declared on the row rather than the table, so the header and the
   faction rows share a track list by construction. `minmax(0, 1fr)` on the
   source columns is load-bearing: a long faction name would otherwise push the
   columns off the right-hand edge instead of wrapping. */
.sb-row {
  display: grid; align-items: center; gap: 8px;
  grid-template-columns: 22px minmax(90px, 1.6fr) 62px repeat(6, minmax(0, 1fr));
  padding: 7px 6px; border-radius: 8px;
}
.sb-head { font-size: 10.5px; color: #8496a8; letter-spacing: .3px; padding-bottom: 2px; }
.sb-head .sb-cell { display: block; text-align: right; }
.sb-head .sb-cell i {
  display: block; font-style: normal; font-size: 9.5px; color: #6f8398;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sb-row + .sb-row { border-top: 1px solid rgba(255, 255, 255, .06); }
.sb-you { background: rgba(240, 192, 74, .09); }
.sb-dead .sb-name, .sb-dead .sb-total { text-decoration: line-through; }
.sb-dead { opacity: .72; }

/* A SIDE's heading row, and the members indented under it. The heading is what
   places, so it is the heavier of the two: the member rows are drawn back a
   little and stepped in, which is what says "these add up to the line above"
   without a second column of arithmetic on screen.
   ⚠️ `.sb-row + .sb-row`'s hairline is suppressed between a side and its own
   members — a rule there cuts the group in half and the indent then reads as two
   unrelated blocks rather than one. */
.sb-side { background: rgba(255, 255, 255, .045); }
.sb-side + .sb-member, .sb-member + .sb-member { border-top: 0; }
.sb-member { padding-top: 4px; padding-bottom: 4px; opacity: .88; }
.sb-member .sb-name { font-size: 12.5px; font-weight: 600; padding-left: 12px; }
.sb-member .sb-total { font-size: 12px; }
.sb-member .sb-cell b { font-size: 11px; }
/* the side's own dot in its team colour, beside its name in the heading row */
.sb-pip {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  margin-right: 6px; vertical-align: middle;
}

.sb-rank { font-size: 12px; color: #7f93a8; text-align: center; }
.sb-name {
  font-size: 13.5px; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sb-name em { font-style: normal; font-weight: 500; color: #9fb0c0; font-size: 11.5px; }
/* IS THIS PLAYER STILL HERE? A small lamp before the name — lit green while
   they hold a socket, hollow grey while they don't. Deliberately a dot rather
   than a word: it sits inside a name cell that already has to ellipsis long
   faction names, and "(away)" would eat the name it is describing. */
.sb-dot {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  margin-right: 6px; vertical-align: middle;
  background: #57d98a; box-shadow: 0 0 6px rgba(87, 217, 138, .75);
}
.sb-away { background: transparent; border: 1.5px solid #6b7a89; box-shadow: none; }
.sb-total { font-size: 14px; font-weight: 800; color: #ffe9b0; text-align: right; }

/* A number with its own bar underneath. The bar is scaled against the largest
   figure in the WHOLE table (see paint()), so a length means the same thing in
   every column. */
.sb-cell { position: relative; display: block; text-align: right; padding-bottom: 5px; }
.sb-cell b { font-size: 12px; font-weight: 600; color: #b6c2cf; }
.sb-best b { color: #ffe9b0; }
/* anchored RIGHT, so the bar grows out from under the digit it belongs to
   rather than starting somewhere off to its left — the numbers are
   right-aligned, and a left-anchored bar reads as belonging to the column
   before it */
.sb-bar {
  position: absolute; right: 0; bottom: 0; height: 2px; min-width: 0;
  border-radius: 2px; opacity: .5;
}
.sb-best .sb-bar { opacity: 1; height: 3px; }
.sb-foot {
  margin-top: 12px; padding-top: 9px; border-top: 1px solid rgba(255, 255, 255, .1);
  font-size: 11px; line-height: 1.5; color: #7f93a8;
}

/* WHAT FEEDS A COLUMN, on hovering its header. A column headed "Captures" is a
   number with no units on it — 300 there could be villages, castles or wonders,
   and without knowing which, a player can neither read what their rival has been
   doing nor work out what would catch them up.

   ⚠️ The header cell RE-ENABLES the pointer under #scoreboard's blanket
   pointer-events: none. That exemption is deliberately as small as it can be:
   the panel is held open with a key over a live map, and everything else in it
   must still let a press through to the ground underneath. */
.sb-what { pointer-events: auto; cursor: help; }
.sb-what:hover { color: #ffe9b0; }
.sb-tip {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 3;
  width: 240px; max-width: 62vw; padding: 9px 11px; border-radius: 9px;
  text-align: left; font-size: 11px; line-height: 1.55; font-weight: 400;
  color: #b6c2cf; letter-spacing: 0;
  background: rgba(8, 12, 18, .98); border: 1px solid var(--ui-line);
  box-shadow: 0 12px 30px rgba(0, 0, 0, .7);
  opacity: 0; visibility: hidden; transition: opacity 120ms ease, visibility 0s linear 120ms;
}
.sb-tip b { display: block; margin-bottom: 4px; color: #ffe9b0; font-size: 11.5px; }
.sb-what:hover .sb-tip { opacity: 1; visibility: visible; transition-delay: 0s; }
/* The leftmost headers would hang their plate off the panel's left edge, so
   those two open to the RIGHT instead. Flagged from paint() by index rather than
   picked out with :nth-of-type — the header row's spans are not all source
   cells, so an nth rule here counts the rank/name/total columns too and lands on
   the wrong ones. */
.sb-tip.sb-tip-l { right: auto; left: 0; }

@media (max-width: 720px) {
  /* the six source columns will not fit beside a name on a phone, so the
     numbers drop their labels and the panel simply scrolls sideways */
  .sb-panel { padding: 12px; }
  .sb-row { grid-template-columns: 18px minmax(70px, 1.3fr) 50px repeat(6, minmax(0, 1fr)); gap: 5px; }
  .sb-head .sb-cell i { display: none; }
  .sb-name { font-size: 12px; }
  .sb-total { font-size: 12.5px; }
  .sb-cell b { font-size: 10.5px; }
}

/* ---- the tutorial coach (see TUTORIAL.md) ---------------------------------
   RIGHT-HAND side, inset from the edge (64px, not flush) so it reads as sitting
   ON the battlefield rather than clipped to the window, and clear of the Build
   disc below it. The left column is the
   busy one in game (#hud, then the Heroes roster under it) and the coach was
   being squeezed between that stack and the hand; the right edge carries only
   the ⚙️ cog at the top and #actionbar at the bottom, so a card in the middle
   of it sits in genuinely free space and never crowds the numbers a player is
   reading.

   ⚠️ It clears #actionbar rather than the hand now: the disc is 128px plus a
   14px margin, so --tut-foot is that plus room to breathe. The HAND no longer
   matters — its row is centred and its own --hand-reserve keeps it off this
   corner — which is why --hand-peek is gone from this rule.

   z-index 43: above the map and the docks, and UNDER the dawn ceremony (46),
   the pause countdown (47) and every modal — the ceremony is a lesson rather
   than an interruption to one. pointer-events only on its own buttons, so it
   can never eat a click meant for the map. */
#tutorial {
  position: fixed; z-index: 43; pointer-events: none;
  /* ⚠️⚠️ ANCHORED BY ITS TOP, NOT ITS BOTTOM — and this is a click-target rule.
     Bottom-anchored, the panel's content height decides where its TOP is, and
     the ✕ lives at the top: the gesture line softening in, the "skip this step"
     row appearing, or the completion card swapping the layout each moved the
     button by tens of pixels — measured in game at 68px, which was enough for a
     press aimed at the ✕ to land on the map behind it instead.
     So the TOP is pinned at where the TALLEST version's top would sit, and the
     card grows downward from there, and nothing on the panel ever moves.

     ⚠️ THE PHONE DOES THE OPPOSITE and anchors by its bottom — see the
     `(pointer: coarse)` block at the end of this section for why, and for what
     it reserves instead to keep the ✕ still.

     ⚠️⚠️ --tut-max IS THE TALLEST CARD. It decides how high the top sits, so at
     that height the bottom of the tallest card lands on the Build disc's line
     and every shorter card simply finishes higher. Measured on an 823px window:
     top 321, which clears the top-left dock stack (289) by 32px.

     ⚠️ MEASURE A CARD WITH ITS GESTURE LINE AND ACTION ROW UP, not just its
     title and body — those two rows are worth ~45px and they are exactly what
     the tallest card carries. Measured in the running game: the worst case is
     `hut` at 311 (two paragraphs, a gesture, and the stuck row), then `decree`
     at 308 (four paragraphs and the stuck row, no gesture), and most steps sit
     at 170-270. 330 is therefore ~19px of slack rather than a snug fit —
     deliberately left, since the body re-wraps at a different width on a narrow
     window.

     ⚠️⚠️ RE-MEASURE BOTH ENDS IF A BODY GROWS, and remember what a card is
     actually made of: a paragraph BREAK costs 8px on top of whatever the text
     re-wraps to (.tut-body p + p), the stuck row is worth ~44, and a GESTURE
     line is worth ~54 whether it wraps or not. `decree` is the cautionary tale —
     with the gesture line it carried before its rewrite it measures 365 and
     scrolls inside its own body, which is what this cap's safety net is for and
     not what it should be doing at an ordinary size.

     ⚠️ `max-height` plus a scrolling body is the safety net for a window too
     short to hold the tallest card between the dock stack and the disc. It
     never engages at an ordinary size — nothing scrolls at 823px — but a
     scrollbar inside the card beats the card sitting on top of the Build disc.
     The flex column is what keeps the action row visible while the body
     scrolls. */
  display: flex; flex-direction: column;
  max-height: var(--tut-max, 330px);
  /* ⚠️⚠️ MEASURED FROM THE CENTRE OF THE SCREEN, NOT FROM THE RIGHT EDGE, and
     that is the whole point of this line. Every card here is a caption on
     something the camera is HOLDING, and the camera holds it at the centre — so
     the card's job is to sit just clear of the middle of the screen, at every
     width. Anchored to the edge (it was `clamp(56px, 13vw, 240px)`) it drifts
     away from its own subject as the window grows: measured on an ultrawide,
     "This is your Castle" sat most of a screen from the Castle, with the mint
     arrow pointing at something the card had no visual connection to.

     So --tut-near is the gap between the centre line and the card's LEFT edge,
     held constant however wide the window gets: `50% - near - width`. 210px is
     what the old rule happened to give on a 1440px window (187 inset + 320 card
     = a 213px gap), which is the width it was tuned on and reads correctly —
     the change is that every wider window now gets that same gap instead of a
     bigger one.

     ⚠️ The `max()` is the narrow-window floor: below ~1100px the arithmetic goes
     under the old 56px margin, and the card must keep its inset from the edge
     rather than being pinned to a gap it no longer has room for.

     ⚠️ It is deliberately NOT scaled to the viewport. The camera is
     ORTHOGRAPHIC and its frustum is set from the vertical view size, so a wider
     window shows more WORLD rather than drawing the subject any bigger — the
     subject's size on screen is a function of the window's HEIGHT, so a gap
     that grew with the width would only ever be too big. */
  --tut-near: 210px;
  right: max(56px, calc(50% - var(--tut-near) - 320px));
  /* ⚠️⚠️ ...AND IT SITS NEAR THE TOP THIRD, NOT ABOVE THE BUILD DISC. It used to
     be the bottom-anchored `100dvh - foot - max` alone, which is a floor rather
     than a position: on an 879px window that puts the card's top at 377 (43% of
     the height) and on a taller one further down still, while the camera holds
     its subject at about 28% (`followPoint`'s `STRUCT_LIFT` lifts a building
     above centre). So the caption sat well below the thing it was captioning,
     and the taller the monitor the worse it got.

     A fraction of the viewport keeps it level with the subject at every height.
     ⚠️ The old expression survives as the clamp's UPPER bound and is doing real
     work — it is what keeps the tallest card clear of the Build disc on a short
     window, where 22dvh would otherwise put the card straight through it.
     ⚠️ The floor clears the ⚙️ cog, which is the only thing in this column above
     the card (the dock stack is the LEFT column's problem); measured, the cog's
     bottom is 87px. ⚠️ When the window is short enough that the upper bound
     falls below the floor, `clamp` returns the floor — which is the right answer
     rather than an accident: at 600px that is a top of 104 against a disc at
     458, so even the 330px card still fits between them. */
  top: clamp(104px, 22dvh, calc(100dvh - var(--tut-foot, 172px) - var(--tut-max, 330px)));
  width: 320px; max-width: calc(100vw - 36px); box-sizing: border-box;
  padding: 14px 16px 13px;
  border-radius: 12px; border: 1px solid var(--ui-line, #2b3a4d);
  /* ⚠️ --ui-BG, not --ui-panel. They are different SHAPES, not two names for
     one colour: the grounds are bare RGB triples so a panel can pick its own
     alpha, and --ui-panel is a finished opaque colour. Written as
     rgba(var(--ui-panel), .95) the declaration is simply INVALID, so the panel
     falls back to transparent and its body text is unreadable over the map —
     which is what this looked like in game before it was caught. */
  background: rgba(var(--ui-bg, 14, 22, 32), .96);
  box-shadow: var(--ui-lift-lg, 0 10px 30px rgba(0, 0, 0, .5));
  color: #dbe6f2; font-size: 13px; line-height: 1.45;
  animation: tut-in .22s ease-out;
}
#tutorial.hidden { display: none; }
#tutorial button { pointer-events: auto; }
@keyframes tut-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
/* ⚠️ `animation: none` and NOT `transform: none` — the preamble card's centring
   lives in transform, so killing that would put it in the top-left corner for
   anyone who has asked for less motion. */
@media (prefers-reduced-motion: reduce) { #tutorial { animation: none; } }

/* ⚠️⚠️ THE PREAMBLE CARD — CENTRED AND WIDE, and the ONE card here that is not
   a caption on something. Every other step has the camera holding its subject,
   which is exactly why the ordinary card is small and pushed off to the side:
   it must not stand in front of the thing it is describing. This one describes
   the GAME, there is nothing behind it worth keeping clear, and it carries four
   paragraphs — at 320px in the right-hand column that is a column of text a
   player scrolls, which is the worst possible first impression.

   ⚠️ It overrides the `right`/`top` anchoring rather than sitting alongside it,
   so all three of `right`, `left` and `top` have to be restated — leave `right`
   set and the box is pinned by both edges and the width is ignored.

   ⚠️ The reserved title/body heights are dropped here. They exist so a control
   cannot slide out from under a click as the card's content changes between
   steps; there is exactly ONE preamble card and its content never changes, so
   nothing on it can move. (It is also vertically centred, so its height decides
   where its own top is — the very thing the reserves were added to prevent —
   which is safe for the same reason and for no other.) */
#tutorial.tut-preamble {
  right: auto; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: min(560px, calc(100vw - 48px));
  max-height: min(600px, calc(100dvh - 96px));
  padding: 22px 26px 20px;
  font-size: 14.5px; line-height: 1.55;
  animation: tut-in-mid .24s ease-out;
}
#tutorial.tut-preamble .tut-step { font-size: 11.5px; margin-bottom: 6px; }
#tutorial.tut-preamble .tut-title { font-size: 23px; line-height: 1.2; margin-bottom: 12px; min-height: 0; }
#tutorial.tut-preamble .tut-body { min-height: 0; }
#tutorial.tut-preamble .tut-body p + p { margin-top: 12px; }
#tutorial.tut-preamble .tut-actions { margin-top: 18px; }
#tutorial.tut-preamble .tut-actions button { padding: 10px 12px; font-size: 13px; }
/* ⚠️ Its own keyframes, because the centring lives in `transform` and the
   ordinary `tut-in` animates that property to `none` — which would snap the
   card to the top-left corner for the length of its own entrance. */
@keyframes tut-in-mid {
  from { opacity: 0; transform: translate(-50%, calc(-50% + 10px)); }
  to { opacity: 1; transform: translate(-50%, -50%); }
}

/* ---- THE TWO PLACEMENTS (placePanel in ui/tutorial.js) ---------------------
   The card's home is the right-hand column, clear of whatever the camera has
   centred. Two beats move it because their subject is not in the middle of the
   screen: the gold beat's is two rows in the HUD (top left), and the worker
   beats centre the worker, so the usual column puts the card exactly where the
   eye has just been sent.

   ⚠️ Both restate `right: auto` for the preamble card's documented reason —
   leave it set and the box is pinned by both edges, the width is ignored, and
   the card silently comes out as the ordinary right-hand column again, which
   reads as the flag having done nothing rather than as a CSS mistake.

   ⚠️ Same specificity as the preamble (1 id + 1 class); they can never both be
   on, since the preamble card carries no `place`. */
/* ⚠️ The MIRROR of the home column's arithmetic, and it has to be — the worker
   the camera has centred is in the middle of the screen from this side too, so
   an edge-anchored card on the left drifts away from it on a wide window in
   exactly the same way. Same --tut-near, so the two placements sit the same
   distance from the subject and a beat that moves the card reads as the card
   changing SIDES rather than changing distance. */
#tutorial.tut-left { right: auto; left: max(56px, calc(50% - var(--tut-near) - 320px)); }
/* Tucked under the stats panel and flush with its left edge (.dock.tl's own
   8px). Its `top` is written inline from that panel's measured rect — its
   height is its own content's, and it grows as the game does. */
#tutorial.tut-stats { right: auto; left: 8px; }

.tut-step {
  font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ui-key, #8fe0a4); opacity: .85; margin-bottom: 4px;
}
/* ⚠️ Both of these carry a reserved height, and the TITLE is the one that
   actually moved the panel: measured across the sixteen steps it is 19px on one
   line and 38px on two, against a body that varies 57–94px inside a 95px
   reserve. See .tut-body below for why a control on this panel must not move. */
.tut-title {
  font-size: 15px; font-weight: 700; color: #f2f7fc; line-height: 1.25;
  margin-bottom: 6px; min-height: 2.5em;
}
/* ⚠️⚠️ A RESERVED HEIGHT, AND IT IS A CLICK-TARGET FIX RATHER THAN A TIDINESS
   ONE. The panel is anchored by its BOTTOM (so it clears the hand), which means
   its content height decides where its TOP is — and the ✕ lives at the top. A
   body one line longer moved the whole panel 19px, so a step advancing between
   somebody deciding to press ✕ and actually pressing it slid the button out
   from under them; measured in game, and the second click of the two-click
   confirm missed entirely. Reserving the tallest body's height pins every
   control on the panel in place for the whole tutorial. */
.tut-body { color: #b9c8d8; min-height: 5.9em; overflow-y: auto; }
/* a body may be more than one paragraph; the first sits flush with the title */
.tut-body p { margin: 0; }
.tut-body p + p { margin-top: 8px; }
/* the exact gesture, spelled out only once somebody has been sitting here */
.tut-gesture {
  margin-top: 9px; padding-top: 9px; border-top: 1px solid var(--ui-line-soft, rgba(255, 255, 255, .09));
  color: #8fe0a4; font-weight: 600;
}
/* ⚠️ THE CUE WEARS THE GESTURE'S LOOK BECAUSE IT MEANS THE SAME THING — "this is
   the bit to act on" — and the two can never be on screen at once (a step whose
   copy ends in an instruction is exactly the step that carries no gesture). What
   differs is the clock, not the styling: a gesture softens in after 45s, a cue
   is up from the first frame. ⚠️ It is a padded BLOCK rather than a bare line
   because `spotlight` may point at it (`#tut-cue`), and .tut-spot's outline and
   green wash need something to sit on — drawn tight to the text they read as a
   box round a sentence rather than as a highlighted row. */
.tut-cue {
  margin-top: 9px; padding: 7px 9px;
  border-radius: 6px; border: 1px solid rgba(143, 224, 164, .28);
  background: rgba(143, 224, 164, .09);
  color: #8fe0a4; font-weight: 600;
}
.tut-cue.hidden, .tut-gesture.hidden, .tut-actions.hidden { display: none; }

/* ---- the reward's own model ------------------------------------------------
   Shown twice: on the OFFER card before the tutorial starts, and on the
   COMPLETION card at the end. ⚠️ The same picture both times, deliberately —
   seeing the thing you were promised, unlocked, is what closes that loop, and
   "the Big Comedy Eyes" means nothing to somebody who has never seen them.
   ⚠️ The image is a rendered thumbnail with its own transparent background, so
   the frame is what gives it a place to sit on either card's ground. */
.tut-prize {
  display: flex; align-items: center; justify-content: center;
  margin: 10px auto 2px; padding: 8px;
  width: 104px; height: 104px; box-sizing: border-box;
  border-radius: 12px; border: 1px solid var(--ui-line, #2b3a4d);
  background: rgba(255, 255, 255, .05);
}
.tut-prize img { width: 100%; height: 100%; object-fit: contain; display: block; }
/* the fallback when the thumbnail could not be rendered — see rewardThumb */
.tut-prize-emoji { font-size: 46px; line-height: 1; }
/* ⚠️ GREEN, and only on the completion card: the ring is the difference between
   "this is what you could win" and "this is yours". Same key green as the
   coach's own spotlight, so the two read as one voice. */
.tut-prize.unlocked {
  border-color: #8fe0a4;
  background: rgba(143, 224, 164, .10);
  box-shadow: 0 0 0 1px rgba(143, 224, 164, .35), 0 0 18px rgba(143, 224, 164, .28);
}
.tut-actions { display: flex; gap: 8px; margin-top: 11px; }
.tut-actions button {
  flex: 1; padding: 7px 10px; border-radius: 8px; font-size: 12px; font-weight: 600; cursor: pointer;
  background: rgba(255, 255, 255, .06); border: 1px solid rgba(255, 255, 255, .16); color: #cfdcea;
}
.tut-actions button:hover { background: rgba(255, 255, 255, .12); }
.tut-actions button.primary { background: #2f7d4a; border-color: #3f9c5f; color: #fff; }
.tut-actions button.primary:hover { background: #379157; }
/* ✕ — one press, and it ASKS: end the tutorial and leave the room, or carry on
   (askSkip in ui/tutorial.js — there is deliberately no "keep this game", see
   the note there). It was a two-click confirm; the notes about a control on this
   panel not being allowed to move predate that and still hold, since a missed
   press is a press on the map. */
.tut-skip {
  position: absolute; top: 8px; right: 8px; width: 22px; height: 22px; padding: 0;
  display: grid; place-items: center; border-radius: 6px; cursor: pointer;
  background: none; border: 1px solid transparent; color: #7b8b9c; font-size: 12px; line-height: 1;
}
.tut-skip:hover { color: #e8a0a0; border-color: rgba(232, 160, 160, .4); }

/* ⚠️ A phone has no free right-hand column at all — the map is barely wider
   than the card — so there the coach goes FULL WIDTH and sits above the hand,
   clearing #rotatebar (bottom-left, z-index 27, at --hand-peek + 30px) as well
   as the hand itself. This is the one place --hand-peek is still read. */
@media (pointer: coarse) {
  #tutorial {
    /* ⚠️⚠️ ANCHORED BY ITS BOTTOM HERE, WHICH IS THE OPPOSITE OF THE DESKTOP
       RULE ABOVE — AND IT IS THE FIX FOR "THE CARD COVERS THE THING IT IS
       TRACKING". The desktop card is a 320px column off to one side of a subject
       the camera holds in the middle; the phone card is FULL WIDTH, so it is a
       BAND ACROSS THE MAP and where that band falls is the whole question.
       Top-anchored it was placed at the height the TALLEST card (--tut-max, 330)
       would need, and every ordinary card — measured 201-287 on a phone —
       simply finished higher and left dead space above the hand. On a 390x844
       screen that put the band at 269-495 with the camera holding a building at
       329 and a unit at 422: both inside it, on every step (reported in play).

       Bottom-anchored the card sits as low as the furniture below it allows and
       a short card sits lower still. `top: auto` is not optional — the base rule
       sets one, and a box with both is stretched between them.

       ⚠️ THE OTHER HALF OF THAT FIX IS THE CAMERA: a phone card is ~226px of an
       844px screen with a 107px hand under it, so no placement of the card alone
       can clear a subject the camera holds at 39% (a building) or 50% (a unit).
       main.js lifts the coach's subject on a coarse pointer (COACH_LIFT) so it
       rides in the free band above this card. Judge the two together. */
    left: max(12px, env(safe-area-inset-left)); right: max(12px, env(safe-area-inset-right));
    width: auto; max-width: none;
    /* ⚠️⚠️ THE FLOOR IS THE PLACEMENT PILL AT ITS RAISED LINE, NOT THE HAND.
       Below the card sit the resting hand (--hand-peek, 107 on any phone — the
       cards hit CARD_W_MIN), the ↻ button beside it (--hand-peek + 30 + 62) and
       #placement-hint, which steps up to --hand-peek + 133 while the hand is
       raised and is ~35 tall. The pill is the highest of the three AND the one
       that must stay readable: it is the only thing on screen that says the
       Rally Flag's middle beat is a press-and-HOLD, which is the tutorial's own
       `rally` step. So the card stops 10px clear of it; anything lower buries
       the pill behind the card (z-index 43 against its 25). */
    --tut-foot: calc(var(--hand-peek, 150px) + 178px + env(safe-area-inset-bottom));
    top: auto;
    bottom: var(--tut-foot);
    /* ⚠️ The cap is what keeps a TALL card off the top of the screen here,
       rather than what it is on the desktop (a safety net that never engages):
       the card grows upward from this line, so on a short phone it would
       otherwise reach the day box. 92px is that box plus air; past it the body
       scrolls, which beats the card sitting on the day counter. */
    max-height: calc(100dvh - var(--tut-foot) - 92px);
    padding: 11px 13px 12px; font-size: 12.5px;
  }
  /* ⚠️⚠️ THE ACTION ROW IS RESERVED HERE WHERE THE DESKTOP LETS IT APPEAR, and
     that is a click-target rule rather than a tidiness one — the same one the
     title and body reserves above exist for, applied to the one thing that is
     still free to move now the card is anchored by its bottom. Its height is its
     own content's, so Continue arriving pushes the card's TOP up ~40px — and the
     ✕ lives there. A thumb already reaching for the cross would miss. Reserved,
     the only thing left that can move the top is the gesture line softening in
     at 45s, by which point the player has been sitting still for the better part
     of a minute.

     ⚠️⚠️ THE `!important` IS NOT SPECIFICITY LAZINESS: the row is hidden by the
     global `.hidden { display: none !important }` at the top of this file, which
     no ordinary rule can beat however specific. Keeping the box IN the layout
     and hiding it with `visibility` is the only way to reserve the space it is
     about to take — and it must be measured, not guessed: 29px is the button's
     own height at these font sizes, and it was silently doing nothing at all
     until the !important went on (the row still collapsed to 0 and the card
     still jumped 41px). */
  .tut-actions.hidden { display: flex !important; visibility: hidden; min-height: 29px; }
  .tut-actions { min-height: 29px; }
  .tut-title { font-size: 14px; }
  /* ⚠️⚠️ ...and the two desktop placements FALL BACK to that full-width card. A
     phone has no free column on either side to move into — and both of them are
     (1 id + 1 class), so left alone they would OUTRANK the `#tutorial` rule
     above and pin the card to a 320px column. Only the properties they set have
     to be put back; `width`/`top` are not in conflict, so they still come from
     the rule above. (placePanel writes no inline `top` on a coarse pointer for
     the same reason — an inline style beats a media query outright.) */
  #tutorial.tut-left, #tutorial.tut-stats {
    left: max(12px, env(safe-area-inset-left)); right: max(12px, env(safe-area-inset-right));
  }
  /* ⚠️ The preamble stays CENTRED here rather than joining the full-width rule
     above — that rule exists so a caption card clears the hand it is describing,
     and this card describes nothing on screen. It only needs to be smaller.
     Measured across the four paragraphs: at the desktop's 14.5px/26px it is 544
     tall on a 360x640 screen against a 544 cap — scrolling on the smallest
     phones — and 496 at these numbers. 390x844 goes 536 → 454.
     ⚠️ `#tutorial.tut-preamble` (1 id + 1 class) already outranks the `#tutorial`
     rule above whatever the source order, so the centring survives — only the
     properties restated here change.
     ⚠️⚠️ ...EXCEPT `bottom`, which it MUST put back now that the phone card is
     anchored by its bottom edge: the preamble sets `top: 50%` and nothing else,
     so with a `bottom` still in force the box is pinned by both edges, its
     height is ignored and the four paragraphs are stretched down the screen.
     The desktop rule needs no such line because the desktop card sets no
     `bottom` at all — the same trap its own `right: auto` documents. */
  #tutorial.tut-preamble {
    bottom: auto;
    padding: 16px 18px 15px; font-size: 13.5px;
  }
  #tutorial.tut-preamble .tut-title { font-size: 20px; margin-bottom: 10px; }
  #tutorial.tut-preamble .tut-body p + p { margin-top: 10px; }
  #tutorial.tut-preamble .tut-actions { margin-top: 14px; }
}


/* ---- the tutorial's progressive reveal (see TUTORIAL.md) -------------------
   The coach opens with the map and its own card and NOTHING ELSE; each surface
   arrives on the beat that first talks about it. Before that a beginner's
   screen carries a stats panel, a day box, a Heroes roster, a cog, a battle bar
   and a hand of cards none of which they have been told about — and the card
   saying "this is your Castle" is competing with all of them.

   ⚠️⚠️ WRITTEN AS `:not([data-tut-show~="x"])` RATHER THAN A STYLE ON THE
   ELEMENT. These surfaces have their own display values (flex, grid, block) and
   several are shown/hidden by their own modules on the 600ms tick — so setting
   display:none from JS would either be fought by that module a moment later, or
   need the coach to remember and restore each original value. Matching on the
   body's attribute means REVEALING is simply the rule ceasing to apply, and the
   element goes back to whatever its owner says it should be.

   ⚠️ `body.tut-hiding` is what arms any of this, and it is set only while a
   coached tutorial is running — so an ordinary game, and a tutorial that has
   been finished or skipped, never match a single one of these rules. */
body.tut-hiding:not([data-tut-show~="day"]) #topbar,
body.tut-hiding:not([data-tut-show~="hud"]) #dock-stats,
body.tut-hiding:not([data-tut-show~="hand"]) #hand,
body.tut-hiding:not([data-tut-show~="hand"]) #actionbar,
/* ⚠️ TWO homes for the roster: on a desktop it lives INSIDE #dock-stats, on a
   phone ui/heroes.js moves it into its own #dock-heroes tab. Gating only the
   phone one would have the roster appear on the desktop along with the stats
   panel at step 2, several beats before anything mentions a Hero. */
body.tut-hiding:not([data-tut-show~="heroes"]) #heroes-box,
body.tut-hiding:not([data-tut-show~="heroes"]) #dock-heroes,
body.tut-hiding:not([data-tut-show~="cog"]) #dock-settings,
body.tut-hiding:not([data-tut-show~="battlebar"]) #battlebar { display: none !important; }

/* ---- the coach pointing at something on screen (see TUTORIAL.md) -----------
   A step may name rows in the HUD; they get an outline and a pulse and an arrow
   parks beside them. The HUD is a dense stack of numbers in the far corner from
   the card, so "the gold row" means nothing to somebody seeing all of them for
   the first time — the point has to be made ON the row.

   ⚠️ Both are applied from JS every 600ms because refreshHud rewrites #hud's
   whole innerHTML on that same tick (see paintSpotlight). */
/* ⚠️⚠️ IT MUST NOT SET `position`. This rule is applied to whatever a step names,
   and half of those are ALREADY positioned: a hand card is `position: absolute`
   and placed by a transform, and `#actionbar` is `position: fixed`. Overriding
   either to `relative` drops the element out of the layout it was placed by and
   into normal flow — seen in game on the `army` beat, where spotlighting the
   three trainer cards flung them out of the fan and one of them off the bottom
   of the screen entirely. Only the HUD rows are statically positioned, so only
   they get the positioning, below. */
.tut-spot {
  /* ⚠️ NO border-radius here: an outline follows the element's OWN radius, and
     the Build disc is a 50% circle. Setting a 6px radius on everything drew a
     rounded rectangle around a round button. The HUD rows, which have no radius
     of their own, get one below. */
  outline: 2px solid #8fe0a4;
  outline-offset: 1px;
  background: rgba(143, 224, 164, .14) !important;
  animation: tut-spot-pulse 1.5s ease-in-out infinite;
}
/* the HUD rows are square, so they get the radius the outline should follow */
/* ⚠️ The HUD rows are the one spotlight target with no positioning of their own
   — so they, and only they, need a stacking context for the outline to sit
   above their neighbours. They also have no radius of their own (the note on
   the base rule above is about the Build disc's 50%). */
[data-row].tut-spot { position: relative; z-index: 1; border-radius: 6px; }
/* ⚠️ A card in the fan overlaps its neighbours and its z-index is written
   INLINE by hand.js's build() (10 + its slot), so a stylesheet rule needs
   `!important` to lift the outlined one clear of the cards either side of it.
   30 clears the whole fan without touching `position`, which is what the rule
   above exists to avoid. */
.hand-card.tut-spot { z-index: 30 !important; }
@keyframes tut-spot-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(143, 224, 164, .55); }
  50%      { box-shadow: 0 0 0 6px rgba(143, 224, 164, 0); }
}
/* ⚠️ Positioned in VIEWPORT coordinates from the rows' own measured rect, not
   by CSS: the HUD's height changes with what it is showing (a compact panel on
   a phone, extra rows mid-game), so anything anchored to a constant drifts off
   the thing it is pointing at. `left`/`top` are written by paintSpotlight. */
#tut-point {
  position: fixed; z-index: 44; pointer-events: none;
  transform: translateY(-50%);
  color: #8fe0a4; font-size: 26px; line-height: 1;
  text-shadow: 0 2px 8px rgba(0, 0, 0, .8);
  animation: tut-point-nudge 1s ease-in-out infinite;
}
#tut-point.hidden { display: none; }
/* ⚠️ Parked on the LEFT of its target instead, for anything close to the right
   edge of the window — the Build disc sits 14px off it, and an arrow beside it
   there is off screen. `scaleX(-1)` turns the glyph round with it. */
#tut-point.flip { animation-name: tut-point-nudge-flip; }
/* nudges TOWARD the row it is pointing at, so the motion reads as an arrow
   indicating rather than a decoration jiggling */
@keyframes tut-point-nudge {
  0%, 100% { transform: translateY(-50%) translateX(4px); }
  50%      { transform: translateY(-50%) translateX(-4px); }
}
@keyframes tut-point-nudge-flip {
  0%, 100% { transform: translateY(-50%) scaleX(-1) translateX(4px); }
  50%      { transform: translateY(-50%) scaleX(-1) translateX(-4px); }
}
/* ⚠️⚠️ ONE ARROW PER TARGET, ABOVE IT, POINTING DOWN (`point: 'down'` on a step).
   Its own element rather than a mode on #tut-point because there is ONE PER
   TARGET — the `army` beat lights three trainer cards and needs three. Centred
   on the point the JS writes by its own transform, so the coordinates stay the
   plain "middle of the card's top edge". */
.tut-point-down {
  position: fixed; z-index: 44; pointer-events: none;
  transform: translate(-50%, -100%);
  color: #8fe0a4; font-size: 26px; line-height: 1;
  text-shadow: 0 2px 8px rgba(0, 0, 0, .8);
  animation: tut-point-down-nudge 1s ease-in-out infinite;
}
.tut-point-down.hidden { display: none; }
/* nudges TOWARD what it is pointing at, like the sideways one */
@keyframes tut-point-down-nudge {
  0%, 100% { transform: translate(-50%, -100%) translateY(-4px); }
  50%      { transform: translate(-50%, -100%) translateY(4px); }
}

/* ⚠️⚠️ A CARD THE CURRENT BEAT WILL NOT LET YOU PLAY (`onlyBuild`). The `hut`
   beat asks for a Worker Hut specifically, and a beginner who drags a Watchtower
   instead has satisfied nothing and spent gold they will want back.
   ⚠️ `pointer-events: none` is the visible half only — ui/panels.js's
   `startBuildPlacement` refuses outright as well, since that is the one door
   every route into arming a build comes through and CSS can be worked around by
   a keyboard or a stale drag. Greyed rather than hidden: what a player can build
   later is part of what this beat is showing them. */
.hand-card.tut-blocked {
  pointer-events: none;
  filter: grayscale(1) brightness(.55);
  opacity: .55;
}

@media (prefers-reduced-motion: reduce) {
  .tut-spot, #tut-point, .tut-point-down { animation: none; }
  #tut-point.flip { transform: translateY(-50%) scaleX(-1); }
}
