/* global React */
// ====================================================================
// EXPEDITION — NODE-GRAPH MOCKS (preview only · not wired to prototype)
// Three layout directions for the expedition map, replacing the single
// linear lane with a branchy, Slay-the-Spire-style directed graph.
//   A · Layered Channels — horizontal columns, the canonical STS DAG
//   B · Ascent Ladder     — vertical climb up through the strata
//   C · Aether Web        — organic constellation, the branchiest
// Fog-of-war: nodes start UNSCANNED (no type/color); type is revealed by
// proximity or scrying. The run fans out to THREE destinations, each a
// distinct payoff and reachable by multiple paths.
// ====================================================================

// ---- Node taxonomy -------------------------------------------------
const NT = {
  launch:     { glyph: '◉', label: 'Launch',     color: 'var(--ink)' },
  forage:     { glyph: '⌘', label: 'Forage',     color: 'var(--el-earth)' },
  cache:      { glyph: '¤', label: 'Coin Cache', color: 'var(--gold)' },
  echo:       { glyph: '◎', label: 'Echo Scry',  color: 'var(--el-air)' },
  hazard:     { glyph: '⚔', label: 'Hazard',     color: 'var(--crit)' },
  lifepod:    { glyph: '✚', label: 'Lifepod',    color: 'var(--gold)' },
  crossroads: { glyph: '⟡', label: 'Crossroads', color: 'var(--stamp-amber)' },
  unknown:    { glyph: '?', label: 'Unscanned',  color: 'var(--ink-faded)' },
  uncharted:  { glyph: '◌', label: 'Uncharted',  color: 'var(--ink-faded)' },
  gate:       { glyph: '☰', label: 'Gate',    color: 'var(--el-air)' },
  rescue:     { glyph: '✚', label: 'Rescue',  color: 'var(--gold)' },
  event:      { glyph: '✦', label: 'Event',   color: 'var(--stamp-amber)' },
  // destinations (always known — they are the goals you route toward)
  geode:      { glyph: '◆', label: 'Singing Geode',    color: 'var(--gold)',     tag: 'RICH HAUL', dest: true },
  wreck:      { glyph: '◈', label: 'Drift-Wreck Field', color: 'var(--el-air)',   tag: 'SALVAGE',   dest: true },
  lee:        { glyph: '◇', label: 'The Lee',           color: 'var(--el-earth)', tag: 'SAFE · +HULL', dest: true },
};

// ---- Edge geometry -------------------------------------------------
function edgePath(a, b, orient) {
  if (orient === 'h') {
    const mx = (a.x + b.x) / 2;
    return `M ${a.x} ${a.y} C ${mx} ${a.y} ${mx} ${b.y} ${b.x} ${b.y}`;
  }
  if (orient === 'v') {
    const my = (a.y + b.y) / 2;
    return `M ${a.x} ${a.y} C ${a.x} ${my} ${b.x} ${my} ${b.x} ${b.y}`;
  }
  const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2;
  return `M ${a.x} ${a.y} Q ${mx} ${my} ${b.x} ${b.y}`;
}

// Derive per-node + per-edge display state from resolved set, vessel
// position, the revealed set and the destination set.
function deriveStates(graph) {
  const resolved = graph.resolved || {};
  const vessel = graph.vessel;
  const reachable = new Set(graph.edges.filter(e => e[0] === vessel).map(e => e[1]));
  const revealed = new Set(graph.revealed || []);
  const dests = new Set(graph.nodes.filter(n => NT[n.type] && NT[n.type].dest).map(n => n.id));
  const nodeState = (id) => {
    if (resolved[id]) return resolved[id];           // 'success' | 'fail'
    if (id === vessel) return 'active';
    if (reachable.has(id)) return 'reachable';
    if (dests.has(id)) return 'dest';
    if (revealed.has(id)) return 'revealed';
    return 'fog';
  };
  const edgeState = (e) => {
    const [from, to] = e;
    if (resolved[from] && (resolved[to] || to === vessel)) return 'traveled';
    if (from === vessel) return 'open';
    return 'dim';
  };
  return { nodeState, edgeState, reachable, dests };
}

