/* ============================================================================
   spatial-manager.css — FFASpatial handles + spatial-overlay states
   (forging-modal-renaissance S4, Track B2 — lands DARK, consumers arrive S5)

   .is-spatial rides on a modal OVERLAY whose shell FFASpatial has taken over
   (inline fixed-position rect). The overlay itself stays fullscreen so the
   backdrop-click / focus-trap / ModalManager contracts are untouched; it only
   dims lighter so a freeform child feels like a window over the vault, not a
   captive dialog. Backdrop-click still closes (Joe-ratified).
   ============================================================================ */

/* Lighter dim while a shell is freeform. Scoped to the .is-open era so a
   closed overlay never paints. */
.is-spatial.is-open,
.is-spatial.active {
  background: rgba(8, 10, 18, 0.35);
}

/* A flex/grid-centering overlay must not fight the fixed-position shell —
   position:fixed children ignore the box anyway; this quiets align/justify
   side effects (padding-based centering) without touching closed state. */
.is-spatial.is-open {
  padding: 0;
}

/* ---------- the Stripped fill (reforging-biography-folio S6) ----------
   A modal enrolled ONLY to fill the canvas (FFASpatial autoFill) owns its own
   scroll: its panel is now a fixed canvas-sized box, so content that used to
   lengthen the panel inside a scrolling overlay scrolls inside the panel. */
