/* ── Bifrost native map ──
   Styles for the Bifrost-owned map pane (BifrostMapPane), the alternative
   renderer to the embedded Foundry iframe. Loaded by v2/index.html.

   Uses the v2 design tokens (--panel, --edge, --text, …) from
   colors_and_type.css where it can, with literal fallbacks so the pane still
   looks deliberate if it is ever mounted somewhere those tokens aren't loaded
   (combat-ui.html, a standalone test page).

   All token/grid geometry is set inline by the renderer in WORLD pixels — this
   file styles appearance only and must not set left/top/width/height on
   .bfmap-token, .bfmap-grid or .bfmap-calbox. */

.bfmap-host {
  position: relative;
  width: 100%;
  height: 100%;
  min-height: 0;
}

.bfmap-root {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* Transparent, NOT --panel-deep. Embedded in the Combat tab the map sits
     beside the roster iframe, whose body is transparent so the shell's
     --bg-field aurora shows through it; a solid slab here made the map read
     as a separate window bolted on beside the roster, with a hard vertical
     step at the divider. Sharing one background is what makes the split read
     as one surface. The fallback covers standalone use, where there is no
     host to inherit from. */
  background: var(--bg-field, var(--panel-deep, #0a0f1c));
  /* The renderer implements pan, pinch-zoom and token drags itself from
     pointer events, so the browser must not also scroll or zoom the pane. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  cursor: grab;
}
.bfmap-root.is-panning { cursor: grabbing; }
/* Box-select tool live (chosen with E, or momentarily with Shift held). The
   crosshair is the whole affordance for "this drag boxes tokens instead of
   moving the view" — the tool is otherwise invisible until you commit to a
   gesture. Beaten by is-panning so a pan already in flight still reads as one. */
.bfmap-root.is-boxselect { cursor: crosshair; }
.bfmap-root.is-boxselect.is-panning { cursor: grabbing; }

/* How much of each edge the docked action panel has reserved — written by
   frameActionPreview (map-pane.js) as it re-frames the camera, zero when
   nothing is docked. The HUD, wall strip, status line and toast all live in
   the pane's corners, which is exactly where the panel now docks too; rather
   than hiding that chrome (the zoom/fit buttons are wanted MORE while
   previewing an action, not less) it steps aside by this amount. See the
   ── HUD ── section below for the rules that consume these. */
.bfmap-root {
  --bfmap-dock-left: 0px;
  --bfmap-dock-right: 0px;
}

.bfmap-viewport {
  position: absolute;
  inset: 0;
}

/* The single transformed container. transform-origin must stay 0 0 — the
   world<->screen maths in map-scene.js assumes screen = pan + zoom * world. */
.bfmap-world {
  position: absolute;
  left: 0;
  top: 0;
  transform-origin: 0 0;
}

/* ── Why will-change is GESTURE-SCOPED and not simply set here ──
   `will-change: transform` promotes this container to its own composited layer
   and FIXES the scale it is rasterized at. Every descendant is then painted
   once at that scale and GPU-stretched by the transform, so zooming in blew a
   1x raster up to 3x: fuzzy id digits, fuzzy condition icons, fuzzy tooltip
   text — everything on the board except the background image, which is a
   bitmap and was meant to interpolate.

   It also produced the tell-tale "sharp for a moment, then blurry": an element
   mid-opacity-transition (the hover label, the condition list) gets its own
   layer rendered at the real device scale, and the instant the transition ends
   it is folded back into this stale one.

   So it is applied only WHILE a pan, pinch or wheel-zoom is actually in flight,
   where a fixed raster is exactly what you want — the frame is moving, nobody
   can read it, and re-rasterizing every frame is the expensive part. The
   renderer drops the class a beat after the gesture settles (see applyView),
   and Chrome then re-rasterizes once at the resting scale. Crisp at rest,
   cheap in motion. */
.bfmap-world.is-gesturing { will-change: transform; }

.bfmap-bg {
  position: absolute;
  left: 0;
  top: 0;
  display: block;
  pointer-events: none;
  /* Battle maps are usually pixel art at 1:1 or hand-painted; smoothing on
     zoom-in is kinder to the latter and unnoticeable on the former. */
  image-rendering: auto;
}

.bfmap-grid {
  position: absolute;
  pointer-events: none;
  /* background-image (the two hairline gradients) and background-size (the
     cell pitch) are set by the renderer: both depend on live zoom and
     calibration.

     CLOSING THE BORDER. The gradients paint a line at the START of each cell —
     x = 0, cell, 2·cell … (grid_w−1)·cell — so the far right and bottom edges
     of the last row and column never get one and the grid reads as open on two
     sides. An inset ring supplies exactly those two missing lines (and harmlessly
     re-draws the left/top ones, which are already there in the same colour and
     width). Inset rather than `border`, because a border would either grow the
     box or, under border-box sizing, shrink the content area and push every
     gradient line half a pixel out of alignment with the tokens.
     --bfmap-grid-line is set by the renderer to 1/zoom. */
  --bfmap-grid-line: 1px;
  box-shadow: inset 0 0 0 var(--bfmap-grid-line) var(--bfmap-grid-color, rgba(255, 255, 255, .25));
}

.bfmap-tokens {
  position: absolute;
  left: 0;
  top: 0;
}

/* ── Tokens ─────────────────────────────────────────────────────────────── */

.bfmap-token {
  position: absolute;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: default;
  /* --bfmap-cell is the live cell size in world px, set per token by the
     renderer. Anything that should look constant relative to a token (ring
     width, label size) is derived from it rather than hard-coded, so a 3x3
     ogre doesn't get a 3x-thick ring. */
  --bfmap-cell: 50px;
}

.bfmap-token.is-draggable { cursor: grab; }
.bfmap-token.is-dragging  { cursor: grabbing; z-index: 30; }

/* ── Why a moving token has no transition rule here ──
   It used to: `transition: left, top` on an .is-animating class. Both are
   LAYOUT properties, so every frame of every slide invalidated paint on
   .bfmap-world — and re-rastering those tiles means re-sampling the background
   image under them at the current scale, which on a large map is the single
   most expensive thing this pane can be asked to do sixty times a second.

   The slide is now a FLIP: the renderer inverts the token to its old position
   with a transform, plays it back to none, and CLEARS the transform at rest.
   That runs on the compositor and touches no layout at all. Duration comes from
   the per-device Settings value, so it is written inline by slideFlip rather
   than living here — a rule here could not read it.

   DO NOT be tempted to give .bfmap-token a permanent transform to simplify
   this. `transform` creates a stacking context, and the id pill, name label,
   condition column and overflow list below all rely on escaping this box to
   paint above .bfmap-target-arrows (z 32). See the note on .bfmap-token.is-felled,
   which uses precisely that mechanism on purpose. */

/* The art must never exceed its cell. Everything that reads as a "ring" is
   therefore drawn INSIDE the box: a border with box-sizing: border-box, and
   inset shadows. `outline` and spread box-shadows were the previous approach
   and both paint outside the border box, which is what made tokens visibly
   wider than their square and overlap their neighbours. Nothing in this rule
   may reintroduce an outward-painting property.

   SQUARE, NOT A DISC. The token occupies whole grid cells and now has a corner
   you can drag to say how many, so the border is the footprint: a circle drew a
   1x2 creature as a stretched pill whose edge agreed with the grid at no point,
   and left the four corners of every tile looking unoccupied when they are not.
   A hairline radius only, to take the razor off the corner without pretending
   the shape is anything but a square. */
.bfmap-token-art {
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  border-radius: calc(var(--bfmap-cell) * 0.05);
  /* --bfmap-token-border is a FRACTION OF A CELL, set on .bfmap-root from the
     GM panel's slider (see tokenBorder in map-pane.js). A fraction rather than
     a length for the same reason every other measurement here is one: the ring
     has to keep its weight relative to the token through zoom, recalibration
     and a 3x3 footprint. The literal here is only the fallback for a pane
     mounted without the renderer having written the property yet. */
  border: calc(var(--bfmap-cell) * var(--bfmap-token-border, 0.0275)) solid transparent;
  /* Only matters for the placeholder-colour case now (see below): keeps that
     fill inside the ring rather than painting under the transparent border,
     the same as when this element also carried the art's background-image. */
  background-clip: padding-box;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-size: calc(var(--bfmap-cell) * 0.34);
  letter-spacing: .02em;
  color: #0a0f1c;
  box-shadow: inset 0 0 0 calc(var(--bfmap-cell) * 0.02) rgba(4, 6, 15, .8);
  pointer-events: none;
  /* Anchors the portrait's `position: absolute`. Deliberately NOT clipped
     (no overflow: hidden): the portrait is drawn at its natural size and
     just rotates in place (see .bfmap-token-portrait), so at a diagonal
     facing its corners bulge a little past this square rather than being
     scaled down to fit — a small, honest overflow instead of the art
     visibly zooming in and out as the creature turns. */
  position: relative;

  /* ── FACING, part one: the FRAME ──
     --bfmap-facing-rot is set per token by the renderer and is a ROTATION, not
     a bearing: 0 means "as drawn". It is also LOCKED TO 90° STEPS (see
     facingBorderRotation in map-scene.js) — never a diagonal 45/135/225/315 —
     which is what keeps this plain, symmetric ring axis-aligned and off the
     neighbouring tile at every facing. For a ring with no directional marking,
     90/180/270 render pixel-identical to 0° anyway; the true facing is what
     the portrait inside shows (below), at the finer 45° the frame cannot
     afford without bleeding.

     The rotation is on the FRAME and not on .bfmap-token, deliberately. The
     token element also carries the id pill, the condition column and the name
     label, and those are readouts ABOUT the creature rather than parts of it —
     an id that reads upside-down at 180° is strictly worse than one that never
     turns.

     Transitioned, because a creature turning is an event worth seeing — and at
     the same easing as a move, so a turn and a step read as one vocabulary. */
  transition: transform 180ms cubic-bezier(.22, .61, .36, 1);
  transform: rotate(var(--bfmap-facing-rot, 0deg));
}

/* ── FACING, part two: the PORTRAIT ──
   The creature's actual artwork (or, with none, an empty layer behind the
   initials): a child of the frame, sized and rotated INDEPENDENTLY of it, so
   it can show the true 45°-resolution facing while the frame above stays
   locked to 90° and never goes diagonal.

   Drawn at its NATURAL size in every orientation (--bfmap-facing-portrait-scale
   is always 1 — see facingPortraitScale) and simply rotated in place. A
   diagonal facing then lets the rotated square's corners bulge a little past
   the frame's own square (which no longer clips — see .bfmap-token-art above)
   instead of being scaled up to cover it: a small, honest overflow beats the
   art visibly zooming in and out as the creature turns. Centred via left/top
   50% + translate(-50%,-50%), which composes with the rotate() below without
   disturbing the centring: transform-origin defaults to the element's own
   centre, so rotating after translating still spins about that centre. */
.bfmap-token-portrait {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--bfmap-facing-portrait-scale, 1) * 100%);
  height: calc(var(--bfmap-facing-portrait-scale, 1) * 100%);
  z-index: 0;
  /* Stretched to the footprint rather than cropped to it. A resize is the GM
     saying how many tiles this creature covers; `cover` answered that by
     silently cutting the art's sides off, so a token dragged to 1x3 showed the
     middle third of its own portrait. */
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  pointer-events: none;
  transition: transform 180ms cubic-bezier(.22, .61, .36, 1);
  transform: translate(-50%, -50%) rotate(var(--bfmap-facing-portrait-rot, 0deg));
}

/* Counter-rotated by exactly what the FRAME turned (never more than 90°), so
   the placeholder letters stay upright inside a token whose ring has snapped
   to a new quarter-turn. Above the portrait in the same stacking context, so
   the initials are always legible over whatever the portrait layer shows.

   ALSO counters --bfmap-scene-rot. The frame's own rotate() only ever shows
   facing-rot on screen when the whole board is unrotated; the moment the
   scene itself is turned (see .bfmap-world's transform in map-pane.js's
   applyView), every descendant — the frame included — picks up that extra
   turn from its ancestor for free, so undoing JUST facing-rot would leave the
   initials tilted by whatever the scene is rotated. Summing the two under one
   rotate() is what keeps them upright at any combination of the two.

   The initials are inside the ART, not the chrome group, so they need this
   of their own: the art is meant to turn with the board (it is the creature,
   not a readout about it) and only its lettering is not. */
.bfmap-token-initials {
  display: block;
  position: relative;
  z-index: 1;
  transition: transform 180ms cubic-bezier(.22, .61, .36, 1);
  transform: rotate(calc(-1 * (var(--bfmap-facing-rot, 0deg) + var(--bfmap-scene-rot, 0deg))));
}

/* ── The chrome group: everything that is a READOUT, held upright ──────────
   Wraps the id badge, condition badges, name label and vitals bar (see
   ensureNode in map-pane.js). One counter-rotation for the group, about the
   token's own centre, instead of one per element.

   WHY A GROUP AND NOT PER-ELEMENT. Each of these is anchored to an edge or a
   corner of the token box — id top-right, label below the bottom edge, vitals
   along it. Rotating the board moves BOTH the glyph orientation and which
   screen edge that anchor lands on, so undoing it per element means undoing a
   rotation AND swapping an anchor, four ways, for every element — four
   near-identical rules per readout, all of which have to agree. Rotating the
   box they are all measured from undoes both at once, for all of them, and a
   new readout added inside inherits the fix instead of needing its own.

   `inset: 0` matches the token box, so at 0deg this is a no-op wrapper and
   the anchors mean exactly what they always meant. The 90/270 rule transposes
   it first — see the --bfmap-token-w/h note in placeNode. */
/* ── Why the z-index is on the WRAPPER and not only on the readouts ────────
   Each readout inside declares its own z-index (33–36) and the comments on
   them say what that was for: keep a token's text above NEIGHBOURING tokens,
   and above the targeting-arrows layer. That worked while this wrapper was a
   plain box — the readouts were positioned descendants of a non-stacking
   parent, so their z-indexes resolved against .bfmap-world and cleared every
   sibling token. The counter-rotation above ended it: `rotate(0deg)` still
   computes to a matrix, and ANY transform other than `none` makes an element a
   stacking context. From then on the group painted atomically at z-index auto,
   the inner values only ordered the readouts against each other, and every
   token drawn later in the layer covered the names, id pills and hover status
   lists of the ones before it.

   So the wrapper carries the z-index the readouts were reaching for. 33 clears
   the tallest a token itself ever goes (30, while dragged) and the committed
   targeting-arrows layer (32), which puts ALL token text above ALL token art
   regardless of DOM order. The inner values still order the readouts within
   this group, which is now the only job they have.

   A FELLED token keeps its own stacking context (see .is-felled) and so keeps
   its chrome trapped and low — deliberate, and the case that rule was written
   for is separately handled by .is-eclipsed hiding a covered corpse's label,
   status and id outright. */
.bfmap-token-chrome {
  position: absolute;
  inset: 0;
  /* Inert: the token itself owns the drag gesture. A wrapper spanning the
     whole box would otherwise swallow every pointerdown meant for the art. */
  pointer-events: none;
  transform: rotate(calc(-1 * var(--bfmap-scene-rot, 0deg)));
  z-index: 33;
}
.bfmap-world[data-bfmap-scene-rot="90"] .bfmap-token-chrome,
.bfmap-world[data-bfmap-scene-rot="270"] .bfmap-token-chrome {
  inset: auto;
  left: 50%;
  top: 50%;
  width: var(--bfmap-token-h, 100%);
  height: var(--bfmap-token-w, 100%);
  transform: translate(-50%, -50%) rotate(calc(-1 * var(--bfmap-scene-rot, 0deg)));
}

/* ── Why hover/selection raises the WRAPPER and not the token ──────────────
   Every token's chrome sits at the same 33, so a name label loses to the id
   badge of whatever token happens to be later in the token layer's DOM order
   (refreshTokenStacking, map-pane.js — which reorders on felled-ness and
   nothing else, so the order is effectively arbitrary for the living).

   The obvious fix — `.bfmap-token:hover { z-index: 31 }` — is a trap, and the
   codebase already documents it twice (this file's "DO NOT be tempted to give
   .bfmap-token a permanent transform" note, and slideFlip's in map-pane.js): a
   z-index on the token ROOT makes the token a stacking context, which traps
   its chrome inside it and drops the very label we are raising BENEATH the
   targeting-arrows layer at 32. That is the felled-token mechanism
   (.bfmap-token.is-felled, z-index 0) used by accident.

   So the wrapper moves instead. The inner ladder tops out at 36 (the
   condition overflow list), so 34 already clears every other token's entire
   chrome group while staying under .bfmap-dist (37, "topmost of anything that
   moves with the board" — a drag readout must not be covered by a label the
   drag flew past) and under .bfmap-action-arrows (38, a live proposal).
   Hover beats selection because a hovered token is the one under the cursor
   right now. */
.bfmap-token.is-selected .bfmap-token-chrome { z-index: 34; }
.bfmap-token:hover       .bfmap-token-chrome { z-index: 35; }

/* Side tint as a ring rather than a fill, so custom art stays readable.
   BLUE ALLY, RED ENEMY — matching token_mirror.py's _SIDE_COLOR and the id
   badge below. These two rules were inverted, which put a red ring around the
   party (and around their own blue id badge). Red reads as threat to everyone
   who has ever used a VTT; there is no reading under which the party is the
   red team. */
.bfmap-token.side-player .bfmap-token-art { border-color: var(--c-blue, #7aa6ff); }
.bfmap-token.side-npc    .bfmap-token-art { border-color: var(--c-red, #f86a63); }

/* The placeholder disc behind the initials, for a token with no art.

   DERIVED FROM `side`, NOT from the row's stored `color`. It used to be an
   inline style read off combat_tokens.color, which is a hex written by
   whichever engine run created the row — so correcting _SIDE_COLOR in Python
   left every EXISTING row still carrying the old value, and the disc stayed
   the wrong colour while the ring around it was right. A colour with two
   sources will eventually disagree with itself; this one now has one.

   Only when there is no art: a token with a transparent PNG should show the
   map through it, not a coloured backdrop. */
.bfmap-token:not(.has-art).side-player .bfmap-token-art { background-color: #60a5fa; }
.bfmap-token:not(.has-art).side-npc    .bfmap-token-art { background-color: #f87171; }

/* The creature this browser owns. Inset glow — an outward one would protrude. */
.bfmap-token.is-mine .bfmap-token-art {
  box-shadow:
    inset 0 0 0 calc(var(--bfmap-cell) * 0.02) rgba(4, 6, 15, .8),
    inset 0 0 calc(var(--bfmap-cell) * 0.16) calc(var(--bfmap-cell) * 0.03) var(--c-teal, #5ee0c2);
}

.bfmap-token.is-selected .bfmap-token-art { border-color: var(--c-yellow, #f6de7d); }

/* The PRIMARY of a multi-selection — the one the movement overlay is actually
   describing. With six tokens boxed, an identical ring on all of them would
   leave the range field and the arrow looking like they belonged to whichever
   token the eye landed on first. The extra halo only shows up when there IS a
   group; on a lone selection there is nothing to disambiguate, so the plain
   yellow border above carries it. */
.bfmap-token.is-primary .bfmap-token-art {
  box-shadow: 0 0 0 calc(var(--bfmap-cell) * 0.03) rgba(246, 222, 125, .55);
}

/* The creature a movement drag is currently hovering — set by
   setDragHighlight in map-pane.js as dragState.hoverTarget changes. Same
   yellow as .is-selected (it is the same "eyes here" signal, just answering
   "who will I land on" instead of "what did I click"), plus the glow
   .is-primary uses so it reads as a live pointer, not a static selection
   ring, on a token that may ALSO be side-ringed blue or red. */
.bfmap-token.is-move-target .bfmap-token-art {
  border-color: var(--c-yellow, #f6de7d);
  box-shadow:
    inset 0 0 0 calc(var(--bfmap-cell) * 0.02) rgba(4, 6, 15, .8),
    0 0 0 calc(var(--bfmap-cell) * 0.04) rgba(246, 222, 125, .65);
}

/* ── Box-select marquee ──
   World-space, so it tracks the grid under zoom. Drawn from cell coordinates
   (it snaps to whole tiles), which is why it can afford a solid fill: the
   rectangle it shows is precisely the set of cells it will select from. */
.bfmap-marquee-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 60;
}
.bfmap-marquee {
  position: absolute;
  box-sizing: border-box;
  border: calc(var(--bfmap-line) * 2) dashed var(--c-yellow, #f6de7d);
  background: rgba(246, 222, 125, .1);
  border-radius: calc(var(--bfmap-line) * 2);
}

/* ── Name label: only while selected or hovered ──
   Off by default so a crowded board stays readable; the token's ID badge is
   the always-on identifier.

   Flush against the token's bottom edge, outside it — the vitals bar now
   lives INSIDE the token's own bounds (see .bfmap-token-vitals below), so
   the two no longer compete for the same "just under the token" real estate
   and the label doesn't need to be pushed down to clear it. */
.bfmap-token-label {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translate(-50%, calc(var(--bfmap-cell) * 0.04));
  font-size: calc(var(--bfmap-cell) * 0.2);
  line-height: 1.1;
  white-space: nowrap;
  padding: calc(var(--bfmap-cell) * 0.03) calc(var(--bfmap-cell) * 0.1);
  border-radius: calc(var(--bfmap-cell) * 0.06);
  background: rgba(4, 6, 15, .82);
  color: var(--text, #e6eaf5);
  pointer-events: none;
  opacity: 0;
  transition: opacity 110ms;
  /* Orders this against the other readouts in the chrome group, and nothing
     more — clearing neighbouring tokens and the targeting-arrows layer is the
     WRAPPER's job now that it is a stacking context. See .bfmap-token-chrome. */
  z-index: 33;
}
.bfmap-token:hover .bfmap-token-label,
.bfmap-token.is-selected .bfmap-token-label { opacity: 1; }

/* ── Vitals: strength + resilience(+shield), flattened to one row ──────────
   Selected-only, docked to the token's own bottom edge and INSIDE its
   bounds (not below it, unlike the name label) — see paintVitals in
   map-pane.js for what feeds it and why it is a compressed cousin of the
   combat UI's two-row bar rather than a port of it.

   TWO COLUMNS, ONE ROW: the combat UI stacks a strength bar over a
   resilience(+shield) bar; at token scale there is not a spare row to stack
   into, so the two sit side by side instead, each keeping its own fill vs.
   empty-gap reading rather than losing it to make room for the other.

   Column widths are NOT a fixed 50/50 split — paintVitals sets each column's
   flex-grow to its own max pool (base_s vs base_r+shieldMax) so the two
   columns take up space proportional to the creature's actual totals, not
   an arbitrary even share. */
.bfmap-token-vitals {
  position: absolute;
  bottom: calc(var(--bfmap-cell) * 0.04);
  left: 50%;
  transform: translateX(-50%);
  width: 92%;
  height: calc(var(--bfmap-cell) * 0.16);
  display: none;   /* switched to flex below, only while selected */
  gap: calc(var(--bfmap-cell) * 0.02);
  pointer-events: none;
  /* Ordering within the chrome group — see .bfmap-token-chrome. */
  z-index: 33;
  /* The roster's --frame-color / the attack panel's --cam-frame, declared
     locally for the same reason .cam-vitals declares its own: this stylesheet
     loads in the v2 shell, which never loads combat-ui.css, so there is no
     body-level --frame-color to inherit. */
  --bfmap-vitals-frame: #fcd34d;
  /* One SCREEN pixel, expressed in world units, capped so a hairline can never
     eat a bar that is only calc(var(--bfmap-cell) * 0.16) tall. --bfmap-inv-zoom
     is written onto .bfmap-world by applyView (map-pane.js:1391); the cap is
     what keeps the frame from becoming a solid gold smear at MIN_ZOOM, where
     the whole row is well under a device pixel. */
  --bfmap-vitals-hair: min(
    calc(1px * var(--bfmap-inv-zoom, 1)),
    calc(var(--bfmap-cell) * 0.03)
  );
}
.bfmap-token.is-selected .bfmap-token-vitals { display: flex; }
/* An eclipsed corpse has its whole identity chrome hidden (see the felled
   rule below); the vitals bar is chrome about the creature too, not a live
   reading anyone standing on top of it needs to see. */
.bfmap-token.is-felled.is-eclipsed .bfmap-token-vitals { display: none; }

.bfmap-token-vitals-col {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
  overflow: hidden;
  border-radius: calc(var(--bfmap-cell) * 0.02);
  /* Gold, not the old dark inset ring — the reference's "framed" style
     (combat-ui.css:1648-1706). A real `border` would change the containing
     block for the absolutely-positioned fills/dividers inside and shift every
     width by a hair, so this stays an inset box-shadow like the rule it
     replaces. The dark halo (non-inset, outside the gold) is what the
     reference gets for free from the roster's dark card — the map does not
     have one, so it is added explicitly here to keep the gold legible against
     pale map art. */
  box-shadow:
    inset 0 0 0 var(--bfmap-vitals-hair) var(--bfmap-vitals-frame),
    0 0 0 var(--bfmap-vitals-hair) rgba(4, 6, 15, .7);
}
/* The UNFILLED remainder of each column IS the empty gap — a dim tint of the
   same hue as the fill, so "how much is gone" reads at a glance without a
   crosshatch pattern that would just be noise at a dozen pixels tall. */
.bfmap-token-vitals-col.is-str { background: rgba(200, 49, 43, .22); }
.bfmap-token-vitals-col.is-res { background: rgba(75, 151, 248, .22); }

.bfmap-token-vitals-fill {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  transition: width 0.25s ease;
}
/* Matches the combat UI's own bar-str-fill / bar-res-fill hues, so a GM
   reading both surfaces never has to relearn the palette. */
.bfmap-token-vitals-col.is-str .bfmap-token-vitals-fill { background: #c8312b; }
.bfmap-token-vitals-col.is-res .bfmap-token-vitals-fill { background: #4b97f8; }

/* Packed immediately after the resilience fill — same "fills pack to the
   left" reading combat-ui-render.js's shield segment gives the RES bar,
   compressed to a plain adjacent block since there is no room here for its
   sliding base_r divider. No border of its own, so a shieldless creature's
   width:0 block paints nothing rather than a stray hairline. */
.bfmap-token-vitals-shield {
  position: absolute;
  top: 0;
  height: 100%;
  width: 0;
  /* Literal, not var(--c-green): --c-green also paints agility, healing,
     movement paths and VFX, so the shield hue has to be set here rather than
     by moving the palette token out from under all of them. Matches
     combat-ui.css's --shield-green (49, 173, 63). */
  background: #31ad3f;
  transition: left 0.25s ease, width 0.25s ease;
}
/* Framed to match the STR/RES ribbon, but ONLY when a shield actually shares
   the pool — gated on .has-shield (toggled by paintVitals) rather than an
   attribute-substring selector, matching the reference's "minus its left
   edge" rule (combat-ui.css:1428-1443) so the shield segment reads as
   flush against the RES box instead of a fourth independently-framed box.
   A shieldless creature's width:0 block keeps painting nothing. */
.bfmap-token-vitals-col.has-shield .bfmap-token-vitals-shield {
  box-shadow:
    inset calc(-1 * var(--bfmap-vitals-hair)) 0 0 0 var(--bfmap-vitals-frame),
    inset 0 var(--bfmap-vitals-hair) 0 0 var(--bfmap-vitals-frame),
    inset 0 calc(-1 * var(--bfmap-vitals-hair)) 0 0 var(--bfmap-vitals-frame);
}

/* The FIXED base_r | base_shield boundary in the combined resilience column —
   the roster's .bar-shield-basediv (combat-ui.css:1512). The shield block
   slides left across it as resilience drops; the line does not move, which is
   the whole reading. It was dropped from this compressed column originally
   (see the .bfmap-token-vitals-shield note above); it is back because it is
   the one ruler mark that says how much of the blue+green ribbon is borrowed.

   REUSED for the STR column's over-strength marker too (Q1 decision, both
   bars get a divider) — same rule, gated by a different class
   (.has-str-marker instead of .has-shield), positioned at base_s within the
   STR column's own scale. It stays hidden at or below full strength because
   the gold FRAME already marks that limit; drawing the line there too would
   be a gold hairline sitting directly on a gold frame edge. Matches the
   roster's .bar-str-max-marker (combat-ui.css:1573/1667/1674).

   No 1px overhang the reference uses (`top:-1px; height: h+2px`) — the map
   column is `overflow:hidden`, so an overhang would be clipped. `top:0;
   bottom:0` is the equivalent. */
.bfmap-token-vitals-basediv {
  position: absolute;
  top: 0; bottom: 0;
  width: max(var(--bfmap-vitals-hair), calc(var(--bfmap-cell) * 0.012));
  transform: translateX(-50%);
  background: var(--bfmap-vitals-frame);
  box-shadow: 0 0 calc(var(--bfmap-cell) * 0.01) rgba(4, 6, 15, .85);
  pointer-events: none;
  z-index: 3;          /* intra-column only; the chrome wrapper is the context */
  display: none;       /* only meaningful when gated on below */
}
.bfmap-token-vitals-col.has-shield .bfmap-token-vitals-basediv { display: block; }
.bfmap-token-vitals-col.has-str-marker .bfmap-token-vitals-basediv { display: block; }

/* What the LAST blow took: the stretch of track it just emptied, sitting in
   the gap immediately RIGHT of what is left of the fill — the attack panel's
   .cam-vbar-loss grammar (combat-attack-menu.css:560), pointed backwards.
   There it is a prediction, so it overlays the fill and the blink says
   "about to"; here it is a record, so it sits on the ground the fill has
   already given up and the blink says "just now". Same white, same keyframe,
   so a GM reading the panel and the board learns one idiom — but on the other
   side of the boundary, because marking the surviving fill instead would blink
   a piece of bar the creature still has.

   ON THE TOKEN NODE, which is long-lived — unlike the arrow layer, whose
   per-element animations cannot survive its wipe-and-rebuild (see the note on
   .bfmap-target-arrows). Tokens are created once and repainted in place, so a
   keyframe here runs normally. Gated by .is-live so a token with no recent
   loss (width 0) is not one of forty idle compositor animations. */
.bfmap-token-vitals-loss {
  position: absolute;
  top: 0;
  height: 100%;
  width: 0;
  background: #fff;
  z-index: 2;                 /* over the fill, under the frame and divider */
  pointer-events: none;
  animation: none;            /* only .is-live blinks — see below */
}
.bfmap-token-vitals-loss.is-live {
  animation: bfmap-vitals-loss-blink 1s ease-in-out infinite;
}
@keyframes bfmap-vitals-loss-blink {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.4; }
}
@media (prefers-reduced-motion: reduce) {
  .bfmap-token-vitals-loss.is-live { animation: none; opacity: 0.85; }
}

/* ── ID badge: always on, top-left of the cell ──
   This is what the GM calls out ("3, you're up") and what every engine command
   takes, so it is the one piece of token chrome that is never hidden.

   ITS COLOUR IS THE ACTION ECONOMY, not the side — the same ladder the creature
   row's id pill uses (combat-ui-render.js / combat-ui.css):

     white   nothing spent
     blue    reaction used this turn, no action yet this round
     red     action used this round, no reaction yet this turn
     purple  both

   Side is not lost by the swap: the ring around the art and the placeholder
   disc both carry it, and did before this. What the board did NOT carry was
   "has this one gone yet" — a question a GM asks of it thirty times a round
   and previously had to answer by looking somewhere else entirely.

   TOP-LEFT AT EVERY SCENE ROTATION, which is not something this rule has to
   arrange for itself: it anchors to the top-left of .bfmap-token-chrome, and
   that wrapper has already undone the board's rotation (see its own comment).
   So `left: 0; top: 0` means screen top-left at 0/90/180/270 alike, and a GM
   calling out "3, you're up" is always pointing at the same corner.

   Top-LEFT specifically because the condition badges own the top-right and
   stack down from it (.bfmap-token-status) — the two would otherwise pile up
   in the same corner on any creature carrying a status. */
.bfmap-token-id {
  position: absolute;
  left: 0;
  top: 0;
  min-width: calc(var(--bfmap-cell) * 0.28);
  height: calc(var(--bfmap-cell) * 0.28);
  padding: 0 calc(var(--bfmap-cell) * 0.05);
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(var(--bfmap-cell) * 0.2);
  font-weight: 800;
  line-height: 1;
  border-radius: calc(var(--bfmap-cell) * 0.06);
  border: calc(var(--bfmap-cell) * 0.014) solid rgba(4, 6, 15, .85);
  pointer-events: none;
  /* Above the targeting-arrows layer (z-index 32 — see .bfmap-target-arrows).
     Arrows are deliberately drawn OVER token art so a shaft crossing a knot
     of melee stays legible, but the id badge is a readout ABOUT the
     creature, not part of it — a GM cannot call out "who's up" if an arrow
     is sitting over the very number that answers it. */
  z-index: 34;
  /* The unspent default. Dark text on white, because at a fifth of a cell the
     digits have to survive being read across a table — an outlined pill the way
     the roster draws it disappears at this size. */
  background: #f4f7ff;
  color: #0a0f1c;
  transition: background-color 160ms, color 160ms, box-shadow 160ms;
}
.bfmap-token-id[data-econ="reaction"] {
  background: var(--c-blue, #7aa6ff);
  color: #06122e;
  box-shadow: 0 0 calc(var(--bfmap-cell) * 0.08) rgba(122, 166, 255, .75);
}
.bfmap-token-id[data-econ="action"] {
  background: var(--c-red, #f86a63);
  color: #2a0708;
  box-shadow: 0 0 calc(var(--bfmap-cell) * 0.08) rgba(248, 106, 99, .75);
}
.bfmap-token-id[data-econ="both"] {
  background: #a78bfa;
  color: #1b0a34;
  box-shadow: 0 0 calc(var(--bfmap-cell) * 0.1) rgba(167, 139, 250, .8);
}

/* ── Condition badges: top-right of the cell, stacked downward ──
   The same chips the creature row shows, in a COLUMN rather than a row: a token
   is about one tile wide, so a row either spills onto the neighbouring square
   or shrinks past legibility, while a column has the whole footprint's height
   to spend and grows by itself on a 2x2 or 3x3 creature.

   When there are more than the column holds, the LAST slot becomes a "+" —
   replacing a badge rather than being added below one, exactly the way the
   roster collapses its trailing chip. Hovering or selecting the token drops the
   full list (.bfmap-token-statuslist) below it. */
.bfmap-token-status {
  position: absolute;
  right: 0;
  top: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: calc(var(--bfmap-cell) * 0.04);
  pointer-events: none;
  /* Ordering within the chrome group — see .bfmap-token-chrome. */
  z-index: 35;
}

.bfmap-token-badge {
  position: relative;
  box-sizing: border-box;
  width: calc(var(--bfmap-cell) * 0.26);
  height: calc(var(--bfmap-cell) * 0.26);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: calc(var(--bfmap-cell) * 0.06);
  /* SOLID. The renderer paints the condition's own tint over this as a
     background-IMAGE layer (see statusBadge), so this stays the opaque plate
     underneath. It used to be the catalog's ~12%-alpha tint applied directly,
     which is right on the creature row's dark card and wrong here: a token sits
     on map art of any brightness, and a translucent badge over a lit floor tile
     was barely there. */
  background-color: #080c17;
  border: calc(var(--bfmap-cell) * 0.014) solid currentColor;
  box-shadow: 0 0 0 calc(var(--bfmap-cell) * 0.012) rgba(0, 0, 0, .55);
  color: var(--muted, #9eb0d2);
  font-size: calc(var(--bfmap-cell) * 0.17);
  font-weight: 800;
  line-height: 1;
}

.bfmap-token-badge-icon {
  width: calc(var(--bfmap-cell) * 0.17);
  height: calc(var(--bfmap-cell) * 0.17);
  display: block;
  filter: drop-shadow(0 0 1px rgba(0, 0, 0, .7));
}

.bfmap-token-badge-glyph { line-height: 1; }

/* Stack count, overhanging the badge's top-left — the OPPOSITE corner from the
   roster's, because this column is pinned to the token's right edge and a
   counter hanging off that side would sit over the next tile. */
.bfmap-token-badge-count {
  position: absolute;
  left: calc(var(--bfmap-cell) * -0.07);
  top: calc(var(--bfmap-cell) * -0.07);
  min-width: calc(var(--bfmap-cell) * 0.15);
  height: calc(var(--bfmap-cell) * 0.15);
  padding: 0 calc(var(--bfmap-cell) * 0.02);
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: rgba(4, 6, 15, .92);
  border: calc(var(--bfmap-cell) * 0.012) solid currentColor;
  color: #fff;
  font-size: calc(var(--bfmap-cell) * 0.11);
  font-variant-numeric: tabular-nums;
}

/* The collapse chip. Neutral rather than coloured: it stands for several
   conditions at once, and borrowing any one of their hues would say the wrong
   thing about what is hidden behind it. */
.bfmap-token-badge.is-more {
  color: var(--text, #e6eaf5);
  background-image: none;
  background-color: #10162a;
}

/* Felled marker — the ONE badge a downed creature carries. */
.bfmap-token-badge.is-felled-mark {
  color: var(--c-red, #f86a63);
  background-image: none;
  background-color: #2a0608;
  font-size: calc(var(--bfmap-cell) * 0.2);
}

/* ── Full condition list ──
   What "hover or click the token" reveals. Sits under the token, above the ID
   and name chrome of its neighbours, and is the only place a collapsed "+" can
   be resolved into names. */
.bfmap-token-statuslist {
  position: absolute;
  left: 50%;
  top: 100%;
  transform: translate(-50%, calc(var(--bfmap-cell) * 0.28));
  display: flex;
  flex-direction: column;
  gap: calc(var(--bfmap-cell) * 0.03);
  padding: calc(var(--bfmap-cell) * 0.05) calc(var(--bfmap-cell) * 0.1);
  border-radius: calc(var(--bfmap-cell) * 0.06);
  background: #080c17;
  box-shadow: 0 0 0 calc(var(--bfmap-cell) * 0.012) rgba(0, 0, 0, .6);
  font-size: calc(var(--bfmap-cell) * 0.16);
  line-height: 1.25;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 110ms;
  /* Ordering within the chrome group — see .bfmap-token-chrome. */
  z-index: 36;
}
/* Empty (no conditions, or felled): nothing to show, so nothing is reserved. */
.bfmap-token-statuslist:empty { display: none; }

/* is-selected only opens the list while it is a lone selection — with several
   tokens selected at once (.bfmap-root.is-multi-select) every one of them
   would otherwise pop its own text list, and a formation reads as a wall of
   overlapping labels instead of a board. Hover is unaffected: only one token
   is ever hovered at a time, so it never piles up the same way. */
.bfmap-token:hover .bfmap-token-statuslist,
.bfmap-root:not(.is-multi-select) .bfmap-token.is-selected .bfmap-token-statuslist { opacity: 1; }

.bfmap-token-statuslist-row {
  display: flex;
  align-items: center;
  gap: calc(var(--bfmap-cell) * 0.06);
}
.bfmap-token-statuslist-dot {
  width: calc(var(--bfmap-cell) * 0.1);
  height: calc(var(--bfmap-cell) * 0.1);
  border-radius: 50%;
  flex: none;
}

/* ── Felled creatures ───────────────────────────────────────────────────────
   Faded rather than removed, by default: where a body fell is information — it
   is a tile that is now open ground, and the map has to keep saying so. The
   ring and disc stay their side colour so it is still obvious whose it was.

   `is-hidden` is the See Fallen toggle's other setting, and is deliberately a
   separate class from `is-felled`: the toggle flips how the board is read, not
   who is down, and keeping the two apart means flipping it never has to
   re-derive the second. */
.bfmap-token.is-felled {
  opacity: 0.38;
  filter: grayscale(0.5);
  /* z-index 0, which does TWO things and neither of them is "move it down one
     layer". Against a living token at `z-index: auto` this is the same paint
     group, so the ordering comes entirely from the DOM — refreshTokenStacking
     keeps the fallen at the front of .bfmap-tokens, which is what puts them
     underneath. What the z-index buys is a STACKING CONTEXT: without one, this
     token's chrome group (z-index 33, see .bfmap-token-chrome) escapes into
     the world context and paints over every living creature on the board, so a
     corpse's chrome would sit on top of the very token that was meant to cover
     it. That is also why a corpse's name stays under a neighbouring token's art
     when every living token's now floats over it: the trap is the point here. */
  z-index: 0;
}
.bfmap-token.is-felled .bfmap-token-id { opacity: 0.85; }
/* …except while it is under the cursor. The z-index above is declared after
   .is-dragging and would otherwise win, leaving a body you are actively
   dragging buried under the tokens you are dragging it past. */
.bfmap-token.is-felled.is-dragging { z-index: 30; }
.bfmap-token.is-hidden { display: none; }

/* Covered by a living token. Ordering alone is not enough: a corpse's ring,
   name, id pill and X all live at the EDGES of its tile, where the disc on top
   of it does not reach, so an uneclipsed body under a standing creature reads
   as a second creature wearing the first one's outline. This drops it to a
   whisper — enough to say "something fell here", not enough to be mistaken for
   the token you are about to act with.

   Still clickable, deliberately. The paint order already hands a click in the
   overlapping region to the living token on top; killing pointer events
   outright would additionally make the parts of a large corpse that nothing is
   standing on unreachable, and a GM does still need to be able to grab a body
   and move it. */
.bfmap-token.is-felled.is-eclipsed { opacity: 0.12; }
.bfmap-token.is-felled.is-eclipsed .bfmap-token-label,
.bfmap-token.is-felled.is-eclipsed .bfmap-token-status,
.bfmap-token.is-felled.is-eclipsed .bfmap-token-id { display: none; }

/* ── Targeting arrows ───────────────────────────────────────────────────────
   Who aimed at whom this turn. An <svg> with a 1px box at the world origin and
   overflow visible, so its children take raw world coordinates — including the
   negative ones a creature standing off the drawn grid legitimately has.

   OVER the tokens. Under them the arrows were legible only where the board
   happened to be empty: the interesting case is a knot of four creatures in
   melee, which is exactly where every shaft disappeared behind the discs it
   connects. On top they read — and now that both ends run to the token CENTRES
   rather than stopping at the discs, the halo on the shaft is doing real work,
   because most of a melee arrow is drawn across two portraits.

   ── THE ARROW GRAMMAR, IN TWO AXES ────────────────────────────────────────
   Every arrow this pane draws answers two questions, and each is one axis of
   its appearance:

     COLOUR = what kind of thing it is.  Orange = an attack. Green = a body
              moving — a walk, or a shove that shifted someone.
     PULSE  = whether it has happened yet.  Translucent and slowly breathing =
              proposed, still being aimed, nothing sent to the engine. Steady
              and opaque = committed, resolved, a record of the past.

   So: a pulsing orange line while targets are being picked, and that same
   line settles into a steady one the instant the attack lands. Pulsing green
   for a move or a knockback being previewed, steady green once it has
   actually displaced someone.

   THE TWO STATES ARE THE SAME DRAWING. Same hue, same stroke weight, same
   centre-to-centre span, same head formula (drawArrowBetween) — committing
   changes nothing but the pulse. That is deliberate: the eye should follow
   one object through the commit rather than watch a preview vanish and a
   record appear in its place. It also means any geometry that diverges
   between the two is a bug, and it will look like one.

   NOT COLOURED BY SIDE. The layer used to split ally/enemy (cyan / orange),
   which spent the colour axis on "whose blow was this" — a question the token
   at each end already answers, and one that left nothing to distinguish an
   attack from a shove or a proposal from a fact. Whose it was is legible from
   the tail; whether it has happened is not, so the pulse carries that. The
   position ghosts below are still coloured by side (blue / red), which is
   where "who is this" belongs.

   z-index 32 clears a dragged token (30, the highest any token reaches) and
   stays under the marquee and the resize handles, which are live controls and
   must never end up behind a record of the past. */
.bfmap-target-arrows {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 1px;
  overflow: visible;
  pointer-events: none;
  z-index: 32;
  --bfmap-arrow-w: 3px;
  /* One colour for the whole layer — every arrow in it is a resolved attack.
     Kept as a variable because two rules consume it (shaft stroke, head fill)
     and they must never drift apart. */
  --bfmap-arrow-color: var(--c-orange, #f0a45c);
}

/* ── The pulse, for every arrow that is a proposal ─────────────────────────
   A proposed arrow is the committed arrow's exact twin — same colour, same
   geometry, same head — held at partial opacity and breathing. It stops
   breathing and goes fully opaque the moment it commits, which is the whole
   tell. Nothing about its SHAPE changes, so the eye tracks one continuous
   object across the commit rather than watching one line be replaced by
   another.

   ON THE LAYER, NOT ON THE ARROWS. This is not a style preference — an arrow
   element does not live long enough to animate. Every preview layer is wiped
   and rebuilt from scratch (drawActionPreview empties previewSvg,
   renderOverlay empties arrowSvg), and during a drag that happens on every
   single pointermove. A CSS animation restarts from 0% on a brand-new
   element, so a per-arrow pulse would be pinned to its first keyframe forever
   and read as a static half-faded line. The two <svg> layers themselves are
   built once in the DOM skeleton and only ever have their children swapped,
   so an animation on them survives the rebuild and the arrows inherit the
   phase mid-cycle, however often they are recreated.

   Opacity is what pulses, and it is the layer's own group opacity, so it is
   colour-blind on purpose: .bfmap-arrow can hold a green movement arrow and
   an orange targeting arrow at the same instant, and a glow animated as a
   single layer-wide colour would be wrong for one of them. Each arrow instead
   carries a STATIC drop-shadow in its own colour (--bfmap-glow below), and
   the layer's opacity waxing and waning is what makes that glow visibly
   breathe. Two mechanisms, one effect, and no place for the colours to
   disagree.

   1.3s: fast enough to read as a live, waiting-on-you thing rather than an
   idle shimmer, still slow enough not to strobe under a moving cursor. */
@keyframes bfmap-preview-pulse {
  0%, 100% { opacity: 0.45; }
  50%      { opacity: 0.92; }
}

.bfmap-action-arrows,
.bfmap-arrow {
  /* The resting value, declared even though the animation covers the same
     property at both ends of its cycle: "semi-transparent" is the load-bearing
     half of this and must not be contingent on the animation running. Anything
     that stops the pulse — the media query below, a browser that drops the
     animation — lands here rather than on a fully opaque arrow that would read
     as committed. */
  opacity: 0.72;
  animation: bfmap-preview-pulse 1.3s ease-in-out infinite;
}

/* A pulse IS motion, and its whole purpose is to catch the eye — exactly what
   this setting asks us not to do. Drop to the flat opacity above: still
   plainly "not committed yet" beside a solid arrow, just no longer moving. */
@media (prefers-reduced-motion: reduce) {
  .bfmap-action-arrows,
  .bfmap-arrow { animation: none; }
}

/* Both layers exist for the whole life of the pane and stand EMPTY for most of
   it — there is no arrow to preview until a panel is open or a drag is in
   flight. An infinite animation on a displayed element runs whether or not it
   has anything to show, and an animating compositor layer is a standing
   promise to produce a frame every 16ms: the tab never goes idle, on a board
   that is not moving, all session. `:empty` is the whole fix and it stays in
   CSS deliberately — the alternative is a class both layers' writers have to
   remember to toggle, and they are emptied from four different places. */
.bfmap-action-arrows:empty,
.bfmap-arrow:empty { animation: none; }

/* The glow each preview arrow carries, in its own stroke colour. Sized off
   the arrow's own width so it holds its proportions at every zoom (both width
   variables are set to `w / view.zoom` in map-pane.js, in the same user units
   this radius is measured in — a fixed px radius would swell and shrink
   against the shaft as the board scaled). */
.bfmap-preview-arrow,
.bfmap-preview-arrow.is-head,
.bfmap-knockback-arrow.is-preview,
.bfmap-knockback-arrow.is-preview.is-head {
  filter: drop-shadow(0 0 calc(var(--bfmap-arrow-w) * 1.6) var(--bfmap-glow));
}
.bfmap-move-arrow,
.bfmap-move-arrow.is-head,
.bfmap-target-hover-arrow,
.bfmap-target-hover-arrow.is-head {
  filter: drop-shadow(0 0 calc(var(--bfmap-move-arrow-w) * 1.6) var(--bfmap-glow));
}

.bfmap-target-arrow {
  stroke: var(--bfmap-arrow-color);
  fill: none;
  stroke-width: var(--bfmap-arrow-w);
  stroke-linecap: round;
  /* Sitting over the art now, so the shaft carries a dark halo rather than
     relying on the tokens being lighter than it. Without this an orange line
     crossing an orange-lit piece of map art vanishes mid-shaft. */
  paint-order: stroke;
  filter: drop-shadow(0 0 calc(var(--bfmap-arrow-w) * 0.7) rgba(4, 6, 15, .85));
  opacity: 0.92;
}

/* Where a creature STOOD when it acted, or was standing when it was hit — a
   dotted silhouette left behind on the tile. It is what lets both ends of an
   arrow stay welded to the moment instead of to the creatures: the shot stays
   where it was fired from, the blow stays on the ground it landed on, and these
   outlines are what say who was standing at each end.

   Coloured by SIDE: blue for the party and its allies, red for the enemy, the
   same reading the token ring gives — a silhouette and the creature that left
   it must never be different colours. The arrows themselves deliberately do
   NOT use these two colours (see the grammar note above): a ghost says "who
   was standing here", an arrow says "what happened, and has it happened yet".

   Thinner than the shaft and only lightly opaque: it is a memory of a position,
   and it must not compete with the token actually standing there now. */
.bfmap-arrow-ghost {
  fill: none;
  stroke: var(--c-red, #f86a63);
  stroke-width: calc(var(--bfmap-arrow-w) * 0.62);
  stroke-linecap: round;
  opacity: 0.65;
}
.bfmap-arrow-ghost.is-enemy { stroke: var(--c-red, #f86a63); }
.bfmap-arrow-ghost.is-ally  { stroke: var(--c-blue, #7aa6ff); }
.bfmap-target-arrow.is-head {
  fill: var(--bfmap-arrow-color);
  stroke: none;
}

/* A knockback's own trajectory — center of the cell shoved FROM to the edge
   of the token at the cell it landed on. GREEN because it is a body moving
   rather than a blow, SOLID because it has already happened — the shove that
   put it there is resolved, exactly like the attack arrows beside it. Its
   pulsing twin is .bfmap-knockback-arrow.is-preview, drawn while the action
   is still being aimed. Turn-scoped and cleared the same way as the arrows above;
   see knockbackArrows in map-pane.js. */
.bfmap-knockback-arrow {
  stroke: var(--c-green, #6cd48c);
  fill: none;
  stroke-width: var(--bfmap-arrow-w);
  stroke-linecap: round;
  paint-order: stroke;
  filter: drop-shadow(0 0 calc(var(--bfmap-arrow-w) * 0.7) rgba(4, 6, 15, .85));
  opacity: 0.92;
}
.bfmap-knockback-arrow.is-head {
  fill: var(--c-green, #6cd48c);
  stroke: none;
}

/* End of turn. The record of the turn that just finished leaves the board as
   one gesture — arrows and silhouettes together — rather than blinking out
   between two frames, which reads as a glitch instead of as an expiry. The
   layer is rebuilt from the new turn once this has run (disperseArrows). */
.bfmap-target-arrows.is-dispersing {
  animation: bfmap-disperse 420ms ease-in forwards;
}
@keyframes bfmap-disperse {
  from { opacity: 1; }
  to   { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .bfmap-target-arrows.is-dispersing { animation-duration: 1ms; }
}

/* Disabled HUD button — Clear Arrows with nothing to clear. */
.bfmap-hud-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

/* ── Decals layer ───────────────────────────────────────────────────────────
   Props between the map art and the creatures. DOM order is the stacking
   order (the renderer sorts by z), so no z-index is needed here and
   send-to-front/back stays a pure data change. */

.bfmap-decals { position: absolute; left: 0; top: 0; }

.bfmap-decal {
  position: absolute;
  box-sizing: border-box;
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  pointer-events: none;          /* inert until Decals mode is on */
  transform-origin: 50% 50%;
}
.bfmap-decal.is-editable {
  pointer-events: auto;
  cursor: grab;
  outline: var(--bfmap-line, 1px) dashed rgba(198, 156, 255, .8);
}
/* The selected decal is the one Delete removes and the one the corner dots
   belong to, so it has to be unmistakable among a board full of props. */
.bfmap-decal.is-selected {
  outline: calc(var(--bfmap-line, 1px) * 2) solid var(--c-purple, #c69cff);
  background-color: rgba(198, 156, 255, .12);
}
/* No art yet: a labelled placeholder box, so an unresolved name is obvious
   rather than invisible. */
.bfmap-decal.is-blank {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(var(--bfmap-cell, 50px) * 0.2);
  color: #f2f6ff;
  background: rgba(198, 156, 255, .3);
  border: var(--bfmap-line, 1px) solid var(--c-purple, #c69cff);
  border-radius: calc(var(--bfmap-cell, 50px) * 0.08);
}

/* ── Fog of war ─────────────────────────────────────────────────────────────
   A canvas one pixel per grid CELL, stretched over the board — see renderFog in
   map-pane.js for why that resolution rather than world pixels. It sits above
   the background and the decals and below everything the player acts with, so
   it dims terrain without ever greying out a token, a range ring or an arrow.

   Tokens the fog hides are not drawn at all (.bfmap-token.is-fogged below); a
   silhouette showing through the murk is a creature you have found. */

.bfmap-fog {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 4;
  pointer-events: none;
  /* The canvas is a handful of pixels blown up hundreds of times, so the
     browser's default smoothing is doing real work here: it is what turns a
     staircase of cell edges into a soft boundary. The blur in renderFog adds
     to it; this just stops a UA that defaults to nearest-neighbour from
     producing hard squares. */
  image-rendering: auto;
  /* Fog lifting is the payoff of walking through a door, so it fades rather
     than cutting. Short enough not to lag the reveal. */
  transition: opacity 180ms ease;
}

/* The GM sees the same three states at a fraction of the opacity (renderFog
   scales the alphas), and this says which is which at a glance: a faint dashed
   edge marks where the players' world stops. */
.bfmap-fog.is-gm { opacity: .85; }

/* A creature standing where the party cannot see. Not merely faded —
   display:none, matching .is-hidden above — because fog that leaves a ghost is
   not fog. */
.bfmap-token.is-fogged { display: none; }

/* ── Walls layer ────────────────────────────────────────────────────────────
   Hidden in normal play — the map art already shows where the walls are; these
   boxes exist so the ENGINE knows. Visible and topmost only while editing.

   DOORS ARE THE EXCEPTION: they are drawn in play too, because a door is a
   control rather than scenery — you have to be able to find one, see whether it
   is shut, and right-click it. */

.bfmap-walls { position: absolute; left: 0; top: 0; }

.bfmap-wall {
  position: absolute;
  box-sizing: border-box;
  cursor: pointer;
  background: repeating-linear-gradient(
    45deg,
    rgba(248, 106, 99, .34) 0,
    rgba(248, 106, 99, .34) 6px,
    rgba(248, 106, 99, .12) 6px,
    rgba(248, 106, 99, .12) 12px
  );
  border: calc(var(--bfmap-line, 1px) * 2) solid var(--c-red, #f86a63);
}
.bfmap-wall.is-drawing { pointer-events: none; opacity: .75; }
.bfmap-wall.is-selected {
  border-color: var(--c-yellow, #f6de7d);
  box-shadow: 0 0 0 calc(var(--bfmap-line, 1px) * 2) rgba(246, 222, 125, .5);
}

/* A LINE wall lies on the boundary between two tiles rather than filling
   either, so it is drawn as a solid bar centred on the gridline — see
   S.wallWorldRect. Solid rather than hatched because at 4px the hatch is mud,
   and because the two kinds have to be told apart at a glance. The generous
   invisible hit area is not decoration: a 4px bar is unclickable on a touch
   screen, and the whole gesture set depends on being able to grab one. */
.bfmap-wall.is-line {
  background: var(--c-red, #f86a63);
  border: 0;
  border-radius: 999px;
  outline: var(--bfmap-line, 1px) solid rgba(4, 6, 15, .7);
}
.bfmap-wall.is-line::before {
  content: "";
  position: absolute;
  inset: -7px;
}

/* ── Wall categories ────────────────────────────────────────────────────────
   Three colours for three rules, and the choice is not arbitrary: red is what
   stops you (the solid wall above), amber is what you can work (a door), and a
   dashed outline with no fill is what you can see through but not walk over
   (invisible). A GM scanning a half-drawn map should be able to tell which is
   which without clicking one. */

/* DOORS. Amber, and the only wall category drawn during play. Shut doors read
   as a solid bar like the wall they interrupt; open ones hollow out, which is
   the whole state change said in one glance across the table. */
.bfmap-wall.is-door {
  background: var(--c-orange, #f0a45c);
  border-color: var(--c-orange, #f0a45c);
  cursor: context-menu;
}
.bfmap-wall.is-line.is-door {
  background: var(--c-orange, #f0a45c);
  outline: var(--bfmap-line, 1px) solid rgba(4, 6, 15, .75);
  /* Squared off rather than the wall's pill, so a door reads as a panel set
     into the wall rather than as a shorter piece of it. */
  border-radius: calc(var(--bfmap-cell, 50px) * 0.03);
  box-shadow: 0 0 calc(var(--bfmap-cell, 50px) * 0.12) rgba(240, 164, 92, .55);
}
.bfmap-wall.is-door.is-open {
  background: transparent;
  border: calc(var(--bfmap-line, 1px) * 2) dashed var(--c-orange, #f0a45c);
  box-shadow: none;
  opacity: .8;
}
/* An open LINE door keeps its bar shape but empties it — a 4px dashed border
   collapses into a dotted smudge, so the hollow is drawn as a translucent bar
   with the ends still marked. */
.bfmap-wall.is-line.is-door.is-open {
  background: repeating-linear-gradient(
    90deg,
    rgba(240, 164, 92, .85) 0,
    rgba(240, 164, 92, .85) 3px,
    transparent 3px,
    transparent 7px
  );
  border: 0;
  outline: 0;
}

/* INVISIBLE WALLS. Present to the GM, absent to sight and to fog: the dashed
   outline with no fill is the standard vocabulary for "this is here but you
   cannot see it", and it cannot be confused with a solid wall's hatch at any
   zoom. Only ever visible in the walls editor — in play the whole point is that
   there is nothing to see. */
.bfmap-wall.is-invisible {
  background: none;
  border: calc(var(--bfmap-line, 1px) * 2) dashed var(--c-cyan, #65d4f0);
  opacity: .85;
}
.bfmap-wall.is-line.is-invisible {
  background: repeating-linear-gradient(
    90deg,
    var(--c-cyan, #65d4f0) 0,
    var(--c-cyan, #65d4f0) 5px,
    transparent 5px,
    transparent 10px
  );
  border: 0;
  outline: 0;
}

/* A separator in the HUD wall strip, between the two shape buttons and the
   three category ones — they are different questions and the strip should not
   read as five equal choices. */
.bfmap-wallbar-sep {
  width: 1px;
  align-self: stretch;
  margin: 2px 4px;
  background: rgba(255, 255, 255, .16);
}

/* The GM seat row in the tools panel: a label and a button rather than the
   label-and-input every other row uses, so it does not read as one more
   preference. */
.bfmap-tool-seat { align-items: center; gap: 8px; }
.bfmap-tool-seat .bfmap-tool-btn { margin-left: auto; }

/* Walls mode repurposes the empty-ground drag from panning to painting, so the
   cursor has to say so. */
.bfmap-root.is-editing-walls { cursor: crosshair; }

/* ── Resize handles ─────────────────────────────────────────────────────────
   One dot per resizable corner (or line end), shared by the decal and wall
   layers. Positioned at a grid intersection in world space and then pulled back
   by half its own size, so the dot is CENTRED on the corner it represents —
   which is what makes dragging it feel like moving that corner.

   --bfmap-handle is set in applyView to 11 screen px divided by the zoom, so
   these stay a constant size on screen at every scale. */

.bfmap-handles { position: absolute; left: 0; top: 0; }

.bfmap-handle {
  position: absolute;
  width: var(--bfmap-handle, 11px);
  height: var(--bfmap-handle, 11px);
  margin-left: calc(var(--bfmap-handle, 11px) / -2);
  margin-top: calc(var(--bfmap-handle, 11px) / -2);
  border-radius: 50%;
  background: var(--c-cyan, #65d4f0);
  border: calc(var(--bfmap-handle, 11px) * .16) solid rgba(4, 6, 15, .85);
  box-sizing: border-box;
  cursor: nwse-resize;
  touch-action: none;
}
.bfmap-handle:hover { background: #fff; }

/* A token's single corner dot. Yellow rather than cyan because that is the
   colour of the selection ring it belongs to — the map already uses cyan for
   the GM's map furniture (walls, decals) and yellow for "the creature you have
   selected", and a resize dot is a property of the selection. */
.bfmap-handle.is-token {
  background: var(--c-yellow, #f6de7d);
  border-radius: calc(var(--bfmap-handle, 11px) * .22);
}
.bfmap-handle.is-token:hover { background: #fff; }

/* ── Palette drop hint ──────────────────────────────────────────────────────
   The tile a dragged palette item will land on. Decals snap to whole tiles, so
   showing the tile (not the cursor) is showing the actual outcome. */

.bfmap-drop-hint {
  position: absolute;
  box-sizing: border-box;
  pointer-events: none;
  border: calc(var(--bfmap-line, 1px) * 2) dashed var(--c-purple, #c69cff);
  background: rgba(198, 156, 255, .18);
  border-radius: calc(var(--bfmap-cell, 50px) * .08);
}

/* ── Decal palette ──────────────────────────────────────────────────────────
   Pictures every image in maps/decals/. Placing props is a look-at-it task, so
   a name list would be the wrong surface. */

.bfmap-palette {
  position: absolute;
  right: 52px;
  top: 10px;
  bottom: 10px;
  z-index: 62;
  width: min(260px, calc(100% - 70px));
  display: flex;
  flex-direction: column;
  border-radius: 10px;
  border: 1px solid var(--edge, #1c243d);
  background: rgba(15, 21, 40, .97);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .5);
  color: var(--text, #e6eaf5);
}
.bfmap-palette-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 10px;
  border-bottom: 1px solid var(--edge, #1c243d);
}
.bfmap-palette-title { font-weight: 700; font-size: 12.5px; letter-spacing: .03em; }
.bfmap-palette-close {
  background: none; border: 0; cursor: pointer;
  color: var(--muted, #9eb0d2); font-size: 13px; padding: 2px 4px;
}
.bfmap-palette-body {
  flex: 1;
  overflow-y: auto;
  padding: 8px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(68px, 1fr));
  gap: 6px;
  align-content: start;
  font-size: 11.5px;
  color: var(--faint, #6e7ba0);
}
.bfmap-palette-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 5px 3px;
  border-radius: 7px;
  border: 1px solid transparent;
  background: var(--panel-deep, #0a0f1c);
  color: var(--text, #e6eaf5);
  font: inherit;
  font-size: 10px;
  cursor: pointer;
}
.bfmap-palette-item:hover { border-color: var(--c-purple, #c69cff); }
.bfmap-palette-thumb {
  width: 100%;
  aspect-ratio: 1;
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
}
.bfmap-palette-name {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bfmap-palette-hint {
  padding: 7px 10px;
  border-top: 1px solid var(--soft-edge, #1b2642);
  font-size: 10.5px;
  line-height: 1.4;
  color: var(--faint, #6e7ba0);
}

/* ── Context menu ───────────────────────────────────────────────────────────*/

.bfmap-menu {
  position: absolute;
  z-index: 70;
  min-width: 160px;
  padding: 4px;
  border-radius: 8px;
  border: 1px solid var(--edge, #1c243d);
  background: rgba(15, 21, 40, .98);
  box-shadow: 0 10px 28px rgba(0, 0, 0, .55);
  font-size: 12.5px;
  /* A full loadout is taller than the pane. map-pane.js caps max-height to
     the visible frame; this makes the overflow scrollable rather than
     clipped, and keeps the title pinned while the list moves. */
  overflow-y: auto;
  overscroll-behavior: contain;
  /* border-box so the max-height map-pane.js computes is the height the menu
     actually occupies — with the default content-box it overshot the frame by
     its own padding and border. */
  box-sizing: border-box;
}
.bfmap-menu-title {
  position: sticky;
  top: 0;
  background: rgba(15, 21, 40, .98);
}
.bfmap-menu-title {
  padding: 4px 8px 6px;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--faint, #6e7ba0);
  border-bottom: 1px solid var(--soft-edge, #1b2642);
  margin-bottom: 3px;
}
.bfmap-menu-item {
  display: flex;
  align-items: baseline;
  gap: 7px;
  width: 100%;
  text-align: left;
  padding: 5px 9px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--text, #e6eaf5);
  font: inherit;
  cursor: pointer;
  /* The menu's width is capped to the visible frame (see fitMenuInFrame), so
     a long ability name has to wrap rather than push the box back out past
     the edge the cap just pulled it inside of. */
  overflow-wrap: anywhere;
}
.bfmap-menu-item:hover { background: var(--panel-raised, #121b33); }
.bfmap-menu-item.is-danger { color: var(--c-red, #f86a63); }

/* ── Hotkey gutter ──
   The ability's own command letter, to the LEFT of its name, in a column of
   fixed width. Fixed rather than shrink-to-fit because the entire point is a
   straight edge the eye can run down: a gutter sized to its contents puts the
   keys at four different indents in one menu, and a name that starts at a
   different x on every row is harder to scan than one with no keys at all.
   Items without a key keep the empty column, so nothing shifts. */
.bfmap-menu-key {
  flex: none;
  width: 1.15em;
  text-align: center;
  font-weight: 700;
  font-size: 0.86em;
  color: var(--faint, #6e7ba0);
}
.bfmap-menu-item:hover .bfmap-menu-key { color: var(--text, #e6eaf5); }
.bfmap-menu-label { flex: 1 1 auto; min-width: 0; }

/* ── Ability lanes ──
   The right-click ability list arrives pre-sorted into lanes (see
   map-ability-bridge.js), so the outline colour is the reading aid that
   turns a long alphabetical-looking list into six recognisable bands: melee
   red, ranged orange, seidr purple, galdr cyan, items grey, everything else
   blue. Same palette the character sheet uses for the matching slot tiles
   and pills, so a weapon is red in both places.

   Outline via border rather than `outline`, so adjacent items in a lane sit
   flush without their rings overlapping, and a 1px inset shadow tints the
   fill just enough to read at a glance without fighting the label. */
.bfmap-menu-item[class*="is-lane-"] {
  border: 1px solid var(--bfmap-lane, var(--c-blue, #7aa6ff));
  margin-bottom: 3px;
  padding: 4px 8px;
}
.bfmap-menu-item[class*="is-lane-"]:last-child { margin-bottom: 0; }
.bfmap-menu-item[class*="is-lane-"]:hover {
  background: color-mix(in oklch, var(--bfmap-lane, var(--c-blue, #7aa6ff)) 22%, transparent);
}
.bfmap-menu-item.is-lane-melee  { --bfmap-lane: var(--c-red,    #f86a63); }
.bfmap-menu-item.is-lane-ranged { --bfmap-lane: var(--c-orange, #f0a45c); }
.bfmap-menu-item.is-lane-seidr  { --bfmap-lane: var(--c-purple, #c69cff); }
.bfmap-menu-item.is-lane-galdr  { --bfmap-lane: var(--c-cyan,   #65d4f0); }
.bfmap-menu-item.is-lane-item   { --bfmap-lane: var(--c-grey,   #9aa6c2); }
.bfmap-menu-item.is-lane-other  { --bfmap-lane: var(--c-blue,   #7aa6ff); }

/* Costs more movement than the creature has left (a Quick(N) ability, plus the
   approach walk when the item is an Action Movement one).

   DIMMED AND STRUCK, NOT DISABLED — the row still clicks. This board marks an
   over-long move rather than refusing it (see .bfmap-dist.is-over and the red
   reachable field), and an ability priced in the same currency follows the same
   rule: the GM is told, and the GM decides. The strike is on the label only, so
   the hotkey column stays readable and the lane colour still identifies what
   kind of thing is being turned down. */
.bfmap-menu-item.is-unaffordable { opacity: .5; }
.bfmap-menu-item.is-unaffordable .bfmap-menu-label {
  text-decoration: line-through;
  text-decoration-color: var(--c-orange, #f0a45c);
  text-decoration-thickness: 1px;
}
/* Hover restores full opacity: a row you are about to click anyway should be
   legible while you read the tooltip that explains the cost. */
.bfmap-menu-item.is-unaffordable:hover { opacity: .85; }

/* A hard cap (the 4-small-tokens-per-tile ceiling on "Move — share this
   tile"), unlike is-unaffordable's advisory overspend above — the row is a
   real `disabled` button here and genuinely refuses the click, so it reads
   as flatly unavailable rather than merely discouraged: no hover lift, no
   strike-through inviting a click anyway, just dimmed and inert. */
.bfmap-menu-item.is-disabled {
  opacity: .4;
  cursor: not-allowed;
}
.bfmap-menu-item.is-disabled:hover { background: none; }

/* ── Move-preview overlay ───────────────────────────────────────────────────
   Lives inside .bfmap-world, so everything here is in world px and inherits
   pan/zoom for free. --bfmap-line is 1/zoom, keeping strokes hairline on
   screen at any scale. All of it is pointer-transparent: the overlay is a
   readout, never a hit target. */

.bfmap-overlay {
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
  --bfmap-line: 1px;
  z-index: 4;
}

/* Movement range: outline only, no fill — a fill would hide the map art the
   player is trying to read a route across. Anchored to the creature's
   turn-start cell, not its live position (see movePreview in map-pane.js). */
/* Reachable ground. A container of tiles rather than one bordered box: under
   5-10-5-10 the reachable set is an octagon, and no rectangle describes it —
   see movementCells. Kept faint, because it is a standing
   backdrop for the whole of a creature's turn, not a momentary highlight like
   the swept path. */
.bfmap-range { position: absolute; left: 0; top: 0; }

/* OUTLINE ONLY, on the boundary tiles. map-pane.js's paintCellRegion adds the
   .edge-* classes for the sides that face out of the reachable set; the
   interior tiles are positioned boxes that draw nothing at all.

   It used to wash every tile it covered and draw an inset line on each. At a
   creature's full Movement that is most of the visible board, tinted green for
   the whole of its turn — it discoloured the map art and laid a second grid
   over the map's own. The edge is the part that answers "how far can I go";
   the interior only ever restated that the middle is nearer than the edge. */
.bfmap-range-cell {
  position: absolute;
  box-sizing: border-box;
  --bfmap-range-edge: calc(var(--bfmap-line, 1px) * 3) solid rgba(108, 212, 140, .85);
}
.bfmap-range-cell.edge-n { border-top: var(--bfmap-range-edge); }
.bfmap-range-cell.edge-e { border-right: var(--bfmap-range-edge); }
.bfmap-range-cell.edge-s { border-bottom: var(--bfmap-range-edge); }
.bfmap-range-cell.edge-w { border-left: var(--bfmap-range-edge); }

/* Every tile a mover sets foot in. Deliberately conspicuous: its whole job is
   to answer "what do I trigger on the way" — traps, auras, zones of control.

   With a whole selection in flight there is one of these fields per creature.
   The ones belonging to tokens other than the primary are lightened, because
   six equally loud paths overlapping is a green wash rather than six routes:
   the primary is the one the player is steering and the range ring describes,
   so it stays the reference. */
.bfmap-path-cell {
  position: absolute;
  background: rgba(108, 212, 140, .16);
  border: var(--bfmap-line) solid rgba(108, 212, 140, .45);
  box-sizing: border-box;
}
.bfmap-path-cell.is-secondary {
  background: rgba(108, 212, 140, .07);
  border-color: rgba(108, 212, 140, .24);
}

/* Boundary of every live aura on the board. Used to be a single bordered box
   per aura — Chebyshev range was always an exact square, so one box drew the
   whole thing. `range` is now a diameter on the 1-2-1 movement metric
   (auras.in_range / map-scene.js's auraCells), whose reachable set is an
   octagon no rectangle describes, so this is a CELL FIELD like
   .bfmap-range-cell / .bfmap-reach-cell: one outlined region via
   map-pane.js's paintCellRegion and its .edge-* classes, near-zero fill so
   tokens underneath stay legible. */
.bfmap-aura-layer {
  position: absolute;
  left: 0;
  top: 0;
}
/* Every live aura's edge, each in its own hue (hashed from the aura id in
   map-pane.js, matched by the Auras panel's swatch). Dashed and quiet by
   default — an aura is standing scenery, and a solid ring competes with the
   movement preview it sits under. The one the selected creature is actually
   standing in goes solid and brightens: that is the aura the player is asking
   about (.is-member, set on the field's container and inherited down since
   every cell in one aura's field shares its --aura-hue and membership). */
.bfmap-aura-cell {
  position: absolute;
  box-sizing: border-box;
  --bfmap-aura-edge: calc(var(--bfmap-line, 1px) * 2) dashed hsl(var(--aura-hue, 168) 70% 62% / .55);
  background: hsl(var(--aura-hue, 168) 70% 62% / .04);
  pointer-events: none;
}
.bfmap-aura-cell.edge-n { border-top: var(--bfmap-aura-edge); }
.bfmap-aura-cell.edge-e { border-right: var(--bfmap-aura-edge); }
.bfmap-aura-cell.edge-s { border-bottom: var(--bfmap-aura-edge); }
.bfmap-aura-cell.edge-w { border-left: var(--bfmap-aura-edge); }
.bfmap-aura-cell.is-member {
  --bfmap-aura-edge: calc(var(--bfmap-line, 1px) * 2) solid hsl(var(--aura-hue, 168) 80% 66%);
  background: hsl(var(--aura-hue, 168) 70% 62% / .10);
  box-shadow: inset 0 0 calc(var(--bfmap-cell, 40px) * .15) hsl(var(--aura-hue, 168) 70% 62% / .18);
}

/* Name tag on the boundary's top-left corner. Without it a board with three
   auras is three anonymous rectangles, and the whole point of drawing them is
   knowing WHICH one you are about to step into.

   Hidden until hovered or clicked (see map-pane.js's updateAuraHover /
   togglePinnedAura) — see plans/gui_f_aura_presentation.plan.md §3.
   `pointer-events` only turns on for this small, already-hidden chip, and
   only once .is-tag-visible is added; the field/cells beneath it
   (.bfmap-aura-cell) stay pointer-events:none always, so nothing here ever
   grows a new hit surface over the board itself. */
.bfmap-aura-tag {
  position: absolute;
  top: 0;
  left: 0;
  /* Like .bfmap-dist/.bfmap-token-chrome: lives in world space (a child of
     .bfmap-world via auraLayerEl/field), so a turned board carries it along
     for free. Rotate is listed BEFORE translateY here (not after, as most of
     this file's --bfmap-scene-rot counter-rotations write it) because this
     chip's world-space anchor point is fixed by JS via left/top rather than
     centred by flex — the translateY(-100%) is a directional "float above
     the anchor" push, and it has to be applied in the SAME already-rotated
     local frame the rotate() produces, so that .bfmap-world's own rotation
     folds it back to true screen-up instead of leaving it turned. */
  transform: rotate(calc(-1 * var(--bfmap-scene-rot, 0deg))) translateY(-100%);
  padding: 0 4px;
  font-size: calc(var(--bfmap-cell, 40px) * .26);
  line-height: 1.5;
  font-weight: 700;
  white-space: nowrap;
  color: hsl(var(--aura-hue, 168) 80% 78%);
  background: rgba(8, 13, 26, .72);
  border-radius: 3px 3px 0 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity .1s ease;
}
.bfmap-aura-tag.is-tag-visible {
  opacity: 1;
  pointer-events: auto;
  cursor: help;
}

/* Silhouettes of where the tokens would land — one per selected creature, so a
   formation move shows every destination and not just the dragged token's. */
.bfmap-ghosts {
  position: absolute;
  left: 0;
  top: 0;
}
.bfmap-ghost {
  position: absolute;
  box-sizing: border-box;
  border-radius: 50%;
  border: calc(var(--bfmap-line) * 2) dashed var(--c-green, #6cd48c);
  background: rgba(108, 212, 140, .12);
}
.bfmap-ghost.is-secondary {
  border-width: var(--bfmap-line);
  background: rgba(108, 212, 140, .07);
}
/* This creature in particular cannot afford the step, whatever the primary's
   badge says. Advisory, like the badge — the move is still permitted. */
.bfmap-ghost.is-over {
  border-color: var(--c-orange, #f0a45c);
  background: rgba(240, 164, 92, .12);
}

/* ── A token standing somewhere it has not paid to be ──
   The action panel holds its approach step as a proposal until the player
   confirms (see map-pane.js's Provisional movement section): the token really
   is drawn on the destination, but no movement has been spent and the engine
   still has the creature at home. The dashed ring says so, borrowing the same
   green dash the move ghosts use for "proposed, not committed" — this is the
   same claim, made by the token itself rather than by a silhouette beside it.
   Cancel puts it back with nothing spent. */
.bfmap-token.is-provisional .bfmap-token-art {
  outline: calc(var(--bfmap-line) * 2) dashed var(--c-green, #6cd48c);
  outline-offset: calc(var(--bfmap-line) * 1);
}

/* Source -> destination arrows, one per mover. An <svg> with overflow visible
   and a 1px box at the world origin, so its children can use raw world
   coordinates including negatives (creatures legitimately stand at negative
   grid_x). */
.bfmap-arrow {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 1px;
  overflow: visible;
  --bfmap-move-arrow-w: 3px;
}
/* GREEN because it is a body moving, PULSING because nothing has been sent
   yet — a drag in progress or an Action Movement plan still on the table.
   There is no solid counterpart for a plain walk (the token simply arrives,
   and the grid it stands on is the record); the steady green on this board
   belongs to .bfmap-knockback-arrow, a displacement that has resolved. See
   the arrow grammar note above .bfmap-target-arrows. */
.bfmap-move-arrow {
  --bfmap-glow: var(--c-green, #6cd48c);
  stroke: var(--c-green, #6cd48c);
  fill: none;
  stroke-width: var(--bfmap-move-arrow-w);
  stroke-linecap: round;
}
.bfmap-move-arrow.is-head {
  --bfmap-glow: var(--c-green, #6cd48c);
  fill: var(--c-green, #6cd48c);
  stroke: none;
}
.bfmap-move-arrow.is-secondary { opacity: .5; }

/* Action Movement's "who you're aiming at" pointer — from the mover's
   previewed destination to the creature under the cursor. ORANGE, the
   grammar's "an attack": the green line beside it is the path this creature
   would walk, the orange one is who it would then swing at. Both are in a
   pulsing layer, because neither has happened. The same line goes steady
   orange (.bfmap-target-arrow) once the blow lands.

   NAMED .bfmap-target-HOVER-arrow, deliberately distinct from
   .bfmap-target-arrow (the committed-attack record drawn in
   .bfmap-target-arrows, above) — the two used to share one class name,
   which meant this rule's preview styling silently won the cascade for BOTH:
   every finished attack rendered as a preview line and never as the settled
   one it was supposed to be. Same class, same specificity, later rule in the
   file — CSS has no idea these are two different features. They now share a
   colour on purpose, and what separates them is which LAYER they land in
   (one pulses, one does not), so keeping the two class names apart is the
   only thing holding that distinction up.

   Twice the shaft weight of a committed arrow's default (--bfmap-arrow-w,
   3px in .bfmap-target-arrows) rather than reusing --bfmap-move-arrow-w
   directly: this is a live preview competing with map art for attention
   under a moving cursor, not a settled record, and at the shared 3px weight
   it all but disappeared until the view was zoomed in close. */
.bfmap-target-hover-arrow {
  --bfmap-glow: var(--c-orange, #f0a45c);
  stroke: var(--c-orange, #f0a45c);
  fill: none;
  stroke-width: calc(var(--bfmap-move-arrow-w) * 2);
  stroke-linecap: round;
}
.bfmap-target-hover-arrow.is-head {
  --bfmap-glow: var(--c-orange, #f0a45c);
  fill: var(--c-orange, #f0a45c);
  stroke: none;
}

/* Tile count on the destination.

   TOPMOST of anything that moves with the board. It sits on the destination
   tile, which during a drag is exactly where the dragged token is — and a
   dragged token takes z-index 30 — so inside .bfmap-overlay (z-index 4) the
   badge spent every drag half swallowed by its own token. 37 clears the token
   (30), the targeting arrows (32) and the whole token-chrome ladder that now
   sits above those arrows (name label 33, vitals 33, id badge 34, condition
   column 35, condition list 36); the marquee at 60 still wins, which is
   right, since a box-select is a gesture and this is a readout.

   It was 34 until the chrome ladder moved up, which silently tied it with the
   id badge and put the condition column (35) over it — so dragging a token
   onto a creature carrying two conditions hid the tile count behind their
   badges, exactly the readout the drag exists to consult.

   Positioned in world coordinates by the renderer, as it was when it lived in
   the overlay — it is a direct child of .bfmap-world now, and that container's
   origin is the same one .bfmap-overlay uses, so the maths is unchanged. */
.bfmap-dist {
  position: absolute;
  z-index: 37;
  /* A NUMBER, so it never turns with the board. It is positioned in world
     space (it has to track the destination tile through pan and zoom) which
     puts it inside the rotated container, but "4" read sideways is not a tile
     count. Rotated about its own centre — the anchor is one corner of a badge
     barely a third of a cell across, so unlike the token chrome group there is
     no meaningful anchor swap to undo, only the glyphs to stand back up. */
  transform: rotate(calc(-1 * var(--bfmap-scene-rot, 0deg)));
  /* Inherited from .bfmap-overlay before the move, and load-bearing: the badge
     sits on the destination tile, directly under the cursor mid-drag, so a
     hit-testable box there would eat the pointermove that is steering it. */
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: calc(var(--bfmap-cell, 50px) * 0.42);
  height: calc(var(--bfmap-cell, 50px) * 0.42);
  padding: 0 calc(var(--bfmap-cell, 50px) * 0.08);
  box-sizing: border-box;
  font-size: calc(var(--bfmap-cell, 50px) * 0.3);
  font-weight: 800;
  line-height: 1;
  color: #06210f;
  background: var(--c-green, #6cd48c);
  border-radius: calc(var(--bfmap-cell, 50px) * 0.1);
  box-shadow: 0 0 calc(var(--bfmap-line) * 5) rgba(0, 0, 0, .5);
}
/* Beyond the creature's Movement. Still permitted — the guide is advisory, not
   a restriction — but it should not look like a legal move. */
.bfmap-dist.is-over { background: var(--c-orange, #f0a45c); color: #2a1503; }
/* Proposed move costs more than the creature has. The reachable outline turns
   red so the overspend reads off the board itself, not only off the number
   badge — which matters more now that a diagonal costs two and a move can go
   over without the destination looking any further away. Recolours the same
   boundary the rule above draws; overriding the variable rather than the four
   border properties is what keeps the two definitions from drifting. */
.bfmap-overlay.is-over .bfmap-range-cell {
  --bfmap-range-edge: calc(var(--bfmap-line, 1px) * 3) solid rgba(248, 106, 99, .85);
}

/* Keyboard-driven preview awaiting confirmation (space, or click the
   destination). Pulsing distinguishes "proposed" from a drag in flight. */
.bfmap-ghost.is-pending {
  animation: bfmap-pending 1.1s ease-in-out infinite;
}
@keyframes bfmap-pending {
  0%, 100% { opacity: 1; }
  50%      { opacity: .45; }
}

/* ── Ability target area ────────────────────────────────────────────────────
   Yellow, and on its own layer rather than inside .bfmap-overlay: the move
   overlay is blanked whenever nothing is selected, and an ability range must be
   able to show without a selection — hovering the ability is itself the gesture.

   YELLOW RATHER THAN RED, which is what this was. Enemy tokens carry a red
   border, so a red targeting wash laid over a cluster of them put the same
   colour on the highlight and on the thing being highlighted, and the tiles
   stopped separating from the bodies standing in them. Nothing else on the
   board claims yellow at cell scale, so it reads cleanly over both sides'
   tokens and over the map art. Green is where you can GO, yellow is what you
   can REACH. */
.bfmap-ability-layer {
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
  --bfmap-line: 1px;
  z-index: 3;
}

/* Container only — the shape is drawn as a field of .bfmap-ability-cell tiles,
   because a ranged reach is a 5-10-5-10 octagon that no rectangle describes
   (see renderAbilityRange in map-pane.js). */
.bfmap-ability-range {
  position: absolute;
  left: 0;
  top: 0;
}

/* Container only, same shape as .bfmap-ability-range above — holds the
   hovered ability's own AOE fill (.bfmap-aoe-cell) and/or the aura it would
   place (.bfmap-aura-preview), painted by renderAbilityRange in map-pane.js. */
.bfmap-ability-aura-preview {
  position: absolute;
  left: 0;
  top: 0;
}

/* One tile of an ability's target area. An inset line rather than a border, so
   adjacent tiles read as one region instead of a lattice. Unlike the movement
   field this one keeps its per-tile wash: it appears only while an ability row
   is under the cursor, so it is a momentary answer to "where does this reach",
   not a backdrop standing for a whole turn. */
.bfmap-ability-cell {
  position: absolute;
  box-sizing: border-box;
  background: rgba(246, 222, 125, .11);
  box-shadow: inset 0 0 0 var(--bfmap-line, 1px) rgba(246, 222, 125, .32);
}

/* ── Action preview ──
   What the docked action panel (combat-attack-menu.js's opts.dock) is
   currently proposing: an AOE template, attack arrow(s), and any knockback
   ghost. Own layers — see the DOM-skeleton comment in map-pane.js for why
   they can't share the move-preview overlay or the committed-arrow layer —
   drawn fresh by drawActionPreview on every panel mutation. */

.bfmap-action-cells {
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
  z-index: 3;
}

/* The AOE template tile — a faint yellow wash, and a yellow line only on the
   sides that face out of the covered set, so the whole blast reads as ONE
   OUTLINED AREA. map-pane.js's paintCellRegion decides which edges those are
   and adds the .edge-* classes; the tiles themselves stay plain positioned
   boxes.

   Yellow for the same reason the ability range above is: this fill lands
   squarely on the enemies it is about to hit, and those tokens are outlined in
   red. A red template over red borders made the blast and its victims one
   indistinct mass exactly when the player needs to count them.

   It used to draw an inset ring on every tile, which meant every INTERNAL edge
   got a line too and a disc came out as a lattice of separate boxes. The
   outline is the shape the player is aiming; the interior lines were never a
   claim about anything.

   `box-sizing: border-box` with the width/height the renderer sets is what
   keeps a bordered tile the same size as an unbordered one — the line is drawn
   inside the cell, so the outline sits on the region's boundary rather than
   half a line outside it.

   Also what a Cleave fan's affected tiles use — see drawActionPreview's
   `info.cleave` branch — so both "this is what gets hit" cases read
   identically regardless of which mechanic produced them. */
.bfmap-aoe-cell {
  position: absolute;
  box-sizing: border-box;
  background: rgba(246, 222, 125, .16);
  --bfmap-aoe-edge: calc(var(--bfmap-line, 1px) * 3) solid var(--c-yellow, #f6de7d);
}
.bfmap-aoe-cell.edge-n { border-top: var(--bfmap-aoe-edge); }
.bfmap-aoe-cell.edge-e { border-right: var(--bfmap-aoe-edge); }
.bfmap-aoe-cell.edge-s { border-bottom: var(--bfmap-aoe-edge); }
.bfmap-aoe-cell.edge-w { border-left: var(--bfmap-aoe-edge); }

/* The aura a cast is ABOUT to leave behind — a Fireshell's ring, a Battle
   Aura's field — while the action panel is still open on it. Drawn by
   drawActionPreview from the ability's projected `aura` record, and by
   renderAbilityRange's picker-hover preview before a target is even locked —
   the instant-before and instant-after of the same cast, so they read as one
   drawing.

   PURPLE, and that is the whole point of it being a separate rule: yellow is
   what this blow hits now, green is how far it can be thrown, and purple is
   the ground that stays changed after it lands. Three claims about the same
   tiles, told apart by colour alone, because a player aiming a shell needs to
   read all three at once.

   Used to be a single bordered box (Chebyshev range was always an exact
   square); `range` is now a diameter on the 1-2-1 movement metric, whose
   reachable set is an octagon, so this is a CELL FIELD like .bfmap-aura-cell
   — one outlined region via paintCellRegion's .edge-* classes. Solid rather
   than the live .bfmap-aura-cell's dashed default: this one is the thing the
   player is actively aiming, the same distinction that rule draws with
   .is-member. `--aura-hue` defaults to purple here (rather than reading the
   live aura's hash) so the shared .bfmap-aura-tag comes out purple too,
   unless the spec named a color. */
.bfmap-aura-preview-cell {
  position: absolute;
  box-sizing: border-box;
  /* No --aura-hue default here (unlike the old single-box .bfmap-aura-preview):
     the fallback in var(--aura-hue, 265) below covers the uncoloured case, and
     leaving the property itself unset lets a wrapping container's
     --aura-hue (set by JS only when the spec named a color) actually reach
     these cells — a default declared on this rule would win over an
     inherited value and the override would never show. */
  --bfmap-aura-preview-edge: calc(var(--bfmap-line, 1px) * 3) solid hsl(var(--aura-hue, 265) 70% 66%);
  background: hsl(var(--aura-hue, 265) 70% 66% / .07);
  pointer-events: none;
}
.bfmap-aura-preview-cell.edge-n { border-top: var(--bfmap-aura-preview-edge); }
.bfmap-aura-preview-cell.edge-e { border-right: var(--bfmap-aura-preview-edge); }
.bfmap-aura-preview-cell.edge-s { border-bottom: var(--bfmap-aura-preview-edge); }
.bfmap-aura-preview-cell.edge-w { border-left: var(--bfmap-aura-preview-edge); }

/* The selected attack's reach, from the attacker's live position — drawn
   UNDER the AOE/Cleave fill above (previewReachEl is created before
   previewCellsEl in the DOM skeleton, so it paints first), outline only, no
   fill, so it reads as "reachable" everywhere the yellow blast fill isn't
   also claiming "and this is what's about to get hit."

   GREEN, and only on the OUTER boundary. Two changes for the same reason:
   this layer and the red blast fill sit on the same tiles at the same moment,
   and it used to draw an orange ring on every tile it covered — a lattice
   dense enough to fight the thing it was drawn behind, in a colour close
   enough to red to read as part of it. Green is the board's "as far as this
   goes" colour (a move proposal's ghost, a legal destination); red stays
   "this is what gets hit". map-pane.js's paintCellRegion picks the boundary
   edges and adds the .edge-* classes — the same helper and the same rule the
   blast fill uses, so the two outlines are the same drawing in two colours.

   No fill at all here, unlike .bfmap-aoe-cell: a reach field is most of the
   visible board on a ranged ability, and a wash that size would tint the map
   art rather than mark anything out. */
.bfmap-reach-cells {
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
  z-index: 3;
}
.bfmap-reach-cell {
  position: absolute;
  box-sizing: border-box;
  --bfmap-reach-edge: calc(var(--bfmap-line, 1px) * 2) solid var(--c-green, #6cd48c);
}
.bfmap-reach-cell.edge-n { border-top: var(--bfmap-reach-edge); }
.bfmap-reach-cell.edge-e { border-right: var(--bfmap-reach-edge); }
.bfmap-reach-cell.edge-s { border-bottom: var(--bfmap-reach-edge); }
.bfmap-reach-cell.edge-w { border-left: var(--bfmap-reach-edge); }

.bfmap-action-ghosts {
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
  z-index: 33;
}

/* A knockback destination previewed before it happens — reuses .bfmap-ghost
   (the same dashed "proposal, not committed" silhouette a keyboard move
   proposal gets) at reduced opacity, since this one is doubly provisional:
   the whole action hasn't been sent yet. */
.bfmap-ghost.is-preview { opacity: 0.7; }

/* ── Bonk! ──
   The destination once the player has declared the slide stops short of it —
   dragged back onto scenery the map paints but the Wall tool never traced (see
   map-pane.js's Bonk! section). Orange rather than green, because green ghosts
   everywhere else on this board mean "proposed, and nothing is in the way", and
   this one is a proposal ABOUT something being in the way. */
.bfmap-ghost.is-capped {
  border-color: var(--c-orange, #f0a45c);
  background: rgba(240, 164, 92, .14);
}

/* The grab handle over that ghost. Transparent — the ghost underneath is the
   visible affordance and a second outline on the same tile would just look
   like a drawing bug — so the cursor is the whole hint, which is why it is a
   grab cursor and not a pointer. Above .bfmap-action-ghosts (33) so a handle
   is never buried under the very silhouette it belongs to. */
.bfmap-kb-grab {
  position: absolute;
  z-index: 34;
  cursor: grab;
  touch-action: none;   /* the drag is ours; do not let the pane pan under it */
}
.bfmap-kb-grab.is-dragging { cursor: grabbing; }

/* WHAT IT HITS — the strip one step past the resting footprint. Marked
   separately from the landing tile because the two say different things: one is
   where the creature ends up, the other is what hurt it. */
.bfmap-bonk {
  position: absolute;
  z-index: 34;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  border: calc(var(--bfmap-line, 1px) * 2) solid var(--c-orange, #f0a45c);
  background: rgba(240, 164, 92, .22);
  border-radius: 4px;
  color: #fff;
  font: 700 calc(var(--bfmap-cell, 32px) * 0.3)/1 system-ui, -apple-system, sans-serif;
  letter-spacing: .02em;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .9);
  overflow: hidden;
  /* Like .bfmap-dist: positioned in world space so it has to live inside the
     rotated .bfmap-world, but "Bonk!" read sideways or upside down on a turned
     board is not a word. Rotated about its own centre to stand the glyphs back
     up; see .bfmap-dist above and .bfmap-token-chrome in this file for the
     other two places that undo --bfmap-scene-rot the same way. */
  transform: rotate(calc(-1 * var(--bfmap-scene-rot, 0deg)));
}

/* Above the committed-arrow layer (32) and the move-preview arrow (inside
   .bfmap-overlay, z-index 4), below the HUD (40) — a live proposal should
   read over a settled record, never under it. */
.bfmap-action-arrows {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 1px;
  overflow: visible;
  pointer-events: none;
  z-index: 38;
  --bfmap-arrow-w: 3.5px;
}

/* The attack preview — source to target, one per currently selected target,
   drawn while the panel is still picking them.

   THE SAME ORANGE AS THE COMMITTED ARROW, and now the same geometry too —
   centre to centre, same head formula (see drawArrowBetween). It used to be
   cyan, from a time when the committed layer split cyan/orange by side and a
   preview needed a colour of its own to avoid claiming one. With colour now
   meaning "what kind of event" rather than "whose", a proposed attack and a
   landed one are the same thing at two moments, so nothing about the drawing
   distinguishes them: the pulse does, and it stops. Confirming an attack
   should look like this line settling, not like it being swapped out.

   No drop-shadow HALO (the dark rim the committed arrows wear to stay legible
   over token art) — this one carries a coloured glow instead, from the shared
   rule up beside the pulse. */
.bfmap-preview-arrow {
  --bfmap-glow: var(--c-orange, #f0a45c);
  stroke: var(--c-orange, #f0a45c);
  fill: none;
  stroke-width: var(--bfmap-arrow-w);
  stroke-linecap: round;
}
.bfmap-preview-arrow.is-head {
  --bfmap-glow: var(--c-orange, #f0a45c);
  fill: var(--c-orange, #f0a45c);
  stroke: none;
}

/* The knockback preview — .bfmap-knockback-arrow's green, one tense earlier.
   It inherits that rule's dark halo, which would fight the coloured glow, so
   the glow rule above overrides `filter` wholesale for the is-preview case.
   Kept a shade lighter than its siblings on top of the layer pulse: a shove
   is a consequence of the action being aimed, not the action itself. */
.bfmap-knockback-arrow.is-preview {
  --bfmap-glow: var(--c-green, #6cd48c);
  opacity: 0.8;
}

/* Rings the attacker and every currently-selected target so the board and
   the panel's target list agree at a glance. Same construction as
   .is-move-target's ring (a dark inset plus a coloured outer glow) but in
   the targeting red, so a token doesn't read as "selected for dragging"
   while the map's normal selection is suppressed. */
.bfmap-token.is-preview-target .bfmap-token-art {
  border-color: var(--c-red, #f86a63);
  box-shadow:
    inset 0 0 0 calc(var(--bfmap-cell) * 0.02) rgba(4, 6, 15, .8),
    0 0 0 calc(var(--bfmap-cell) * 0.04) rgba(248, 106, 99, .65);
}

/* The acting creature, while its own action panel is docked. Every other
   token has just become a target toggle, so the one that can still be MOVED
   has to say so: the grab cursor is the affordance, and the dashed ring is
   what makes it findable without hunting with the mouse.

   Cyan, and drawn as an OUTLINE outside the red .is-preview-target border
   the attacker also wears — two facts are true of this token at once ("you
   are swinging" and "you may still move"), and two rings say that where one
   colour forced to mean both would say neither. Yellow is spoken for: it is
   selection, which the attacker frequently also is, and a third yellow ring
   would read as nothing at all. Cyan is now used by nothing else on the
   board, which is exactly what makes it legible here — it was the preview
   arrows' colour until those went orange to match the blows they become. */
.bfmap-token.is-action-actor { cursor: grab; }
.bfmap-token.is-action-actor.is-dragging { cursor: grabbing; }
.bfmap-token.is-action-actor .bfmap-token-art {
  outline: calc(var(--bfmap-cell) * 0.035) dashed var(--c-cyan, #65d4f0);
  outline-offset: calc(var(--bfmap-cell) * 0.07);
}

/* ── Creature detail panel ──
   An iframe (gui/mockups/creature-detail.html) the pane parks against one
   edge, showing the Creatures tab's expanded card for the selected token. It
   takes whichever side the docked action panel does not, so the two never
   stack — map-pane.js's detailSide() picks, and paneInsets() tells the HUD and
   the camera to stay clear of whichever it chose.

   Absolutely positioned inside .bfmap-root, so it clips with the pane and
   needs no wrapper of its own; the iframe IS the panel, which is why the
   border and shadow live here rather than in the document inside it.

   QUALIFIED BY .bfmap-root, and it has to be. When the pane is embedded in the
   Combat tab it sits inside .cs-foundry-body, whose `iframe { width:100%;
   height:100% }` (v2/abilities.css — written for the Foundry frame that fills
   the pane) outranks a bare class selector on specificity and stretched this
   panel to the whole board. */
.bfmap-root .bfmap-detail-dock {
  position: absolute;
  top: 10px;
  /* Stated as a height rather than left to top/bottom, for two reasons that
     both come from this being an <iframe>. .cs-foundry-body iframe sets
     height:100% (v2/abilities.css, written for the Foundry frame that fills
     the pane) and an absolutely positioned box with both a height and a bottom
     ignores the bottom; and `height: auto` is no escape either, because on a
     REPLACED element auto means the intrinsic 150px, not "stretch". */
  height: calc(100% - 20px);
  width: min(360px, 42%);
  z-index: 60;              /* over the board, under the action panel (70) */
  border: 1px solid var(--edge, #1c243d);
  border-radius: var(--r-md, 10px);
  background: var(--panel, #0f1528);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5);
  overflow: hidden;
}
.bfmap-root .bfmap-detail-dock.is-left  { left: 10px; }
.bfmap-root .bfmap-detail-dock.is-right { right: 10px; }
.bfmap-root .bfmap-detail-dock[hidden]  { display: none; }

/* A phone has no room to give a side panel 42% and still have a board. It
   becomes a bottom sheet instead, which is where the rest of the pane's
   overlays go at this width. */
@media (max-width: 720px) {
  .bfmap-root .bfmap-detail-dock {
    top: auto;
    left: 8px;
    right: 8px;
    bottom: 8px;
    width: auto;
    height: 45%;
  }
}

/* The AOE template's drag surface — a world-space div covering the
   template's bounding box (map-pane.js's layoutAreaGrab), so a drag on it
   moves the template instead of panning the board underneath. Hidden except
   while an area is placed. */
.bfmap-aoe-grab {
  position: absolute;
  z-index: 39;
  pointer-events: auto;
  cursor: move;
}

/* ── HUD ────────────────────────────────────────────────────────────────── */

.bfmap-hud {
  position: absolute;
  top: 10px;
  /* Steps left of the docked action panel's reserved column — see
     --bfmap-dock-right on .bfmap-root above. Zero (plain 10px) when nothing
     is docked. */
  right: calc(10px + var(--bfmap-dock-right, 0px));
  display: flex;
  flex-direction: column;
  gap: 6px;
  z-index: 40;
  transition: right 160ms ease;
}

.bfmap-hud-btn {
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  font-size: 15px;
  line-height: 1;
  border-radius: 8px;
  border: 1px solid var(--edge, #1c243d);
  background: rgba(15, 21, 40, .92);
  color: var(--text, #e6eaf5);
  cursor: pointer;
}
.bfmap-hud-btn:hover { background: var(--panel-raised, #121b33); }
.bfmap-hud-btn.is-gm { border-color: var(--c-yellow, #f6de7d); color: var(--c-yellow, #f6de7d); }
/* An active layer mode, or the settings panel being open. These buttons are
   toggles and a toggle has to show its state — without this the ⚙ gives no hint
   that a second press will close what the first opened. */
.bfmap-hud-btn.is-on {
  border-color: var(--c-cyan, #65d4f0);
  color: var(--c-cyan, #65d4f0);
  background: rgba(101, 212, 240, .14);
}

/* ── Wall shape strip ───────────────────────────────────────────────────────
   Which kind of wall a drag draws. Sits beside the HUD column and only exists
   while the Walls layer is on, because outside it the choice means nothing. */

.bfmap-wallbar {
  position: absolute;
  top: 10px;
  right: calc(52px + var(--bfmap-dock-right, 0px));
  z-index: 41;
  display: flex;
  gap: 4px;
  transition: right 160ms ease;
}
.bfmap-wallbar-btn {
  padding: 6px 9px;
  font: inherit;
  font-size: 11px;
  line-height: 1;
  border-radius: 8px;
  border: 1px solid var(--edge, #1c243d);
  background: rgba(15, 21, 40, .92);
  color: var(--muted, #9eb0d2);
  cursor: pointer;
  white-space: nowrap;
}
.bfmap-wallbar-btn:hover { color: var(--text, #e6eaf5); }
.bfmap-wallbar-btn.is-on {
  border-color: var(--c-red, #f86a63);
  color: var(--c-red, #f86a63);
  background: rgba(248, 106, 99, .14);
}

.bfmap-status {
  position: absolute;
  left: calc(10px + var(--bfmap-dock-left, 0px));
  bottom: 10px;
  z-index: 40;
  font-size: 11px;
  padding: 3px 8px;
  border-radius: 6px;
  background: rgba(4, 6, 15, .8);
  color: var(--faint, #6e7ba0);
  pointer-events: none;
  transition: left 160ms ease;
}
.bfmap-status.is-ok    { color: var(--c-green, #6cd48c); }
.bfmap-status.is-error { color: var(--c-red, #f86a63); }
/* Board is drawable but incomplete — e.g. a creature the engine reports that
   has no combat_tokens row. Not an error (everything else works), but not
   something to leave looking normal either. */
.bfmap-status.is-warn  { color: var(--c-yellow, #f6de7d); }

/* ── Frame-rate readout ─────────────────────────────────────────────────────
   Opt-in, per-device, off by default (Settings > Display > Show frame rate).

   BOTTOM RIGHT, which is the only corner nothing else claims: the HUD column
   and wall strip are top-right, the tools panel is top-left, and the status
   line is bottom-left. Follows the same dock inset as those so it steps aside
   for a docked action panel instead of sitting under it.

   Tabular numerals because the whole point is watching four numbers change once
   a second — proportional digits make the line jitter sideways as they do, which
   is exactly the reading you cannot do while also watching the board. */
.bfmap-perf {
  position: absolute;
  right: calc(10px + var(--bfmap-dock-right, 0px));
  bottom: 10px;
  z-index: 40;
  padding: 3px 8px;
  border-radius: 6px;
  background: rgba(4, 6, 15, .8);
  color: var(--c-cyan, #65d4f0);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  pointer-events: none;
  transition: right 160ms ease;
}

.bfmap-toast {
  position: absolute;
  /* Centres on the VISIBLE half of the pane, not the whole pane, while a
     dock reserves a column — a toast centred on the pane's true midpoint
     while a right-docked panel covers the right third reads as off-centre. */
  left: calc(50% + (var(--bfmap-dock-left, 0px) - var(--bfmap-dock-right, 0px)) / 2);
  bottom: 16px;
  transform: translate(-50%, 8px);
  z-index: 50;
  max-width: min(420px, 90%);
  padding: 8px 14px;
  border-radius: 8px;
  border: 1px solid var(--edge, #1c243d);
  background: rgba(15, 21, 40, .96);
  color: var(--text, #e6eaf5);
  font-size: 12.5px;
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 160ms, transform 160ms;
}
.bfmap-toast.is-visible { opacity: 1; transform: translate(-50%, 0); }
.bfmap-toast.is-warn { border-color: var(--c-orange, #f0a45c); }

/* ── GM tools panel ─────────────────────────────────────────────────────── */

.bfmap-tools {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 60;
  width: min(310px, calc(100% - 20px));
  max-height: calc(100% - 20px);
  display: flex;
  flex-direction: column;
  border-radius: 10px;
  border: 1px solid var(--edge, #1c243d);
  background: rgba(15, 21, 40, .97);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .5);
  color: var(--text, #e6eaf5);
  font-size: 12.5px;
}

.bfmap-tools-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 9px 11px;
  border-bottom: 1px solid var(--edge, #1c243d);
}
.bfmap-tools-title { font-weight: 700; letter-spacing: .03em; }
.bfmap-tools-close {
  background: none;
  border: 0;
  color: var(--muted, #9eb0d2);
  cursor: pointer;
  font-size: 13px;
  padding: 2px 4px;
}
.bfmap-tools-close:hover { color: var(--text, #e6eaf5); }

.bfmap-tools-body { padding: 9px 11px 11px; overflow-y: auto; }

.bfmap-tool-sec { margin-top: 12px; }
.bfmap-tool-sec-title {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .09em;
  color: var(--faint, #6e7ba0);
  margin-bottom: 6px;
  padding-bottom: 3px;
  border-bottom: 1px solid var(--soft-edge, #1b2642);
}

.bfmap-tool-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 5px 0;
}
.bfmap-tool-row.is-stacked { flex-direction: column; align-items: stretch; gap: 3px; }
.bfmap-tool-label { color: var(--muted, #9eb0d2); flex: 0 1 auto; }

.bfmap-tool-input {
  flex: 0 0 88px;
  min-width: 0;
  padding: 4px 7px;
  border-radius: 6px;
  border: 1px solid var(--edge, #1c243d);
  background: var(--panel-deep, #0a0f1c);
  color: var(--text, #e6eaf5);
  font: inherit;
  font-size: 12px;
}
.bfmap-tool-input.is-wide { flex: 1 1 auto; }
.bfmap-tool-input:focus { outline: 1px solid var(--c-cyan, #65d4f0); outline-offset: 0; }

.bfmap-tool-check { accent-color: var(--c-cyan, #65d4f0); }

/* A slider plus the value it is currently at. The readout is not decoration:
   the whole row is "drag this and watch the board", and without a number there
   is no way to get back to a setting you liked or to tell a second device what
   to match. Fixed-width and tabular so the label does not jitter sideways as
   the digits change under the thumb. */
.bfmap-tool-range {
  flex: 1 1 auto;
  min-width: 0;
  accent-color: var(--c-cyan, #65d4f0);
}
.bfmap-tool-readout {
  flex: 0 0 auto;
  min-width: 76px;
  text-align: right;
  color: var(--muted, #9eb0d2);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

.bfmap-tool-btn {
  width: 100%;
  margin-top: 6px;
  padding: 6px 10px;
  border-radius: 7px;
  border: 1px solid var(--edge, #1c243d);
  background: var(--panel-raised, #121b33);
  color: var(--text, #e6eaf5);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.bfmap-tool-btn:hover { border-color: var(--c-cyan, #65d4f0); }
.bfmap-tool-btn.is-primary { border-color: var(--c-yellow, #f6de7d); color: var(--c-yellow, #f6de7d); }

/* ── Scenes popup ───────────────────────────────────────────────────────────
   Saved bundles of a session and the map it was played on — its own docked
   panel (opened from the ▦ HUD button), not a section of the GM tools panel.
   Docks to the right at the same offset as the decal palette (52px: clears
   the HUD column, which is 32px wide plus a 10px margin on each side), so the
   two never overlap if both happen to be open. */

.bfmap-scenes {
  position: absolute;
  right: 52px;
  top: 10px;
  bottom: 10px;
  z-index: 61;
  width: min(320px, calc(100% - 70px));
  display: flex;
  flex-direction: column;
  border-radius: 10px;
  border: 1px solid var(--edge, #1c243d);
  background: rgba(15, 21, 40, .97);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .5);
  color: var(--text, #e6eaf5);
  font-size: 12.5px;
}
.bfmap-scenes-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 9px 11px;
  border-bottom: 1px solid var(--edge, #1c243d);
}
.bfmap-scenes-title { font-weight: 700; font-size: 12.5px; letter-spacing: .03em; }
.bfmap-scenes-close {
  background: none; border: 0; cursor: pointer;
  color: var(--muted, #9eb0d2); font-size: 13px; padding: 2px 4px;
}
.bfmap-scenes-save {
  padding: 9px 11px;
  border-bottom: 1px solid var(--edge, #1c243d);
}

/* COLUMNS COME FROM THE PANEL'S WIDTH, not from a breakpoint or a mode
   toggle: auto-fill + minmax means one column at the default 320px, and a
   second, third, fourth as the GM drags the panel wider (see
   .bfmap-scenes-grip). A scene row stops being readable under ~250px — the
   name ellipsises to nothing next to a 52px thumbnail — so that is the
   minimum a column is allowed to shrink to before the count drops again. */
.bfmap-scenes-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 8px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  align-content: start;
  gap: 6px;
}
/* Anything that is not a scene row spans the full width. A folder holds its
   own grid (below), and a section label or an empty-state hint that sat in
   one column would read as an item in the list rather than a heading over
   it. */
.bfmap-scenes-list > .bfmap-scenes-folder,
.bfmap-scenes-list > .bfmap-scenes-recent-label,
.bfmap-scenes-list > .bfmap-tool-hint { grid-column: 1 / -1; }

/* The grip is the panel's left edge — the panel is docked to the RIGHT, so
   widening it means dragging that edge leftward. Sits just outside the
   border and overlaps it slightly, so the cursor changes on the edge itself
   rather than in a gap beside it. */
.bfmap-scenes-grip {
  position: absolute;
  left: -4px;
  top: 0;
  bottom: 0;
  width: 9px;
  cursor: ew-resize;
  z-index: 1;
  border-radius: 10px 0 0 10px;
}
.bfmap-scenes-grip:hover,
.bfmap-scenes-grip.is-dragging { background: var(--c-cyan, #65d4f0); opacity: .35; }

/* Each entry is one row: a thumbnail, the big load button, a small delete —
   the common action is the whole row and the destructive one has to be aimed
   at, same split the old in-panel list used. */
/* cursor: grab on the row itself, not its buttons (each sets its own pointer
   cursor) — a whitespace/gap affordance that the row as a whole is what you
   drag into a folder, dragging is a mouse-down-and-move gesture so it does
   not stop the thumbnail/load/delete buttons underneath from still clicking
   normally on a plain click. */
.bfmap-scenes-row { display: flex; gap: 6px; align-items: stretch; cursor: grab; }
.bfmap-scenes-row.is-pinned { border: 1px solid var(--edge, #1c243d); border-radius: 7px; padding: 2px; }

.bfmap-scenes-thumb {
  flex: 0 0 auto;
  width: 52px;
  height: 52px;
  border-radius: 7px;
  border: 1px solid var(--edge, #1c243d);
  background-color: var(--panel-deep, #0a0f1c);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  padding: 0;
  cursor: default;   /* a hover target, not a second load button */
}
/* No background image on the saved scene: a plain swatch rather than an empty
   box, so the row still reads as "this one has no picture" instead of "this
   thumbnail is broken". */
.bfmap-scenes-thumb.is-blank {
  background-image:
    linear-gradient(135deg, transparent 45%, var(--edge, #1c243d) 45% 55%, transparent 55%);
}

.bfmap-scenes-load {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  text-align: left;
  padding: 6px 9px;
  border-radius: 7px;
  border: 1px solid var(--edge, #1c243d);
  background: var(--panel-deep, #0a0f1c);
  color: var(--text, #e6eaf5);
  font: inherit;
  cursor: pointer;
}
.bfmap-scenes-load:hover { border-color: var(--c-cyan, #65d4f0); }
.bfmap-scenes-load:disabled { opacity: .5; cursor: progress; }
.bfmap-scenes-name {
  font-size: 12px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bfmap-scenes-meta {
  font-size: 10px;
  color: var(--faint, #6e7ba0);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A small round ✕. It used to be a full-height pill in its own flex column,
   which cost every row ~34px of width — a third of the name on a one-column
   panel — to sit permanently beside a control nobody presses often. */
.bfmap-scenes-del {
  flex: 0 0 auto;
  align-self: center;
  width: 17px;
  height: 17px;
  padding: 0;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--edge, #1c243d);
  background: var(--panel-deep, #0a0f1c);
  color: var(--faint, #6e7ba0);
  font: inherit;
  font-size: 9px;
  line-height: 1;
  cursor: pointer;
}
.bfmap-scenes-del:hover { border-color: var(--c-red, #f86a63); color: var(--c-red, #f86a63); }

/* On a SCENE row it comes out of the flow entirely and tucks into the
   corner, so the name and the meta line get the whole width back rather
   than the difference between a pill and a disc. Only the name is padded
   clear of it — the meta line runs underneath and can use the full row. */
.bfmap-scenes-row { position: relative; }
.bfmap-scenes-row > .bfmap-scenes-del {
  position: absolute;
  top: 3px;
  right: 3px;
  z-index: 1;
}
.bfmap-scenes-row .bfmap-scenes-name { padding-right: 20px; }

/* A row being dragged (scene OR folder) — the drop targets are what react
   visually on hover, this just marks the thing in flight so the list doesn't
   look inert while a drag is happening. */
.bfmap-scenes-row.is-dragging,
.bfmap-scenes-folder.is-dragging { opacity: .4; }

/* ── Folders ──────────────────────────────────────────────────────────────
   A folder is one collapsible block: a head row (thumbnail + name/count +
   collapse toggle + delete) and a body that only exists in the DOM's flow
   while open. Visually a heavier cousin of .bfmap-scenes-row — same
   thumbnail size, same row rhythm — so a folder reads as "a scene row that
   holds more rows" rather than a different kind of object. */
.bfmap-scenes-folder {
  border: 1px solid var(--edge, #1c243d);
  border-radius: 7px;
  background: rgba(10, 15, 28, .5);
}
/* The whole block is the drop target (head OR body), so a drag doesn't have
   to land pixel-perfect on the header to file into a folder that's open. */
.bfmap-scenes-folder.is-drop-target {
  border-color: var(--c-cyan, #65d4f0);
  background: rgba(101, 212, 240, .1);
}
.bfmap-scenes-folder-head {
  display: flex;
  gap: 6px;
  align-items: stretch;
  padding: 6px;
}
.bfmap-scenes-folder-toggle {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  text-align: left;
  padding: 6px 9px;
  border-radius: 7px;
  border: 1px solid transparent;
  background: none;
  color: var(--text, #e6eaf5);
  font: inherit;
  cursor: pointer;
}
.bfmap-scenes-folder-toggle:hover { border-color: var(--c-cyan, #65d4f0); }
/* Caret + name on ONE line, count underneath — the toggle is still a column,
   but it is a two-row column (title, meta) matching a scene row's name/meta
   rhythm, not a three-row one that spends a line on the disclosure arrow. */
.bfmap-scenes-folder-line {
  display: flex;
  align-items: baseline;
  gap: 2px;
  min-width: 0;
}
.bfmap-scenes-folder-name {
  font-size: 12px;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bfmap-scenes-folder-name::before { content: "\1F4C1  "; }  /* 📁, then two spaces */
.bfmap-scenes-folder-caret {
  flex: 0 0 auto;
  width: 1em;
  transition: transform 120ms;
}
/* "New folder in here". Sits beside the delete ✕ and is deliberately the
   same disc, so the head row keeps its two-control rhythm. Left in the flow
   rather than tucked into the corner like a scene row's ✕: there are two
   buttons here, and stacking both in one corner puts a destructive control
   under the cursor that was aiming for the other. */
.bfmap-scenes-folder-add {
  flex: 0 0 auto;
  align-self: center;
  width: 17px;
  height: 17px;
  padding: 0;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--edge, #1c243d);
  background: none;
  color: var(--muted, #9eb0d2);
  font: inherit;
  font-size: 11px;
  line-height: 1;
  cursor: pointer;
}
.bfmap-scenes-folder-add:hover { border-color: var(--c-cyan, #65d4f0); color: var(--c-cyan, #65d4f0); }
/* Same auto-fill grid as the top-level list, so a folder's contents break
   into columns on exactly the same rule its siblings do — a wide panel that
   columnised everything EXCEPT the open folder would look like the folder
   had failed to load.

   AND AT THE SAME WIDTH. This used to be indented by a whole thumbnail
   column (64px) to line its rows up under the head's name. That reads fine
   in one column and badly in several: the folder's grid was 64px narrower
   than the list's, so it fitted one fewer column, and a panel wide enough
   for three abruptly showed two inside every open folder. The rows are
   full-bleed now and the folder's own border does the indenting — the 1px
   inset is the only difference between an item in a folder and one outside,
   which keeps the column count identical at every width. */
.bfmap-scenes-folder-body {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  align-content: start;
  gap: 6px;
  padding: 0 2px 2px;
}
/* No body, no padding. An empty (or collapsed-to-nothing) folder was
   reserving a strip of blank space under its head row. */
.bfmap-scenes-folder-body:empty { display: none; }
/* A subfolder is a block that holds rows, so it spans the full width of its
   parent's grid exactly as a top-level folder does. */
.bfmap-scenes-folder-body > .bfmap-scenes-folder { grid-column: 1 / -1; }
/* Nested rows keep their own thumbnail/load/delete triple — this just tightens
   the gap so a folder's contents read as indented, not as a second list. */
.bfmap-scenes-folder-body .bfmap-scenes-row { gap: 6px; }

/* Pinned "recently loaded" rows sit above everything, folders included — a
   thin label marks where the pin ends and the normal (folders-first)
   ordering resumes, so the jump from "what I just used" to "everything
   else" doesn't read as an unexplained gap. */
.bfmap-scenes-recent-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--faint, #6e7ba0);
  padding: 2px 2px 0;
}

/* The "more total picture" hover expansion. A FLOATING card, not the
   thumbnail growing in place — growing in the flow would reflow every row
   below it, so the list would jump under the cursor the instant it landed.
   Positioned in JS from the real rendered rects (see showScenesPreview); the
   only thing CSS owns here is what it looks like once placed.
   `contain`, not `cover`: the small thumbnail already gives the cropped,
   at-a-glance version, so the one thing worth spending the bigger box on is
   showing the WHOLE image uncropped. */
.bfmap-scenes-preview {
  position: absolute;
  z-index: 65;
  border-radius: 10px;
  border: 1px solid var(--edge, #1c243d);
  background-color: var(--panel-deep, #0a0f1c);
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  box-shadow: 0 14px 38px rgba(0, 0, 0, .55);
  pointer-events: none;   /* never eats the hover it exists to answer */
}

/* Suggested cell sizes — the common divisors of the background's dimensions. */
.bfmap-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.bfmap-chip {
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--edge, #1c243d);
  background: var(--panel-deep, #0a0f1c);
  color: var(--text, #e6eaf5);
  font: inherit;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.bfmap-chip:hover { border-color: var(--c-cyan, #65d4f0); }
.bfmap-chip.is-on {
  border-color: var(--c-yellow, #f6de7d);
  color: var(--c-yellow, #f6de7d);
  font-weight: 700;
}

.bfmap-tool-hint {
  margin: 5px 0 0;
  font-size: 11px;
  line-height: 1.45;
  color: var(--faint, #6e7ba0);
}

.bfmap-tools-flash {
  margin: 0 11px 10px;
  padding: 6px 9px;
  border-radius: 6px;
  border: 1px solid var(--c-orange, #f0a45c);
  background: rgba(240, 164, 92, .1);
  color: var(--c-orange, #f0a45c);
  font-size: 11.5px;
  display: none;
}
.bfmap-tools-flash.is-visible { display: block; }

/* ── Grid alignment square ──────────────────────────────────────────────── */
/* Lives inside .bfmap-world, so it is welded to the background through pan and
   zoom. --bfmap-cal-line is 1/zoom, keeping its lines hairline on screen. */

.bfmap-calbox {
  position: absolute;
  z-index: 20;
  cursor: move;
  --bfmap-cal-line: 1px;
  background-image:
    linear-gradient(to right, var(--c-yellow, #f6de7d) var(--bfmap-cal-line), transparent var(--bfmap-cal-line)),
    linear-gradient(to bottom, var(--c-yellow, #f6de7d) var(--bfmap-cal-line), transparent var(--bfmap-cal-line));
  box-shadow: 0 0 0 var(--bfmap-cal-line) var(--c-yellow, #f6de7d) inset;
  outline: var(--bfmap-cal-line) solid var(--c-yellow, #f6de7d);
}

.bfmap-calbox-handle {
  position: absolute;
  right: 0;
  bottom: 0;
  width: calc(var(--bfmap-cal-line) * 14);
  height: calc(var(--bfmap-cal-line) * 14);
  transform: translate(35%, 35%);
  background: var(--c-yellow, #f6de7d);
  border-radius: calc(var(--bfmap-cal-line) * 3);
  cursor: nwse-resize;
}

.bfmap-calbox-readout {
  position: absolute;
  left: 0;
  bottom: 100%;
  --bfmap-cal-scale: 1;
  /* Counter-scale out of world space so the readout stays legible at any zoom
     instead of growing and shrinking with the square. */
  transform: scale(var(--bfmap-cal-scale));
  transform-origin: 0 100%;
  white-space: nowrap;
  font-size: 11px;
  padding: 2px 6px;
  margin-bottom: 2px;
  border-radius: 4px;
  background: var(--c-yellow, #f6de7d);
  color: #0a0f1c;
  font-weight: 700;
  pointer-events: none;
}

/* Quick-snap sizes, on the OPPOSITE side of the same strip the readout
   occupies — both sit immediately above the square, one at each corner,
   rather than one stacked on top of the other. --bfmap-cal-scale is set on
   .bfmap-calbox itself (not just the readout) so this sibling inherits the
   same counter-scale and stays a constant screen size at any zoom too. */
.bfmap-calbox-snaps {
  position: absolute;
  right: 0;
  bottom: 100%;
  transform: scale(var(--bfmap-cal-scale, 1));
  transform-origin: 100% 100%;
  margin-bottom: 2px;
  display: flex;
  gap: 4px;
  /* Unlike the readout, these are live controls — the square's own drag
     handlers are bound on pointerdown too, so each button also stops that
     event from bubbling (see showCalBox) or a click here would double as the
     start of a move-drag. */
  pointer-events: auto;
}

.bfmap-calbox-snap {
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  padding: 3px 7px;
  border-radius: 4px;
  border: 1px solid var(--c-yellow, #f6de7d);
  background: rgba(246, 222, 125, .14);
  color: var(--c-yellow, #f6de7d);
  cursor: pointer;
  white-space: nowrap;
}
.bfmap-calbox-snap:hover { background: rgba(246, 222, 125, .26); }
.bfmap-calbox-snap.is-on {
  background: var(--c-yellow, #f6de7d);
  color: #0a0f1c;
}

/* ── Multiplayer presence ────────────────────────────────────────────────────
   Three surfaces, all fed by map-presence.js: the players panel (who is here),
   remote cursors (where they are pointing), and the borrowed-token treatment
   (what they have picked up). See the header of map-presence.js for the wire.

   Every one of them is ephemeral. Nothing in this section describes state the
   board would still be in after a reload, which is why none of it reuses the
   selection/ghost vocabulary above — a dashed green ghost means "a move is
   about to be committed from THIS browser", and a peer's proposal is not that. */

/* ── The players panel ──
   Bottom-left, stacked directly above the status line rather than replacing
   it: the two answer different questions ("who is at the table" vs "is this
   board trustworthy") and a GM debugging a dead engine needs both at once.

   Follows the same --bfmap-dock-left inset as the status line so a docked
   action panel pushes it aside instead of covering it. */
.bfmap-players {
  position: absolute;
  left: calc(10px + var(--bfmap-dock-left, 0px));
  bottom: 34px;
  z-index: 41;
  width: max-content;
  max-width: min(260px, calc(100% - 24px));
  border-radius: 8px;
  border: 1px solid var(--edge, #1c243d);
  background: rgba(10, 15, 28, .92);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .45);
  color: var(--text, #e6eaf5);
  font-size: 11.5px;
  overflow: hidden;
  transition: left 160ms ease;
}

.bfmap-players-head {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 5px 8px;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.bfmap-players-head:hover { background: rgba(255, 255, 255, .05); }

/* Green while the presence channel is carrying traffic, grey otherwise. The
   panel is the only place that can say so: a page with no Supabase config
   still draws a perfectly good single-player board, and "Players: 1" with no
   indication that the wire is down would be a lie of omission. */
.bfmap-players-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--faint, #6e7ba0);
  flex: 0 0 auto;
}
.bfmap-players.is-live .bfmap-players-dot { background: var(--c-green, #6cd48c); }

.bfmap-players-title { flex: 1 1 auto; }
.bfmap-players-count {
  font-variant-numeric: tabular-nums;
  color: var(--muted, #9eb0d2);
}
.bfmap-players-chev {
  color: var(--faint, #6e7ba0);
  font-size: 9px;
  transition: transform 140ms ease;
}
.bfmap-players.is-collapsed .bfmap-players-chev { transform: rotate(-90deg); }
.bfmap-players.is-collapsed .bfmap-players-body { display: none; }

.bfmap-players-body {
  border-top: 1px solid var(--edge, #1c243d);
  padding: 4px;
  display: flex;
  flex-direction: column;
  gap: 1px;
  max-height: 40vh;
  overflow-y: auto;
}

.bfmap-player {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 4px;
  border-radius: 5px;
  cursor: pointer;
}
.bfmap-player:hover { background: rgba(255, 255, 255, .06); }
.bfmap-player.is-self { background: rgba(255, 255, 255, .04); }

/* The same disc, at the same size, as the one riding this player's cursor —
   that pairing is the whole navigation aid. Somebody's pointer crosses the
   board, you read the number on it, you find the same number in the panel. */
.bfmap-player-id,
.bfmap-cursor-id {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--bfmap-peer, #65d4f0);
  color: #06101c;
  font-size: 9.5px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
/* No creature claimed yet (or a GM, who has the whole board rather than one
   creature). Hollow rather than filled: there is no id to read, and a solid
   disc with a dash in it looks like a number that failed to load. */
.bfmap-player-id.is-empty,
.bfmap-cursor-id.is-empty {
  background: transparent;
  border: 1.5px solid var(--bfmap-peer, #65d4f0);
  color: var(--bfmap-peer, #65d4f0);
  font-weight: 700;
}

.bfmap-player-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bfmap-player-you {
  flex: 0 0 auto;
  color: var(--faint, #6e7ba0);
  font-size: 9.5px;
  letter-spacing: .04em;
  text-transform: uppercase;
}
/* One extra disc per additional creature the player holds. Ids only — the
   names are in the row's tooltip, and a panel that grows a line per creature
   would swamp the point of it on a GM's board. */
.bfmap-player-extra {
  flex: 0 0 auto;
  color: var(--faint, #6e7ba0);
  font-size: 9.5px;
  font-variant-numeric: tabular-nums;
}

.bfmap-players-empty {
  padding: 5px 6px;
  color: var(--faint, #6e7ba0);
}

/* ── Remote cursors ──
   A WORLD-space layer, so pan and zoom carry every pointer for free and two
   players with wildly different views still agree on which tile is being
   pointed at. The cost is that the marker would scale with the map, which is
   wrong — a pointer is chrome, not terrain — so the inner element undoes both
   the zoom and the scene rotation. See --bfmap-inv-zoom, written by applyView.

   Above everything else in the world container (marquee is 60): a pointer
   that can be hidden behind a token is a pointer you have to hunt for. */
.bfmap-cursors {
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
  z-index: 65;
}
.bfmap-cursor {
  position: absolute;
  left: 0;
  top: 0;
  /* Positioned by transform rather than left/top so a stream of ~18 updates a
     second runs on the compositor instead of invalidating layout on the world
     layer — which, at map sizes, means re-rastering the background tiles
     underneath. The transition is what turns those discrete frames back into
     motion; linear because anything eased reads as the pointer overshooting
     and settling, which is not what the other person's hand did. */
  transition: transform 70ms linear;
  will-change: transform;
}
/* A peer who just appeared has no previous position to travel from, so the
   first frame must land rather than sweep in from the world origin. */
.bfmap-cursor.is-new { transition: none; }

.bfmap-cursor-inner {
  position: absolute;
  left: 0;
  top: 0;
  transform: scale(var(--bfmap-inv-zoom, 1)) rotate(calc(-1 * var(--bfmap-scene-rot, 0deg)));
  transform-origin: 0 0;
  display: flex;
  align-items: flex-start;
  gap: 2px;
}
.bfmap-cursor-arrow {
  width: 13px;
  height: 19px;
  flex: 0 0 auto;
  fill: var(--bfmap-peer, #65d4f0);
  stroke: rgba(4, 6, 15, .85);
  stroke-width: 1.4;
  stroke-linejoin: round;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .55));
}
/* Offset down and right of the hotspot so the tag never sits under the arrow
   it belongs to, the way every collaborative editor places a cursor label. */
.bfmap-cursor-tag {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: 11px;
  padding: 2px 5px 2px 2px;
  border-radius: 999px;
  background: rgba(4, 6, 15, .86);
  border: 1px solid var(--bfmap-peer, #65d4f0);
  white-space: nowrap;
  font-size: 10px;
  line-height: 1;
  color: var(--text, #e6eaf5);
  max-width: 150px;
  overflow: hidden;
}
.bfmap-cursor-name {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── A token somebody else is carrying ──
   The token itself moves — that is the feature — so what this adds is the
   attribution: whose hand is on it, in the same colour as their cursor, so a
   board where three things are moving at once is still readable.

   A solid ring rather than the dashed green of a local preview. Dashed-green
   means "this browser is about to commit this"; a peer's drag is somebody
   else's business and cannot be committed from here. */
.bfmap-token.is-remote-moving .bfmap-token-art {
  border-color: var(--bfmap-peer, #65d4f0);
  box-shadow: 0 0 0 calc(var(--bfmap-line) * 2) var(--bfmap-peer, #65d4f0),
              0 4px 14px rgba(0, 0, 0, .5);
}
/* Held rather than dragged — the keyboard route's proposal, which has NOT
   moved the creature and may yet be abandoned. Halfway to transparent so it
   reads as a suggestion; the ghost left behind at the real cell (below) is
   what says where it would be coming from. */
.bfmap-token.is-remote-proposed { opacity: .72; }

/* Where the creature actually still stands while a peer carries its token.
   Without this the board silently lies during a drag: the token is over
   there, its row says here, and nothing on screen shows the difference — so a
   player deciding whether they are in reach measures from the wrong tile. */
.bfmap-remote-ghosts {
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
  z-index: 29;                      /* under a dragged token (30), over the rest */
}
.bfmap-remote-ghost {
  position: absolute;
  box-sizing: border-box;
  border-radius: 50%;
  border: calc(var(--bfmap-line) * 2) dotted var(--bfmap-peer, #65d4f0);
  background: rgba(255, 255, 255, .05);
  opacity: .8;
}

/* ── Phone ──────────────────────────────────────────────────────────────── */

@media (max-width: 720px) {
  .bfmap-hud { top: 8px; right: 8px; }
  .bfmap-hud-btn { width: 36px; height: 36px; }   /* thumb-sized */
  .bfmap-tools { width: calc(100% - 16px); top: 8px; left: 8px; }
  /* Collapsed by default is already the phone case (see readPlayersOpen), so
     this only has to keep the head from spanning the pane when it is open. */
  .bfmap-players { max-width: calc(100% - 20px); }
}