// ---- Atoms ---------------------------------------------------------
function Vessel({ size = 46 }) {
  return (
    <svg viewBox="0 0 60 50" style={{ width: size, height: size * 0.83, overflow: 'visible' }}>
      <ellipse cx="30" cy="18" rx="26" ry="11" fill="var(--paper-light)" stroke="var(--ink)" strokeWidth="1.4" />
      <circle cx="30" cy="18" r="4" stroke="var(--el-air)" strokeWidth="1" fill="none" />
      <path d="M14 30 L46 30 L42 38 L18 38 Z" fill="var(--paper-light)" stroke="var(--ink)" strokeWidth="1.4" />
      <circle cx="22" cy="34" r="1.5" fill="var(--ink)" />
      <circle cx="30" cy="34" r="1.5" fill="var(--ink)" />
      <circle cx="38" cy="34" r="1.5" fill="var(--ink)" />
      <line x1="14" y1="30" x2="20" y2="22" stroke="var(--ink)" strokeWidth="0.7" />
      <line x1="46" y1="30" x2="40" y2="22" stroke="var(--ink)" strokeWidth="0.7" />
    </svg>
  );
}

// --- node sub-atoms (radial gauge) ----------------------------------
// An element "rune" seated on the stock ring — the element(s) a node's check
// reads, learned by visiting. A dashed `?` before you've surveyed it.
function RingRune({ src, unknown, muted }) {
  return (
    <span style={{
      width: 15, height: 15, borderRadius: '50%',
      border: `1px ${unknown ? 'dashed' : 'solid'} var(--ink-faded)`,
      background: 'var(--paper-light)', boxShadow: '0 1px 2px rgba(20,16,10,0.2)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      opacity: muted ? 0.5 : 1, flexShrink: 0,
    }}>
      {unknown
        ? <span className="mono" style={{ fontSize: 9, lineHeight: 1, color: 'var(--ink-faded)' }}>?</span>
        : <img src={src} alt="" style={{ width: 10, height: 10, objectFit: 'contain', display: 'block' }} />}
    </span>
  );
}

