/* ==========================================================================
   network-motion.css — the constellation, alive
   ==========================================================================
   Additive. Loaded after network-graph.css, paired with network-motion.js, and
   it touches nothing the graph owns except by override. Remove the two tags and
   the section is the static graph again, byte for byte.

   Four ideas, in the order a reader meets them:

   1. Ignition. The picture assembles itself when it scrolls into view — hubs
      first, then the edges drawing outward, then the services and the agencies
      they reach. A network that draws itself explains its own hierarchy in a
      second and a half; the same picture arriving whole explains nothing.
   2. Life. Nothing in here is at rest. The graph runs as a live spring system
      whose equilibrium is the layout as drawn — edges are springs, nodes are
      pulled home, and a small wander keeps it from ever settling. A force
      layout that has stopped moving reads as a diagram; one still breathing
      reads as a system.
   3. Traffic. Light travels the edges — outward from whatever you point at,
      and idly, one edge at a time, when you point at nothing. The lines stop
      being where a relationship is drawn and start being where it moves.
   4. Touch. Nodes lean toward the cursor and can be picked up and dragged, with
      their neighbours pulled after them and the whole cluster falling back into
      place on release. The graph moves like something joined together.

   All four are off under prefers-reduced-motion: see the last block.
   ========================================================================== */


/* --------------------------------------------------------------------------
   01. Motion, and the transform contract
   --------------------------------------------------------------------------
   network-graph.css centres each node with translate(-50%,-50%). Everything the
   simulation does is one more px offset folded into that same translate, so the
   layout stays percentage-based and untouched and the physics only ever
   describes a departure from it. --dx/--dy are written every frame by JS;
   nothing else writes this property. */
.net-stage .net-node{
  transform:translate(calc(-50% + var(--dx,0px)), calc(-50% + var(--dy,0px)));
  will-change:transform;
}

/* Two staggers, one property. --hd is a trace's hop delay: the source lights
   first, its neighbours a beat later, their neighbours a beat after that, so a
   trace propagates rather than snapping on. --ed is the entrance delay. Only
   one is ever non-zero at a time. */
.net-stage.mo-live .net-node{transition:opacity .38s ease var(--hd,0ms)}
.net-stage.mo-live .net-links line{
  transition:stroke .34s ease, stroke-opacity .34s ease var(--hd,0ms), stroke-width .34s ease;
}


/* --------------------------------------------------------------------------
   02. Ignition
   --------------------------------------------------------------------------
   Armed the moment the script runs, so the graph is never seen un-ignited;
   released by an IntersectionObserver when the stage is actually on screen.

   Deliberately opacity-only while armed. network-graph.js places the service
   labels from measured boxes, and it re-measures when the web font lands — a
   scale() on an armed node would hand that solver a half-size mark and a
   half-size label to route around, and the wrong answer would outlive the
   animation. The pop lives on .net-mark instead, and only during the run. */
.net-stage.mo-arm .net-node{opacity:0}
.net-stage.mo-arm .net-links line{stroke-opacity:0}

.net-stage.mo-run .net-node{
  opacity:1;
  transition:opacity .75s ease var(--ed,0ms);
}
.net-stage.mo-run .net-mark{
  position:relative;
  animation:mo-pop .95s cubic-bezier(.22,.86,.3,1.16) var(--ed,0ms) both;
}
@keyframes mo-pop{
  from{transform:scale(.34)}
  62% {transform:scale(1.05)}
  to  {transform:scale(1)}
}
/* The hubs get one extra beat the others do not: a halo that expands out of the
   mark and dies, which is what makes the six of them read as the origins of the
   thing rather than as the six largest dots in it. */
.net-stage.mo-run .n-discipline .net-mark::after{
  content:"";position:absolute;inset:-4px;border-radius:50%;
  border:2px solid var(--ac);
  animation:mo-halo 1.7s ease-out var(--ed,0ms) both;
}
@keyframes mo-halo{
  from{transform:scale(.5);opacity:0}
  25% {opacity:.75}
  to  {transform:scale(2.1);opacity:0}
}

/* Edges draw from the hub end outward. The dash is set in JS from the edge's
   length in device pixels — see the note there — and cleared the moment the run
   ends, because the simulation moves the endpoints from then on and a stale
   dasharray would crawl. */
.net-stage.mo-run .net-links line{
  stroke-opacity:var(--so,.9);
  transition:stroke-dashoffset 1.25s cubic-bezier(.32,.66,.28,1) var(--ed,0ms),
             stroke-opacity .8s ease var(--ed,0ms);
}


/* --------------------------------------------------------------------------
   03. Effects layer
   --------------------------------------------------------------------------
   HTML, not SVG, and that is not arbitrary. The link SVG is a 0..100 box with
   preserveAspectRatio="none" — a circle drawn in it comes out an ellipse on
   every stage that is not square. These sit in their own layer in the stage's
   own aspect, share the nodes' percentage coordinates, and stay round.

   Between the links and the nodes in source order, so traffic runs over the
   wires and under the logos, which is the only stacking that reads correctly. */
.net-fx{position:absolute;inset:0;pointer-events:none;overflow:hidden;border-radius:inherit}

