/* ── Combat Attack Menu styles ──
   Popup pickers (attack / target), the Confirm modal, and a transient toast.
   Namespaced `cam-*` so nothing here collides with the roster / abilities CSS.
   Colors lean on the combat-ui / mock-base tokens where they exist, with
   dark-first fallbacks so the module also renders standalone. */

:root {
  --cam-surface: #0f172a;
  --cam-surface-2: #1e293b;
  --cam-border: #334155;
  --cam-text: #e2e8f0;
  --cam-muted: #94a3b8;
  --cam-accent: #38bdf8;
  --cam-accent-ink: #08131f;
  /* Amber, matching the map's own over-budget colour (--c-orange in map.css):
     an unaffordable ability and an over-long walk are the same warning. */
  --cam-warn: #f0a45c;

  /* ── semantic metric colours ──
     The panel's readouts name five different quantities in one comma-run
     ("range 2 · 4.1 break · 2.8 impact · 34% stagger"), and a single muted
     grey made them one undifferentiated string. Each gets the hue the rest of
     the interface already associates with it, declared here rather than
     reached for as --c-* so this module keeps rendering standalone.

     NOTE range is GREEN here, deliberately diverging from the Abilities tab's
     .m-rng → --c-pink (v2/abilities.css). Requested; flagged so the next
     person to notice the disagreement knows it is a choice, not a drift. */
  --cam-dmg:     #f86a63;   /* damage  — --c-red    */
  --cam-range:   #6cd48c;   /* range   — --c-green  */
  --cam-break:   #7aa6ff;   /* break   — --c-blue   */
  --cam-impact:  #f0a45c;   /* impact  — --c-orange */
  --cam-stagger: #f6de7d;   /* stagger — --c-yellow */

  /* Shield, matching combat-ui.css's --shield-green and the map token's
     .bfmap-token-vitals-shield. Pure green, not --c-green: see the note there. */
  --cam-shield: #31ad3f;

  /* Allegiance, relative to the PC — the same blue/red the roster card's
     left border (.roster-card.is-ally/.is-enemy) and the map token's art
     border (.bfmap-token.side-player/.side-npc) already use. */
  --cam-ally:  #7aa6ff;
  --cam-enemy: #f86a63;
}

/* ── floating picker menu ── */
.cam-menu-backdrop {
  position: fixed;
  inset: 0;
  z-index: 9000;
  background: transparent;
}
.cam-menu {
  position: fixed;
  min-width: 230px;
  max-width: min(340px, 92vw);
  max-height: 70vh;
  overflow-y: auto;
  background: var(--cam-surface);
  border: 1px solid var(--cam-border);
  border-radius: 10px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
  padding: 6px;
  color: var(--cam-text);
  font-size: 13px;
}
.cam-menu-head {
  padding: 6px 8px 8px;
  font-weight: 600;
  border-bottom: 1px solid var(--cam-border);
  margin-bottom: 4px;
}
.cam-menu-sub { color: var(--cam-muted); font-weight: 400; font-size: 11px; }
.cam-menu-body { display: flex; flex-direction: column; gap: 2px; }

.cam-loading, .cam-empty, .cam-error, .cam-note {
  padding: 8px; color: var(--cam-muted); font-size: 12px;
}
.cam-error { color: #fca5a5; }

.cam-divider {
  margin: 6px 4px 2px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--cam-muted);
  border-top: 1px dashed var(--cam-border);
  padding-top: 6px;
}

/* ── an attack / target row ── */
.cam-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  background: transparent;
  border: 0;
  border-radius: 7px;
  padding: 7px 8px;
  color: var(--cam-text);
  cursor: pointer;
  font: inherit;
}
.cam-row:hover:not(.is-out) { background: var(--cam-surface-2); }
.cam-row.is-out { opacity: 0.5; cursor: default; }
/* Arrow-key focus — same visual language as .cam-target-row.on (an accent
   border), so keyboard and mouse selection read as the same kind of state. */
.cam-row.is-focused { background: var(--cam-surface-2); box-shadow: inset 0 0 0 1px var(--cam-accent); }
.cam-row-swatch {
  width: 9px; height: 9px; border-radius: 2px; flex: 0 0 auto;
}
.cam-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cam-row-meta { flex: 0 0 auto; color: var(--cam-muted); font-variant-numeric: tabular-nums; }

/* The ability's movement price (a Quick(N) keyword). Between the name and the
   damage estimate, and only present on the few abilities that cost movement at
   all — a "0 mv" on every row would bury the ones that matter. */