// A single graph node — RADIAL GAUGE. The marker carries a stock ring (arc =
// how much draw is left), element runes seat on the ring, and the "draw" status
// reads at a glance: a live perch keeps a coloured arc, a worked-out one drops to
// a faint dotted ring + a scored disc, a beaten hazard reads ✓ passage.
// `state`: success|fail|active|reachable|revealed|dest|fog
function GraphNode({ node, state, labelSide = 'below', onClick, hideShip, holdWarn, intel }) {
  const meta = NT[node.type];
  const isDest = !!meta.dest;
  const isFog = state === 'fog';
  const activeHere = state === 'active';
  // survey "draw" status — is there still a reason to come here?
  //   'live'    — unworked stores / unspent offering / a hazard to clear
  //   'spent'   — worked-out perch, no draw left → scored out, the eye skips it
  //   'cleared' — beaten hazard: open road, no loot → quiet, passable
  const draw = intel ? intel.draw : null;
  const spent = draw === 'spent' && !activeHere;
  const clearedPass = draw === 'cleared' && !activeHere;
  // per-ToD magnitude (Z0 bake): node.magK scales the marker — a node can swell
  // at the hour its content wakes. Absent → 1 (T2 zones + mocks unchanged).
  const magK = node.magK || 1;
  const d = Math.round((isFog ? 44 : (isDest ? 60 : 48)) * magK);

  let border = meta.color, fg = meta.color, bg = 'var(--paper)', dash = false, ring = null;
  let glyph = meta.glyph, name = meta.label;

  if (state === 'success') { border = 'var(--ok)'; fg = 'var(--ok)'; bg = 'rgba(77,110,63,0.16)'; }
  else if (state === 'fail') { border = 'var(--crit)'; fg = 'var(--crit)'; bg = 'rgba(161,43,34,0.12)'; }
  else if (state === 'active') { border = 'var(--stamp-amber)'; fg = meta.color; ring = 'amber'; }
  else if (state === 'reachable') { border = 'var(--stamp-amber)'; fg = meta.color; ring = 'ready'; }
  else if (state === 'revealed') { border = meta.color; fg = meta.color; bg = 'rgba(244,236,214,0.7)'; }
  else if (state === 'dest') { border = meta.color; fg = meta.color; bg = `color-mix(in srgb, ${meta.color} 14%, var(--paper))`; }
  else if (isFog) { dash = true; border = 'var(--ink-faded)'; fg = 'var(--ink-faded)'; bg = 'rgba(244,236,214,0.35)'; glyph = '?'; name = null; }

  // STALE / SPENT — overrides the (mis-reading) green "success" so a worked-out
  // perch looks inert: desaturated ink shell + a scored crosshatch over the disc.
  if (spent && !isFog) { border = 'var(--ink-faded)'; fg = 'var(--ink-faded)'; bg = 'rgba(26,46,74,0.05)'; ring = ring === 'ready' ? null : ring; }
  // CLEARED PASSAGE — beaten hazard, open road. Quiet, not scored.
  else if (clearedPass && !isFog) { border = 'var(--ink-faded)'; fg = 'var(--sepia)'; bg = 'rgba(244,236,214,0.55)'; }

  // "have to hold it": moving here would burn a slot through the sleep window
  // (+sleep-skip fatigue). Quiet the eager OPEN cue into a held, dashed marker.
  const hold = holdWarn && state === 'reachable';
  if (hold) { dash = true; border = 'var(--ink-faded)'; ring = 'hold'; }

  const showDC = Boolean(node.dc) && (state === 'reachable' || state === 'active');

  const labelPos = labelSide === 'right'
    ? { left: 'calc(100% + 14px)', top: '50%', transform: 'translateY(-50%)', textAlign: 'left' }
    : labelSide === 'left'
    ? { right: 'calc(100% + 14px)', top: '50%', transform: 'translateY(-50%)', textAlign: 'right' }
    : labelSide === 'above'
    ? { bottom: 'calc(100% + 12px)', left: '50%', transform: 'translateX(-50%)', textAlign: 'center' }
    : { top: 'calc(100% + 12px)', left: '50%', transform: 'translateX(-50%)', textAlign: 'center' };

  // ---- the radial STOCK gauge ----------------------------------------
  // Ring fraction + colour come from intel; a faint dotted ring stands in when
  // there's nothing to draw (worked / cleared / empty). Hidden on fog + the
  // active node (you're already there — the console carries full detail).
  const RING_PAD = 7;
  const showRing = !!intel && !activeHere && !isFog;
  const stock = intel ? Math.max(0, Math.min(1, intel.stock)) : 0;
  const ringColor = intel ? intel.ringColor : 'var(--ink-faded)';
  const ringDotted = spent || clearedPass || stock <= 0;
  const svgSize = d + RING_PAD * 2;
  const R = d / 2 + 3;
  const CIRC = 2 * Math.PI * R;

  // runes seat on the ring, on the side OPPOSITE the label so they never collide
  const showRunes = !!intel && !activeHere && !isFog && ((intel.keyIcons && intel.keyIcons.length) || intel.keyUnknown);
  const runePos = labelSide === 'above'
    ? { bottom: -(RING_PAD + 9), left: '50%', transform: 'translateX(-50%)' }
    : { top: -(RING_PAD + 9), left: '50%', transform: 'translateX(-50%)' };

  // a short status word under the name — only when the ring can't say it itself
  // (fresh/low forage rely on the ring; the OFFER chip covers worked+offer). DEST
  // nodes already carry their own tag (RICH HAUL / SALVAGE / …), so no word there.
  const sw = (!intel || isDest) ? null
    : draw === 'cleared' ? { t: '✓ passage', c: 'var(--sepia)' }
    : intel.role === 'danger' ? { t: '⚔ hazard', c: 'var(--crit)' }
    : intel.role === 'recruit' ? { t: 'soul adrift', c: 'var(--el-air)' }
    : spent ? { t: 'worked', c: 'var(--ink-faded)' }
    : null;
  // a dest's ring takes the marker's own colour so it reads as one emblem (a gold
  // geode shouldn't wear a blue recruit ring); other nodes keep the draw colour.
  const effRingColor = (isDest && intel && intel.role === 'recruit') ? meta.color : ringColor;

  return (
    <div style={{
      position: 'absolute', left: node.x, top: node.y,
      transform: 'translate(-50%, -50%)', zIndex: state === 'active' || state === 'reachable' ? 6 : (isDest ? 5 : 4),
      cursor: onClick ? 'pointer' : 'default', opacity: hold ? 0.72 : (spent ? 0.9 : 1),
    }} onClick={onClick}>
      {state === 'active' && !hideShip && (
        <div style={{ position: 'absolute', left: '50%', bottom: 'calc(100% + 4px)', transform: 'translateX(-50%)' }}>
          <Vessel size={44} />
        </div>
      )}
      {ring === 'amber' && (
        <div style={{ position: 'absolute', inset: -7, border: '1.5px solid var(--stamp-amber)', borderRadius: '50%', opacity: 0.7 }} />
      )}

      {/* STOCK RING — arc = draw left; faint dotted when there's nothing to pull */}
      {showRing && (
        <svg width={svgSize} height={svgSize} style={{ position: 'absolute', left: -RING_PAD, top: -RING_PAD, pointerEvents: 'none' }}>
          <circle cx={svgSize / 2} cy={svgSize / 2} r={R} fill="none" stroke="var(--ink-faded)" strokeWidth="1"
            strokeDasharray={ringDotted ? '2 3' : 'none'} opacity={ringDotted ? 0.7 : 0.32} />
          {!ringDotted && stock > 0 && (
            <circle cx={svgSize / 2} cy={svgSize / 2} r={R} fill="none" stroke={effRingColor} strokeWidth="3.25" strokeLinecap="round"
              strokeDasharray={`${stock * CIRC} ${CIRC}`} transform={`rotate(-90 ${svgSize / 2} ${svgSize / 2})`} />
          )}
        </svg>
      )}

      {/* element rune(s) seated on the ring (dashed ? until surveyed) */}
      {showRunes && (
        <div style={{ position: 'absolute', display: 'flex', gap: 2, zIndex: 7, ...runePos }}>
          {intel.keyUnknown
            ? <RingRune unknown />
            : intel.keyIcons.map((src, i) => <RingRune key={i} src={src} muted={spent} />)}
        </div>
      )}

      <div style={{
        width: d, height: d, borderRadius: '50%',
        border: `${isDest && !isFog ? 2 : 1.5}px ${dash ? 'dashed' : 'solid'} ${border}`,
        background: bg, color: fg,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontFamily: 'IM Fell English SC, serif', fontSize: Math.round((isFog ? 20 : (isDest ? 27 : 22)) * Math.min(magK, 1.2)),
        boxShadow: ring === 'ready' ? '0 0 0 0 var(--stamp-amber)' : (isFog || spent ? 'none' : '0 1px 4px rgba(0,0,0,0.12)'),
        animation: ring === 'ready' ? 'marker-pulse 1.8s ease-in-out infinite' : 'none',
        position: 'relative', overflow: 'hidden',
      }}>
        {/* scored-out hatch marks a worked-out perch */}
        {spent && <div className="crosshatch" style={{ position: 'absolute', inset: 0, opacity: 0.5, pointerEvents: 'none' }} />}
        <span style={{ position: 'relative', opacity: spent ? 0.5 : 1 }}>{glyph}</span>
        {state === 'fail' && (
          <span style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: d, color: 'var(--crit)', opacity: 0.4, lineHeight: 1 }}>✕</span>
        )}
      </div>
      {/* label */}
      {(name || (isDest && !isFog)) && (
        <div style={{ position: 'absolute', whiteSpace: 'nowrap', ...labelPos }}>
          <div className="fell" style={{ fontSize: isDest ? 12 : 11, lineHeight: 1.1, color: spent ? 'var(--ink-faded)' : (state === 'revealed' ? 'var(--ink-soft)' : 'var(--ink)') }}>{name}</div>
          {isDest && !isFog && <div className="label" style={{ fontSize: 7.5, color: spent ? 'var(--ink-faded)' : meta.color, marginTop: 1 }}>{meta.tag}</div>}
          {showDC && <div className="mono" style={{ fontSize: 9, color: 'var(--sepia)' }}>DC {node.dc}</div>}
          {intel && !isFog && (sw || (intel.offerIcon && intel.offerAvail)) && (
            <div style={{ marginTop: 2, display: 'inline-flex', flexDirection: 'column', alignItems: 'center', gap: 2, lineHeight: 1.2 }}>
              {sw && <span className="mono" style={{ fontSize: 8, letterSpacing: '0.06em', color: sw.c }}>{sw.t}</span>}
              {/* an unspent OFFERING — a real reason to return to a worked perch */}
              {intel.offerIcon && intel.offerAvail && (
                <div className="row" style={{ gap: 3, alignItems: 'center' }}>
                  <span className="mono" style={{ fontSize: 7.5, letterSpacing: '0.1em', color: 'var(--el-air)' }}>OFFER</span>
                  <img src={intel.offerIcon} alt="" style={{ width: 11, height: 11, objectFit: 'contain', display: 'block' }} />
                </div>
              )}
            </div>
          )}
        </div>
      )}
      {state === 'reachable' && node.forecast && (
        <div style={{ position: 'absolute', top: -10, left: -16, transform: 'rotate(-6deg)', zIndex: 8 }}
          title="What you'll find on arrival — the move advances the clock">
          <span className="stamp stamp-amber" style={{ fontSize: 6.5, padding: '1px 4px' }}>◐ BY {node.forecast}</span>
        </div>
      )}
      {state === 'reachable' && (
        <div style={{ position: 'absolute', top: -10, right: -14, transform: 'rotate(7deg)', zIndex: 8 }}>
          {hold
            ? <span className="stamp" style={{ fontSize: 6.5, padding: '1px 4px', borderColor: 'var(--ink-faded)', color: 'var(--sepia)', background: 'var(--paper)' }}>⧖ HOLD</span>
            : spent
            ? <span className="stamp" style={{ fontSize: 6.5, padding: '1px 4px', borderColor: 'var(--ink-faded)', color: 'var(--ink-faded)', background: 'var(--paper)' }}>PASS</span>
            : <span className="stamp stamp-amber" style={{ fontSize: 6.5, padding: '1px 4px' }}>OPEN</span>}
        </div>
      )}
    </div>
  );
}