.mo-pulse{
  position:absolute;
  width:var(--ps,7px);height:var(--ps,7px);
  margin:calc(var(--ps,7px) / -2) 0 0 calc(var(--ps,7px) / -2);
  border-radius:50%;
  background:var(--pc,#7a7f8c);
  box-shadow:0 0 0 2px rgb(255 255 255 / .55), 0 0 12px var(--pc,#7a7f8c);
  opacity:var(--po,1);
}
/* The wake behind a pulse. Same colour, no shadow, scaled down and fading. */
.mo-trail{
  position:absolute;width:5px;height:5px;margin:-2.5px 0 0 -2.5px;
  border-radius:50%;background:var(--pc,#7a7f8c);
  animation:mo-trail .62s linear forwards;
}
@keyframes mo-trail{
  from{opacity:.45;transform:scale(1)}
  to  {opacity:0;transform:scale(.3)}
}

/* A ring is emitted twice: when a pulse lands on a node, small; and when a
   discipline is chosen, large, out of the hub that was chosen. The second is
   the only feedback in the section that says the click reached the map and the
   directory too, both of which are off screen at that moment. */
.mo-ring{
  position:absolute;border-radius:50%;
  border:1.5px solid var(--pc,#7a7f8c);
  animation:mo-ring var(--rd,.9s) cubic-bezier(.16,.72,.28,1) forwards;
}
@keyframes mo-ring{
  from{transform:translate(-50%,-50%) scale(.18);opacity:.75}
  to  {transform:translate(-50%,-50%) scale(1);opacity:0}
}

/* The pointer's warm patch. Behind the links — it lifts the ground the graph is
   drawn on, it does not wash over the graph. */
.net-spot{
  position:absolute;inset:0;pointer-events:none;
  opacity:0;transition:opacity .55s ease;
  background:radial-gradient(circle 210px at var(--mx,50%) var(--my,50%),
             color-mix(in srgb, var(--ac,#8a8fa0) 13%, transparent),
             transparent 70%);
}
.net-stage.mo-hover .net-spot{opacity:1}


/* --------------------------------------------------------------------------
   04. Pointing at something
   --------------------------------------------------------------------------
   The graph already dims the rest of the picture on a trace. What it did not do
   was mark the thing you are actually on, past a 12% scale it shares with plain
   hover. A ring that sits outside the mark and holds while the trace holds. */
.net-stage.is-tracing .net-node.is-source .net-mark{
  box-shadow:0 0 0 3px var(--white), 0 0 0 5px var(--ac), 0 6px 22px rgb(20 22 30 / .18);
}
/* Lit neighbours get the same idea an order quieter, so the two hops read as
   two hops and not as one undifferentiated glow. */
.net-stage.is-tracing .net-node.is-lit:not(.is-source) .net-mark{
  box-shadow:0 0 0 2px color-mix(in srgb, var(--ac,#8a8fa0) 45%, transparent);
}

/* -- picking one up --------------------------------------------------------
   Two states, and the grab cursor has to be on the node rather than the stage
   or it reads as "the canvas pans", which it does not. */
.net-stage.mo-live .net-node{cursor:grab}
.net-stage.mo-drag,
.net-stage.mo-drag .net-node{cursor:grabbing}
/* The held node lifts off the board: it is the one thing on the stage the
   layout is not currently deciding the position of. */
.net-node.is-held{z-index:9}
.net-node.is-held .net-mark{
  transform:scale(1.16);
  box-shadow:0 0 0 3px var(--white), 0 0 0 5px var(--ac), 0 10px 26px rgb(20 22 30 / .28);
}
/* Nothing on the stage should be selectable while a node is being dragged
   across labels and logos. */
.net-stage.mo-drag{user-select:none;-webkit-user-select:none}

/* A traced edge stops being a drawn line and becomes a lit one. */
.net-stage.mo-live.is-tracing .net-links line.is-lit{
  filter:drop-shadow(0 0 3px var(--ac));
}


/* --------------------------------------------------------------------------
   05. Devices that cannot hover
   --------------------------------------------------------------------------
   Keyed on the input, not on the width. Most of what this file adds is answers
   to a pointer — the trace, the warm patch, the lean, the drag — and none of it
   exists on a touchscreen, which can be any size. The graph's own taps still
   filter and open profiles; what goes is the copy promising things a finger
   cannot ask for, and the grab cursor promising a drag the script declines to
   start. What stays is the entrance and the simulation, which are the two parts
   that never needed a pointer.

   The pair of hint spans lives in the page's markup; this decides which one is
   the live sentence. Both are in the DOM either way, so the choice follows a
   device that changes its mind — a tablet with a keyboard attached — with no
   script involved. */
.net-hint-coarse{display:none}
@media (hover:none),(pointer:coarse){
  .net-hint-fine{display:none}
  .net-hint-coarse{display:inline}
  .net-hint-drag{display:none}
  .net-stage.mo-live .net-node{cursor:pointer}
}


/* --------------------------------------------------------------------------
   06. Reduced motion
   --------------------------------------------------------------------------
   Not a softening — an off switch. JS checks the same query and never starts
   the loop, never emits a pulse and never arms the entrance; this is the half
   that guarantees nothing is left mid-animation if the preference changes. */
@media (prefers-reduced-motion:reduce){
  .net-stage .net-node{transform:translate(-50%,-50%);will-change:auto}
  .net-stage.mo-arm .net-node{opacity:1}
  .net-stage.mo-arm .net-links line{stroke-opacity:revert-layer}
  .net-stage.mo-run .net-mark,
  .net-stage.mo-run .n-discipline .net-mark::after{animation:none}
  .net-stage.mo-live .net-node,
  .net-stage.mo-live .net-links line{transition-delay:0ms}
  /* Dragging is off with the loop, so the grab cursor would be a promise the
     section does not keep. */
  .net-stage.mo-live .net-node{cursor:pointer}
  .mo-pulse,.mo-trail,.mo-ring,.net-spot{display:none}
}