.cam-row-move {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  font-size: 0.85em;
  color: var(--cam-muted);
}

/* Costs more movement than the creature has left.

   MARKED, NOT DISABLED. `.is-out` above is out of REACH — geometry, genuinely
   unavailable, and the row stops responding. This is a budget, and every other
   budget on this board is advisory: the map badges an over-long walk orange and
   still lets it happen. So the price goes amber and the row stays clickable. */
.cam-row.is-unaffordable .cam-row-move {
  color: var(--cam-warn, #f0a45c);
  font-weight: 600;
}
.cam-row.is-unaffordable .cam-row-name {
  text-decoration: line-through;
  text-decoration-color: var(--cam-warn, #f0a45c);
  text-decoration-thickness: 1px;
}

/* Rows list immediately and their damage estimates stream in one at a time, so
   a row can be on screen and pickable before it has a number. Mark the wait on
   the meta cell only — the row itself stays fully legible and clickable. */
.cam-row.is-pending .cam-row-meta,
.cam-cmp-row.is-pending .cam-cmp-cell {
  animation: cam-pulse 1.4s ease-in-out infinite;
}
@keyframes cam-pulse {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 0.85; }
}
@media (prefers-reduced-motion: reduce) {
  .cam-row.is-pending .cam-row-meta,
  .cam-cmp-row.is-pending .cam-cmp-cell { animation: none; }
}

/* Damage-calculator pop-up beside the focused row (arrow-key or mouse) — see
   wireRowNav/showEstimatePopup. Its own fixed-position element rather than a
   child of .cam-menu, which clips overflow for its own scrollbar. */
.cam-estimate-popup {
  position: fixed;
  z-index: 9010;
  min-width: 170px;
  max-width: 240px;
  background: var(--cam-surface);
  border: 1px solid var(--cam-border);
  border-radius: 10px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
  padding: 8px 10px;
  color: var(--cam-text);
  font-size: 12px;
  pointer-events: none;
}
.cam-estimate-title {
  font-weight: 600;
  margin-bottom: 6px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--cam-border);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cam-estimate-wait { color: var(--cam-muted); }
.cam-estimate-grid {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 3px 10px;
}
.cam-estimate-grid span { color: var(--cam-muted); }
.cam-estimate-grid b { font-variant-numeric: tabular-nums; text-align: right; }

/* ── Confirm modal ── */
.cam-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 9100;
  background: rgba(2, 6, 23, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}
/* Confined to one pane rather than the whole window — openModal writes the
   left/top/width/height. Used by the map: a panel raised by right-clicking a
   token belongs over the board, not centred across a two-pane layout where
   half of it lands on the creature column. `inset: auto` releases the full-
   window inset above so the inline geometry takes effect. */
.cam-modal-backdrop.cam-anchored {
  inset: auto;
}
.cam-modal-backdrop.cam-anchored .cam-modal {
  /* The pane is narrower than the window, so size to it rather than to 96vw. */
  width: min(460px, 100%);
  max-height: 100%;
}

/* ── Docked mode ──
   A COMPANION to the map, not a cover over it — see openModal's opts.dock.
   Three differences from the plain modal above, and each is load-bearing:

     · no dim — the board behind is the thing being previewed, and dimming it
       makes the arrows and the AOE template the panel exists to explain the
       hardest part of the screen to read;
     · pointer-events: none on the backdrop, auto on the panel — this is the
       ONLY thing keeping the map clickable underneath a fixed element that
       covers the whole pane at z-index 9100. Anything added inside
       .cam-docked that takes pointer-events re-breaks token clicks;
     · no click-outside dismissal (openModal skips the listener) — every map
       click is now a target toggle, and dismissing the panel on one would
       make it unusable.

   Confined to the pane by .cam-anchored, same as the plain modal; the corner
   classes just re-aim the flex box that used to centre it. */
.cam-modal-backdrop.cam-docked {
  background: none;
  pointer-events: none;
  padding: 10px;
}
.cam-modal-backdrop.cam-docked .cam-modal {
  pointer-events: auto;
  /* Narrower than the 460px modal: this is a side column beside a board, and
     every px it takes is a px the map does not get. */
  width: min(380px, 100%);
  max-height: 100%;
  animation: cam-dock-in 160ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
}
.cam-dock-tr { justify-content: flex-end;   align-items: flex-start; }
.cam-dock-tl { justify-content: flex-start; align-items: flex-start; }
.cam-dock-br { justify-content: flex-end;   align-items: flex-end;   }
.cam-dock-bl { justify-content: flex-start; align-items: flex-end;   }

/* Slide in FROM THE EDGE it docks against, so the motion says where the panel
   came from and where Escape will send it. One keyframe, four start offsets. */
.cam-dock-tr .cam-modal, .cam-dock-br .cam-modal { --cam-dock-from: 24px; }
.cam-dock-tl .cam-modal, .cam-dock-bl .cam-modal { --cam-dock-from: -24px; }
@keyframes cam-dock-in {
  from { transform: translateX(var(--cam-dock-from, 24px)); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .cam-modal-backdrop.cam-docked .cam-modal { animation: cam-dock-fade 120ms linear both; }
  @keyframes cam-dock-fade { from { opacity: 0; } to { opacity: 1; } }
}

/* Mobile: a docked/anchored card sized for a mouse is too small to be worth
   opening on a phone board — it becomes nearly the whole pane instead. The
   collapse toggle (.cam-collapse-btn / .cam-modal.is-collapsed below) is
   what gets the board back without closing the panel outright. */
@media (max-width: 720px) {
  .cam-modal-backdrop.cam-docked .cam-modal,
  .cam-modal-backdrop.cam-anchored .cam-modal {
    width: 100%;
  }
}

.cam-modal {
  width: min(460px, 96vw);
  max-height: 90vh;
  overflow-y: auto;
  background: var(--cam-surface);
  border: 1px solid var(--cam-border);
  border-radius: 14px;
  box-shadow: 0 20px 48px rgba(0, 0, 0, 0.6);
  padding: 16px;
  color: var(--cam-text);
  font-size: 13px;
  display: flex;
  flex-direction: column;
  transition: width 200ms ease, max-width 200ms ease;
}
.cam-confirm-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
  font-size: 14px;
  margin-bottom: 6px;
}
.cam-confirm-head-text { flex: 1 1 auto; min-width: 0; }

/* Collapse toggle — slides everything but Confirm/Cancel out of the way so
   the board underneath is reachable (drag a token, watch the attack and
   knockback preview react), then slides back. See .cam-panel-body and
   .cam-modal.is-collapsed; wired in wireConfirm. */
.cam-collapse-btn {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 7px;
  border: 1px solid var(--cam-border);
  background: var(--cam-surface-2);
  color: var(--cam-text);
  cursor: pointer;
  font-size: 13px;
  line-height: 1;
}
.cam-collapse-btn:hover { border-color: var(--cam-accent); }

.cam-panel-body {
  overflow: hidden;
  transition: max-height 200ms ease, opacity 160ms ease, transform 200ms ease;
  max-height: 2000px;
}
/* Collapsed: the body slides right and shrinks to nothing, and the modal
   narrows to just the header toggle and the actions bar — Confirm/Cancel
   never move, since they live outside .cam-panel-body. */
.cam-modal.is-collapsed .cam-panel-body {
  max-height: 0;
  opacity: 0;
  transform: translateX(16px);
  pointer-events: none;
  margin: 0;
}
.cam-modal.is-collapsed .cam-confirm-head-text { display: none; }
.cam-modal-backdrop.cam-docked .cam-modal.is-collapsed,
.cam-modal-backdrop.cam-anchored .cam-modal.is-collapsed,
.cam-modal.is-collapsed {
  width: auto;
  max-width: min(220px, 92vw);
}
.cam-confirm-dmg {
  font-size: 12px; color: var(--cam-muted); margin-bottom: 10px; min-height: 18px;
}
.cam-confirm-dmg.is-out { color: #fca5a5; }
.cam-dmg-big { font-size: 20px; font-weight: 700; color: var(--cam-text); }
.cam-dmg-sub { font-size: 11px; }
.cam-dmg-meta { font-size: 11px; color: var(--cam-muted); margin-top: 2px; }

/* Confirmed, and waiting on the move that has to land first (see
   confirmAction). The panel stays legible — the player should be able to read
   what they just sent — but nothing in it can be changed any more, because the
   command was captured before the wait began. */
.cam-modal.is-busy .cam-panel-body { pointer-events: none; opacity: 0.65; }
.cam-modal.is-busy [data-role="confirm"] { opacity: 0.6; cursor: progress; }

/* ── metric colours ──
   One hue per quantity across the damage line and the meta run beneath it, so
   the figure a player is hunting for is found by colour rather than by reading
   the whole comma-run. cam- prefixed rather than reusing the Abilities tab's
   .m-atk/.m-brk/… on purpose: those live in v2/abilities.css, which the shell
   loads and the combat iframe does too, so bare .m-* names here would be two
   stylesheets arguing over one selector — and range deliberately disagrees
   with that set anyway (green here, pink there). */
.cam-m-dmg { color: var(--cam-dmg); }
.cam-m-rng { color: var(--cam-range); }
.cam-m-brk { color: var(--cam-break); }
.cam-m-imp { color: var(--cam-impact); }
.cam-m-stg { color: var(--cam-stagger); }

/* Allegiance on the two creature names in the panel head, relative to the PC.
   Weight as well as hue: the head is small text over a busy board, and colour
   alone at 12px is not a reliable signal. */
.cam-actor { font-weight: 600; }
.cam-actor.is-ally  { color: var(--cam-ally); }
.cam-actor.is-enemy { color: var(--cam-enemy); }

/* Target's Strength/Resilience(/Shield) bars — a single-creature echo of the
   Combat tab roster's own component (combat-ui.css's .bar-str/.bar-res),
   same stat colors and order, scoped to just this one creature instead of
   the roster's board-normalized width. See combat-attack-menu.js's
   refreshVitals for the geometry. */
/* Poise ring, then the bar stack — the same reading order the roster card
   uses, and the reason the ring is here at all: a blow's impact matters as
   much as its damage, and the panel used to state it only as a number buried
   in the meta run. */
.cam-vitals {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}
/* Duplicated from combat-ui.css's .rs-* rather than shared: this stylesheet is
   loaded in the v2 shell, which never loads combat-ui.css. See
   poiseWheelHtml's note for the same argument on the JS side. */
.cam-poise {
  flex: 0 0 auto;
  position: relative;
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: default;
  /* Per-state colour reaches the arc stroke through currentColor. */
  color: var(--poise-standing, #f6de7d);
}
.cam-poise-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.cam-poise-track { fill: none; stroke: rgba(255,255,255,0.10); stroke-width: 3; opacity: 0.55; }
.cam-poise-fill  { fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: butt;
                   transition: stroke-dasharray 0.3s ease; }
/* The slice this blow would take. Inset and thinner so it reads as sitting
   behind the current arc, and coloured for the rung the hit could reach —
   which is a DIFFERENT state class from the wheel's own, hence the currentColor
   override rather than inheriting it. */
.cam-poise-loss  { fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: butt;
                   opacity: 0.85; animation: cam-chip-blink 1s ease-in-out infinite; }
.cam-poise-loss.state-staggered { stroke: var(--poise-staggered, #f0a45c); }
.cam-poise-loss.state-prone     { stroke: var(--poise-prone,     #f86a63); }
.cam-poise-loss.state-stunned   { stroke: var(--poise-stunned,   #8a5a2b); }
.cam-poise-val {
  position: relative; z-index: 1;
  font-size: 11px; font-weight: 800; color: #fff;
  text-shadow: 0 1px 2px rgba(0,0,0,0.85);
  font-variant-numeric: tabular-nums; line-height: 1;
}
.cam-poise.state-normal    { color: var(--poise-standing,  #f6de7d); }
.cam-poise.state-staggered { color: var(--poise-staggered, #f0a45c); }
.cam-poise.state-prone     { color: var(--poise-prone,     #f86a63); }
.cam-poise.state-stunned   { color: var(--poise-stunned,   #8a5a2b); }
/* The bars, drawn as the Combat tab's roster card draws them
   (combat-ui.css's .bar-res / .bar-str / .bar-shield under
   body[data-str-style="framed"]): gold-outlined boxes sitting flush in one
   ribbon, sized against a single shared scale that refreshVitals computes.

   Duplicated from combat-ui.css rather than shared, for the same reason
   .cam-poise-* duplicates .rs-*: this stylesheet loads in the v2 shell, which
   never loads combat-ui.css at all. */
.cam-vbar-stack {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column;
  /* No gap: the RES frame drops its bottom edge and the STR frame's top edge
     serves as the single shared pixel between them, so the two read as one
     framed ribbon rather than two stacked tabs. The roster's .vitals-bars
     does exactly this. */
  gap: 0;
}

/* The gold of the frames. Named separately from --cam-accent because what it
   matches is the roster's --frame-color, not this panel's own accent. */
.cam-vitals { --cam-frame: #fcd34d; }

/* The RES row is a positioning context rather than a bar: the resilience box
   is only as wide as the creature's resilience span, and the shield segment
   continues rightward from wherever the resilience FILL ends — so the shield
   has to be placed against the row, not inside the box it overruns. */
.cam-vbar-row { position: relative; height: 9px; }

.cam-vbar {
  position: relative;
  overflow: hidden;
  box-sizing: border-box;
  background: rgba(255, 255, 255, 0.06);
}
.cam-vbar-res {
  position: absolute; left: 0; top: 0;
  height: 100%;
  border: 1px solid var(--cam-frame);
  /* Shared seam with the STR frame below — see .cam-vbar-stack's gap note. */
  border-bottom: 0;
}
.cam-vbar-str {
  height: 18px;
  /* No border of its own: the ::after outline below traces the frame, and that
     spans max(base_s, current) rather than the whole row — so a creature whose
     shield widened the shared scale still gets a frame that stops at its own
     strength cap instead of being stretched to meet the shield. */
  border: 0;
}
.cam-vbar-fill { position: absolute; inset: 0 auto 0 0; height: 100%; z-index: 1; }
.cam-vbar-res .cam-vbar-fill { background: #4b97f8; }
.cam-vbar-str .cam-vbar-fill { background: linear-gradient(90deg, #ff3c3c, #c20000); }

/* The STR bar's gold frame, as an outline above the fill — the roster's
   body[data-str-style="framed"] .bar-str::after, with the same z-order
   reasoning: above the fill (z:1) and the loss overlay (z:2), below the label
   (z:5), so the outline always traces the whole rectangle and the digits
   always sit on top of it. */
.cam-vbar-str::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  height: 100%;
  width: var(--cam-frame-pct, 100%);
  border: 1px solid var(--cam-frame);
  box-sizing: border-box;
  background: transparent;
  pointer-events: none;
  z-index: 3;
}

/* Shield, on the resilience row and starting where its fill stops — the
   roster's model (combat-ui-render's shieldSeg) and the map token's
   (.bfmap-token-vitals-shield), not a third bar on a scale of its own. Framed
   in the same gold, minus its left edge, so it butts flush against the
   resilience box and the two read as one ribbon. */
.cam-vbar-shield-seg {
  position: absolute;
  top: 0;
  height: 100%;
  box-sizing: border-box;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--cam-frame);
  border-left: 0;
  border-bottom: 0;
}
.cam-vbar-shield-fill {
  position: absolute; inset: 0 auto 0 0; height: 100%;
  background: var(--cam-shield);
  z-index: 1;
}
/* Spent down to nothing but not yet repaired — an empty red-tinted frame,
   matching the roster's .bar-shield--broken. */
.cam-vbar-shield-seg.is-broken {
  background: rgba(150, 60, 60, 0.14);
  border-color: rgba(210, 90, 90, 0.75);
}

/* Resilience the creature has already lost, displaced past the shield so the
   two empties join on the right — the roster's .bar-res-lost-far, same blue
   hatch and same gold frame minus its left edge, because it butts against the
   shield segment and the three make up one ribbon. Without it the shield sits
   on top of the RES box's empty tail and a badly worn creature reads as
   untouched here while its roster card says otherwise. */
.cam-vbar-res-lost-far {
  position: absolute;
  top: 0;
  height: 100%;
  box-sizing: border-box;
  border: 1px solid var(--cam-frame);
  border-left: 0;
  border-bottom: 0;
  z-index: 3;
  background: repeating-linear-gradient(
    -45deg,
    rgba(55, 85, 140, 0.70) 0px,
    rgba(55, 85, 140, 0.70) 3px,
    rgba(55, 85, 140, 0.12) 3px,
    rgba(55, 85, 140, 0.12) 6px
  );
}

/* This attack's expected loss — mean_damage against Strength, mean_break
   against Resilience or Shield (shield only while one is actually raised — see
   refreshVitals' `blocking`) — eating into the right edge of the current fill.
   Blinks with the
   same keyframe as the auto-reaction chip: a claim about to happen, not one
   that already has. This is the one thing the panel draws that the roster
   does not, so it sits above the fills and below the frame. */
.cam-vbar-loss {
  position: absolute;
  top: 0;
  height: 100%;
  background: #fff;
  z-index: 2;
  animation: cam-chip-blink 1s ease-in-out infinite;
}

/* All three numbers in one readout on the STR bar, anchored LEFT — the
   roster's .bar-str-label. Left-aligned so the digits stay put instead of
   travelling with the fill, and above the frame (z:5) so the gold edge never
   paints over them. */
.cam-vbar-label {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 7px;
  padding-left: 5px;
  overflow: hidden;
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
  pointer-events: none;
}
/* Each number in the colour of the bar it counts, so the readout can be read
   without working out which figure is which: strength white, resilience blue
   (.cam-vbar-res's fill), shield green (--cam-shield). */
.cam-vbar-num-res { color: rgba(122, 204, 255, 0.95); }
/* Lightened off --cam-shield rather than set to it flat, the same way the RES
   number is a lightened #4b97f8: these digits sit ON the red strength fill,
   against which the bar colours at full saturation barely read. */
.cam-vbar-num-shield { color: color-mix(in srgb, var(--cam-shield) 62%, #ffffff); }
.cam-vbar-num-shield.is-broken { color: rgba(210, 120, 120, 0.95); }

.cam-section { margin-bottom: 12px; }
.cam-label {
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--cam-muted); margin-bottom: 6px;
}

/* option chips */
.cam-chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
.cam-chip {
  background: var(--cam-surface-2);
  border: 1px solid var(--cam-border);
  border-radius: 999px;
  padding: 5px 10px;
  color: var(--cam-text);
  cursor: pointer;
  font: inherit; font-size: 12px;
}
.cam-chip.on { background: var(--cam-accent); color: var(--cam-accent-ink); border-color: var(--cam-accent); }
.cam-chip-key {
  opacity: 0.6; font-size: 10px; margin-left: 2px;
  border: 1px solid currentColor; border-radius: 3px; padding: 0 3px;
}

/* The reaction that fires on its own if the player declares none — see
   renderReactions, which drops this class (and the blink) the moment any
   reaction (including this one) is explicitly settled. */
.cam-chip.is-blinking { animation: cam-chip-blink 1.4s ease-in-out infinite; }
@keyframes cam-chip-blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

/* The "No reaction" chip (D-6). Load-bearing, not a convenience: arming ANY
   reaction here — auto or manual — trips the engine's pre-tagged bypass, so
   this is the only control that hands a connected defender their own prompt
   back. It needs to read as a distinct KIND of state, not a fourth reaction
   option, so it borrows the map's own "diverging from the default" amber
   (--cam-warn) rather than the cyan --cam-accent an armed reaction uses —
   "explicitly nothing" must never look like "something armed". The dashed
   border at rest is the "give it prominence" ask: a solid pill among other
   solid pills reads as one more chip to skim past. */
.cam-chip-none {
  border-style: dashed;
  border-color: var(--cam-warn);
  color: var(--cam-warn);
}
.cam-chip-none.on {
  background: var(--cam-warn);
  border-style: solid;
  color: var(--cam-accent-ink);
}

/* tempo */
.cam-tempo { display: flex; align-items: center; gap: 12px; }
.cam-tempo-btn {
  width: 30px; height: 30px; border-radius: 8px;
  background: var(--cam-surface-2); border: 1px solid var(--cam-border);
  color: var(--cam-text); font-size: 18px; cursor: pointer; line-height: 1;
}
.cam-tempo-btn:hover { border-color: var(--cam-accent); }
.cam-tempo-val { font-variant-numeric: tabular-nums; min-width: 20px; text-align: center; }

/* The tempo the SITUATION supplies — flanking, the target's stagger state, its
   openings. Folded into the number so the readout matches the damage line
   above it, and marked with an asterisk so it never reads as "what you typed".
   `help` cursor and a wider hit area because the asterisk is a promise that
   hovering explains it. */
.cam-tempo-val.has-auto {
  color: var(--cam-accent);
  font-weight: 700;
  cursor: help;
  padding: 0 4px;
}

/* The same explanation, spelled out. On screen rather than only in the title,
   because a bonus that appears when an ally happens to step into the right tile
   has to be *learnable* — a player who never hovers should still find out that
   standing opposite a friend is worth something. */
.cam-tempo-why {
  margin-top: 5px;
  font-size: 11px;
  line-height: 1.35;
  color: var(--cam-muted, #97a2bd);
}
.cam-tempo-why:empty { display: none; }

/* targets — every legal target listed at once, its number its hotkey */
.cam-target-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 2px;
}
/* Two target choices per row — the list can run to a dozen-plus creatures,
   and a single flat column of one-per-row buttons cost far more vertical
   space than the panel (especially docked, beside the board) can spare. */
.cam-target-item {
  display: flex;
  gap: 6px;
}
.cam-target-item .cam-target-row {
  flex: 1 1 0;
  min-width: 0;
}
.cam-target-row {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  background: transparent; border: 1px solid transparent; border-radius: 7px;
  padding: 6px 8px; color: var(--cam-text); cursor: pointer; font: inherit; font-size: 12px;
}
.cam-target-row:hover:not(.is-primary) { background: var(--cam-surface-2); }
.cam-target-row.on { border-color: var(--cam-accent); background: var(--cam-surface-2); }
.cam-target-row.is-primary { cursor: default; }
.cam-target-num {
  flex: 0 0 auto; min-width: 18px; text-align: center;
  border: 1px solid var(--cam-border); border-radius: 4px;
  font-size: 10px; line-height: 16px; color: var(--cam-muted);
  font-variant-numeric: tabular-nums;
}
.cam-target-row.on .cam-target-num { border-color: var(--cam-accent); color: var(--cam-accent); }
.cam-target-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cam-target-tag {
  flex: 0 0 auto; color: var(--cam-muted); font-size: 10px;
  text-transform: uppercase; letter-spacing: 0.06em;
}

/* The primary's asterisk (see refreshTargets). Not a word, so the tag's
   uppercase-microcaps treatment would only shrink it into invisibility — it
   gets the accent and enough size to read as a mark. */
.cam-target-row.is-primary .cam-target-tag {
  color: var(--cam-accent); font-size: 14px; letter-spacing: 0; line-height: 1;
}

/* An ally is a legal target — a swing catches whatever stands in it — but it is
   almost never the one meant, so the row is marked in the same amber the panel
   already uses for "you may, and you probably shouldn't". */
.cam-target-row.is-ally .cam-target-tag { color: var(--cam-warn); }
.cam-target-row.is-ally.on { border-color: var(--cam-warn); }
.cam-target-row.is-ally.on .cam-target-num { border-color: var(--cam-warn); color: var(--cam-warn); }

/* The overflow dropdown — the eleventh candidate onward. Past ten rows the
   list stops growing and the tail moves in here: one control, above the rows it
   feeds, instead of a column that pushes Confirm off the bottom of the panel.
   Picking from it promotes that creature to a real row, so the dropdown is a
   way IN to the list rather than a second place targets can hide. */
.cam-target-more {
  display: block; width: 100%; margin: 0 0 6px;
  background: var(--cam-surface-2); color: var(--cam-text);
  border: 1px solid var(--cam-border); border-radius: 7px;
  padding: 6px 8px; font: inherit; font-size: 12px; cursor: pointer;
}
.cam-target-more:hover { border-color: var(--cam-accent); }
.cam-target-more[hidden] { display: none; }

/* Filled in by the attack's Cleave rather than picked by hand (see
   syncTargets). Dashed, because it is a proposal: one click takes it back off,
   and it stays off from then on. */
.cam-target-row.is-auto { border-style: dashed; }

/* AOE "Target Area" — the list is derived from the template on the map and
   locked (auto-synced, no manual toggling), so its rows read as reporting
   rather than as buttons. */
.cam-target-row.is-locked { cursor: default; opacity: 0.95; }
.cam-target-row.is-locked:hover { background: transparent; }
.cam-target-row.is-locked .cam-target-num { visibility: hidden; }
.cam-area-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.cam-area-meta { color: var(--cam-muted); font-size: 11px; font-variant-numeric: tabular-nums; }
.cam-area-hint { margin-top: 6px; font-size: 11px; color: var(--cam-muted); }
.cam-tail {
  width: 100%; box-sizing: border-box;
  background: var(--cam-surface-2); border: 1px solid var(--cam-border);
  border-radius: 8px; padding: 7px 9px; color: var(--cam-text); font: inherit; font-size: 13px;
}
/* The focus ring is drawn OUTSIDE the border box, and this input is the full
   width of a panel body that clips (.cam-panel-body's overflow: hidden, which
   the collapse animation needs). So the default ring ran past both side edges
   and lost its rounded corners to the clip. A negative offset the width of the
   ring itself puts the whole rounded rectangle back inside the box, where
   nothing shaves it. */
.cam-tail:focus {
  outline: 2px solid #fff;
  outline-offset: -2px;
}

/* ── Bonk! readout ──
   Only visible while the map's knockback ghost has been dragged short of its
   natural landing (see combat-attack-menu.js's refreshBonk). Orange, matching
   the impact marker the map paints on the tile the creature slams into, so the
   two halves of one claim — the spot on the board and the sentence about it —
   read as the same statement made twice rather than as two facts. */
.cam-bonk {
  display: flex; align-items: center; gap: 8px;
  margin-top: 8px; padding: 7px 9px;
  border: 1px solid var(--cam-warn, #f0a45c); border-radius: 8px;
  background: rgba(240, 164, 92, .10);
  font-size: 12px; line-height: 1.35; color: var(--cam-text);
}
/* `.cam-bonk`'s own `display: flex` outranks the UA `[hidden]` rule, so
   without this the section would show through its own hidden attribute — the
   same trap `.cam-compare[hidden]` documents a few rules down. */
.cam-bonk[hidden] { display: none; }
.cam-bonk-text { flex: 1 1 auto; min-width: 0; }
.cam-bonk-clear {
  flex: 0 0 auto;
  background: var(--cam-surface-2); color: var(--cam-text);
  border: 1px solid var(--cam-border); border-radius: 7px;
  padding: 4px 8px; font: inherit; font-size: 11px; cursor: pointer;
}
.cam-bonk-clear:hover { border-color: var(--cam-accent); }

/* compare */
.cam-compare-toggle {
  background: transparent; border: 0; color: var(--cam-accent);
  cursor: pointer; font: inherit; font-size: 12px; padding: 0;
}
.cam-compare { margin-top: 8px; display: flex; flex-direction: column; gap: 2px; }
/* `.cam-compare`'s own `display: flex` above outranks the UA `[hidden]` rule
   (author styles beat the UA stylesheet even at equal specificity), so toggling
   the `hidden` attribute alone never actually hid this — it would expand on the
   first click and stay visually open on every click after. This rule is what
   lets `hidden` win. */
.cam-compare[hidden] { display: none; }
.cam-cmp-row {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  background: transparent; border: 1px solid transparent; border-radius: 6px;
  padding: 5px 7px; color: var(--cam-text); cursor: pointer; font: inherit; font-size: 12px;
}
.cam-cmp-row:hover { background: var(--cam-surface-2); }
.cam-cmp-row.sel { border-color: var(--cam-accent); }
.cam-cmp-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cam-cmp-cell { flex: 0 0 auto; color: var(--cam-muted); font-variant-numeric: tabular-nums; min-width: 52px; text-align: right; }

/* command preview + actions */
.cam-cmd-preview {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  background: #020617;
  border: 1px solid var(--cam-border);
  border-radius: 8px;
  padding: 8px 10px;
  color: #7dd3fc;
  margin: 6px 0 12px;
  word-break: break-all;
}
/* The predicted expansion, hung directly under .cam-cmd-preview. Dimmer and
   amber rather than cyan, so it reads as a derived claim about the command
   above it and not as a second command you could send. */
.cam-cmd-expanded {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  background: #020617;
  border: 1px solid var(--cam-border);
  border-top: none;
  border-radius: 0 0 8px 8px;
  padding: 6px 10px;
  color: #fbbf24;
  margin: -14px 0 12px;
  word-break: break-all;
  opacity: 0.9;
}
.cam-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
.cam-keyhint { margin-right: auto; color: var(--cam-muted); font-size: 11px; }
.cam-btn {
  background: var(--cam-surface-2); border: 1px solid var(--cam-border);
  border-radius: 8px; padding: 8px 16px; color: var(--cam-text); cursor: pointer; font: inherit;
}
.cam-btn:hover { border-color: var(--cam-accent); }
.cam-primary { background: var(--cam-accent); color: var(--cam-accent-ink); border-color: var(--cam-accent); font-weight: 600; }

/* toast */
.cam-toast {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%) translateY(8px);
  z-index: 9200;
  background: var(--cam-surface-2); border: 1px solid var(--cam-border); border-radius: 10px;
  padding: 10px 16px; color: var(--cam-text); font-size: 13px; max-width: 90vw;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  opacity: 0; pointer-events: none; transition: opacity 0.18s, transform 0.18s;
}
.cam-toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); }

/* Light-theme softening for the standalone / light hosts. */
@media (prefers-color-scheme: light) {
  :root {
    --cam-surface: #ffffff;
    --cam-surface-2: #f1f5f9;
    --cam-border: #cbd5e1;
    --cam-text: #0f172a;
    --cam-muted: #64748b;
    --cam-accent: #0284c7;
    --cam-accent-ink: #ffffff;
  }
  .cam-cmd-preview { background: #0f172a; color: #7dd3fc; }
  .cam-cmd-expanded { background: #0f172a; color: #fbbf24; }
}