// SVG edges under a layer of GraphNode divs. Coords are raw px in w×h.
function GraphCanvas({ graph, w, h, orient, labelSide }) {
  const { nodeState, edgeState } = deriveStates(graph);
  const byId = Object.fromEntries(graph.nodes.map(n => [n.id, n]));
  const stroke = (s) => {
    if (s === 'traveled') return { stroke: 'var(--stamp-amber)', width: 2.4, dash: '', op: 0.9 };
    if (s === 'open') return { stroke: 'var(--ink)', width: 1.8, dash: '', op: 0.85 };
    return { stroke: 'var(--ink-faded)', width: 1, dash: '4 6', op: 0.65 };
  };
  return (
    <div style={{ position: 'absolute', inset: 0 }}>
      <svg viewBox={`0 0 ${w} ${h}`} width={w} height={h} style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
        {[...graph.edges].sort((a, b) => {
          const order = { dim: 0, open: 1, traveled: 2 };
          return order[edgeState(a)] - order[edgeState(b)];
        }).map((e, i) => {
          const a = byId[e[0]], b = byId[e[1]];
          if (!a || !b) return null;
          const st = stroke(edgeState(e));
          return <path key={i} d={edgePath(a, b, orient)} fill="none" stroke={st.stroke} strokeWidth={st.width} strokeDasharray={st.dash} opacity={st.op} strokeLinecap="round" />;
        })}
        {graph.edges.filter(e => edgeState(e) === 'open').map((e, i) => {
          const a = byId[e[0]], b = byId[e[1]];
          const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2;
          const ang = Math.atan2(b.y - a.y, b.x - a.x);
          const ax = Math.cos(ang), ay = Math.sin(ang), px = -ay, py = ax;
          return <path key={'arr' + i} d={`M ${mx - ax * 5 + px * 4} ${my - ay * 5 + py * 4} L ${mx + ax * 5} ${my + ay * 5} L ${mx - ax * 5 - px * 4} ${my - ay * 5 - py * 4}`} fill="none" stroke="var(--ink)" strokeWidth="1.4" opacity="0.6" strokeLinecap="round" strokeLinejoin="round" />;
        })}
      </svg>
      {graph.nodes.map(n => (
        <GraphNode key={n.id} node={n} state={nodeState(n.id)} labelSide={typeof labelSide === 'function' ? labelSide(n) : labelSide} />
      ))}
    </div>
  );
}