.ffs-fill {
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* ---------- drag ---------- */
.ffs-drag-handle {
  cursor: grab;
}

.ffs-dragging,
.ffs-dragging .ffs-drag-handle {
  cursor: grabbing;
}

body.ffs-no-select,
body.ffs-no-select * {
  user-select: none !important;
  -webkit-user-select: none !important;
}

/* ---------- perimeter drag (Issue 1, directing-desktop-scenes S2) ----------
   The decorative surround (shell minus working panel) MOVES the window. The
   working-panel regions restore their own cursor so content interaction
   reads as content, not chrome. Selector list mirrors CONTENT_SEL in
   spatial-manager.js. */
.ffs-shell-drag {
  cursor: grab;
}

.ffs-shell-drag .fergi-bespoke-body,
.ffs-shell-drag .bio-modal-body,
.ffs-shell-drag .ffs-content,
.ffs-shell-drag [data-ffw-content] {
  cursor: auto;
}

/* ---------- resize band: 8 edge/corner handles (Issue 1) ----------
   FLEET RESIZE-CHROME CONTRACT — LIMB 1 (truing-vessel-chrome S3, Joe-ratified
   2026-07-23; scoped, not weakened, by armed-bracket-perimeter S1):
   every grip lives at POSITIVE insets hugging the shell's own BOUNDING BOX —
   never negative offsets — so a host's overflow:hidden can never clip it.
   Host themes must not re-position the band: content-lift selectors
   (`> :not(.ffv-chassis-layer)`) exclude .ffs-handle. Both invariants are
   machine-guarded in window-manager.smoke.cjs and are UNCHANGED.

   ⚠️ What limb 1 could never promise: that the bbox edge IS the edge the eye
   reads as the window border. On a themed shell it is not — the silhouette is
   drawn inset (and partly outside) that box, so these bands sit in empty space
   away from the ink. LIMB 2 below moves the HIT SURFACE onto the drawn edge;
   these grips remain the fallback for un-themed (genuinely rectangular) shells
   and the carriers of the corner cues. */
.ffs-handle {
  position: absolute;
  z-index: 6;
}

.ffs-handle--n  { top: 0; left: 26px; right: 26px; height: 10px; cursor: ns-resize; }
.ffs-handle--s  { bottom: 0; left: 26px; right: 26px; height: 10px; cursor: ns-resize; }
.ffs-handle--e  { right: 0; top: 26px; bottom: 26px; width: 10px; cursor: ew-resize; }
.ffs-handle--w  { left: 0; top: 26px; bottom: 26px; width: 10px; cursor: ew-resize; }
.ffs-handle--ne { top: 0; right: 0; width: 24px; height: 24px; cursor: nesw-resize; }
.ffs-handle--nw { top: 0; left: 0; width: 24px; height: 24px; cursor: nwse-resize; }
.ffs-handle--sw { bottom: 0; left: 0; width: 24px; height: 24px; cursor: nesw-resize; }
.ffs-handle--se { bottom: 0; right: 0; width: 24px; height: 24px; cursor: nwse-resize; }

/* Always-on corner cues (#4): a subtle L-bracket at each of the FOUR corners,
   aligned to the visible chassis edge, brightening under the pointer and
   mid-resize — discoverability no longer rides a single hover-only chevron. */
.ffs-handle--ne::after,
.ffs-handle--nw::after,
.ffs-handle--sw::after,
.ffs-handle--se::after {
  content: "";
  position: absolute;
  width: 9px;
  height: 9px;
  border: 0 solid rgba(157, 95, 224, 0.75);
  opacity: 0.5;
  /* border-width/colour/filter join opacity so ARMING (D2, below) is a fade
     rather than a jump. Reduced motion drops the whole transition at the foot
     of this file. */
  transition: opacity 0.2s, border-width 0.15s, border-color 0.2s, filter 0.2s;
  filter: drop-shadow(0 0 5px rgba(157, 95, 224, 0.7));
  pointer-events: none;
}
.ffs-handle--ne::after { top: 5px; right: 5px; border-top-width: 2px; border-right-width: 2px; border-top-right-radius: 4px; }
.ffs-handle--nw::after { top: 5px; left: 5px; border-top-width: 2px; border-left-width: 2px; border-top-left-radius: 4px; }
.ffs-handle--sw::after { bottom: 5px; left: 5px; border-bottom-width: 2px; border-left-width: 2px; border-bottom-left-radius: 4px; }
.ffs-handle--se::after { bottom: 5px; right: 5px; border-bottom-width: 2px; border-right-width: 2px; border-bottom-right-radius: 4px; }

.ffs-handle:hover::after,
.ffs-resizing .ffs-handle::after {
  opacity: 1;
}

/* ---------- D2: ARMED BRACKETS (armed-bracket-perimeter S2) ----------------
   The four cues above are the ONLY corner chrome FFASpatial draws, and D2
   promotes them from decoration to control rather than inventing a new mark:
   quiet at rest (the block above, opacity 0.5), thickening and glowing along
   the axis the pointer is approaching on. Nothing new appears on screen — if
   this change added something to look at, it solved the wrong problem.

   ⚠️ `:hover` CANNOT DRIVE THIS ANY MORE. Once the shell-path band is live the
   grips are `pointer-events:none` (they stood down so the affordance would MOVE
   rather than double, limb 2), so the `:hover` rule above is unreachable on a
   themed shell and the cue would sit frozen at rest opacity. FFASpatial stamps
   `.ffs-arm-<axis>` on the SHELL from the same `dirAtPoint` that decides the
   gesture, so the lit bracket and the resize you would get cannot disagree.
   The `:hover` rule is NOT deleted — it still serves un-themed shells, and it
   is the fallback if the arming listener never binds.

   PER AXIS, NOT PER CORNER. Approaching the top edge arms the top limb of BOTH
   top brackets — the eye reads "this whole edge is live". A corner approach
   composes two axis classes, so the ne bracket lights on both limbs and reads
   as fully armed, which is exactly the gesture it will perform. */
.ffs-arm-n .ffs-handle--nw::after,
.ffs-arm-n .ffs-handle--ne::after { border-top-width: 3px; }
.ffs-arm-s .ffs-handle--sw::after,
.ffs-arm-s .ffs-handle--se::after { border-bottom-width: 3px; }
.ffs-arm-w .ffs-handle--nw::after,
.ffs-arm-w .ffs-handle--sw::after { border-left-width: 3px; }
.ffs-arm-e .ffs-handle--ne::after,
.ffs-arm-e .ffs-handle--se::after { border-right-width: 3px; }

/* The armed pair also brightens and grows its halo. Listed per axis-and-corner
   so an armed bracket is lit only on the axes actually being solicited. */
.ffs-arm-n .ffs-handle--nw::after, .ffs-arm-n .ffs-handle--ne::after,
.ffs-arm-s .ffs-handle--sw::after, .ffs-arm-s .ffs-handle--se::after,
.ffs-arm-w .ffs-handle--nw::after, .ffs-arm-w .ffs-handle--sw::after,
.ffs-arm-e .ffs-handle--ne::after, .ffs-arm-e .ffs-handle--se::after {
  opacity: 1;
  border-color: rgba(186, 140, 240, 0.95);
  filter: drop-shadow(0 0 8px rgba(157, 95, 224, 0.9));
}

/* ⛔ D5, THE BELT-AND-BRACES HALF. A maximised room refuses the resize gesture
   (`displayMaxed` in spatial-manager.js), so a bracket there promises something
   the window will not do. `.ffa-display-max` and `.ffw-maximized` both suppress
   the grips outright in ffv-modal-theme.css — the two maximised states are ONE
   rule and must be edited together. This restates it against the CUES for the
   case where a host has FFASpatial but not the theme sheet, and it is the
   visual twin of the `.ffs-perimeter-hit` stand-down further down this file. */
.ffa-display-max .ffs-handle::after,
.ffw-maximized .ffs-handle::after {
  display: none;
}

/* ---------- LIMB 2: the shell-path perimeter (armed-bracket-perimeter S1) ----
   The hit region derived from the SAME geometry that draws the frame.
   spatial-manager.js clones each painted `path.ffvc-body` into a sibling
   `path.ffs-perimeter-hit` inside the same `.ffvc-frame` SVG, so it inherits
   the viewBox, the preserveAspectRatio="none" stretch and any transform by
   construction — no coordinate maths, no sampling, nothing inferred from
   control points or a bounding box.

   ⚠️ NOT A WIDER BAND — A BAND IN THE RIGHT FRAME. `.ffvc-body` paints at
   stroke-width 7 viewBox units (≈6 device px at fleet sizes); a 14px band
   centred on it reaches ~4px past the ink either side. It replaces the bbox
   bands' hit role rather than adding to it (see .ffs-has-perimeter below), so
   the total grabbable area does not grow — it MOVES to where the eye aims.
   `--ffs-perimeter-band` is the one dial; S5 tunes it against live Chrome. */
.ffs-perimeter-hit {
  fill: none;
  stroke: transparent;
  stroke-width: var(--ffs-perimeter-band, 14px);
  stroke-linejoin: round;
  stroke-linecap: round;
  /* ⛔ INERT UNTIL VOUCHED FOR. The band is drawn from whatever ink the theme
     painted, and FFASpatial does not trust that ink until `perimeterSane` has
     confirmed it really spans this window. Base state is therefore NO hit
     surface at all — a rejected, degenerate or stripped silhouette leaves the
     band as pure decoration and the legacy grips carrying the whole job, which
     is exactly today's behaviour.
     ⚠️ Caught by mutation: with `pointer-events:stroke` in the BASE rule, an
     absurd silhouette (guard forced to reject) still swallowed every pointer
     on its stroke WHILE the grips came back — today's behaviour PLUS a rogue
     band, which is not what "fails open" means. */
  pointer-events: none;
}

/* Stamped by FFASpatial only after the band passed its sanity guard. This one
   class is the whole switch: the band takes the hit surface and the bbox grips
   stand down from it together, so the affordance MOVES rather than doubling. */
.ffs-has-perimeter .ffs-perimeter-hit {
  /* The chassis layer is pointer-events:none; the band re-enables ONLY its own
     stroke, so the silhouette's interior never intercepts content. */
  pointer-events: stroke;
}
/* ⛔ THE FOUR EDGE GRIPS STAND DOWN — THE FOUR CORNER GRIPS DO NOT (amended by
   armed-bracket-perimeter S2; S1 stood down all eight, and that was too far).
   The edges' hit job genuinely MOVED onto the ink, so leaving those bands live
   would double the affordance. The corners are the opposite case, and S2
   MEASURED it: on a bevelled room the silhouette carries NO INK AT ALL inside
   the 26px corner zone — nearest ink is 48px away on librarians-desk, 86px on
   wardrobe-console — so with the corner grips also stood down, the diagonal
   resize had no surface anywhere and simply stopped being available. Standing
   them back up is not a widening: nothing grows, an affordance that had no
   replacement is restored to exactly the 24px square it always occupied.

   It is also what D2 MEANS. The armed brackets are drawn by these very
   elements (`.ffs-handle--<c>::after`) — they are the corner affordance the
   eye is being invited to aim at, and an invitation you cannot accept is worse
   than no invitation. The bracket you see IS the grip you press. */
.ffs-has-perimeter > .ffs-handle--n,
.ffs-has-perimeter > .ffs-handle--s,
.ffs-has-perimeter > .ffs-handle--e,
.ffs-has-perimeter > .ffs-handle--w {
  pointer-events: none;
}

/* Maximised rooms already hide the grips (ffv-modal-theme.css) and FFASpatial
   refuses the gesture; the band stands down with them so a maximised frame
   never shows a resize cursor it will not honour. Source order matters — this
   ties on specificity with the .ffs-has-perimeter rule above and must win.
   (D5 proper is S2's; this is the half that would otherwise ship a live lie.) */
.ffa-display-max .ffs-perimeter-hit,
.ffw-maximized .ffs-perimeter-hit {
  pointer-events: none;
}

/* ---------- tri-state Maximized (ruling R5) ----------
   FFVWindowManager owns the geometry while maximized; kill any shell
   transition so canvas tracking (CP collapse, resize) lands instantly. */
.ffw-maximized {
  transition: none !important;
}

/* ---------- D6: ARMING IS FOR A POINTER THAT CAN HOVER (S3) ----------------
   The behavioural half lives in `armDirAt` (spatial-manager.js), which refuses
   to stamp an arm for a pointer that cannot hover. This is its twin, for the
   case where that listener never binds — and it asks the question about the
   DEVICE'S PRIMARY POINTER, which is the only thing CSS can ask.

   ⚠️ `(hover: none)` is NOT "has a touchscreen". A touchscreen laptop's primary
   pointer is its mouse, so it reports `hover: hover` and is untouched here —
   which is correct, and is why the JS gate (which tests the actual pointer of
   the actual event) is the primary and this is the backstop. On a touch-ONLY
   device the arm styling can never mean anything, so it stands down and the
   brackets rest quiet: still drawn, still the thing you press, never lit by a
   promise the input cannot make. Measured: a finger armed the bracket in 2 of
   4 touch samples before the gate went in.

   ⛔ IT MIRRORS THE ARMED SELECTORS ONE FOR ONE — it does not blanket
   `border-width: 0`. A blanket reset would erase the RESTING L as well (the
   armed selectors out-specify any plain `.ffs-handle--ne::after` restating it,
   whatever the source order), leaving a touch device with no corner mark at
   all — removing the affordance instead of removing the promise. Each pair
   below therefore returns exactly the limb its armed twin thickened, to exactly
   the resting 2px. */
@media (hover: none) {
  .ffs-arm-n .ffs-handle--nw::after,
  .ffs-arm-n .ffs-handle--ne::after { border-top-width: 2px; }
  .ffs-arm-s .ffs-handle--sw::after,
  .ffs-arm-s .ffs-handle--se::after { border-bottom-width: 2px; }
  .ffs-arm-w .ffs-handle--nw::after,
  .ffs-arm-w .ffs-handle--sw::after { border-left-width: 2px; }
  .ffs-arm-e .ffs-handle--ne::after,
  .ffs-arm-e .ffs-handle--se::after { border-right-width: 2px; }

  .ffs-arm-n .ffs-handle--nw::after, .ffs-arm-n .ffs-handle--ne::after,
  .ffs-arm-s .ffs-handle--sw::after, .ffs-arm-s .ffs-handle--se::after,
  .ffs-arm-w .ffs-handle--nw::after, .ffs-arm-w .ffs-handle--sw::after,
  .ffs-arm-e .ffs-handle--ne::after, .ffs-arm-e .ffs-handle--se::after {
    opacity: 0.5;
    border-color: rgba(157, 95, 224, 0.75);
    filter: drop-shadow(0 0 5px rgba(157, 95, 224, 0.7));
  }
}

@media (prefers-reduced-motion: reduce) {
  .ffs-handle::after {
    transition: none;
  }
}

/* Mobile is inert for the whole spatial system */
@media (max-width: 768px) {
  .ffs-handle {
    display: none;
  }
  /* The chassis is desktop-only so the band should never exist here; belt and
     braces, because `display:none` on a sibling is not a contract for an SVG
     child of another subtree. */
  .ffs-perimeter-hit {
    pointer-events: none;
  }
}