// ---- Shared chrome -------------------------------------------------
function ExpHeader({ days = 23, hull = 72, sub }) {
  const SEG = 20, filled = Math.ceil((hull / 100) * SEG);
  return (
    <>
      <header style={{ display: 'grid', gridTemplateColumns: '420px 1fr 240px', gap: 24, alignItems: 'center', paddingRight: 8 }}>
        <div className="col gap-4">
          <div className="label">Hull Integrity</div>
          <div className="row gap-12">
            <div className="meter" style={{ width: 250, height: 20 }}>
              {Array.from({ length: SEG }).map((_, i) => (
                <div key={i} className="meter-segment" style={{ background: i < filled ? 'var(--ok)' : 'rgba(20,16,10,0.55)' }} />
              ))}
            </div>
            <span className="fell odometer" style={{ fontSize: 22 }}>{hull.toString().padStart(3, '0')}<span className="label" style={{ marginLeft: 4, fontSize: 10 }}>/ 100</span></span>
          </div>
        </div>
        <div className="col" style={{ alignItems: 'center', gap: 2 }}>
          <div className="label">Atlas · Run Chart</div>
          <div className="fell" style={{ fontSize: 22, lineHeight: 1.1 }}>The Whispering Strata</div>
          <div className="row gap-8" style={{ fontSize: 11, color: 'var(--sepia)', marginTop: 2 }}>
            <span className="elite">{sub || 'Choose your bearing · branch resolves on arrival'}</span>
          </div>
        </div>
        <div className="col gap-4" style={{ alignItems: 'flex-end' }}>
          <div className="label">Days Remaining</div>
          <div className="row gap-8" style={{ alignItems: 'baseline' }}>
            <span className="fell" style={{ fontSize: 34, lineHeight: 1 }}>{days.toString().padStart(2, '0')}</span>
            <span className="label">DAYS</span>
          </div>
        </div>
      </header>
      <hr className="divider-h" style={{ borderColor: 'var(--ink)', margin: 0 }} />
    </>
  );
}

// Bearing Inspector — replaces the telemetry log. Shows the open branches
// you're choosing between + detail on the focused one.
function BearingInspector({ depth = 2, total = 5, bearings, focus = 0, dests }) {
  const f = bearings[focus];
  return (
    <div className="panel" style={{ display: 'flex', flexDirection: 'column', minHeight: 0, height: '100%' }}>
      <div className="row" style={{ padding: '8px 14px', borderBottom: '1px solid var(--ink-faded)', justifyContent: 'space-between' }}>
        <span className="label label-ink">Bearing Inspector</span>
        <span className="mono" style={{ fontSize: 10, color: 'var(--sepia)' }}>DEPTH {depth} / {total}</span>
      </div>
      <div style={{ padding: '11px 14px', overflow: 'hidden', flex: 1 }}>
        <div className="label" style={{ marginBottom: 6 }}>Open bearings · {bearings.length}</div>
        <div className="col gap-6" style={{ marginBottom: 8 }}>
          {bearings.map((b, i) => (
            <div key={b.name} className="row gap-8" style={{
              padding: '5px 9px', alignItems: 'center',
              border: `1.5px solid ${i === focus ? 'var(--stamp-amber)' : 'var(--ink-faded)'}`,
              background: i === focus ? 'rgba(184,99,42,0.07)' : 'transparent',
            }}>
              <span style={{ width: 24, height: 24, flexShrink: 0, borderRadius: '50%', border: `1.5px solid ${b.color}`, color: b.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'IM Fell English SC, serif', fontSize: 14 }}>{b.glyph}</span>
              <div className="col" style={{ flex: 1, minWidth: 0 }}>
                <span className="fell" style={{ fontSize: 13.5, lineHeight: 1.05 }}>{b.name}</span>
                <span className="elite" style={{ fontSize: 9.5, color: 'var(--sepia)' }}>{b.kicker}</span>
              </div>
              {b.dc != null && <span className="mono" style={{ fontSize: 11, color: 'var(--ink)' }}>DC {b.dc}</span>}
            </div>
          ))}
        </div>
        <hr className="divider-h-dashed" style={{ margin: '4px 0' }} />
        <div className="row gap-8" style={{ alignItems: 'baseline', marginBottom: 4 }}>
          <span className="stamp stamp-amber" style={{ fontSize: 8 }}>SELECTED</span>
          <span className="fell" style={{ fontSize: 15 }}>{f.name}</span>
        </div>
        <div className="elite" style={{ fontSize: 10.5, color: 'var(--ink-soft)', lineHeight: 1.38, marginBottom: 6 }}>{f.body}</div>
        <DetailRow k="Check" v={f.check} />
        <DetailRow k="Reward" v={f.reward} accent="var(--gold)" />
        <DetailRow k="Risk" v={f.risk} accent="var(--crit)" />
        <DetailRow k="Leads to" v={f.leads} />
      </div>
      <div style={{ padding: '9px 14px', borderTop: '1px solid var(--ink-faded)' }}>
        <button className="btn" style={{ width: '100%', padding: '8px 14px', fontSize: 13.5, background: 'var(--ink)', color: 'var(--paper-light)', letterSpacing: '0.08em' }}>Set Bearing · {f.name} ▸</button>
      </div>
    </div>
  );
}
function DetailRow({ k, v, accent }) {
  return (
    <div className="row" style={{ justifyContent: 'space-between', alignItems: 'baseline', padding: '2px 0', borderBottom: '1px dotted var(--ink-ghost)' }}>
      <span className="label" style={{ fontSize: 9 }}>{k}</span>
      <span className="mono" style={{ fontSize: 11.5, color: accent || 'var(--ink)', textAlign: 'right' }}>{v}</span>
    </div>
  );
}

function MiniSystems({ veins, hull = 72, fatigue = 18, haul = { feed: 6, coins: 3 }, h }) {
  const Row = ({ label, value, max, accent }) => (
    <div style={{ marginBottom: 9 }}>
      <div className="row" style={{ justifyContent: 'space-between', marginBottom: 3 }}>
        <span className="label label-ink">{label}</span>
        <span className="mono fell" style={{ fontSize: 13 }}>{value.toString().padStart(2, '0')}<span style={{ fontSize: 10, color: 'var(--sepia)', marginLeft: 2 }}>/{max}</span></span>
      </div>
      <div style={{ height: 6, background: 'var(--ink-trace)', border: '0.5px solid var(--ink-faded)' }}>
        <div style={{ height: '100%', width: `${Math.min(1, value / max) * 100}%`, background: accent, opacity: 0.85 }} />
      </div>
    </div>
  );
  return (
    <div className="panel" style={{ display: 'flex', flexDirection: 'column', height: h }}>
      <div className="row" style={{ padding: '8px 14px', borderBottom: '1px solid var(--ink-faded)', justifyContent: 'space-between' }}>
        <span className="label label-ink">Ship Systems · Run State</span>
        <span className="stamp stamp-blue" style={{ fontSize: 8 }}>LIVE</span>
      </div>
      <div style={{ padding: 14, flex: 1 }}>
        <div className="label" style={{ marginBottom: 6 }}>Spirit's veins</div>
        {veins.map(v => <Row key={v.label} label={v.label} value={v.value} max={20} accent={v.color} />)}
        <hr className="divider-h-dashed" />
        <Row label="Hull" value={hull} max={100} accent={hull < 30 ? 'var(--crit)' : 'var(--ok)'} />
        <Row label="Fatigue" value={fatigue} max={100} accent="var(--stamp-amber)" />
        <hr className="divider-h-dashed" />
        <div className="label" style={{ marginBottom: 6 }}>Run Haul</div>
        <div className="row" style={{ justifyContent: 'space-between', marginBottom: 4, fontSize: 12 }}>
          <span className="elite">Feed</span><span className="mono fell" style={{ fontSize: 14 }}>{haul.feed.toString().padStart(2, '0')}</span>
        </div>
        <div className="row" style={{ justifyContent: 'space-between', fontSize: 12 }}>
          <span className="elite">Coins</span><span className="mono fell" style={{ fontSize: 14, color: 'var(--gold)' }}>{haul.coins.toString().padStart(2, '0')}</span>
        </div>
      </div>
    </div>
  );
}

function GraphLegend({ items }) {
  return (
    <div className="row" style={{ gap: 13, flexWrap: 'wrap', alignItems: 'center' }}>
      <span className="label">LEGEND</span>
      {items.map(it => (
        <div key={it.label} className="row gap-6" style={{ fontSize: 11, color: 'var(--sepia)' }}>
          <span style={{ width: 18, height: 18, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', border: `1.5px ${it.dash ? 'dashed' : 'solid'} ${it.color}`, background: it.fill || 'transparent', color: it.color, fontFamily: 'IM Fell English SC, serif', fontSize: 11 }}>{it.glyph}</span>
          <span className="elite">{it.label}</span>
        </div>
      ))}
    </div>
  );
}

const DEMO_VEINS = [
  { label: 'Fire', value: 14, color: 'var(--el-fire)' },
  { label: 'Earth', value: 11, color: 'var(--el-earth)' },
  { label: 'Water', value: 8, color: 'var(--el-water)' },
];

// Open bearings shown in the inspector (mock, focus = Crossroads).
const DEMO_BEARINGS = [
  { name: 'Crossroads', glyph: '⟡', color: 'var(--stamp-amber)', kicker: 'Choice nexus · no check', dc: null,
    body: 'A divergence in the aether. Lock a sub-route for a guaranteed reveal two hops on.',
    check: '— none —', reward: 'Reveal · +1 scry', risk: '—', leads: 'Geode · Wreck' },
  { name: 'Hazard', glyph: '⚔', color: 'var(--crit)', kicker: 'Skirmish · COMBAT check', dc: 18,
    body: 'A wyrm patrol rides the thermals. Clear it and the wreck-field opens.',
    check: 'COMBAT d20+14 vs 18', reward: '+1 specialist lead', risk: '−8 Hull on fail', leads: 'Wreck · The Lee' },
];

const LEGEND_ITEMS = [
  { label: 'Unscanned', glyph: '?', color: 'var(--ink-faded)', dash: true },
  { label: 'Forage', glyph: '⌘', color: 'var(--el-earth)' },
  { label: 'Cache', glyph: '¤', color: 'var(--gold)' },
  { label: 'Echo', glyph: '◎', color: 'var(--el-air)' },
  { label: 'Hazard', glyph: '⚔', color: 'var(--crit)' },
  { label: 'Lifepod', glyph: '✚', color: 'var(--gold)' },
  { label: 'Crossroads', glyph: '⟡', color: 'var(--stamp-amber)' },
  { label: 'Destination', glyph: '◆', color: 'var(--gold)' },
];

Object.assign(window, {
  NT, GraphCanvas, GraphNode, Vessel, ExpHeader, BearingInspector, MiniSystems,
  GraphLegend, DEMO_VEINS, DEMO_BEARINGS, LEGEND_ITEMS, deriveStates,
});
