# Topology Studio — User Manual

A comprehensive guide to building cinematic, animated network topology visualizations with step-by-step choreography.

---

## Table of Contents

1. [Quick Start](#1-quick-start)
2. [Architecture Overview](#2-architecture-overview)
3. [UI Components](#3-ui-components)
4. [Nodes](#4-nodes)
5. [Links](#5-links)
6. [Anchors](#6-anchors)
7. [Acts, Steps & Phases — The Choreography System](#7-acts-steps--phases--the-choreography-system)
8. [The Animation System](#8-the-animation-system)
9. [Focus & Spotlight](#9-focus--spotlight)
10. [Custom Render Hooks](#10-custom-render-hooks)
11. [The Render Context (ctx)](#11-the-render-context-ctx)
12. [Playback Modes](#12-playback-modes)
13. [Keyboard Shortcuts](#13-keyboard-shortcuts)
14. [Glossary System](#14-glossary-system)
15. [URL State](#15-url-state)
16. [CSS Theme & Tokens](#16-css-theme--tokens)
17. [SVG Filters & Visual Effects](#17-svg-filters--visual-effects)
18. [The Visual Editor](#18-the-visual-editor)
19. [Responsive Layout](#19-responsive-layout)
20. [Accessibility](#20-accessibility)
21. [Complete Example: Building a Topology from Scratch](#21-complete-example-building-a-topology-from-scratch)
22. [Implementing Link Draw Animations](#22-implementing-link-draw-animations)
23. [Plugin System](#23-plugin-system)
24. [SVG Diffing & Incremental Rendering](#24-svg-diffing--incremental-rendering)
25. [Smart Link Routing](#25-smart-link-routing)
26. [Choreography Smoothing](#26-choreography-smoothing)
27. [Layout Engine](#27-layout-engine)
28. [Template System](#28-template-system)
29. [Theme Engine](#29-theme-engine)
30. [Animated GIF Export](#30-animated-gif-export)
31. [Temporal Digital Twin](#31-temporal-digital-twin)
32. [Isometric Tilt Mode](#32-isometric-tilt-mode)
33. [Intelligence Layer](#33-intelligence-layer)
34. [Ghosting Engine](#34-ghosting-engine)
35. [Conditional Step Logic](#35-conditional-step-logic)
36. [Flow Paths](#36-flow-paths)
37. [Policy Markers](#37-policy-markers)
38. [Layer System](#38-layer-system)
39. [Export (PNG / SVG / PDF / Standalone HTML)](#39-export-png--svg--pdf--standalone-html)
40. [Node Type Browser](#40-node-type-browser)
41. [Link Waypoints & Routing API](#41-link-waypoints--routing-api)
42. [Zone / Region Annotations](#42-zone--region-annotations)
43. [Drag-to-Reposition & Coordinate Readback](#43-drag-to-reposition--coordinate-readback)
44. [Step Thumbnail Previews](#44-step-thumbnail-previews)
45. [draw.io XML Import](#45-drawio-xml-import)
46. [Auto-Layout Engine Improvements](#46-auto-layout-engine-improvements)
47. [Mobile Quick-Edit Mode](#47-mobile-quick-edit-mode)
48. [Self-Contained HTML Export & Embed](#48-self-contained-html-export--embed)
49. [AI-Assisted Topology Generation](#49-ai-assisted-topology-generation)
50. [Engagement Analytics](#50-engagement-analytics)
51. [Layout Edit Mode](#51-layout-edit-mode)
52. [Presentation Mode](#52-presentation-mode)
53. [Dark / Light Theme Toggle](#53-dark--light-theme-toggle)
54. [Enterprise Templates Gallery](#54-enterprise-templates-gallery)
55. [Removal & Unsubscribe API](#55-removal--unsubscribe-api)
56. [REST API & OpenAPI Spec](#56-rest-api--openapi-spec)
57. [TopologyGraph — Graph Data Model](#57-topologygraph--graph-data-model)

---

## 1. Quick Start

### Minimal Setup

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="topology-ds.css">
</head>
<body>
  <div id="app"></div>
  <script src="topology-ds.js"></script>
  <script>
    const topo = new TopologyDesigner({
      title: 'My Network',
      subtitle: 'A simple topology',
      viewBox: '0 0 800 500',
      phaseMs: 600,
    });

    // Register nodes
    topo.node('R1', { type: 'router', x: 200, y: 250, label: 'Router 1' });
    topo.node('SW1', { type: 'switch', x: 500, y: 250 });

    // Register a link
    topo.link('r1-sw1', { type: 'line', from: 'R1', to: 'SW1', color: '#01a982' });

    // Register an act and step
    topo.act('a1', { label: 'Act 1 · Setup', color: '#01a982' });
    topo.addStep('deploy', {
      act: 'a1',
      name: 'Deploy Devices',
      goal: 'Place the router and switch on the network.',
      focus: [],
      phases: [
        { show: ['R1'], diff: 'Router 1 appears' },
        { show: ['SW1', 'r1-sw1'], diff: 'Switch 1 connects to Router' },
      ],
    });

    // Mount and go
    topo.mount('app');
  </script>
</body>
</html>
```

### Zero Dependencies

The engine is pure vanilla JavaScript + SVG + CSS. No build tools, no npm, no frameworks. Open the HTML file in any modern browser.

---

## 2. Architecture Overview

```
TopologyDesigner
├── Registration API      — Declaratively define nodes, links, acts, steps
├── Plugin System         — registerNodeType / registerLinkType with lifecycle hooks
├── Choreography Engine   — Acts → Steps → Phases control what appears when
├── Render Pipeline       — SVG diffing with incremental updates (dirty element tracking)
├── Smart Link Routing    — AABB collision detection + auto-detour around nodes
├── Animation System      — CSS animations + SVG animateMotion + choreography smoothing
├── Layout Engine         — 5 auto-layout algorithms (force-directed, hierarchical, circular, grid, magnetic north)
├── Template System       — 7 built-in templates + diagram-as-code parser
├── Theme Engine          — 5 presets, brand color generation, WCAG contrast checking
├── UI Shell              — Sidebar, toolbar, narrator, controls
└── Playback Engine       — Manual/auto/presenter modes with timer
```

### Rendering Flow

```
mount(containerId)
  → _buildIndex()         Build step lookup tables
  → _scaffoldHTML()       Create the DOM shell (sidebar, toolbar, canvas, narrator)
  → _bindEvents()         Attach keyboard/mouse/button handlers
  → _loadURL()            Restore step from URL query params
  → _buildPips()          Create progress indicator dots
  → _buildSidebar()       Create act/step navigation tree
  → render()              Generate SVG and update all UI
      → _renderSVG()
          → _svgDefs()       SVG filter & gradient definitions
          → _svgAmbient()    Background ambient light overlays
          → For each step:
              → For each phase in step.phases:
                  → For each element in phase.show:
                      → _renderNodeSVG() or _renderLinkSVG()
                  → phase.custom() if defined
              → step.onRender(ctx) if defined
          → Render anchor handles
      → _updateNarrator()
      → _updateUI()
      → _updateURL()
```

---

## 3. UI Components

The design system renders a complete interactive UI with these zones:

### 3.1 Top Bar (`.tds-topbar`)

Displays the topology title and subtitle. Styled with green glow text in uppercase.

### 3.2 Sidebar (`.tds-sidebar`)

A left-side navigation panel showing the step hierarchy grouped by acts:

- **Act headers** — Collapsible groups with colored labels. Click to expand/collapse.
- **Step rows** — Numbered entries within each act. Click to jump to that step.
- **Visual states:**
  - Default (gray) — step not yet reached
  - Active (dimmed highlight) — step has been visited
  - Current (green glow) — currently displayed step

### 3.3 Toolbar (`.tds-toolbar`)

A horizontal control bar above the canvas containing:

| Element | Description |
|---------|-------------|
| **Progress pips** | Small bars, one per step. Green = visited. Clickable. |
| **Step label** | Shows `Step N/Total │ Act · Step Name` |
| **Mode selector** | Dropdown: Manual / Auto / Presenter |
| **Speed slider** | Controls auto-play interval (2s–7s). Only visible in Auto/Presenter modes. |
| **Phase slider** | Controls the stagger delay between successive phase reveals (100ms–1500ms). Adjusts `phaseMs` in real time. |
| **Draw slider** | Controls the stroke draw-in animation duration (0.2s–2.0s). Adjusts `drawDuration` in real time. |
| **Fade slider** | Controls the phase fade-in transition duration (0.1s–1.5s). Adjusts `fadeDuration` in real time. |
| **Glossary button** | Opens the glossary modal. Keyboard shortcut: `G`. |
| **Mode indicator** | Shows SELECT or LINK MODE for interactive editing. Click to toggle. |
| **AI Assist button** | Opens the AI-Assisted Topology Generation modal (see [Section 49](#49-ai-assisted-topology-generation)). |
| **Keyboard hint** | Shows available shortcuts. |

### 3.4 Canvas (`.tds-canvas`)

The main SVG visualization area. Features:

- Glassmorphism border with animated gradient glow
- Subtle 3D perspective transform (flattens on hover)
- Corner pulse ambient light
- Contains the `<svg>` element with all rendered topology content

### 3.5 Narrator (`.tds-narrator`)

A right-side panel providing narrative context for the current step:

- **Header** — Traffic light dots (r/y/g), "NARRATOR" label, step counter badge
- **Toggle button** — Collapse/expand the narrator panel. Keyboard: `N`.
- **Body content:**
  - Act label + step name
  - Goal description
  - "Changed" bullets listing each phase's `diff` text
  - Act intro card (shown at the first step of each act)

### 3.6 Controls (`.tds-controls`)

Bottom control bar with playback buttons:

| Button | Action |
|--------|--------|
| **⟲ Reset** | Go to step 0 (empty canvas) |
| **◂ Prev** | Go back one step |
| **▶ Play / ⏸ Pause** | Start/stop auto-play |
| **Next ▸** | Advance one step |

### 3.7 Glossary Modal (`.tds-modal`)

A centered overlay showing term definitions. Opened via button or `G` key. Click outside or the Close button to dismiss.

### 3.8 Properties Panel

A floating inspector panel that appears when you double-click a node/link or click an anchor. Shows element properties (type, position, label) and allows editing link labels, swapping link direction, or deleting anchors.

---

## 4. Nodes

### Registration

```javascript
topo.node(id, config)
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | `string` | Unique identifier (e.g. `'SEA'`, `'HOST'`) |
| `config.type` | `string` | Node type (see table below) |
| `config.x` | `number` | X position in SVG coordinates |
| `config.y` | `number` | Y position in SVG coordinates |
| `config.label` | `string` | Text label displayed below the node |
| `config.sublabel` | `string` | Secondary label (smaller, gray) |
| `config.labelOffset` | `number` | Vertical offset for label (default: 24) |
| `config.color` | `string` | Override color for the node |
| `config.sideLabel` | `object` | `{ x, y, label, sublabel, color, anchor }` — label placed to the side |
| `config.zoneIndicator` | `string` | `'left'` or `'right'` — shows WAN/LAN zone labels |
| `config.labelY` | `number` | Absolute Y position for the label (overrides `labelOffset`) |
| `config.labelColor` | `string` | Override color for the node label text |
| `config.haloColor` | `string` | Override color for the focus halo filter |
| `config.zoneLabel` | `object` | `{ text, color }` — dashed zone rectangle below the node |
| `config.zoneLabelOffset` | `number` | Vertical offset for the zone label (default: 22) |
| `config.render` | `function` | Custom render function for `type: 'custom'` |
| `config.sub1` | `string` | First subtitle line (cloud type) |
| `config.sub2` | `string` | Second subtitle line (cloud type) |
| `config.innerClouds` | `string` | Inner cloud puffs: `'both'` \| `'left'` \| `'right'` \| `'none'` (cloud type) |
| `config.managed` | `boolean` | Show managed badge (host type) |
| `config.agent` | `boolean` | Show agent hub icon (host type) |
| `config.agentColor` | `string` | Override agent icon color (host type, default: `'#65aef9'`) |
| `config.pe` | `boolean` | Enable Private Edge variant (connector type) |
| `config.mode` | `string` | `'id'` \| `'auth'` — card mode (idcard type) |
| `config.user` | `string` | Username field (idcard type) |
| `config.host` | `string` | Hostname field (idcard type) |
| `config.role` | `string` | Role field (idcard type) |
| `config.spans` | `string[]` | Node IDs to encompass (overlayCloud type) |
| `config.padding` | `number` | Extra padding around bounding box (overlayCloud type, default: 90) |
| `config.variant` | `string` | EC variant: `'generic'` \| `'virtual'` \| `'physical'` \| `'aws'` \| `'azure'` \| `'gcp'` \| `'oracle'` |
| `config.vrrp` | `string` | VRRP status: `'active'` \| `'standby'` — shows indicator on EC LEDs |
| `config.copperPorts` | `number` | Number of copper (RJ45) ports (switchEnterprise type) |
| `config.fiberPorts` | `number` | Number of fiber (SFP) ports (switchEnterprise type) |
| `config.fontSize` | `number` | Font size for text nodes |
| `config.fontWeight` | `string` | Font weight for text nodes |
| `config.size` | `number` | Size for shape nodes |
| `config.width` | `number` | Width override for shape nodes |
| `config.height` | `number` | Height override for shape nodes |
| `config.direction` | `string` | Direction variant for shape nodes (e.g. `'left'`, `'right'`, `'up'`, `'down'`) |
| `config.zOrder` | `number` | Z-order layering value for stacking order |
| `config.hideId` | `boolean` | Hide the ID label when ID display is toggled on |

### Node Types

| Type | Renderer | Visual Description |
|------|----------|--------------------|
| `ec` | `renderEC` | EdgeConnect appliance — rounded rect with triangle logo, LED indicators, variant support |
| `switch` | `renderSwitch` | Network switch — rect with 8 port indicators |
| `switchEnterprise` | `renderSwitchEnterprise` | Enterprise switch — chassis with configurable copper/fiber ports, status LEDs, console port |
| `cloud` | `renderCloud` | Cloud service — double ellipse with inner puffs, label/sublabel/sub2 |
| `host` | `renderHost` | Laptop/device — screen + keyboard shape, optional managed badge + agent icon |
| `connector` | `renderConnector` | ZTNA connector — rect with hub-spoke network graphic |
| `apps` | `renderApps` | Private apps rack — tall rect with 3 shelf units + LEDs |
| `saas` | `renderSaaS` | SaaS cloud — ellipse with 4 colored app squares |
| `server` | `renderServer` | Server rack — square with 2 shelf units |
| `router` | `renderRouter` | Router — circle with 4-directional arrows |
| `firewall` | `renderFirewall` | Firewall — rect with brick wall pattern |
| `database` | `renderDatabase` | Database — cylinder shape with dashed ring |
| `idcard` | `renderIdCard` | Identity badge — card with user/host/role fields |
| `ap` | `renderAP` | Access point — rect with antenna + signal wave arcs |
| `overlayCloud` | `renderOverlayCloud` | Spanning overlay — dashed ellipse encompassing multiple nodes |
| `text` | `renderText` | Freeform text label — configurable font size, weight, and multi-line sublabel |
| `shapeArrow` | `renderShape` | Arrow shape with configurable direction |
| `shapeSquare` | `renderShape` | Square shape |
| `shapeRectangle` | `renderShape` | Rectangle shape |
| `shapeTriangle` | `renderShape` | Triangle shape |
| `shapeCircle` | `renderShape` | Circle shape |
| `shapeEllipse` | `renderShape` | Ellipse shape |
| `shapeDiamond` | `renderShape` | Diamond shape |
| `shapePentagon` | `renderShape` | Pentagon shape |
| `shapeHexagon` | `renderShape` | Hexagon shape |
| `shapeStar` | `renderShape` | Star shape |
| `shapeCross` | `renderShape` | Cross shape |
| `custom` | User-provided | Uses `config.render(x, y, cfg)` |

### Host Variants

The `host` type supports progressive visual states:

```javascript
// Basic host (gray)
topo.node('H1', { type: 'host', x: 100, y: 200, label: 'Laptop' });

// Managed host (green border + shield badge)
TopologyDesigner.renderHost(x, y, { managed: true });

// Managed + Agent installed (green border + shield + agent hub icon)
TopologyDesigner.renderHost(x, y, { managed: true, agent: true });
```

### IdCard Configuration

The `idcard` type displays an identity badge card with user, host, and role fields:

```javascript
topo.node('ID1', {
  type: 'idcard', x: 300, y: 200,
  mode: 'id',              // 'id' (gold/identified) | 'auth' (green/authenticated)
  user: 'mike@acme.com',   // User field
  host: 'LAPTOP-001',      // Host field
  role: 'MANAGED',         // Role field
});
```

| Property | Default | Description |
|----------|---------|-------------|
| `mode` | `'id'` | `'id'` = gold "IDENTIFIED" badge, `'auth'` = green "AUTHENTICATED" badge |
| `user` | `'User'` | Username displayed on the card |
| `host` | `'Host'` | Hostname displayed on the card |
| `role` | `'MANAGED'` | Role text displayed on the card |

### Connector Variants

The `connector` type supports a Private Edge variant:

```javascript
// Standard connector (blue)
topo.node('C1', { type: 'connector', x: 200, y: 300, label: 'Connector' });

// Private Edge connector (green, thicker border + "PRIVATE EDGE" badge)
topo.node('C2', { type: 'connector', x: 400, y: 300, label: 'PE Connector', pe: true });
```

### EC Variants

EdgeConnect nodes support cloud-provider and deployment variants:

```javascript
// Generic (default)
topo.node('EC1', { type: 'ec', x: 100, y: 200, label: 'Generic', variant: 'generic' });

// Virtual appliance
topo.node('EC2', { type: 'ec', x: 200, y: 200, label: 'Virtual', variant: 'virtual' });

// Physical appliance (rack ears + ventilation)
topo.node('EC3', { type: 'ec', x: 300, y: 200, label: 'Physical', variant: 'physical' });

// Cloud-provider variants with distinct color schemes and iconography
topo.node('EC4', { type: 'ec', x: 400, y: 200, label: 'AWS', variant: 'aws' });
topo.node('EC5', { type: 'ec', x: 500, y: 200, label: 'Azure', variant: 'azure' });
topo.node('EC6', { type: 'ec', x: 600, y: 200, label: 'GCP', variant: 'gcp' });
topo.node('EC7', { type: 'ec', x: 700, y: 200, label: 'Oracle', variant: 'oracle' });
```

### VRRP Indicators

EC nodes can show VRRP active/standby status on their green status LEDs:

```javascript
topo.node('EC_A', { type: 'ec', x: 200, y: 200, label: 'Active', vrrp: 'active' });
topo.node('EC_S', { type: 'ec', x: 400, y: 200, label: 'Standby', vrrp: 'standby' });
```

### Enterprise Switch

The `switchEnterprise` type renders a detailed chassis with configurable port counts:

```javascript
topo.node('SW1', {
  type: 'switchEnterprise', x: 300, y: 200,
  label: 'Core Switch',
  copperPorts: 24,   // Number of RJ45 ports (default: 24)
  fiberPorts: 4,     // Number of SFP fiber ports (default: 4)
});
```

### Text Nodes

Freeform text labels with multi-line sublabel support:

```javascript
topo.node('T1', {
  type: 'text', x: 400, y: 100,
  label: 'Section Header',
  sublabel: 'Line 1\nLine 2\nLine 3',  // Multi-line sublabel
  fontSize: 16,
  fontWeight: '600',
  color: '#01a982',
});
```

### Basic Shapes

11 geometric shape types with configurable properties:

```javascript
topo.node('S1', {
  type: 'shapeArrow', x: 200, y: 200,
  label: 'Data Flow',
  color: '#01a982',
  size: 40,          // Base size
  direction: 'right', // 'left' | 'right' | 'up' | 'down'
});

topo.node('S2', {
  type: 'shapeHexagon', x: 400, y: 200,
  label: 'Service',
  color: '#65aef9',
  size: 30,
});
```

Available shapes: `shapeArrow`, `shapeSquare`, `shapeRectangle`, `shapeTriangle`, `shapeCircle`, `shapeEllipse`, `shapeDiamond`, `shapePentagon`, `shapeHexagon`, `shapeStar`, `shapeCross`.

### Cloud Configuration

```javascript
topo.node('HPE', {
  type: 'cloud', x: 500, y: 100,
  label: 'HPE SSE',          // Primary label
  color: '#65aef9',           // Cloud color
  sub1: 'SWG · ZTNA · CASB', // First subtitle
  sub2: 'PDP / Policy Engine', // Second subtitle
  innerClouds: 'both',       // 'both' | 'left' | 'right' | 'none'
});
```

### Overlay Cloud

Spans across multiple nodes, drawing a dashed ellipse that encompasses them:

```javascript
topo.node('OVERLAY', {
  type: 'overlayCloud',
  x: 0, y: 0,  // Ignored when spans is used
  label: 'SD-WAN Fabric',
  color: '#7764fc',
  spans: ['SEA', 'LAX', 'DEN'],  // Node IDs to encompass
  padding: 90,                    // Extra padding around bounding box
});
```

### Custom Node Types

Register new node types globally:

```javascript
TopologyDesigner.registerNodeType('myDevice', (x, y, cfg) => {
  return `<rect x="${x-20}" y="${y-20}" width="40" height="40" rx="8"
    fill="#292d3a" stroke="${cfg.color || '#01a982'}" stroke-width="1.2"/>
    <text x="${x}" y="${y+4}" text-anchor="middle" fill="${cfg.color || '#01a982'}"
    font-size="10">${cfg.icon || '?'}</text>`;
});

topo.node('DEV1', { type: 'myDevice', x: 300, y: 200, icon: '⚡', color: '#fc6161' });
```

---

## 5. Links

### Registration

```javascript
topo.link(id, config)
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | `string` | Unique identifier |
| `config.type` | `string` | Link type (see below) |
| `config.from` | `string` | Source node ID or anchor ID |
| `config.to` | `string` | Destination node ID or anchor ID |
| `config.color` | `string` | Line/tunnel color |
| `config.label` | `string` | Text label on the link |
| `config.sublabel` | `string` | Secondary label (packet type only) |
| `config.dashed` | `boolean` | Dashed line style (line type only, see also `dashStyle`) |
| `config.dashStyle` | `string` | Dash pattern preset: `'solid'`, `'dashed'`, `'dotted'`, `'longDash'`, `'dashDot'`, `'dashDotDot'`, `'dense'`, `'sparse'` |
| `config.animateFlow` | `boolean` | Enable animated flow particles on line-type links |
| `config.animateSpeed` | `number` | Animation speed for flow particles |
| `config.labelOffsetY` | `number` | Vertical offset for link labels |
| `config.zOrder` | `number` | Z-order layering value for stacking order |
| `config.dots` | `boolean` | Show animated particles (default: true for tunnels/flows) |
| `config.path` | `string` | Custom SVG path string (flow type) |
| `config.labelPos` | `object` | `{ x, y }` for label placement (flow type) |
| `config.fromLabel` | `string` | Port label near source endpoint (e.g. `'lan0'`) |
| `config.toLabel` | `string` | Port label near destination endpoint (e.g. `'e0'`) |
| `config.reason` | `string` | Reason text (blocked type only) |
| `config.opacity` | `number` | Override opacity (default: auto-computed from focus state of endpoints) |

### Link Types

#### `line` — Simple Connection
```javascript
topo.link('r1-sw1', {
  type: 'line', from: 'R1', to: 'SW1',
  color: '#01a982',
  dashed: false,     // Optional: use dashed stroke
});
```
A straight line between two points. Draws with a stroke-dashoffset animation when first appearing. Dashed lines fade in instead.

#### `tunnel` — IPsec/Encrypted Tunnel
```javascript
topo.link('ipsec', {
  type: 'tunnel', from: 'swgSea', to: 'swgHpe',
  color: '#65aef9',
  label: 'SWG IPsec Tunnel',
  dots: true,         // Animated particles traveling both directions
});
```
A thick, visually prominent connection with:
- Wide glow background (10px, low opacity)
- Main draw stroke (2.5px, dashed animation)
- Detail dashes (1px overlay)
- Optional bidirectional animated particles
- Glassmorphism label badge

#### `wireguard` — WireGuard Tunnel
```javascript
topo.link('wg1', {
  type: 'wireguard', from: 'wgMike', to: 'wgHpe',
  label: 'WireGuard',
  dots: true,
});
```
Always blue (#65aef9). Dashed style with bloom-filtered animated particle.

#### `flow` — Curved Path
```javascript
topo.link('overlay', {
  type: 'flow', from: 'SEA', to: 'LAX',
  color: '#01a982',
  label: 'SD-WAN Overlay',
  path: 'M280,274 C430,220 620,220 770,274',  // Custom SVG path
  labelPos: { x: 525, y: 234 },
  dots: true,
});
```
Follows a custom SVG path (cubic Bezier curves, etc). If no `path` is provided, defaults to a straight line `M...L...`. Includes:
- Wide glow background
- Draw stroke animation
- Detail dashes
- Up to 2 animated particles with staggered start times

#### `packet` — Burst Indicator
```javascript
topo.link('pkt1', {
  type: 'packet', from: 'HOST', to: 'SW1',
  color: '#deb146',
  label: 'RADIUS Request',
  sublabel: '802.1X EAP',
});
```
Directional packet flow with a large (4.5r) animated particle, dashed trail, and glassmorphism label+sublabel badge.

#### `blocked` — Blocked Connection
```javascript
topo.link('blk1', {
  type: 'blocked', from: 'SEA', to: 'LAX',
  reason: 'No authorized tunnel — requires App Gate',
});
```
Shows a red X mark at the midpoint with a pulsing glow, "BLOCKED" badge, and optional reason text.

#### `wifi` — Wireless Link
```javascript
topo.link('wifi1', {
  type: 'wifi', from: 'AP1', to: 'HOST',
  color: '#00a4b3',
  label: 'Wi-Fi 6',
});
```
3D tube-style link with perpendicular signal wave arcs at the midpoint, wifi icon, bidirectional animated particles, and optional label.

#### `poe` — Power over Ethernet
```javascript
topo.link('poe1', {
  type: 'poe', from: 'SW1', to: 'AP1',
  color: '#deb146',
  label: 'PoE 802.3af',
});
```
Line with a lightning bolt icon at the midpoint, indicating power delivery over the Ethernet cable.

#### `optical` — Fiber Optic
```javascript
topo.link('fiber1', {
  type: 'optical', from: 'SW1', to: 'SW2',
  color: '#fc6161',
  label: '10G Fiber',
});
```
Line with a laser warning triangle icon at the midpoint, representing fiber optic connections.

### Dash Style Presets

Links support 8 dash style presets via the `dashStyle` property:

| Style | Pattern | Description |
|-------|---------|-------------|
| `solid` | `—————` | No dashes (default) |
| `dashed` | `— — —` | Standard dashes |
| `dotted` | `· · · ·` | Small dots |
| `longDash` | `——  ——` | Long dashes |
| `dashDot` | `— · — ·` | Alternating dash-dot |
| `dashDotDot` | `— · · —` | Dash with two dots |
| `dense` | `- - - -` | Tightly spaced dashes |
| `sparse` | `—   —` | Widely spaced dashes |

```javascript
topo.link('styled', {
  type: 'line', from: 'A', to: 'B',
  color: '#01a982',
  dashStyle: 'dashDot',
});
```

### Animate Flow

Line-type links can display animated flow particles:

```javascript
topo.link('data', {
  type: 'line', from: 'SW1', to: 'R1',
  color: '#01a982',
  animateFlow: true,
  animateSpeed: 2,   // Particle animation speed in seconds
});
```

### Z-Order Layering

Links (and nodes) support `zOrder` for controlling visual stacking:

```javascript
topo.link('top', { type: 'line', from: 'A', to: 'B', zOrder: 10 });  // On top
topo.link('bottom', { type: 'line', from: 'C', to: 'D', zOrder: 1 }); // Behind
```

### Port Labels (Endpoint Labels)

Any link type can have port labels that appear near the source/destination:

```javascript
topo.link('uplink', {
  type: 'line', from: 'SW1', to: 'R1',
  color: '#01a982',
  fromLabel: 'ge0/1',   // Shown near SW1
  toLabel: 'eth0',       // Shown near R1
});
```

---

## 6. Anchors

Anchors are invisible position markers used as link endpoints when you don't want links to connect directly to node centers.

```javascript
topo.anchor(id, { x, y, label, color, labelOffsetX, labelOffsetY, hideId })
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `x` | `number` | X position |
| `y` | `number` | Y position |
| `label` | `string` | Optional text label displayed near the anchor |
| `color` | `string` | Optional label color |
| `labelOffsetX` | `number` | Horizontal offset for the label position |
| `labelOffsetY` | `number` | Vertical offset for the label position |
| `hideId` | `boolean` | Hide the ID label when ID display is toggled on |

### Use Cases

- **Tunnel endpoints** that should attach to a specific spot on a node's edge:
  ```javascript
  topo.anchor('swgSea', { x: 292, y: 266 });  // Near Seattle EC's top-left
  topo.anchor('swgHpe', { x: 495, y: 76 });   // Near HPE cloud's bottom

  topo.link('swg-tunnel', {
    type: 'tunnel', from: 'swgSea', to: 'swgHpe',
    color: '#65aef9', label: 'SWG IPsec Tunnel',
  });
  ```

- **Waypoints** for complex routing (links can connect anchor-to-anchor, node-to-anchor, etc.)

### Interactive Features

- Anchors render as small blue diamond handles on the canvas
- **Drag** to reposition (mousedown + move)
- **Click** to show properties panel with position info and delete button
- Toggle anchor/node ID labels with the `I` key

---

## 7. Acts, Steps & Phases — The Choreography System

This is the three-level hierarchy that controls the **narrative sequence** of your topology. Elements don't just appear — they're revealed through a cinematic, step-by-step choreography.

### Hierarchy

```
Topology
└── Act (thematic group)
    └── Step (narrative unit — one click/advance)
        └── Phase (sub-animation within a step — timed sequence)
```

### 7.1 Acts

Acts are thematic groups of steps. They have no visual presence in the SVG — they organize the sidebar and provide intro cards in the narrator.

```javascript
topo.act(id, {
  label: 'Act 1 · Infrastructure',  // Displayed in sidebar header
  color: '#01a982',                  // Accent color for this act
  intro: [                           // Lines shown in narrator intro card
    'Baseline topology and cloud security services.',
    'Transport is not trust — we build trust with gates.',
  ],
});
```

**Key properties:**
- `label` — Shown in sidebar headers and narrator
- `color` — Tints sidebar headers and narrator badges
- `intro[]` — Array of intro lines shown when entering this act's first step
- `steps[]` — Auto-populated when steps are registered with `act: id`

### 7.2 Steps

Steps are the primary navigation unit. Each "Next" click or auto-play tick advances one step. A step represents a meaningful narrative moment.

```javascript
topo.addStep(id, {
  act: 'a1',                         // Which act this belongs to
  name: 'The Network',               // Display name (sidebar + toolbar)
  goal: 'Branch + DC + Internet.',   // Description (narrator)
  focus: ['SEA', 'LAX'],             // Spotlight these elements (others dim)
  phases: [ ... ],                   // What appears and when
  onRender: (ctx) => svgString,      // Optional custom render hook
});
```

**Key properties:**
| Property | Type | Description |
|----------|------|-------------|
| `act` | `string` | Parent act ID |
| `name` | `string` | Step display name |
| `goal` | `string` | Description shown in narrator |
| `focus` | `string[]` | Element IDs to spotlight. Empty array = everything at full brightness. |
| `phases` | `object[]` | Array of phase objects controlling timed element appearance |
| `onRender` | `function` | Custom render hook called after phases (see Section 10) |

### 7.3 Phases

Phases are sub-steps within a step that create a **timed reveal sequence**. When you advance to a step, its phases play out with staggered delays (controlled by `phaseMs`).

```javascript
phases: [
  { show: ['SEA'],              diff: 'Seattle EC appears' },          // delay: 0ms
  { show: ['INET', 'sea-inet'], diff: 'Internet + uplink' },          // delay: 600ms
  { show: ['LAX', 'lax-inet'],  diff: 'Los Angeles EC' },             // delay: 1200ms
  { show: ['overlay'],          diff: 'SD-WAN overlay draws' },       // delay: 1800ms
]
```

**Phase object:**

| Property | Type | Description |
|----------|------|-------------|
| `show` | `string[]` | Element IDs (nodes and/or links) to reveal in this phase |
| `diff` | `string` | Narrative text shown in the narrator's "Changed" bullets |
| `delayMs` | `number` | Per-phase timing override in milliseconds (overrides computed delay from `phaseMs * phaseNum`) |
| `custom` | `function\|string` | Custom SVG to render for this phase (see Section 10) |
| `flow` | `object` | Declarative flow path: `{ path|through, color, label, labelPos, dots, dimIds, hideAfter }` |
| `label` | `object` | Declarative text label: `{ at, text, color, size, weight, opacity, offsetX, offsetY, anchor }` |
| `badge` | `object` | Declarative badge: `{ at, text, color, width, height, offsetX, offsetY, border, rx, textOffsetY }` |
| `callout` | `object` | Declarative callout: `{ x, y, w, h, lines: [{text, color, size, weight}], border, dimIds, opacity }` |
| `blocked` | `object` | Declarative blocked indicator: `{ from, to, fromOffset, toOffset, reason }` |
| `rerender` | `object` | Re-render a node with new config: `{ node, type, x, y, cfg, label, sublabel, labelOffset, sublabelOffset, hideAfter }` |

**How phases work internally:**

1. The engine calls `_ph(stepId, phaseNum)` which returns:
   - `{ show: true, anim: true, delay: N }` — Current step: element visible with entrance animation at delay N
   - `{ show: true, anim: false, delay: 0 }` — Past step: element visible, no animation
   - `{ show: false, anim: false, delay: 0 }` — Future step: element hidden

2. The `delay` for phase N is calculated as: `phaseNum * phaseMs / 1000` seconds

3. Elements listed in a phase's `show[]` array **only appear starting from that step**. Once shown, they remain visible in all subsequent steps.

### 7.4 Visibility Rules

An element is visible if its step has been reached (current step >= element's step index):

```
_vis(stepId)  →  this.step >= this._stepIndex[stepId]
```

This means:
- **Step 1 shows ['A', 'B']** — A and B visible from step 1 onward
- **Step 3 shows ['C']** — C visible from step 3 onward, A and B still visible
- **Going back to step 2** — C disappears, A and B remain

### 7.5 Element Appearance Order

The first step+phase where an element ID appears in a `show[]` array determines when it enters the visualization. Each element should appear in exactly one phase's `show[]` array — it persists automatically from that point forward.

```javascript
// Step 1: Router appears
{ show: ['R1'], diff: 'Router deployed' }

// Step 2: Switch and link appear (Router still visible from step 1)
{ show: ['SW1', 'r1-sw1'], diff: 'Switch connected' }
```

### 7.6 Empty Phases

Phases can have empty `show: []` arrays. This is useful when you want to trigger custom render hooks without adding new persistent elements:

```javascript
phases: [
  { show: [], diff: 'RADIUS Request flows through network' },
  { show: [], diff: 'NAC verifies identity' },
  { show: [], diff: 'Access-Accept returns' },
]
```

The animations for these phases come from the step's `onRender` hook instead.

### 7.7 Persistent Visibility with showDuring

For elements that should be visible across a range of steps (not tied to a single phase), use `showDuring`:

```javascript
topo.showDuring(['dashed-line', 'badge'], { from: 'identity', until: 'agent' });
```

The element IDs listed will be visible from step `from` through step `until` (inclusive), regardless of which phases they appear in. This is useful for temporary annotations, highlight lines, or callout badges that span multiple steps.

---

## 8. The Animation System

### 8.1 Link Drawing Animation (stroke-dashoffset)

This is the signature animation — links appear to "draw" from one endpoint to the other.

**How it works:**

1. The line is rendered with `stroke-dasharray="2000"` (a dash length longer than any line)
2. Initially, `stroke-dashoffset` is set to `2000` — the entire dash is offset, making the line invisible
3. The CSS animation `tds-drawStroke` transitions `stroke-dashoffset` from 2000 to 0 over 0.7s
4. As the offset decreases, the line progressively reveals from start to end

**CSS:**
```css
.tds-draw-phase {
  animation: tds-drawStroke 0.7s ease backwards;
}
@keyframes tds-drawStroke {
  from {
    stroke-dashoffset: 2000;
    opacity: 0;
  }
  /* to: stroke-dashoffset: 0 (already set on element) */
}
```

**SVG output for a line link:**
```html
<line x1="280" y1="280" x2="525" y2="148"
  stroke="#b1b9be" stroke-width="2"
  stroke-dasharray="2000" stroke-dashoffset="0"
  class="tds-draw-phase"
  style="animation-delay:0.6s"/>
```

**To implement this for your own custom SVG elements:**

```javascript
// In a custom render function:
const drawLine = `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"
  stroke="${color}" stroke-width="2"
  stroke-dasharray="2000" stroke-dashoffset="0"
  class="tds-draw-phase"
  style="animation-delay:${delay}s"/>`;
```

The `animation-delay` is what creates the staggered effect — each phase has a progressively larger delay.

### 8.2 Phase-In Fade Animation

Nodes and non-drawing elements use a simple fade-in:

```css
.tds-phase-in {
  animation: tds-phaseIn 0.55s ease backwards;
}
@keyframes tds-phaseIn {
  from { opacity: 0; }
}
```

The `backwards` fill mode ensures the element starts invisible before the animation delay elapses.

### 8.3 Animated Particles (SVG animateMotion)

Particles are small circles that continuously travel along a path:

```html
<circle r="3" fill="#01a982" opacity="0.8" filter="url(#tds-bloom)">
  <animateMotion dur="2s" repeatCount="indefinite"
    path="M280,280 L770,280"/>
</circle>
```

- `dur` controls speed (2–3s typical)
- `repeatCount="indefinite"` makes it loop
- `begin="1s"` offsets the start for staggered multi-particle effects
- `filter="url(#tds-bloom)"` adds a soft glow

### 8.4 Ambient Background Animations

The canvas background has subtle pulsing overlays:

```html
<rect width="1050" height="700" fill="url(#tds-ambientGreen)">
  <animate attributeName="opacity" values=".5;.8;.5"
    dur="12s" repeatCount="indefinite"/>
</rect>
```

Three ambient layers (green, purple, blue) pulse at different rates (12s, 16s, 10s) to create organic depth.

### 8.5 Pulsing Elements

For attention-drawing effects (like blocked indicators):

```html
<ellipse rx="22" ry="22" fill="#fc6161" opacity=".06">
  <animate attributeName="opacity" values=".04;.1;.04"
    dur="1.5s" repeatCount="indefinite"/>
</ellipse>
```

### 8.6 Reduced Motion Support

The engine respects `prefers-reduced-motion`:

```javascript
this.reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
```

When enabled:
- All `animateMotion` particles are suppressed
- Draw animations are skipped (elements appear immediately)
- Auto-play is disabled
- Mode is forced to `manual`

### 8.7 Glow-Depth Lift

Focused nodes use an advanced SVG filter chain (`tds-depth-lift`) that makes them physically "lift" off the canvas:

- `feGaussianBlur` creates a soft drop shadow beneath the node
- `feColorMatrix` tints the shadow with the node's accent color
- `feComposite` blends the shadow with colored bloom

This creates a 3D depth effect where focused elements appear to float above dimmed ones.

### 8.8 Ambient Atmosphere Effects

Three optional background effects add visual richness (all motion-safe with `reducedMotion` checks):

| Effect | Description |
|--------|-------------|
| **Scanning Grid** | Subtle grid lines that scan across the canvas |
| **Floating Data Bits** | Small particles that drift slowly across the background |
| **Pulse Ring Radar** | Concentric rings that pulse outward from a center point |

---

## 9. Focus & Spotlight

The focus system controls visual hierarchy — which elements are bright vs. dimmed.

### How Focus Works

Each step has a `focus` array listing element IDs that should be spotlighted:

```javascript
topo.addStep('services', {
  focus: ['HPE', 'NAC', 'SEA'],  // These stay bright
  // Everything else dims to 35% opacity + blur
});
```

**Behavior:**
- `focus: ['A', 'B']` — A and B at opacity 1.0, everything else at 0.35
- `focus: []` — Nothing focused = everything at full brightness
- Step 0 (no step active) — Everything at full brightness

### Visual Effects

Focused elements get:
- Full opacity (1.0)
- A colored halo filter (e.g. `tds-focus-halo-green`)

Unfocused elements get:
- Reduced opacity (0.35)
- Depth-of-field blur filter (`tds-dof-blur`)

### Focus for Links

Links automatically compute their opacity from both endpoints:
```javascript
op = Math.min(this._dimFor(linkCfg.from), this._dimFor(linkCfg.to))
```
A link is only bright if both its source and destination are focused.

---

## 10. Custom Render Hooks

For complex animations that go beyond simple show/hide, use custom render hooks.

### 10.1 Phase-Level Custom

Add a `custom` property to a phase object:

```javascript
phases: [
  {
    show: ['NAC'],
    diff: 'Cloud NAC appears',
    custom: (ctx) => {
      const NAC = ctx.pos('NAC');
      return ctx.pw('myStep', 1, ctx.dim('NAC'),
        `<text x="${NAC.x}" y="${NAC.y + 40}" text-anchor="middle"
          fill="#7764fc" font-size="7.5" opacity=".6">IdP: Entra / Okta</text>`
      );
    }
  },
]
```

### 10.2 Step-Level Custom (onRender)

For more complex per-step rendering, use `onRender`:

```javascript
topo.addStep('identity', {
  act: 'a3',
  name: 'Network Gate',
  focus: ['HOST', 'SSW', 'SEA', 'NAC'],
  phases: [
    { show: [], diff: 'RADIUS Request flows' },
    { show: [], diff: 'NAC verifies identity' },
    { show: [], diff: 'Access-Accept returns' },
  ],
  onRender: (ctx) => {
    let svg = '';
    const HOST = ctx.pos('HOST'), NAC = ctx.pos('NAC');

    // Draw animated flow path
    const path = `M${HOST.x},${HOST.y} L${NAC.x},${NAC.y}`;
    svg += ctx.renderFlow('identity', 0, path, '#deb146',
      'RADIUS Request', 400, 200, ctx.dim('HOST'));

    // Conditional rendering based on what's visible
    if (ctx.vis('identity') && ctx.ph('identity', 2).show) {
      svg += ctx.pw('identity', 2, 1,
        `<text x="400" y="300" fill="#05cc93" font-size="10">
          Access Granted ✅</text>`
      );
    }

    return svg;
  },
});
```

---

## 11. The Render Context (ctx)

Both `phase.custom` and `step.onRender` receive a context object with these helpers:

### Position & Visibility

| Method | Returns | Description |
|--------|---------|-------------|
| `ctx.pos(id)` | `{ x, y }` | Get position of a node or anchor |
| `ctx.vis(stepId)` | `boolean` | Has this step been reached? (step >= stepIndex) |
| `ctx.dim(id)` | `number` | Opacity for element (1.0 if focused, 0.35 if not) |
| `ctx.dimAll(...ids)` | `number` | Minimum opacity across multiple elements |
| `ctx.pathThrough(...ids)` | `string` | SVG path (M...L...) through sequence of node/anchor positions |
| `ctx.pathBetween(fromId, toId, opts?)` | `string` | Cubic bezier SVG path between two elements. `opts.bulge` controls curve amount |

### Phase Control

| Method | Returns | Description |
|--------|---------|-------------|
| `ctx.ph(stepId, phaseNum)` | `{ show, anim, delay }` | Phase visibility and animation state |
| `ctx.pw(stepId, phaseNum, opacity, svgString)` | `string` | Wrap SVG in a phase-aware `<g>` with animation/opacity |

### Render Helpers

| Method | Description |
|--------|-------------|
| `ctx.renderFlow(stepId, phaseNum, path, color, label, lx, ly, opacity, dots)` | Render an animated flow path |
| `ctx.renderLine(stepId, phaseNum, x1, y1, x2, y2, color, opacity, dashed)` | Render a simple line |
| `ctx.renderTunnel(stepId, phaseNum, x1, y1, x2, y2, color, label, opacity, dots)` | Render a tunnel |
| `ctx.renderWG(stepId, phaseNum, x1, y1, x2, y2, label, opacity, dots)` | Render a WireGuard tunnel |
| `ctx.renderBlocked(stepId, phaseNum, x1, y1, x2, y2, reason)` | Render a blocked indicator |
| `ctx.renderCallout(stepId, phaseNum, x, y, w, h, lines, borderColor, opacity)` | Render a callout badge (see lines format below) |
| `ctx.renderPoE(stepId, phaseNum, x1, y1, x2, y2, color, label, opacity)` | Render a Power over Ethernet link with lightning bolt icon |
| `ctx.renderOptical(stepId, phaseNum, x1, y1, x2, y2, color, label, opacity)` | Render a fiber optic link with laser warning triangle icon |
| `ctx.renderNode(type, x, y, cfg)` | Render a node by type |

### Callout Lines Format

The `lines` parameter for `ctx.renderCallout()` is an array of line objects:

```javascript
ctx.renderCallout('myStep', 0, 100, 200, 180, 40, [
  { text: 'Header Text', color: '#01a982', size: '10', weight: '700' },
  { text: 'Detail text', color: '#b1b9be', size: '8', weight: '400' },
], '#01a982', 1);
```

| Property | Default | Description |
|----------|---------|-------------|
| `text` | — | The text content for this line |
| `color` | `'#b1b9be'` | Text fill color |
| `size` | `'8'` | Font size in pixels |
| `weight` | `'400'` | Font weight (e.g. `'400'`, `'600'`, `'700'`) |

### Other Properties

| Property | Type | Description |
|----------|------|-------------|
| `ctx.step` | `number` | Current step number (1-based) |
| `ctx.stepId` | `string` | Current step's ID |
| `ctx.reducedMotion` | `boolean` | Whether reduced motion is active |
| `ctx.anim` | `boolean` | Whether animations should play (phase-level only) |
| `ctx.delay` | `number` | Animation delay in seconds (phase-level only) |

---

## 12. Playback Modes

Set via the toolbar dropdown or constructor:

### Manual (default)
- Advance/retreat one step at a time via buttons or keyboard
- Play button advances one step then stops

### Auto
- Play button starts continuous advancement
- Each step plays for `speedMs` milliseconds (configurable 2s–7s via slider)
- Stops when all steps are completed
- Pause button stops advancement

### Presenter
- Like Auto, but **pauses at act boundaries**
- When reaching the first step of a new act, playback stops
- Click Play again to continue into the next act
- Useful for presentations where you want to pause between sections

### Play Behavior in Manual Mode

Calling `topo.play()` while in Manual mode automatically switches the mode to Auto and begins playback. The mode selector in the toolbar updates to reflect this change, and the speed slider becomes visible. If `play()` is called when the current step is the last step, playback wraps back to step 0 before starting.

When `prefers-reduced-motion` is active, `play()` does nothing and auto-play is disabled entirely.

### Programmatic Control

```javascript
topo.play();       // Start playback (switches to auto if in manual mode)
topo.pause();      // Stop playback
topo.next();       // Advance one step
topo.prev();       // Go back one step
topo.reset();      // Go to step 0
topo.goTo(5);      // Jump to step 5
topo.render();     // Force a re-render of the current state
```

### Chaining API

All registration methods (`node`, `anchor`, `link`, `act`, `addStep`, `glossary`, `mount`) return `this`, enabling method chaining:

```javascript
const topo = new TopologyDesigner({ title: 'My Net' })
  .node('R1', { type: 'router', x: 200, y: 200, label: 'Router' })
  .node('SW1', { type: 'switch', x: 400, y: 200 })
  .link('r1-sw1', { type: 'line', from: 'R1', to: 'SW1', color: '#01a982' })
  .act('a1', { label: 'Act 1', color: '#01a982' })
  .addStep('deploy', { act: 'a1', name: 'Deploy', goal: 'Set up.', phases: [{ show: ['R1', 'SW1', 'r1-sw1'] }] })
  .mount('app');
```

---

## 13. Keyboard Shortcuts

| Key | Action |
|-----|--------|
| `Space` | Play / Pause |
| `→` (Right Arrow) | Next step |
| `←` (Left Arrow) | Previous step |
| `Home` | Reset to step 0 |
| `End` | Jump to last step |
| `G` | Open glossary |
| `N` | Toggle narrator panel |
| `I` | Toggle node/anchor ID labels |
| `?` | Show keyboard shortcuts cheat sheet |
| `F5` | Enter presentation mode |
| `F` | Toggle fullscreen while presenting |
| `L` | Toggle layout edit mode |
| `Escape` | Exit link / layout / presentation mode, hide properties panel |

### Editor-Specific Shortcuts

| Key | Action |
|-----|--------|
| `T` | Insert text label |
| `Delete` / `Backspace` | Delete the current selection (single or multi) |
| `]` | Bring selected element forward |
| `[` | Send selected element backward |
| `Ctrl+]` | Bring selected element to front |
| `Ctrl+[` | Send selected element to back |
| `Ctrl+G` | Group selection into a zone |
| `Ctrl+Shift+G` | Ungroup the enclosing zone |
| `Ctrl+L` | Lock / unlock the current selection |
| `Ctrl+Shift+C` | Copy format from selected element |
| `Ctrl+Shift+V` | Paste format to selected element |
| `Arrow Keys` | Move selected element 1px |
| `Shift+Arrow Keys` | Move selected element 10px |
| `Ctrl+Z` | Undo |
| `Ctrl+Shift+Z` | Redo |
| `Shift+C` | Open Choreographer Builder |

---

## 14. Glossary System

Register terms that users can reference:

```javascript
topo.glossary([
  { t: 'SSE', d: 'Security Service Edge: cloud-delivered security.' },
  { t: 'ZTNA', d: 'Zero Trust Network Access: per-app access control.' },
  { t: 'NAC', d: 'Network Access Control: 802.1X identity at the edge.' },
]);
```

Opens as a modal overlay. Each term is displayed with its title in green and description in white.

---

## 15. URL State

The engine automatically persists state in the URL query string:

```
?step=3&mode=auto
```

- `step` — Current step number (0 = reset)
- `mode` — Playback mode (manual/auto/presenter)

This enables:
- Sharing a direct link to a specific step
- Browser back/forward navigation
- Bookmarking specific states

---

## 16. CSS Theme & Tokens

All colors and timing values are CSS custom properties in `:root`:

### Surface Colors
| Token | Value | Usage |
|-------|-------|-------|
| `--tds-bg` | `#1d1f27` | Page background |
| `--tds-panel` | `#22252e` | Panel backgrounds |
| `--tds-panel2` | `#292d3a` | Node fills |
| `--tds-border` | `#3e4550` | Borders |

### Text Colors
| Token | Value | Usage |
|-------|-------|-------|
| `--tds-text` | `#e6e8e9` | Primary text |
| `--tds-muted` | `#7d8a92` | Secondary text |
| `--tds-muted2` | `#606a70` | Tertiary text |
| `--tds-bright` | `#b1b9be` | Highlighted text |

### Accent Palette
| Token | Value | Usage |
|-------|-------|-------|
| `--tds-green` | `#01a982` | Primary accent (EdgeConnect, SD-WAN) |
| `--tds-green-ok` | `#05cc93` | Success, authenticated |
| `--tds-blue` | `#65aef9` | WireGuard, tunnels |
| `--tds-purple` | `#7764fc` | NAC, identity |
| `--tds-gold` | `#deb146` | Apps, database |
| `--tds-red` | `#fc6161` | Blocked, firewall |
| `--tds-orange` | `#ec8c25` | Warning indicators |
| `--tds-teal` | `#00a4b3` | Switches |
| `--tds-pe` | `#068667` | Private Edge |
| `--tds-coral` | `#d25f4b` | SaaS, internet traffic |

### Glassmorphism
| Token | Value | Description |
|-------|-------|-------------|
| `--tds-glass` | `rgba(34,37,46,.55)` | Glass panel background |
| `--tds-glass-border` | `rgba(255,255,255,.06)` | Glass border |
| `--tds-glass-highlight` | `rgba(255,255,255,.03)` | Inner glass highlight glow |
| `--tds-glass-blur` | `16px` | Backdrop blur amount |
| `--tds-glass-saturate` | `1.4` | Backdrop saturation boost |

### Glow Colors
| Token | Value | Description |
|-------|-------|-------------|
| `--tds-glow-green` | `rgba(1,169,130,.35)` | Green ambient glow |
| `--tds-glow-purple` | `rgba(119,100,252,.25)` | Purple ambient glow |
| `--tds-glow-blue` | `rgba(101,174,249,.2)` | Blue ambient glow |

### Timing
| Token | Value | Usage |
|-------|-------|-------|
| `--tds-phase-ms` | `600ms` | Default phase delay |
| `--tds-transition` | `0.55s ease` | Standard fade-in transition |
| `--tds-transition-fast` | `0.35s ease` | Fast transition (auto-computed as ~64% of `--tds-transition`) |
| `--tds-draw-duration` | `0.7s` | Stroke draw-in animation duration |

These timing tokens are automatically updated when you adjust the toolbar sliders or set `phaseMs`, `drawDuration`, or `fadeDuration` programmatically. The engine calls `_applyCSSTimings()` to push values into the CSS custom properties so all animations update in real time.

---

## 17. SVG Filters & Visual Effects

All visual effects are SVG filters defined in `<defs>`, ensuring they work in exported standalone SVGs.

### Glow Filters
| Filter ID | Description |
|-----------|-------------|
| `tds-glow` | Subtle 2.2px gaussian blur |
| `tds-glow-strong` | 4px blur, doubled merge |
| `tds-glow-green` | 6px blur + green flood (#01a982) |
| `tds-glow-blue` | 6px blur + blue flood (#65aef9) |
| `tds-glow-purple` | 5px blur + purple flood (#7764fc) |
| `tds-glow-red` | 5px blur + red flood (#fc6161) |
| `tds-glow-gold` | 4px blur + gold flood (#deb146) |
| `tds-bloom` | 3px blur, doubled — used for LED/particle effects |

### Focus Halos
| Filter ID | Description |
|-----------|-------------|
| `tds-focus-halo-green` | 8px green halo for focused nodes |
| `tds-focus-halo-blue` | 8px blue halo |
| `tds-focus-halo-purple` | 7px purple halo |
| `tds-focus-halo-gold` | 7px gold halo |

### Depth-of-Field & Depth Lift
| Filter ID | Description |
|-----------|-------------|
| `tds-dof-blur` | 1.8px blur for unfocused elements |
| `tds-dof-blur-strong` | 2.8px blur for heavily unfocused |
| `tds-depth-lift` | Advanced filter chain: gaussian blur + color matrix + composite for 3D depth lift on focused nodes |

### Gradients
| Gradient ID | Description |
|-------------|-------------|
| `tds-labelGlass` | Dark vertical gradient for label backgrounds |
| `tds-ambientGreen` | Radial green glow at 20%, 35% |
| `tds-ambientPurple` | Radial purple glow at 80%, 70% |
| `tds-ambientBlue` | Radial blue glow at 55%, 10% |
| `tds-vignette` | Radial edge darkening |
| `tds-grid` | 24x24 dot grid pattern |

---

## 18. The Visual Editor

The file `editor.html` provides a full-featured visual editor for creating topologies without writing code.

### Editor Layout

| Zone | Description |
|------|-------------|
| **Top Toolbar** | Grouped dropdown menus: Core tools, Insert, View, Select, File |
| **Left Sidebar** | Act/Step navigation tree |
| **Center Canvas** | SVG editing area with grid and snap-to-grid |
| **Right Properties** | Inspector for selected node/link/anchor |
| **Bottom Panel** | Collapsible choreography panel with timeline UI |

### Toolbar Organization

The toolbar groups related actions into dropdown menus:

| Group | Contents |
|-------|----------|
| **Core Tools** | Select, +Node, +Link, Hand (always visible) |
| **Insert** | Multi-Link, Anchor, Act, Step, Guides |
| **View** | Guides toggle, Grid, Preview, Choreographer Builder, Line Style, Auto Layout |
| **Select** | Select-by-type functionality |
| **File** | Export HTML, Import HTML, Diagram-as-Code, Preview, Demo, Templates, Theme, GIF Export, Clear |
| **Undo/Redo** | Always visible, with click-and-hold history popover |

### Core Features

- **Drag-and-drop** node placement on a grid-snapped canvas
- **Node palette** — 18+ node types including shapes, with type-specific options
- **Link creation** — select source and destination, choose from 9 link types
- **Link type selection** — line, tunnel, wireguard, flow, packet, blocked, wifi, poe, optical
- **Property editing** — change node types, colors, labels, variants, dash styles via the inspector
- **Anchor placement** with optional labels and offset positioning
- **Double-click inline editing** — edit link labels directly on the canvas

### Undo/Redo

Unlimited undo/redo history:
- **Undo**: Ctrl+Z
- **Redo**: Ctrl+Shift+Z
- **History popover**: Click and hold on undo/redo buttons to see a scrollable list of changes. Hover to highlight affected range; click to jump to any point.

### Canvas Interaction

- **Hand mode**: Left-click-hold (300ms) on empty canvas to pan. Double-click to return to select mode.
- **Arrow key movement**: Move selected nodes/anchors by 1px (or 10px with Shift). Alignment guides display during movement. Locked elements are skipped.
- **Alignment guides**: Horizontal and vertical snap guides appear when nodes align with other nodes or anchors.
- **Multi-select delete**: Select multiple items with Shift+click or marquee, then press **Delete** / **Backspace** to delete them in one operation. Cascades remove dependent links, zones, markers, flow paths, and anchors in a single undo entry.
- **Batch operations**: Shift+click to multi-select, then use:
  - Align Horizontal / Align Vertical
  - Distribute Horizontal / Distribute Vertical
  - **Group into Zone** (creates an overlayCloud around selected nodes)
  - **Ungroup** (removes the enclosing zone wrapper; keeps nodes)
  - **Lock / Unlock** (toggles the `locked` flag on every element in the selection)
- **Copy/Paste Format**: Right-click context menu (or Ctrl+Shift+C / Ctrl+Shift+V) to copy visual properties (color, type, dash style, animation) from one element and apply to another.
- **Z-order**: Right-click context menu or `]`/`[`/`Ctrl+]`/`Ctrl+[` to control stacking order.

### Lock / Unlock

Any node, link, anchor, zone, or marker can be locked to freeze its position and shape. Locked elements:

- Cannot be moved via drag or arrow keys
- Cannot be resized
- Still participate in choreography, export, and analysis
- Show a padlock glyph in the properties header

Lock an individual element via its properties header toggle, or lock/unlock a multi-selection via the batch toolbar. Keyboard shortcut: **Ctrl/Cmd + L**.

### Group / Ungroup

- **Group** wraps the current multi-selection in an `overlayCloud` zone so related nodes move as a unit. Shortcut: **Ctrl/Cmd + G** (when multiple items are selected).
- **Ungroup** removes the enclosing overlay wrapper while preserving the child nodes and links. Shortcut: **Ctrl/Cmd + Shift + G**.

Both operations are single undo entries.

### Layer Panel

The layers panel (View → Layers) lists every layer defined on the topology and lets you:

- **Toggle visibility** — click the eye icon to hide or show all elements on the layer
- **Rename** — double-click the layer name to edit it in place
- **Change color** — click the color swatch to open a color picker with presets and a custom hex input
- **Lock** — padlock icon freezes all elements on the layer
- **Per-step overrides** — link a step's `layerVisibility` map to this layer to hide/show it during specific steps

Layer reorder is drag-and-drop; deleting a layer prompts to reassign orphan elements to a default layer.

### Visual Choreographer Builder

A full-screen overlay for building animation sequences intuitively, inspired by PowerPoint's Animation Pane. Access via **Shift+C**, **View > Choreographer Builder**, or the purple **Choreographer Builder** button in the bottom choreography panel.

#### Three-Tab Workflow

| Tab | Purpose |
|-----|---------|
| **Nodes** | Shows all placed nodes with type icons, labels, sublabels, and (x,y) positions. Click **+** on any card to add it to the animation sequence. |
| **Links** | Shows all links with endpoint labels (e.g. "Branch Router → Cloud Gateway"), link type, and color coding. Click **+** to add. |
| **Sequence** | PowerPoint-style ordered animation list. Drag items to reorder. Click the effect badge to change animation type. |

#### Element Cards

Unlike the old element picker that showed only raw IDs (N1, N2, L1), the Choreographer Builder shows rich context for every element:

- **Nodes**: Type icon + label + node type + coordinates (e.g. `⬢ Core Router — router (400, 250)`)
- **Links**: Endpoint labels + link type (e.g. `⤳ Branch Router → Cloud Gateway — tunnel`)
- **Search/filter**: Type in the search box to filter by any property

#### Animation Effects

Each element in the sequence can have one of these effects:

| Effect | Visual | Best For |
|--------|--------|----------|
| **Appear** | Fade in | Nodes, general elements |
| **Draw** | Stroke animation | Links, connections |
| **Wipe Left** | Slide from right | Horizontal reveals |
| **Wipe Up** | Slide from bottom | Vertical reveals |
| **Zoom** | Scale up from center | Emphasis elements |
| **Pop** | Bounce-scale entrance | Attention-grabbing items |

#### Inline Preview

The right side of the builder is a **live preview canvas** that updates as you build the sequence. Preview controls at the bottom:

- **|«** — Reset to start
- **«** — Previous step
- **»** — Next step
- **»|** — Jump to end
- **Play** — Auto-advance through all steps (1.2s per step)

This eliminates the need to toggle Editor Preview mode to see your choreography.

#### Auto-Sequence

Click **Auto-Sequence** to automatically order all nodes (top-to-bottom by Y position, then X) followed by all links (ordered by when both endpoints become visible). A quick starting point you can then refine by dragging.

#### Apply & Close

Click **Apply & Close** to convert the sequence into acts, steps, and phases. Each sequence item becomes its own step, with nodes using "appear" phases and links using "draw" phases. Focus is auto-assigned per step.

### Enhanced Element Picker

The existing phase element picker (the **+** button in the bottom choreography table) now shows rich descriptions:

- **Nodes** display as `⬢ Core Router — router` with type and coordinate tooltips
- **Links** display as `⤳ Branch Router → Cloud Gateway — tunnel` with endpoint labels
- **Focus dropdowns** and **phase tags** also show labels instead of bare IDs

This fixes the N1/N2/N3 correlation problem throughout the editor.

### Timeline UI

A video-editor-style visual representation of acts, steps, and phases:
- Compact and expanded view modes
- Drag-to-reorder phases within steps
- Visual overview of the entire choreography

### Export & Import

| Feature | Description |
|---------|-------------|
| **Export HTML** | Standalone HTML file with embedded topology code |
| **Import HTML** | Load a previously exported topology |
| **Diagram-as-Code Export** | Generate Mermaid-esque text representation |
| **Diagram-as-Code Import** | Import from text format with syntax reference |
| **Animated GIF Export** | High-quality animated GIF with real choreography frames |
| **Template Gallery** | Visual card-based browser for 7 built-in templates |
| **Theme Selector** | Dropdown with color swatch previews |

### File System Access API

The editor supports live syncing to disk via the File System Access API:
- Auto-saves every 2 seconds when a file handle is active
- Change detection via hash comparison (only writes on actual changes)
- Visual sync status indicator in the toolbar

### Tablet & Touch Support

- Single-finger touch maps to mousedown/mousemove/mouseup for node dragging
- Two-finger pinch-to-zoom with simultaneous pan
- `touch-action: none` on canvas prevents browser scroll/zoom interference
- Touch support for panel resize handles

---

## 19. Responsive Layout

The design system adapts to smaller viewports with a responsive breakpoint at **980px**.

### Desktop Layout (> 980px)

```
┌──────────────────────────────────────────────┐
│                   Top Bar                     │
├────────┬─────────────────────────────────────┤
│        │            Toolbar                   │
│Sidebar ├─────────────────────────┬───────────┤
│ (230px)│       Canvas (flex)     │ Narrator  │
│        │                         │  (280px)  │
│        ├─────────────────────────┴───────────┤
│        │            Controls                  │
└────────┴─────────────────────────────────────┘
```

- Sidebar has a fixed width of 230px with a subtle 3D perspective tilt
- Canvas stretches to fill available space
- Narrator is 280px wide, collapsible

### Mobile Layout (≤ 980px)

At the 980px breakpoint, the layout stacks vertically:

- **Sidebar** becomes full-width with a max-height of 260px (scrollable)
- **Canvas** removes 3D perspective transforms
- **Narrator** becomes full-width below the canvas
- When narrator is collapsed on mobile, it expands to show content (unlike desktop where it collapses to a thin strip)
- **Controls** stretch to full width and wrap as needed

All interactive features (keyboard shortcuts, anchor dragging, properties panel) remain functional at mobile sizes.

---

## 20. Accessibility

The design system includes several accessibility features:

### Reduced Motion Support

The engine respects the `prefers-reduced-motion: reduce` media query:

```javascript
this.reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
```

When reduced motion is active:
- All `<animateMotion>` particles are suppressed (no animated dots on links)
- Stroke draw animations (`tds-draw-phase`) are skipped — elements appear immediately
- SVG ambient light pulsing animations are not rendered
- Auto-play is disabled — calling `play()` does nothing
- Mode is forced to `manual`
- The Play button is disabled with a tooltip explaining why

### Keyboard Navigation

Full step-by-step navigation is available without a mouse:

| Key | Action |
|-----|--------|
| `Space` | Play / Pause |
| `→` (Right Arrow) | Next step |
| `←` (Left Arrow) | Previous step |
| `Home` | Reset to step 0 |
| `End` | Jump to last step |
| `G` | Open glossary modal |
| `N` | Toggle narrator panel |
| `I` | Toggle node/anchor ID labels |
| `Escape` | Exit link mode, hide properties panel |

Keyboard handlers are automatically disabled when focus is on `<input>`, `<select>`, or `<textarea>` elements to avoid interfering with text input.

### Focus Indicators

Interactive elements (buttons, inputs, links) support standard browser focus rings for keyboard-tab navigation.

### ARIA Support

The glossary modal uses `role="dialog"` and `aria-modal="true"` attributes for proper screen reader announcement.

### Semantic HTML

The UI uses standard semantic elements (`<button>`, `<input>`, `<select>`, `<h1>`) rather than non-semantic `<div>` wrappers for interactive controls.

### Color Contrast

The dark theme (`--tds-bg: #1d1f27`) with light text (`--tds-text: #e6e8e9`) maintains readable contrast ratios. Accent colors are used for emphasis rather than conveying essential information alone.

### Node ID Display

The `I` keyboard shortcut (or `toggleNodeIds()` method) overlays element IDs on the diagram in red text, useful for debugging and understanding the topology structure without needing to inspect the source code.

---

## 21. Complete Example: Building a Topology from Scratch

Here's a full example demonstrating all major features:

```javascript
const topo = new TopologyDesigner({
  title: 'Data Center Network',
  subtitle: 'Core · Distribution · Access',
  viewBox: '0 0 900 600',
  phaseMs: 500,
});

// ── NODES ──

// Core layer
topo.node('CORE1', { type: 'router', x: 350, y: 100, label: 'Core-1', color: '#01a982' });
topo.node('CORE2', { type: 'router', x: 550, y: 100, label: 'Core-2', color: '#01a982' });

// Distribution
topo.node('FW1', { type: 'firewall', x: 250, y: 250, label: 'Firewall' });
topo.node('SW1', { type: 'switch', x: 450, y: 250 });
topo.node('SW2', { type: 'switch', x: 650, y: 250 });

// Access
topo.node('SRV1', { type: 'server', x: 350, y: 400, label: 'App Server', color: '#01a982' });
topo.node('DB1', { type: 'database', x: 550, y: 400, label: 'Database' });
topo.node('HOST1', { type: 'host', x: 150, y: 400, label: 'Admin' });

// External
topo.node('INET', { type: 'cloud', x: 450, y: 30, label: 'Internet', color: '#b1b9be' });

// ── LINKS ──

topo.link('inet-core1', { type: 'line', from: 'INET', to: 'CORE1', color: '#b1b9be' });
topo.link('inet-core2', { type: 'line', from: 'INET', to: 'CORE2', color: '#b1b9be' });
topo.link('core-core', { type: 'tunnel', from: 'CORE1', to: 'CORE2', color: '#01a982', label: 'iBGP', dots: true });
topo.link('core1-fw', { type: 'line', from: 'CORE1', to: 'FW1', color: '#fc6161' });
topo.link('core1-sw1', { type: 'line', from: 'CORE1', to: 'SW1', color: '#01a982' });
topo.link('core2-sw2', { type: 'line', from: 'CORE2', to: 'SW2', color: '#01a982' });
topo.link('sw1-srv', { type: 'line', from: 'SW1', to: 'SRV1', color: '#01a982' });
topo.link('sw2-db', { type: 'line', from: 'SW2', to: 'DB1', color: '#deb146' });
topo.link('fw-host', { type: 'line', from: 'FW1', to: 'HOST1', color: '#fc6161' });

// ── ACTS & STEPS ──

topo.act('a1', { label: 'Act 1 · Core', color: '#01a982', intro: ['The backbone of the data center.'] });
topo.act('a2', { label: 'Act 2 · Services', color: '#deb146', intro: ['Application and data tiers.'] });
topo.act('a3', { label: 'Act 3 · Access', color: '#fc6161', intro: ['Security and user access.'] });

// Act 1 steps
topo.addStep('core', {
  act: 'a1', name: 'Core Routers', goal: 'Deploy redundant core routing.',
  focus: ['CORE1', 'CORE2'],
  phases: [
    { show: ['INET'], diff: 'Internet cloud' },
    { show: ['CORE1', 'inet-core1'], diff: 'Core-1 with uplink' },
    { show: ['CORE2', 'inet-core2'], diff: 'Core-2 with uplink' },
    { show: ['core-core'], diff: 'iBGP peering tunnel' },
  ],
});

// Act 2 steps
topo.addStep('distro', {
  act: 'a2', name: 'Distribution Layer', goal: 'Switches connect core to services.',
  focus: ['SW1', 'SW2', 'SRV1', 'DB1'],
  phases: [
    { show: ['SW1', 'core1-sw1'], diff: 'Switch 1 connects to Core-1' },
    { show: ['SW2', 'core2-sw2'], diff: 'Switch 2 connects to Core-2' },
    { show: ['SRV1', 'sw1-srv'], diff: 'App server behind Switch 1' },
    { show: ['DB1', 'sw2-db'], diff: 'Database behind Switch 2' },
  ],
});

// Act 3 steps
topo.addStep('security', {
  act: 'a3', name: 'Firewall & Access', goal: 'Secure admin access through firewall.',
  focus: ['FW1', 'HOST1'],
  phases: [
    { show: ['FW1', 'core1-fw'], diff: 'Firewall deployed' },
    { show: ['HOST1', 'fw-host'], diff: 'Admin workstation via firewall' },
  ],
});

// ── GLOSSARY ──

topo.glossary([
  { t: 'iBGP', d: 'Internal BGP: routing protocol between core routers in the same AS.' },
  { t: 'Distribution', d: 'Network layer providing policy-based connectivity between access and core.' },
]);

// ── MOUNT ──

topo.mount('app');
```

---

## 22. Implementing Link Draw Animations

This section explains exactly how to replicate the link-drawing-from-one-end-to-the-other animation in your own custom code.

### The Core Technique: stroke-dashoffset Animation

The trick relies on SVG's `stroke-dasharray` and `stroke-dashoffset` properties:

1. Set `stroke-dasharray="2000"` — this creates a single dash of 2000px followed by a 2000px gap
2. Set `stroke-dashoffset="0"` on the element (the final state)
3. Animate from `stroke-dashoffset: 2000` to `0` — as the offset decreases, the dash slides into view, creating a "drawing" effect

### Step-by-Step Implementation

#### 1. Basic Drawing Line

```html
<svg viewBox="0 0 800 400">
  <style>
    .draw-line {
      animation: drawStroke 0.7s ease backwards;
    }
    @keyframes drawStroke {
      from {
        stroke-dashoffset: 2000;
        opacity: 0;
      }
    }
  </style>
  <line x1="100" y1="200" x2="700" y2="200"
    stroke="#01a982" stroke-width="2"
    stroke-dasharray="2000" stroke-dashoffset="0"
    class="draw-line"/>
</svg>
```

#### 2. Drawing with Delay (for sequencing)

```html
<!-- First line draws immediately -->
<line ... class="draw-line" style="animation-delay: 0s"/>

<!-- Second line draws after 0.6s -->
<line ... class="draw-line" style="animation-delay: 0.6s"/>

<!-- Third line draws after 1.2s -->
<line ... class="draw-line" style="animation-delay: 1.2s"/>
```

The `backwards` fill mode in the animation ensures the element stays invisible during the delay period.

#### 3. Drawing a Curved Path

The same technique works with SVG `<path>` elements:

```html
<path d="M100,300 C250,100 550,100 700,300"
  fill="none" stroke="#65aef9" stroke-width="2"
  stroke-dasharray="2000" stroke-dashoffset="0"
  class="draw-line" style="animation-delay: 0.6s"/>
```

#### 4. Multi-Layer Drawing (Tunnel Effect)

For richer visuals, layer multiple elements:

```html
<!-- Layer 1: Wide glow background -->
<line x1="100" y1="200" x2="700" y2="200"
  stroke="#65aef9" stroke-width="10" opacity=".03"
  filter="url(#tds-glow)"/>

<!-- Layer 2: Main draw stroke -->
<line x1="100" y1="200" x2="700" y2="200"
  stroke="#65aef9" stroke-width="2.5"
  stroke-dasharray="2000" stroke-dashoffset="0"
  class="draw-line" opacity=".5"/>

<!-- Layer 3: Detail dashes (static) -->
<line x1="100" y1="200" x2="700" y2="200"
  stroke="#65aef9" stroke-width="1"
  stroke-dasharray="6 5" opacity=".3"/>

<!-- Layer 4: Animated particle -->
<circle r="3" fill="#65aef9" opacity=".8" filter="url(#tds-bloom)">
  <animateMotion dur="2s" repeatCount="indefinite"
    path="M100,200 L700,200"/>
</circle>
```

#### 5. Using the Engine's Built-in Methods

In custom render hooks, use the engine's link renderers directly:

```javascript
onRender: (ctx) => {
  let svg = '';

  // Draw a flow from point A to point B when phase 0 is active
  svg += ctx.renderFlow(
    'myStep',           // step ID
    0,                  // phase number
    'M100,200 C300,100 500,100 700,200',  // SVG path
    '#05cc93',          // color
    'Data Flow',        // label text
    400, 140,           // label position (x, y)
    ctx.dim('NODE1'),   // opacity (based on focus)
    true                // show animated dots
  );

  // Draw a simple line in phase 1
  svg += ctx.renderLine(
    'myStep', 1,
    100, 300, 700, 300,
    '#fc6161',
    ctx.dim('NODE2'),
    true  // dashed
  );

  return svg;
}
```

#### 6. Controlling Draw Direction

The line draws from `(x1,y1)` to `(x2,y2)`. To reverse the direction, swap the coordinates:

```javascript
// Draws left-to-right
{ from: 'LEFT_NODE', to: 'RIGHT_NODE' }

// Draws right-to-left (swap from/to)
{ from: 'RIGHT_NODE', to: 'LEFT_NODE' }
```

For paths, reverse the path string:

```javascript
// Forward: draws from HOST to NAC
const forward = `M${HOST.x},${HOST.y} L${NAC.x},${NAC.y}`;

// Return: draws from NAC back to HOST
const returnPath = `M${NAC.x},${NAC.y} L${HOST.x},${HOST.y}`;
```

---

## 23. Plugin System

The engine uses a formal plugin interface for node and link types.

### Registering Node Plugins

```javascript
TopologyDesigner.registerNodeType('myDevice', {
  // Required: render function
  render(x, y, cfg) {
    return `<rect x="${x-20}" y="${y-20}" width="40" height="40" fill="#292d3a" stroke="${cfg.color}" />`;
  },

  // Optional: default configuration values
  defaults: { color: '#01a982', size: 40 },

  // Optional: hit box for click detection
  hitBox(x, y, cfg) {
    return { x: x - 20, y: y - 20, width: 40, height: 40 };
  },

  // Optional: validation
  validate(cfg) {
    return cfg.size > 0;
  },

  // Optional: metadata
  meta: { label: 'My Device', category: 'custom' },

  // Optional: lifecycle hooks
  init(cfg) { /* called on registration */ },
  destroy(cfg) { /* called on removal */ },
});
```

### Registering Link Plugins

```javascript
TopologyDesigner.registerLinkType('myLink', {
  render(fromPt, toPt, cfg, ctx) {
    return `<line x1="${fromPt.x}" y1="${fromPt.y}" x2="${toPt.x}" y2="${toPt.y}"
      stroke="${cfg.color}" stroke-width="2" />`;
  },
  defaults: { color: '#01a982' },
  meta: { label: 'My Link', category: 'custom' },
});
```

### Lifecycle Hooks

The engine supports lifecycle hooks for observing render and step change events:

```javascript
topo.on('beforeRender', (ctx) => {
  // Called before the SVG is rendered
});

topo.on('afterRender', (ctx) => {
  // Called after the SVG is rendered
});

topo.on('onStepChange', ({ prevStep, step, designer }) => {
  // Called when the current step changes
});
```

### Querying Registered Types

```javascript
// Get all registered node types (built-in + plugins)
TopologyDesigner.getRegisteredNodeTypes();
// → ['ec', 'switch', 'switchEnterprise', 'cloud', 'host', 'connector', ...]

// Get all registered link types (built-in + plugins)
TopologyDesigner.getRegisteredLinkTypes();
// → ['line', 'tunnel', 'wireguard', 'flow', 'packet', 'blocked', 'wifi', 'poe', 'optical']
```

---

## 24. SVG Diffing & Incremental Rendering

The engine uses a virtual-DOM-lite approach for efficient rendering:

1. **Dirty element tracking**: On each phase transition, `computeDirtyElements()` identifies which nodes and links need updating.
2. **Incremental render**: `incrementalRender()` only updates changed SVG elements rather than re-rendering the entire canvas.
3. **Single element render**: `renderSingleElement()` updates individual nodes or links in-place.

This significantly improves performance for large topologies where only a few elements change per step.

---

## 25. Smart Link Routing

Links automatically detect and route around obstructing nodes:

1. **AABB collision detection**: Each node's bounding box is checked against the straight-line path of each link.
2. **Auto-detour**: When a collision is detected, the link is rerouted using a quadratic Bezier curve that arcs around the obstructing node.
3. **Dynamic recalculation**: Routes update when nodes are moved or the topology changes.

This prevents links from overlapping with nodes in dense topologies.

---

## 26. Choreography Smoothing

The `setNodePositions()` API enables position keyframes across steps:

```javascript
topo.setNodePositions('step2', {
  'R1': { x: 300, y: 200 },
  'SW1': { x: 500, y: 300 },
});
```

When advancing to a step with position keyframes, nodes animate smoothly to their new positions using cubic ease-in-out interpolation via `requestAnimationFrame`. This enables nodes to move during the narrative, creating dynamic topology animations.

---

## 27. Layout Engine

Seven auto-layout algorithms are available:

| Algorithm | Description |
|-----------|-------------|
| `force-directed` | Physics simulation with repulsion and spring forces for organic layouts |
| `hierarchical` | Top-down layered layout following link direction |
| `circular` | Nodes arranged in a circle |
| `grid` | Regular grid arrangement |
| `magnetic-north` | Type-aware force-directed layout that zones nodes by their role |
| `spine-leaf` | Data-center optimized: high-degree spine nodes on top row, leaf nodes below |
| `cluster` | Groups nodes by connected components; each cluster is laid out circularly |

```javascript
// Via API
topo.autoLayout('force-directed');

// Via modules (if loaded)
const layout = new LayoutEngine(topo);
layout.apply('hierarchical', { spacing: 100, animate: true });
```

Layouts animate transitions when the `animate` option is enabled.

### Magnetic North Layout

A type-aware force-directed algorithm that assigns vertical zones based on node type, producing layouts that reflect real-world network hierarchy (cloud at the top, hosts at the bottom).

```javascript
const positions = LayoutEngine.magneticNorth(nodes, links, {
  width: 1050,
  height: 700,
  iterations: 200,
  padding: 60,
  repulsion: 5000,
  attraction: 0.005,
  damping: 0.92,
  minDistance: 80,
  magnetStrength: 0.3,
});
// Returns a Map of { nodeId: { x, y } }
```

**Options:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `width` | `number` | `1050` | Canvas width |
| `height` | `number` | `700` | Canvas height |
| `iterations` | `number` | `200` | Number of simulation iterations |
| `padding` | `number` | `60` | Edge padding |
| `repulsion` | `number` | `5000` | Repulsion force between nodes |
| `attraction` | `number` | `0.005` | Attraction force along links |
| `damping` | `number` | `0.92` | Velocity damping per iteration |
| `minDistance` | `number` | `80` | Minimum distance between nodes |
| `magnetStrength` | `number` | `0.3` | Strength of the vertical zone pull |

**Zone mapping (vertical position as fraction of height):**

| Zone | Node Types | Y Position |
|------|------------|------------|
| Top | `cloud` (0.12), `overlayCloud` (0.08), `saas` (0.18) | Near top of canvas |
| Center | `router` (0.45), `firewall` (0.42), `switch` (0.48), `ec` (0.55) | Middle of canvas |
| Bottom | `host` (0.82), `database` (0.78), `apps` (0.75) | Near bottom of canvas |

Nodes are attracted toward their zone's Y position by `magnetStrength`, while standard repulsion and attraction forces handle horizontal spacing and link proximity.

---

## 28. Template System

7 built-in templates for common topologies:

| Template | Description |
|----------|-------------|
| `hub-spoke` | Central hub with radial connections |
| `3-tier` | Classic three-tier architecture (core/distribution/access) |
| `mesh` | Full or partial mesh interconnection |
| `star` | Star topology with central node |
| `spine-leaf` | Data center spine-leaf fabric |
| `sd-wan` | SD-WAN overlay architecture |
| `firewall-dmz` | Firewall with DMZ zones |

```javascript
topo.loadTemplate('spine-leaf');
```

### Diagram-as-Code

Import topologies from a Mermaid-esque text format:

```javascript
topo.importFromText(`
  R1[router] -- line --> SW1[switch]
  SW1 -- tunnel --> SW2[switch]
  SW2 -- line --> SRV1[server]
`);
```

The editor includes a Diagram-as-Code modal with syntax reference and both import/export capabilities.

---

## 29. Theme Engine

5 built-in color presets plus custom palette generation:

```javascript
// Apply a built-in theme
topo.applyTheme('default-dark');  // Green accent, dark background
topo.applyTheme('midnight');     // Subdued, cool tones
topo.applyTheme('charcoal');     // High contrast
topo.applyTheme('corporate');    // Professional blue
topo.applyTheme('warm-dark');    // Orange/amber tones
```

Features:
- **Brand color generation**: Generate a full palette from a single brand color
- **WCAG contrast checking**: Validates that color combinations meet accessibility contrast ratios
- **CSS export**: Export theme as CSS custom properties
- **Runtime switching**: Change themes without re-rendering the topology

---

## 30. Animated GIF Export

The editor can export the topology choreography as an animated GIF:

1. **Real choreography frames**: Creates a temporary `TopologyDesigner` per step, stepping through the actual choreography (step 0 → N) to capture genuinely different frames.
2. **Median-cut quantization**: Samples pixels across multiple frames to build an optimal 256-color palette.
3. **Floyd-Steinberg dithering**: Error diffusion at 40% strength for smooth gradients on dark backgrounds.
4. **Resolution**: 1200px wide for sharp output.
5. **Per-frame timing**: First frame holds 1.5s, last frame holds 3s, intermediate steps at 2.5s each.

Access via **File > Export GIF** in the editor toolbar.

---

## 31. Temporal Digital Twin

The system supports three temporal modes that transform the topology from a static diagram into a live operational view or historical incident replay.

### Temporal Modes

| Mode | Description |
|------|-------------|
| `design` | Default mode. Planned/architectural view with no live data. |
| `operational` | Live metrics mode. Nodes display real-time status, latency, jitter, and loss. |
| `incident` | Historical replay mode. Used for post-mortem analysis of past incidents. |

### API

```javascript
// Switch temporal mode
topo.setTemporalMode('operational');

// Set live metrics on a single element
topo.setElementState('R1', {
  status: 'degraded',
  latency: 45,    // milliseconds
  jitter: 12,     // milliseconds
  loss: 2.5,      // percentage
});

// Bulk-set state data for multiple elements
topo.setStateData({
  'R1': { status: 'healthy', latency: 5, jitter: 1, loss: 0 },
  'FW1': { status: 'degraded', latency: 120, jitter: 30, loss: 1.2 },
  'SW1': { status: 'down', latency: 0, jitter: 0, loss: 100 },
});
```

### Constructor Options

```javascript
new TopologyDesigner({
  temporalMode: 'design',                    // Initial temporal mode
  onTemporalModeChange: (mode) => {          // Callback on mode switch
    console.log('Switched to', mode);
  },
});
```

### Visual Effects by Status

| Status | Visual Effect |
|--------|--------------|
| `healthy` | Green status dot |
| `degraded` | Yellow pulsing ring + jitter SVG filter (displacement map) |
| `down` | Red pulsing ring + desaturate and blur filter |
| `breached` | Red noise displacement filter + red glow |

### Toolbar

A temporal mode dropdown selector appears in the toolbar with three options: Design, Operational, and Incident. Switching modes triggers the `onTemporalModeChange` callback and re-renders the topology with the appropriate visual treatment.

---

## 32. Isometric Tilt Mode

Pseudo-3D orthographic projection that gives topologies a cinematic SOC dashboard aesthetic. When enabled, the entire canvas tilts into an isometric perspective while text labels counter-rotate to stay readable.

### Enabling Isometric Tilt

**Viewer** — Click the **Tilt** button in the toolbar. The button highlights green when active.

**Editor** — Open the **View** menu and select **Isometric Tilt**, or press <kbd>Shift+T</kbd>. The menu item highlights when active.

**Programmatic API:**

```javascript
// Toggle isometric view (returns current boolean state)
const isOn = topo.toggleIsometric();       // Toggle
const isOn = topo.toggleIsometric(true);   // Force enable
const isOn = topo.toggleIsometric(false);  // Force disable
```

### Constructor Option

```javascript
new TopologyDesigner({
  isometricMode: false,   // Start in isometric mode (default: false)
});
```

### CSS Class

The toggle adds the `tds-isometric` class to the canvas container and `tds-isometric-svg` to the SVG element:

```css
/* Canvas tilt */
.tds-canvas.tds-isometric {
  transform: perspective(1200px) rotateX(45deg) rotateZ(-45deg);
  transform-origin: center center;
  transition: transform 0.8s cubic-bezier(.4, 0, .2, 1);
}

/* Subtle hover shift for interactivity */
.tds-canvas.tds-isometric:hover {
  transform: perspective(1200px) rotateX(42deg) rotateZ(-43deg);
}

/* Counter-rotate text labels so they remain readable */
.tds-isometric-svg text {
  transform: rotateZ(45deg) rotateX(-45deg);
  transform-origin: center;
  transform-box: fill-box;
}
```

### How It Works

1. A `perspective(1200px)` creates the 3D projection context.
2. `rotateX(45deg)` tilts the canvas backward (top recedes).
3. `rotateZ(-45deg)` rotates the plane to produce the classic isometric angle.
4. Text elements inside the SVG receive the inverse rotation (`rotateZ(45deg) rotateX(-45deg)`) so labels remain upright and legible.
5. On hover the angles ease slightly (`42deg` / `-43deg`) to provide subtle interactive feedback.
6. The transition uses a `cubic-bezier(.4, 0, .2, 1)` easing over 0.8 seconds for a smooth toggle animation.

### Limitations

- **Drag precision** — In the Editor, node dragging coordinates are unaffected by the visual transform, so nodes move along the original (flat) axes rather than the projected axes. This can feel unintuitive while editing; disable tilt for precise positioning work.
- **Hit testing** — Click targets remain in flat 2D space. Pointer coordinates are not inverse-projected, so elements may appear slightly offset from their click zones at extreme zoom levels.
- **Print / export** — CSS 3D transforms are not captured by SVG-based export. Exported images will show the flat (non-tilted) view.
- **Performance** — The 3D transform triggers GPU compositing. On low-powered devices with very large topologies, enabling tilt may reduce frame rates during pan/zoom.

---

## 33. Intelligence Layer

Smart topology analysis with security validation and blast radius visualization.

### Path Validation

Validate that a path between two nodes meets security rules:

```javascript
const result = topo.validatePath('HOST1', 'SRV1', {
  requireTypes: ['firewall'],   // Path must traverse a firewall node
  forbidDirect: true,           // Direct connections are not allowed
});

// result = {
//   valid: true|false,
//   path: ['HOST1', 'FW1', 'SW1', 'SRV1'],   // Discovered path
//   violations: ['Direct connection without firewall'],  // If invalid
// }
```

### Topology Validation

Validate all links against security rules:

```javascript
const violations = topo.validateTopology({
  requireTypes: ['firewall'],
});
// Returns array of violation objects
// Shows red pulsing halos on violating elements
```

### Security Mode

```javascript
// Toggle security analysis mode
topo.toggleSecurityMode();          // Toggle
topo.toggleSecurityMode(true);      // Force enable
```

In security mode, clicking a node activates blast radius visualization for that node.

### Blast Radius

Visualize the set of nodes reachable from a given node within N hops via BFS traversal:

```javascript
// Get the set of reachable node IDs within 2 hops
const reachable = topo.getBlastRadius('FW1', 2);
// Returns Set of node IDs

// Set the blast radius center node (dims everything outside the radius)
topo.setBlastRadiusNode('FW1');

// Clear blast radius visualization
topo.setBlastRadiusNode(null);
```

### Constructor Option

```javascript
new TopologyDesigner({
  blastRadiusHops: 2,   // Default hop count for blast radius (default: 2)
});
```

### Behavior

- In security mode, clicking a node automatically calls `setBlastRadiusNode()` for that node
- Elements outside the blast radius are dimmed
- Path violations display with red warning icons and tooltip text
- Topology validation shows red pulsing halos on elements that violate the rules

### Toolbar

A "Security" button in the toolbar toggles security analysis mode.

---

## 34. Ghosting Engine

Previous act elements desaturate and blur instead of vanishing, maintaining spatial context across act transitions.

### Constructor Option

```javascript
new TopologyDesigner({
  ghosting: true,   // Enable ghosting (default: true)
});
```

### Behavior

When ghosting is enabled and the topology transitions between acts, elements from previous acts are not hidden. Instead, they automatically receive a ghost treatment:

- **Desaturation**: Reduced to 15% saturation
- **Blur**: Subtle 0.8px blur applied
- **Opacity fade**: Reduced opacity based on act distance (elements from deeper in the past become more transparent, with a minimum opacity of 0.08)
- **SVG filter**: Uses the `tds-ghost` SVG filter for the desaturation and blur effect

This preserves the spatial layout context so viewers can still see where previous infrastructure was placed, while clearly distinguishing current-act elements from past ones.

---

## 35. Conditional Step Logic

State variables and conditional phase evaluation for dynamic topologies. This is part of the Diagram-as-Code 2.0 feature set.

> **See also:** [Flow Paths](#36-flow-paths), [Policy Markers](#37-policy-markers), and [Layer System](#38-layer-system) for additional overlay and grouping features.

### State Variables

```javascript
// Set a state variable
topo.setState('firewall.status', 'breached');

// Get a state variable value
const status = topo.getState('firewall.status');
// → 'breached'
```

### Conditional Phases

Phases can include a `condition` property that is evaluated against the current state. If the condition is falsy, the phase is skipped.

```javascript
topo.addStep('quarantine', {
  act: 'a2',
  name: 'Automated Response',
  goal: 'System reacts to breach detection.',
  focus: ['FW', 'tunnel-01'],
  phases: [
    {
      show: ['tunnel-01'],
      condition: 'firewall.status == "breached"',
      actions: [
        { changeLink: 'tunnel-01', toType: 'blocked', reason: 'Automated Quarantine' },
        { changeNode: 'FW', cfg: { color: '#fc6161' } },
        { setState: { 'quarantine.active': true } },
      ],
    },
  ],
});
```

### Condition Syntax

Conditions are evaluated against the topology state variables. Supported operators:

| Operator | Example | Description |
|----------|---------|-------------|
| `==` | `'firewall.status == "breached"'` | Equality check |
| `!=` | `'quarantine.active != true'` | Inequality check |
| `>` | `'metrics.latency > 100'` | Greater than |
| `<` | `'metrics.loss < 5'` | Less than |
| *(none)* | `'quarantine.active'` | Truthy check |

### Phase Actions

When a conditional phase evaluates to true, its `actions` array is executed in order:

| Action | Description |
|--------|-------------|
| `{ changeLink: id, toType: type, reason?: string }` | Change a link's type (e.g., from `tunnel` to `blocked`) |
| `{ changeNode: id, cfg: { ... } }` | Update a node's configuration (e.g., change color) |
| `{ setState: { key: value, ... } }` | Set one or more state variables |

---

## 36. Flow Paths

Flow paths are animated overlay paths that follow waypoints through the topology, visualizing data flows, traffic patterns, or service chains across multiple nodes.

### Registration

```javascript
topo.flowPath(id, config)
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | `string` | Unique identifier (e.g. `'fp_1'`) |
| `config.waypoints` | `string[]` | Ordered array of node/anchor IDs the path passes through |
| `config.color` | `string` | Path color |
| `config.animation` | `string` | Animation style: `'particles'` \| `'dashed'` \| `'pulse'` (default: `'particles'`) |
| `config.speed` | `number` | Animation speed in seconds (default: 2) |
| `config.direction` | `string` | `'forward'` \| `'reverse'` \| `'bidirectional'` (default: `'forward'`) |
| `config.width` | `number` | Path stroke width |
| `config.opacity` | `number` | Path opacity |
| `config.label` | `string` | Text label for the flow path |
| `config.layer` | `string` | Layer ID this flow path belongs to (default: `'physical'`) |
| `config.name` | `string` | Display name for the flow path |

### Example

```javascript
topo.flowPath('user-traffic', {
  waypoints: ['USER1', 'SW1', 'FW1', 'INET'],
  color: '#01a982',
  animation: 'particles',
  speed: 2,
  direction: 'forward',
  label: 'User Traffic',
  layer: 'flow_1',
});
```

### Bulk Set

```javascript
// Set all flow paths at once (e.g. when loading from saved state)
topo.setFlowPaths([
  ['fp_1', { waypoints: ['A', 'B', 'C'], color: '#01a982' }],
  ['fp_2', { waypoints: ['X', 'Y'], color: '#65aef9' }],
]);
```

---

## 37. Policy Markers

Policy markers are badges placed on nodes to visualize security or routing policy actions such as inspection, allow, deny, redirect, encryption, and more.

### Registration

```javascript
topo.policyMarker(id, config)
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | `string` | Unique identifier (e.g. `'pm_1'`) |
| `config.nodeId` | `string` | The node this marker is attached to |
| `config.type` | `string` | Policy action type (see table below) |
| `config.color` | `string` | Badge color override |
| `config.label` | `string` | Custom label text |
| `config.flowPathId` | `string` | Associated flow path ID (optional) |
| `config.layer` | `string` | Layer ID this marker belongs to |

### Policy Types

| Type | Description |
|------|-------------|
| `inspect` | Deep packet inspection |
| `allow` | Traffic permitted |
| `deny` | Traffic blocked |
| `redirect` | Traffic redirected |
| `encrypt` | Encryption applied |
| `decrypt` | Decryption applied |
| `nat` | Network address translation |
| `load-balance` | Load balancing |
| `log` | Logging/auditing |

### Example

```javascript
topo.policyMarker('pm-fw-inspect', {
  nodeId: 'FW1',
  type: 'inspect',
  color: '#deb146',
  label: 'DPI',
  flowPathId: 'user-traffic',
  layer: 'policy_1',
});
```

### Bulk Set

```javascript
topo.setPolicyMarkers([
  ['pm_1', { nodeId: 'FW1', type: 'inspect', color: '#deb146' }],
  ['pm_2', { nodeId: 'R1', type: 'allow', color: '#01a982' }],
]);
```

---

## 38. Layer System

Layers group topology elements into visual planes that can be independently toggled, faded, and locked. This enables complex multi-layer visualizations where physical infrastructure, traffic flows, and policy overlays coexist without visual clutter.

### Default Layer

Every topology starts with a single default layer:

```javascript
{ id: 'physical', name: 'Physical', type: 'physical', visible: true, opacity: 1, locked: false, color: '#01a982', order: 0 }
```

### Registration

```javascript
topo.layer(id, config)
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | `string` | Unique identifier (e.g. `'flow_1'`, `'policy_1'`) |
| `config.name` | `string` | Display name |
| `config.type` | `string` | Layer type: `'physical'` \| `'flow'` \| `'policy'` |
| `config.visible` | `boolean` | Whether the layer is visible (default: `true`) |
| `config.opacity` | `number` | Layer opacity 0-1 (default: `1`) |
| `config.locked` | `boolean` | Whether elements in this layer can be edited (default: `false`) |
| `config.color` | `string` | Accent color for the layer |
| `config.order` | `number` | Stacking order (higher = on top) |

### Example

```javascript
// Create a flow overlay layer
topo.layer('flow_1', {
  name: 'User Traffic Flow',
  type: 'flow',
  visible: true,
  opacity: 0.8,
  color: '#65aef9',
  order: 1,
});

// Create a policy layer
topo.layer('policy_1', {
  name: 'Security Policies',
  type: 'policy',
  visible: true,
  opacity: 1,
  color: '#fc6161',
  order: 2,
});
```

### Assigning Elements to Layers

Nodes and links can be assigned to a layer via the `layer` property in their configuration:

```javascript
topo.node('FW1', { type: 'firewall', x: 300, y: 200, label: 'Firewall', layer: 'physical' });
topo.flowPath('fp_1', { waypoints: ['A', 'B'], color: '#01a982', layer: 'flow_1' });
topo.policyMarker('pm_1', { nodeId: 'FW1', type: 'inspect', layer: 'policy_1' });
```

### Bulk Set

```javascript
// Replace all layers at once (e.g. when loading from saved state)
topo.setLayers([
  { id: 'physical', name: 'Physical', type: 'physical', visible: true, opacity: 1, locked: false, color: '#01a982', order: 0 },
  { id: 'flow_1', name: 'Traffic', type: 'flow', visible: true, opacity: 0.8, color: '#65aef9', order: 1 },
]);
```

### Per-Step Layer Visibility

Steps can override layer visibility using the `layerVisibility` property:

```javascript
topo.addStep('show-flows', {
  act: 'a1',
  name: 'Traffic Flows',
  goal: 'Visualize traffic patterns.',
  layerVisibility: {
    'flow_1': { opacity: 1 },       // Show flow layer at full opacity
    'policy_1': { opacity: 0.3 },   // Dim policy layer
  },
  phases: [{ show: ['FW1'], diff: 'Firewall visible' }],
});
```

The engine walks backwards from the current step to find the most recent `layerVisibility` override for each layer.

---

## 39. Export (PNG / SVG / PDF / Standalone HTML)

Topology Studio can export the current canvas state as a high-resolution PNG, a vector SVG, or a PDF. Export works from both the visual editor's **File** menu and programmatically via the API.

### API

#### `topo.exportPNG()`

Exports the current step's diagram as a PNG file. The output is rendered at **2400 px wide** (aspect-ratio-preserving), with the dark background (`#1d1f27`) filled in. The file is downloaded automatically with a sanitized version of the topology title as the filename.

```javascript
await topo.exportPNG();
// Downloads: "My_Network.png" at 2400×1371px (for a 1050×700 viewBox)
```

> **Note:** `exportPNG()` is `async` because it rasterises the SVG through an `<img>` + `<canvas>` pipeline. Await it if you need to chain follow-up actions.

#### `topo.exportSVG()`

Exports the current step's diagram as a standalone SVG file with embedded font hints and a background rectangle. The file is downloaded automatically.

```javascript
topo.exportSVG();
// Downloads: "My_Network.svg"
```

The SVG embed includes:
- `<style>` block declaring the JetBrains Mono font family
- `<rect>` background fill `#1d1f27`
- Full inner SVG content at the topology's configured `viewBox` dimensions

#### `topo.exportPDF()`

Opens a print-optimized window containing the current diagram and triggers the browser's **Print** dialog. The user can then choose **Save as PDF** from their OS print dialog to produce a landscape PDF.

```javascript
topo.exportPDF();
// Opens print dialog — user selects "Save as PDF"
```

This approach uses zero external dependencies — it relies entirely on the browser's native print-to-PDF capability. The print view includes:
- `@page { size: landscape; margin: 0 }` for edge-to-edge output
- Dark background preserved (`#1d1f27`)
- SVG rendered at the topology's full `viewBox` dimensions

> **Note:** If your browser blocks the pop-up, allow pop-ups for the page and retry.

### Editor Integration

In the visual editor (`editor.html`):
- **File menu → Export PNG** — exports at 2×+ retina resolution
- **File menu → Export SVG** — exports vector-clean SVG

In the standalone viewer (`viewer.html`), an **Export** button in the top toolbar opens a dropdown with PNG, SVG, PDF, Standalone HTML, Copy Embed Code, and Analytics options.

> **See also:** [Section 48 — Self-Contained HTML Export & Embed](#48-self-contained-html-export--embed) for details on the standalone HTML and embed code features.

### Filename Derivation

Filenames are derived from `topo.title` with non-alphanumeric characters (except `_` and `-`) replaced by underscores. **When a step is active**, the step number and name are appended:

```
# No active step:
title: "SD-WAN / ZTNA Demo" → "SD-WAN___ZTNA_Demo.png"

# Step 3 "Deploy" active:
title: "SD-WAN / ZTNA Demo" → "SD-WAN___ZTNA_Demo_Step_3_Deploy.png"
```

---

## 40. Node Type Browser

The **Node Type Browser** is a searchable modal that shows all registered node types with live SVG previews, descriptions, and click-to-copy code snippets.

### Opening

| Method | Action |
|--------|--------|
| Keyboard shortcut | **Shift + N** in the editor |
| Toolbar button | Click **Shapes** in the editor toolbar |
| Programmatic | `showNodeTypeBrowser()` (editor context) |

### Layout

The browser groups node types by category (e.g. *Core Infrastructure*, *Cloud & SaaS*, *Endpoints*, *Custom*). Each card shows:

- **SVG preview** — rendered via `TopologyDesigner.renderNodePreview(type, 36)` at 36 px
- **Type name** — the string to pass to `topo.node()` as `type`
- **Description** — one-line human label

### Search / Filter

A search box at the top of the modal filters cards in real time by type name or description (case-insensitive substring match). Categories with no visible children are automatically hidden.

### Click-to-Copy

Clicking any card copies a ready-to-use `topo.node()` snippet to the clipboard:

```javascript
topo.node('ID', { type: 'router', x: 200, y: 200 });
```

A brief **"Copied!"** toast confirms the copy.

### Programmatic Preview

You can render individual node previews outside the browser:

```javascript
// Returns an <svg> string at 40×40 px, viewBox "-25 -25 50 50"
const svgHtml = TopologyDesigner.renderNodePreview('firewall', 40);
document.getElementById('my-preview').innerHTML = svgHtml;
```

### Node Type Catalog

Retrieve the full type catalog (same data the browser uses):

```javascript
const catalog = TopologyDesigner.getNodeTypeCatalog();
// Returns: [{ category: 'Core Infrastructure', types: [{ name, description }, ...] }, ...]
```

---

## 41. Link Waypoints & Routing API

Waypoints allow you to bend links through specific points, giving precise control over how connections are drawn. Routing style controls whether the segment between waypoints is drawn straight, orthogonally, or as a curve.

### Methods

#### `topo.addWaypoint(linkId, pos, index?)`

Add a waypoint to an existing link.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `linkId` | `string` | ✓ | ID of the link to modify |
| `pos` | `{ x: number, y: number }` | ✓ | Canvas coordinates of the new waypoint |
| `index` | `number` | — | Insertion index (0-based). Appends if omitted or out of range. |

Returns `this` (chainable). Clears the route cache for the link.

```javascript
// Bend link 'r1-sw1' through two intermediate points
topo.addWaypoint('r1-sw1', { x: 400, y: 150 });        // append
topo.addWaypoint('r1-sw1', { x: 500, y: 200 });        // append
topo.addWaypoint('r1-sw1', { x: 350, y: 100 }, 0);     // insert before index 0
topo.render();
```

#### `topo.removeWaypoint(linkId, index)`

Remove a single waypoint by its 0-based index.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `linkId` | `string` | ✓ | Link ID |
| `index` | `number` | ✓ | 0-based index of the waypoint to remove |

Returns `this`. No-ops if `index` is out of range.

```javascript
topo.removeWaypoint('r1-sw1', 0);  // remove first waypoint
topo.render();
```

#### `topo.clearWaypoints(linkId)`

Remove all waypoints from a link, reverting it to a direct connection.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `linkId` | `string` | ✓ | Link ID |

Returns `this`.

```javascript
topo.clearWaypoints('r1-sw1');
topo.render();
```

#### `topo.setLinkRouting(linkId, style)`

Set the routing style for a link. This controls how the path between waypoints (and endpoints) is drawn.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `linkId` | `string` | ✓ | Link ID |
| `style` | `string` | ✓ | `'straight'` \| `'orthogonal'` \| `'curved'` |

Returns `this`. Clears the route cache.

```javascript
topo.setLinkRouting('r1-sw1', 'orthogonal');
topo.render();
```

### Routing Styles

| Style | Description |
|-------|-------------|
| `straight` | Direct line segments between waypoints (default) |
| `orthogonal` | Right-angle segments (horizontal then vertical) |
| `curved` | Cubic bezier curves through waypoints |

### Waypoints in Link Registration

You can also declare waypoints at link registration time:

```javascript
topo.link('r1-sw1', {
  type: 'line',
  from: 'R1',
  to: 'SW1',
  lineStyle: 'orthogonal',
  waypoints: [
    { x: 350, y: 150 },
    { x: 500, y: 150 },
  ],
});
```

### Serialization

Waypoints and routing style are included in `topo.toJSON()` and restored by `topo.fromJSON()` automatically.

---

## 42. Zone / Region Annotations

Zones are visual region overlays that draw a labeled, bordered rectangle encompassing a set of nodes. Use them to annotate network segments such as LAN, DMZ, WAN, data-center racks, or security boundaries.

### API

#### `topo.zone(id, cfg)`

Register or update a zone annotation.

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `id` | `string` | ✓ | — | Unique zone identifier (e.g. `'zone_lan'`) |
| `cfg.label` | `string` | — | `id` | Display label rendered at the top-left of the zone rect |
| `cfg.nodes` | `string[]` | — | `[]` | IDs of nodes to encompass |
| `cfg.color` | `string` | — | `'#7d8a92'` | Border and label color |
| `cfg.borderStyle` | `string` | — | `'dashed'` | `'dashed'` \| `'solid'` \| `'dotted'` |
| `cfg.padding` | `number` | — | `40` | Extra padding around node bounding boxes (px) |
| `cfg.description` | `string` | — | — | Optional subtitle rendered below the label |

Returns `this` (chainable).

```javascript
// Mark the LAN segment
topo.zone('zone_lan', {
  label: 'LAN',
  nodes: ['SW1', 'SRV1', 'SRV2', 'HOST1'],
  color: '#01a982',
  borderStyle: 'dashed',
  padding: 40,
});

// Mark a DMZ
topo.zone('zone_dmz', {
  label: 'DMZ',
  description: 'Screened subnet',
  nodes: ['FW1', 'DMZ_SRV'],
  color: '#fc6161',
  borderStyle: 'solid',
  padding: 50,
});

topo.render();
```

#### `topo.removeZone(id)`

Remove a zone annotation by ID.

```javascript
topo.removeZone('zone_dmz');
topo.render();
```

#### `topo.getChildZones(zoneId)`

Return an array of child zone IDs whose `parentZone` matches the given zone.

```javascript
topo.zone('campus', { label: 'Campus', nodes: ['R1', 'SW1', 'SW2'] });
topo.zone('bldg1', { label: 'Building 1', nodes: ['SW1'], parentZone: 'campus' });
topo.zone('bldg2', { label: 'Building 2', nodes: ['SW2'], parentZone: 'campus' });

topo.getChildZones('campus'); // → ['bldg1', 'bldg2']
topo.getChildZones('bldg1'); // → []
```

#### `topo.setZones(entries)`

Replace all zones at once. Used internally by the viewer when loading from designer state.

```javascript
topo.setZones([
  ['zone_lan', { label: 'LAN', nodes: ['SW1'], color: '#01a982', borderStyle: 'dashed', padding: 40 }],
  ['zone_wan', { label: 'WAN', nodes: ['R1', 'EC1'], color: '#65aef9', borderStyle: 'dotted', padding: 40 }],
]);
```

### Bounding Rect Computation

The zone rectangle is **auto-computed** from the `x`/`y` positions of the listed nodes. The engine:

1. Finds the min/max x and y across all listed node positions
2. Expands the rect by `cfg.padding` on each side
3. Renders a `<rect>` with the chosen `borderStyle` and `color`
4. Places the `label` at `(minX + 8, minY + 14)`
5. Places `description` (if any) at `(minX + 8, minY + 26)`

Zones are rendered behind nodes (inserted first in the SVG stack).

### Editor Integration

In the visual editor:

| Access Point | Action |
|-------------|--------|
| **Insert** menu | *Add Zone* — creates a zone around the current selection |
| **Right-click** canvas | *Add Zone from selection* |
| **Properties panel** | Edit label, color, border style, and padding for the selected zone |
| Zone rectangle click | Selects the zone for editing |

### Serialization

Zones are included in `topo.toJSON()` under the `zones` key (as an array of `[id, cfg]` pairs) and are restored by `topo.fromJSON()` automatically.

---

## 43. Drag-to-Reposition & Coordinate Readback

All nodes support mouse and touch drag to reposition them on the canvas. After repositioning, you can copy the new coordinates back into your source code.

### Drag Behavior

- **Mouse**: Click and drag any node icon. A live `x: NNN  y: NNN` tooltip follows the cursor.
- **Touch**: Single-finger drag on touch devices. Same live tooltip appears.
- The drag snaps to integer pixel positions.
- Links connected to the dragged node re-render in real time.

### Live Coordinate Tooltip

While dragging, a small tooltip element (`.tds-coord-tooltip`) appears near the cursor showing the current `x` and `y` values. It disappears on mouse/touch release.

### Coordinate Readback API

#### `topo.getPositionSnippet()`

Returns a multi-line JavaScript string that recreates the current position of every node via the `topo.node()` API. Useful for pasting back into your source.

```javascript
const snippet = topo.getPositionSnippet();
console.log(snippet);
// td.node('R1', { ...cfg, x: 212, y: 304 });
// td.node('SW1', { ...cfg, x: 540, y: 304 });
// td.node('SRV1', { ...cfg, x: 540, y: 440 });
```

#### `topo.copyPositions()`

Copies all current node positions to the clipboard as a JSON map and returns a Promise resolving to that map.

```javascript
const positions = await topo.copyPositions();
// Clipboard contains:
// {
//   "R1":   { "x": 212, "y": 304 },
//   "SW1":  { "x": 540, "y": 304 },
//   "SRV1": { "x": 540, "y": 440 }
// }
```

If `navigator.clipboard` is unavailable (e.g. non-HTTPS context), the clipboard write is silently skipped and the positions object is still returned.

### Typical Workflow

1. Mount the topology
2. Drag nodes to desired positions
3. Call `topo.copyPositions()` or `topo.getPositionSnippet()`
4. Paste the output back into your source file as the definitive `x`/`y` values

---

## 44. Step Thumbnail Previews

When hovering over a step row in the sidebar, a miniature SVG preview of that step's topology appears in a floating tooltip. This lets you quickly scan step content without navigating away from the current step.

### Behavior

- **Trigger**: `mouseenter` on a step row in the navigation sidebar
- **Tooltip**: A `div.tds-thumb-preview` is appended to `document.body` and positioned to the right of the hovered row
- **Dimensions**: 200 × 112 px by default
- **Dismiss**: `mouseleave` hides the tooltip (`display: none`)

### Thumbnail Content

The thumbnail SVG is rendered by `topo._renderThumbnail(stepNum, width, height)`:

- **Links** are drawn as simplified `<line>` elements at 60 % opacity in the link's configured color
- **Nodes** are drawn as `<circle>` elements (radius 12) colored by `node.color`
- **Focus highlighting**: Nodes in the step's `focus` array render at full opacity; all others at 30 %
- **Background**: `#1d1f27` with `border-radius: 4px`

### Programmatic Thumbnail

```javascript
// Generate a thumbnail SVG string for step 3, at 320×180 px
const svg = topo._renderThumbnail(3, 320, 180);
document.getElementById('preview-pane').innerHTML = svg;
```

Note: `_renderThumbnail` is an internal method. Its signature is stable but it is prefixed with `_` indicating it may change in future major versions.

---

## 45. draw.io XML Import

`importDrawio()` parses a **draw.io / diagrams.net** XML export and populates the topology with equivalent nodes and links, enabling migration of existing diagrams into Topology Studio.

### API

#### `topo.importDrawio(xml, options?)`

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `xml` | `string` | ✓ | — | Raw draw.io XML string |
| `options.offsetX` | `number` | — | `0` | X offset applied to all imported node positions |
| `options.offsetY` | `number` | — | `0` | Y offset applied to all imported node positions |
| `options.typeMap` | `object` | — | `{}` | Additional shape-to-type mappings (merged with built-in map) |
| `options.defaultNodeType` | `string` | — | `'host'` | Fallback node type for unrecognized shapes |
| `options.defaultLinkType` | `string` | — | `'line'` | Fallback link type for unrecognized edge styles |
| `options.autoAct` | `boolean` | — | `true` | Auto-create an act + step showing all imported elements |

Returns `this` (chainable).

### Shape Mapping

The importer maps draw.io shape names to Topology Studio node types:

| draw.io Shape | Topology Studio Type |
|---------------|---------------------|
| `mxgraph.network.server`, `mxgraph.network.server2` | `server` |
| `mxgraph.network.firewall` | `firewall` |
| `mxgraph.network.router` | `router` |
| `mxgraph.network.switch` | `switch` |
| `mxgraph.network.cloud` | `cloud` |
| `mxgraph.network.hub` | `switch` |
| `mxgraph.network.laptop`, `mxgraph.network.desktop`, `mxgraph.network.workstation` | `host` |
| `mxgraph.network.database`, `mxgraph.network.storage` | `database` |
| `mxgraph.network.wireless_hub` | `ap` |
| `mxgraph.cisco.routers.router` | `router` |
| `mxgraph.cisco.switches.layer_3_switch` | `switch` |
| `mxgraph.cisco.firewalls.firewall` | `firewall` |
| `mxgraph.cisco.servers.standard_server` | `server` |
| `mxgraph.cisco.clouds.cloud` | `cloud` |
| `ellipse` | `cloud` |
| `cylinder3` | `database` |

You can extend or override this map with `options.typeMap`:

```javascript
topo.importDrawio(xml, {
  typeMap: {
    'mxgraph.aws4.resourceIcon': 'server',
    'mxgraph.azure.server': 'server',
  },
});
```

### Link Extraction

Edge cells with both `source` and `target` attributes are mapped to links. Link type is inferred from style:

| draw.io Style | Topology Studio Type |
|---------------|---------------------|
| `dashed=1` | `tunnel` |
| `endArrow=block` or `endArrow=classic` | `flow` |
| *(default)* | `defaultLinkType` (usually `'line'`) |

### Auto Act / Step

When `autoAct: true` (default), a single act (`'drawio-import'`) and step (`'drawio-overview'`) are created showing all imported nodes and links in one phase:

```javascript
// Act: "Imported Diagram"
// Step: "draw.io Import" — shows all imported elements at once
```

Set `autoAct: false` to suppress this and manually create your own acts/steps.

### Node ID Prefix

All imported node IDs are prefixed with `drio-` (e.g. `drio-3`, `drio-7`) and link IDs with `drio-link-` to avoid collisions with manually-defined elements.

### Complete Example

```javascript
const xmlString = `<?xml version="1.0" ...>
<mxGraphModel>
  <root>
    <mxCell id="0" />
    <mxCell id="1" parent="0" />
    <mxCell id="2" value="Router" style="shape=mxgraph.network.router;" vertex="1" parent="1">
      <mxGeometry x="100" y="100" width="60" height="60" as="geometry" />
    </mxCell>
    <mxCell id="3" value="Server" style="shape=mxgraph.network.server;" vertex="1" parent="1">
      <mxGeometry x="300" y="100" width="60" height="60" as="geometry" />
    </mxCell>
    <mxCell id="4" edge="1" source="2" target="3" parent="1">
      <mxGeometry relative="1" as="geometry" />
    </mxCell>
  </root>
</mxGraphModel>`;

const topo = new TopologyDesigner({ title: 'Imported', viewBox: '0 0 1050 700' });
topo.importDrawio(xmlString, { offsetX: 50, offsetY: 50 });
topo.mount('app');
```

---

## 46. Auto-Layout Engine Improvements

Topology Studio's `LayoutEngine` module ships with two new algorithms — **Spine-Leaf** and **Cluster** — plus a post-processing **Overlap Removal** pass that can be applied after any layout.

### Spine-Leaf Layout

Optimized for data-center spine-leaf (Clos) fabrics. Spine nodes (high-degree routers/switches) are placed on a top row; leaf nodes are arranged below, sorted by their primary spine connection for visual alignment.

```javascript
const positions = LayoutEngine.spineLeaf(topo._nodes, topo._links, {
  width: 1050,
  height: 700,
  padding: 80,
  spineY: 0.25,
  leafY: 0.7,
  minSpineConnections: 3,
});
LayoutEngine.apply(topo, positions, true); // true = animate transition
```

**Options:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `width` | `number` | `1050` | Canvas width in px |
| `height` | `number` | `700` | Canvas height in px |
| `padding` | `number` | `80` | Edge padding in px |
| `spineY` | `number` | `0.25` | Vertical position of spine row (0–1, fraction of canvas height) |
| `leafY` | `number` | `0.7` | Vertical position of leaf row (0–1) |
| `minSpineConnections` | `number` | `3` | Minimum degree for a spine-type node to be classified as spine |

**Spine classification:** A node is classified as spine if it is a core type (`router`, `switch`, `switchEnterprise`, `firewall`) **and** has at least `minSpineConnections` links, **or** if it has 4+ links regardless of type. Remaining nodes are leaves.

### Cluster Layout

Groups nodes by connected component (island detection via BFS), then lays each cluster out in a circle. Clusters are arranged on a grid across the canvas.

```javascript
const positions = LayoutEngine.cluster(topo._nodes, topo._links, {
  width: 1050,
  height: 700,
  padding: 80,
  clusterPadding: 40,
});
LayoutEngine.apply(topo, positions, true);
```

**Options:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `width` | `number` | `1050` | Canvas width in px |
| `height` | `number` | `700` | Canvas height in px |
| `padding` | `number` | `80` | Canvas edge padding in px |
| `clusterPadding` | `number` | `40` | Inner padding per cluster (shrinks the circle radius) |

Single-node clusters are placed at the cluster cell center.

### Overlap Removal (Post-Processing)

`LayoutEngine.removeOverlaps()` is a post-processing pass that nudges nodes apart whenever two are closer than `minDistance`. Run it after any layout:

```javascript
let positions = LayoutEngine.hierarchical(topo._nodes, topo._links);
positions = LayoutEngine.removeOverlaps(positions, {
  minDistance: 80,
  iterations: 50,
  width: 1050,
  height: 700,
  padding: 40,
});
LayoutEngine.apply(topo, positions);
```

**Options:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `minDistance` | `number` | `80` | Minimum allowed distance between node centers (px) |
| `iterations` | `number` | `50` | Max nudge iterations (stops early if no overlaps remain) |
| `width` | `number` | `1050` | Canvas width (used for boundary clamping) |
| `height` | `number` | `700` | Canvas height |
| `padding` | `number` | `40` | Minimum margin from canvas edges |

### `topo.autoLayout()` Integration

All algorithms are accessible via the high-level `autoLayout` method:

```javascript
topo.autoLayout('spine-leaf');
topo.autoLayout('cluster');
```

---

## 47. Mobile Quick-Edit Mode

Mobile Quick-Edit provides a touch-friendly editing interface for repositioning nodes and making common topology changes on phones and tablets.

### Enabling

```javascript
topo.enableMobileQuickEdit();
```

Call after `topo.mount()`. Returns `this` (chainable). Calling it again when already enabled is a no-op.

### Disabling

```javascript
topo.disableMobileQuickEdit();
```

Removes the FAB and panel from the DOM.

### UI Components

#### Floating Action Button (FAB)

A circular **✎** button (`.tds-mobile-fab`) anchored to the container. Tapping it opens the quick-edit panel.

#### Quick-Edit Panel (`.tds-mobile-qe-panel`)

A slide-up panel with three sections:

**1. Add Node**

Row of buttons for the most common node types: `ec`, `switch`, `router`, `firewall`, `cloud`, `host`, `server`, `database`. Tapping a button places a new node of that type at the center of the viewBox with a generated ID (`qe-<type>-<timestamp>`).

**2. Actions**

| Button | Action |
|--------|--------|
| Copy Positions | Calls `topo.copyPositions()` — copies `{ nodeId: {x,y} }` JSON to clipboard |
| Auto Layout | Runs `LayoutEngine.magneticNorth()` with animated transition |
| Export JSON | Downloads `topo.toJSON()` as a `.json` file |
| Undo Last Move | Reverts the most recent drag move |

**3. Selected Node** *(appears when a node is tapped)*

Shows the selected node's label, type, and current `x`/`y` coordinates. Includes a **Delete Node** button that removes the node from the topology.

### Node Selection (Mobile)

Tapping a node on the SVG canvas:
1. Sets it as the selected element
2. Shows the **Selected Node** section in the panel with live coordinate readout
3. Enables the **Delete Node** action

### Touch Drag

Touch drag (single finger) works the same as mouse drag — nodes can be repositioned freely on touch devices with a live coordinate tooltip.

### Example

```javascript
const topo = new TopologyDesigner({ title: 'Mobile Demo', viewBox: '0 0 1050 700' });
topo.node('R1', { type: 'router', x: 525, y: 350, label: 'Router' });
topo.act('a1', { label: 'Setup', color: '#01a982' });
topo.addStep('s1', { act: 'a1', name: 'Start', phases: [{ show: ['R1'], diff: 'Router placed' }] });
topo.mount('app');
topo.enableMobileQuickEdit();  // enable after mount
```

---

## 48. Self-Contained HTML Export & Embed

Topology Studio can export the current topology as a fully self-contained, single-file HTML document that includes all CSS and JavaScript inline. This enables sharing topology visualizations via email, embedding in wikis, or hosting on any static file server — no dependencies required.

### Standalone HTML Export

#### `topo.exportStandaloneHTML()`

Generates and downloads a complete HTML file containing:

- Inline `<style>` block with the full `topology-ds.css` stylesheet
- Inline `<script>` block with the full `topology-ds.js` engine
- Serialized topology data via `topo.toJSON()`
- Offline system font fallback stack (JetBrains Mono → Fira Code → Consolas → Courier New) — no CDN dependency
- Auto-mounting script that recreates the topology on page load

```javascript
await topo.exportStandaloneHTML();
// Downloads: "My_Network_standalone.html"
```

The filename is derived from the topology title with non-alphanumeric characters replaced by underscores, suffixed with `_standalone`.

> **Note:** This method fetches `topology-ds.css` and `topology-ds.js` via relative `fetch()` calls, so it must be called from a page served over HTTP(S) where those files are accessible. It will not work from a `file://` URL.

### Embed Code

#### `topo.copyEmbedCode()`

Copies a ready-to-use `<iframe>` embed snippet to the clipboard:

```html
<!-- Replace the src with your hosted URL -->
<iframe src="./My_Network_standalone.html" width="100%" height="600" frameborder="0"
  style="border-radius:12px;border:1px solid #3e4550" loading="lazy" allowfullscreen></iframe>
```

The `src` attribute is pre-filled with the topology's filename. A toast notification ("Embed code copied — paste your hosted URL in the src attribute") confirms the copy. Update the `src` to match your hosting URL.

### Editor Integration

In the visual editor (`editor.html`), both features are accessible from the **File** menu under the **Distribution** group:

| Menu Item | Action |
|-----------|--------|
| **Export Standalone HTML** | Calls `exportStandaloneHTML()` on the active topology |
| **Copy Embed Code** | Calls `copyEmbedCode()` on the active topology |

In the standalone viewer, both options appear in the **Export** button dropdown alongside PNG, SVG, and Analytics.

### CSS: Toast Notification

The embed code copy uses a `.tds-toast` notification styled with glassmorphism:

```css
.tds-toast {
  position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
  background: linear-gradient(135deg, rgba(34,37,46,.95), rgba(29,31,39,.98));
  backdrop-filter: blur(16px);
  border: 1px solid rgba(1,169,130,.3); border-radius: 10px;
  padding: 8px 20px; font-size: 11px; color: var(--tds-green);
  z-index: 9999;
  animation: tds-toastIn .3s ease;
}
```

### Deploying Standalone HTML Files

The exported standalone HTML file is a single file with zero dependencies — host it anywhere that serves static files:

**GitHub Pages**
1. Create a repo (or use an existing one) and commit the exported `.html` file
2. Go to **Settings → Pages** and set the source to the branch/folder containing the file
3. Your topology is live at `https://<user>.github.io/<repo>/<filename>.html`

**Netlify / Vercel**
1. Drag-and-drop the HTML file into the Netlify deploy dashboard, or push to a connected Git repo
2. The file is served automatically at your project URL

**Amazon S3 + CloudFront**
1. Upload the HTML file to an S3 bucket with static website hosting enabled
2. Optionally front with CloudFront for HTTPS and caching

**Any static server**
```bash
# Quick local preview
python3 -m http.server 8080
# or
npx serve .
```

> **Tip:** The exported file is fully offline — it works from `file://` for local viewing. A web server is only needed for the `<iframe>` embed or sharing via URL.

---

## 49. AI-Assisted Topology Generation

Topology Studio includes a local, fully offline AI-assisted authoring feature that generates topology layouts from plain-English descriptions. No API keys or external services are required — all parsing is done client-side using keyword matching and pattern detection.

### Opening the AI Assist Modal

| Method | Action |
|--------|--------|
| **Toolbar button** | Click the **AI Assist** button in the top toolbar |
| **Programmatic** | `topo.showAIAssist()` |

Clicking the button again while the modal is open closes it (toggle behavior).

### Modal UI

The AI Assist modal contains:

1. **Textarea** — Describe your network topology in plain English
2. **Quick-start chips** — Pre-built prompts you can click to fill the textarea:
   - `3-tier web app`
   - `hub-and-spoke network`
   - `mesh VPN topology`
   - `microservices architecture`
3. **Generate button** — Parses the description and shows the result
4. **Result panel** — Shows how many nodes and links were generated
5. **Apply to Canvas** — Adds the generated topology to the current design

### Pattern Recognition

The parser recognizes these topology patterns:

| Pattern | Trigger Keywords | Generated Layout |
|---------|-----------------|------------------|
| **3-Tier Architecture** | `3-tier`, `3 tier` | Load balancer → 3 app servers → database (vertical tiers) |
| **Hub and Spoke** | `hub-and-spoke`, `hub and spoke` | Central router hub with 5 radial spoke switches |
| **Star** | `star` | Same as hub-and-spoke |
| **Mesh** | `mesh` | 4 router nodes with all-to-all tunnel connections |
| **Free-form** | *(any other text)* | Keyword extraction + connection phrase parsing |

### Keyword-to-Node Mapping (Free-form Mode)

When no pattern is detected, the parser scans for device keywords:

| Keywords | Node Type | Default Label |
|----------|-----------|---------------|
| `load balancer`, `lb`, `balancer` | `ec` | Load Balancer |
| `firewall`, `fw`, `waf` | `firewall` | Firewall |
| `database`, `db`, `sql`, `postgres`, `mysql`, `mongo` | `database` | Database |
| `router`, `gateway`, `gw` | `router` | Router |
| `switch`, `l2 switch`, `l3 switch` | `switch` | Switch |
| `cloud`, `aws`, `azure`, `gcp` | `cloud` | Cloud |
| `server`, `app server`, `web server`, `backend`, `api` | `host` | Server |
| `client`, `user`, `endpoint`, `laptop`, `desktop` | `ec` | Client |

Count prefixes are supported (e.g., "3 app servers" creates 3 server nodes, capped at 10).

### Connection Detection (Free-form Mode)

The parser detects connection phrases between components:

```
"A load balancer connects to 3 app servers"
"Router linked to switch"
"Firewall -> database"
"Server talks to cloud"
```

Supported phrases: `connects to`, `linked to`, `->`, `talks to`, `communicates with`.

If no explicit connections are found and multiple nodes exist, a default chain topology is created (each node connects to the next).

### Fallback

If no keywords or patterns are recognized at all, a simple 3-node topology is generated: Client → Server → Database.

### Auto-Layout

Generated nodes are automatically positioned:
- **Pattern-based** topologies use hardcoded positions optimized for the pattern
- **Free-form** topologies use a grid layout based on `Math.ceil(Math.sqrt(count))` columns

### Programmatic API

#### `topo.showAIAssist()`

Opens (or closes if already open) the AI Assist modal.

#### `topo._parseAITopologyDescription(text)`

Parses a natural language description and returns a result object:

```javascript
const result = topo._parseAITopologyDescription('3-tier web app');
// result = {
//   nodes: [
//     { id: 'lb_1', type: 'ec', label: 'Load Balancer', x: 450, y: 100 },
//     { id: 'app_2', type: 'host', label: 'App Server 1', x: 250, y: 320 },
//     { id: 'app_3', type: 'host', label: 'App Server 2', x: 450, y: 320 },
//     { id: 'app_4', type: 'host', label: 'App Server 3', x: 650, y: 320 },
//     { id: 'db_5', type: 'database', label: 'Database', x: 450, y: 540 },
//   ],
//   links: [
//     { from: 'lb_1', to: 'app_2', type: 'line', label: '' },
//     ...
//   ],
// }
```

#### `topo._applyGeneratedTopology(result)`

Applies a parsed result to the current topology and re-renders.

```javascript
const result = topo._parseAITopologyDescription('mesh VPN topology');
topo._applyGeneratedTopology(result);
```

> **Note:** `_parseAITopologyDescription` and `_applyGeneratedTopology` are prefixed with `_` indicating internal APIs; their signatures are stable but may evolve in future major versions.

### CSS Classes

| Class | Description |
|-------|-------------|
| `.tds-ai-backdrop` | Full-screen overlay backdrop |
| `.tds-ai-modal` | Glassmorphism modal container |
| `.tds-ai-textarea` | Input textarea for descriptions |
| `.tds-ai-chips` | Container for quick-start chip buttons |
| `.tds-ai-chip` | Individual quick-start chip |
| `.tds-ai-generate` | Green gradient "Generate" button |
| `.tds-ai-result` | Result panel (hidden by default, `.visible` to show) |
| `.tds-ai-apply` | "Apply to Canvas" button |

---

## 50. Engagement Analytics

Topology Studio tracks user engagement throughout a viewing session and presents the data in a glassmorphism analytics dashboard. Analytics are collected automatically — no configuration required.

### What's Tracked

| Metric | Description |
|--------|-------------|
| **Session duration** | Time since the `TopologyDesigner` was constructed |
| **Step views** | Which steps the user visited, with view count and dwell time |
| **Step transitions** | Total number of next/prev navigations |
| **Completion rate** | Fraction of the topology the user has progressed through (0–1) |
| **Interaction log** | Timestamped log of all play, pause, next, prev, and reset actions |

### Completion Rate Calculation

Completion is based on the **furthest step reached** relative to total steps:

```
completionRate = furthestStepIndex / totalSteps
```

This means skipping forward counts toward completion, even if intermediate steps were not viewed.

### API

#### `topo.getAnalytics()`

Returns a summary object with all engagement data:

```javascript
const analytics = topo.getAnalytics();
// {
//   sessionDuration: 125000,          // milliseconds since construction
//   stepsViewed: 5,                   // unique steps visited
//   totalStepTransitions: 12,         // total next/prev navigations
//   completionRate: 0.75,             // 0–1 progress
//   averageStepDwell: 8500,           // average ms spent per step
//   stepBreakdown: {                  // per-step detail
//     'deploy':    { count: 3, totalMs: 12000 },
//     'services':  { count: 1, totalMs: 5000 },
//     ...
//   },
//   interactionLog: [                 // raw event log
//     { type: 'next', target: { step: 1 }, timestamp: 1719500000000 },
//     { type: 'play', target: { step: 2 }, timestamp: 1719500005000 },
//     ...
//   ],
// }
```

#### `topo.showAnalyticsDashboard()`

Opens a glassmorphism modal displaying:

- **Session Duration** — formatted as `Xm Ys` or `Xs`
- **Completion** — percentage with a progress bar
- **Steps Viewed** — `N / total`
- **Step Transitions** — total navigation count
- **Average Step Dwell** — formatted as seconds or milliseconds
- **Step Dwell Times** — horizontal bar chart showing relative time spent on each step (hover for tooltip with exact values)

The dashboard is accessible from the **Export** dropdown ("Analytics" option) in the viewer toolbar.

### Analytics Data Structure

Internally, analytics are stored on `this._analytics`:

```javascript
{
  sessionId: 'a7f3b2c9',        // Random 8-char session identifier
  startTime: 1719500000000,      // Date.now() at construction
  stepViews: {                   // stepId → { count, totalMs }
    'deploy': { count: 2, totalMs: 8000 },
  },
  interactions: [],              // { type, target, timestamp }
  completionRate: 0,             // 0–1
  totalSteps: 0,                 // cached step count
}
```

### Step Dwell Tracking

Dwell time is tracked automatically:

1. When a step is navigated to (via `next()`, `prev()`, `goTo()`), the current timestamp is recorded
2. When navigating away, the elapsed time is added to that step's `totalMs`
3. Calling `getAnalytics()` closes out the current step's dwell time (non-destructively) so the returned data is always up-to-date

### Tracked Interactions

The following actions are automatically logged to the interaction array:

| Action | Trigger |
|--------|---------|
| `play` | `topo.play()` called |
| `pause` | `topo.pause()` called |
| `next` | `topo.next()` called |
| `prev` | `topo.prev()` called |
| `reset` | `topo.reset()` called |

Each entry includes `{ type, target: { step }, timestamp }`.

### CSS Classes

| Class | Description |
|-------|-------------|
| `.tds-analytics-backdrop` | Full-screen overlay |
| `.tds-analytics-modal` | Glassmorphism dashboard container |
| `.tds-analytics-stat` | Key-value stat row |
| `.tds-analytics-stat-label` | Stat label (uppercase, muted) |
| `.tds-analytics-stat-value` | Stat value (green, bold) |
| `.tds-analytics-bar` | Completion progress bar track |
| `.tds-analytics-bar-fill` | Completion progress bar fill (green gradient) |
| `.tds-analytics-chart` | Bar chart container for step dwell times |
| `.tds-analytics-chart-bar` | Individual bar in the dwell chart |
| `.tds-analytics-chart-label` | Step ID label below each bar |

---

## 51. Layout Edit Mode

Layout Edit Mode is an interactive workflow for repositioning nodes with pixel-precise feedback, keyboard nudging, and one-click coordinate readback. Unlike casual drag-to-reposition, it's a dedicated mode that reveals alignment guides, dims non-node chrome, and exposes a "Copy positions" button so layout tweaks can be copied back into source code.

### Entering & Exiting

```javascript
topo.enterLayoutEditMode();     // Turn on
topo.exitLayoutEditMode();      // Turn off
topo.toggleLayoutEditMode();    // Flip state, returns the new boolean
```

All three methods are chainable (except `toggleLayoutEditMode()` which returns the resulting mode flag). The toolbar also exposes a **✛ Layout** button that calls `toggleLayoutEditMode()` and an `aria-pressed` attribute that reflects the active state.

While the mode is active:

- Each node becomes draggable with the cursor
- A live coordinate tooltip follows the pointer, showing the node ID and its current `(x, y)` rounded to the nearest integer
- Alignment guides appear when the dragged node lines up with another node's x or y within a small threshold
- Arrow keys nudge the selection by 1 px; `Shift+Arrow` nudges by 10 px
- The **📋 Positions** button appears in the toolbar; clicking it calls `copyPositions()`

### Coordinate Readback

```javascript
await topo.copyPositions();           // Copy JSON to clipboard, returns positions object
const snippet = topo.getPositionSnippet(); // Return a JS snippet (string) recreating all node positions
```

`copyPositions()` writes an object of the shape `{ nodeId: { x, y } }` to the clipboard as a JSON string. It is `async` because it uses the Clipboard API. The resolved value is the same positions object.

`getPositionSnippet()` returns a multi-line JavaScript snippet such as:

```javascript
topo.node('R1', { x: 320, y: 210 });
topo.node('SW1', { x: 520, y: 210 });
topo.node('H1', { x: 720, y: 210 });
```

This is handy for pasting into your source file after you've finessed a layout visually.

### Keyboard Shortcut

Pressing **L** while nothing is focused toggles Layout Edit Mode. The shortcut is listed in the in-app keyboard shortcuts overlay.

### CSS Classes

| Class | Description |
|-------|-------------|
| `.tds-layout-edit` | Added to `.tds-root` while the mode is active |
| `.tds-coord-tooltip` | Live coordinate tooltip shown near the cursor |
| `.tds-align-guide` | Alignment guide line (horizontal or vertical) |

### Relationship to Drag-to-Reposition

Section 43 (Drag-to-Reposition & Coordinate Readback) covers the casual, always-on drag workflow. Layout Edit Mode is a *modal* version of the same workflow that surfaces extra affordances (guides, tooltip, copy button). Both share the same backing data — any drag in either mode updates node positions in-place.

---

## 52. Presentation Mode

Topology Studio includes a built-in presentation mode that turns any topology walkthrough into a fullscreen, step-by-step presentation — perfect for demos, meetings, and stakeholder reviews.

### Entering Presentation Mode

```javascript
topo.present({ autoAdvance: 0, showNotes: false });
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `autoAdvance` | `number` | `0` | Auto-advance interval in milliseconds. `0` = manual navigation only |
| `showNotes` | `boolean` | `false` | Show the presenter notes panel at the bottom of the screen |

When called, the library:
1. Adds the `.tds-presenting` class to the `.tds-root` element
2. Requests fullscreen via the Fullscreen API
3. Creates a step counter overlay (`.tds-present-counter`)
4. Optionally creates an auto-advance progress bar (`.tds-present-progress`)
5. Optionally opens the presenter notes panel (`.tds-presenter-notes`)

### Exiting Presentation Mode

```javascript
topo.exitPresentation();
```

Exits fullscreen, removes all presentation overlays (counter, progress bar, notes panel), clears auto-advance timers, and removes the `.tds-presenting` class. Also fires automatically when the user exits fullscreen via the browser.

### Step Notes

Attach presenter notes to any step using the `notes` field:

```javascript
topo.addStep('s1', {
  act: 'demo',
  name: 'Deploy Core',
  goal: 'Provision the core router.',
  notes: 'Talk about redundancy requirements here',
  phases: [
    { show: ['CORE'], diff: 'Core router online' },
  ],
});
```

When `showNotes: true` is passed to `present()`, notes appear in a dedicated panel (`#tds-presenterNotes`) at the bottom of the screen. Notes update automatically as you navigate between steps.

### Keyboard Shortcuts (Presentation)

| Key | Action |
|-----|--------|
| **F5** | Enter presentation mode |
| **Escape** | Exit presentation mode |
| **F** | Toggle fullscreen while presenting |
| **→** / **Space** | Next step |
| **←** | Previous step |

### Step Counter Overlay

While presenting, a step counter badge appears showing the current step number and total (e.g., "3 / 12"). This overlay is styled via the `.tds-present-counter` class.

### Toolbar Button

The editor toolbar includes a **Present** button (▶ icon). Clicking it calls `topo.present()` with default options. The button is wired automatically when the editor chrome is mounted.

### CSS Classes

| Class | Description |
|-------|-------------|
| `.tds-presenting` | Added to `.tds-root` during presentation |
| `.tds-present-counter` | Step counter overlay badge |
| `.tds-present-progress` | Auto-advance progress bar track |
| `.tds-present-progress-fill` | Animated fill inside the progress bar |
| `.tds-presenter-notes` | Presenter notes panel |

---

## 53. Dark / Light Theme Toggle

Topology Studio ships with full dark and light theme support, with automatic OS preference detection and localStorage persistence.

### Setting the Theme

```javascript
topo.setTheme('dark');   // Switch to dark theme
topo.setTheme('light');  // Switch to light theme
```

`setTheme()` accepts `'dark'` or `'light'`. Any other value is silently ignored. The method:
1. Toggles the `.tds-light` class on the root element
2. Updates the SVG background fill (`#1d1f27` for dark, `#f0f1f3` for light)
3. Persists the choice to `localStorage` under the key `tds-theme`
4. Updates the toolbar toggle button icon
5. Returns `this` for chaining

### Reading the Current Theme

```javascript
const theme = topo.getTheme();  // 'dark' or 'light'
```

Returns the internal `_theme` value. Defaults to `'dark'` if unset.

### Automatic OS Preference Detection

On initialization, the library reads the OS color scheme preference via `prefers-color-scheme` and applies it — unless a previously saved theme exists in `localStorage`. The detection order is:

1. Check `localStorage.getItem('tds-theme')`
2. If not found, check `window.matchMedia('(prefers-color-scheme: light)')` 
3. Apply whichever is found; fall back to `'dark'`

### Toolbar Toggle

The toolbar includes a theme toggle button (`#tds-themeBtn`) that displays:
- **☀️** when the current theme is dark (click to switch to light)
- **🌙** when the current theme is light (click to switch to dark)

Clicking the button calls `topo.setTheme(topo._theme === 'dark' ? 'light' : 'dark')`.

### CSS Custom Properties

All theme colors are defined as CSS custom properties on `:root`. Override these to create custom themes:

| Variable | Dark Default | Purpose |
|----------|-------------|----------|
| `--tds-bg` | `#1d1f27` | Primary background |
| `--tds-bg-secondary` | `#23252e` | Secondary background |
| `--tds-panel` | `#22252e` | Panel background |
| `--tds-panel2` | `#292d3a` | Alternate panel background |
| `--tds-border` | `#3e4550` | Border color |
| `--tds-canvas-bg` | `#1d1f27` | SVG canvas background |
| `--tds-text` | `#e6e8e9` | Primary text |
| `--tds-muted` | `#7d8a92` | Muted text |
| `--tds-muted2` | `#606a70` | Secondary muted text |
| `--tds-bright` | `#b1b9be` | Bright/emphasized text |
| `--tds-green` | `#01a982` | Green accent |
| `--tds-green-ok` | `#05cc93` | Success green |
| `--tds-blue` | `#65aef9` | Blue accent |
| `--tds-purple` | `#7764fc` | Purple accent |
| `--tds-gold` | `#deb146` | Gold/warning accent |
| `--tds-red` | `#fc6161` | Red/error accent |
| `--tds-orange` | `#ec8c25` | Orange accent |
| `--tds-teal` | `#00a4b3` | Teal accent |
| `--tds-pe` | `#068667` | Policy engine green |
| `--tds-coral` | `#d25f4b` | Coral accent |
| `--tds-node-bg` | `rgba(35,37,46,.85)` | Node background (translucent) |
| `--tds-node-bg-solid` | `#292d3a` | Node background (solid) |
| `--tds-glass` | `rgba(34,37,46,.55)` | Glassmorphism base |
| `--tds-glass-border` | `rgba(255,255,255,.06)` | Glassmorphism border |
| `--tds-glass-highlight` | `rgba(255,255,255,.03)` | Glassmorphism highlight |
| `--tds-glass-blur` | `16px` | Glassmorphism blur radius |
| `--tds-glass-saturate` | `1.4` | Glassmorphism saturation |
| `--tds-glass-shadow` | `rgba(0,0,0,.3)` | Glassmorphism shadow |
| `--tds-glow-green` | `rgba(1,169,130,.35)` | Green glow effect |
| `--tds-glow-purple` | `rgba(119,100,252,.25)` | Purple glow effect |
| `--tds-glow-blue` | `rgba(101,174,249,.2)` | Blue glow effect |
| `--tds-glow-opacity` | `0.6` | Glow effect opacity |
| `--tds-overlay-white` | `rgba(255,255,255,.06)` | White overlay |
| `--tds-overlay-white-subtle` | `rgba(255,255,255,.03)` | Subtle white overlay |
| `--tds-overlay-white-strong` | `rgba(255,255,255,.12)` | Strong white overlay |
| `--tds-overlay-black` | `rgba(0,0,0,.3)` | Black overlay |
| `--tds-grid-stroke` | `rgba(255,255,255,.03)` | Grid line stroke |
| `--tds-phase-ms` | `600ms` | Phase animation duration |
| `--tds-transition` | `0.55s ease` | Standard transition |
| `--tds-transition-fast` | `0.35s ease` | Fast transition |

---

## 54. Enterprise Templates Gallery

Topology Studio ships with a gallery of enterprise-grade templates that generate complete topologies — nodes, links, acts, steps, and zones — in a single call.

### Loading a Template

```javascript
topo.loadTemplate('sd-wan');                         // Load with defaults
topo.loadTemplate('spine-leaf', { spines: 4, leaves: 8 }); // Override options
```

`loadTemplate(templateId, options)` looks up the template by ID, runs its `generate(options)` function, applies the result via `TemplateParser.applyToDesigner()`, rebuilds the index, and re-renders. Returns `this` for chaining.

Options are template-specific (see table below). The following global options are also supported:

| Option | Default | Description |
|--------|---------|-------------|
| `layout` | `'hierarchical'` | Layout algorithm to apply |
| `clearFirst` | `true` | Clear existing topology before applying |

### Editor Access

In the editor, use **File → New from Template** to open the template gallery. Each template shows its icon, name, and description. Selecting one generates the full topology with nodes, links, acts, and steps.

### Available Templates

| ID | Icon | Name | Description | Options |
|----|------|------|-------------|--------|
| `hub-spoke` | ⊛ | Hub & Spoke | Central device with N branch connections | `branches` (default 4), `hubType`, `spokeType` |
| `3-tier` | ▦ | 3-Tier Architecture | Load balancer → App servers → Database | `appServers` (default 2) |
| `mesh` | ◈ | Full Mesh | Every node connected to every other | `count` (default 4), `nodeType` |
| `star` | ✦ | Star Topology | Central switch with connected endpoints | `endpoints` (default 6) |
| `spine-leaf` | ▤ | Spine-Leaf Fabric | Data center fabric with spine and leaf switches | `spines` (default 2), `leaves` (default 4) |
| `sdwan` | ☁ | SD-WAN + ZTNA | Branch offices with cloud security and ZTNA | — |
| `firewall-dmz` | 🔥 | Firewall + DMZ | Internet → Firewall → DMZ → Internal network | — |
| `sd-wan` | 🌐 | SD-WAN Enterprise | Hub-and-spoke SD-WAN with cloud orchestrator, data centers, and branch sites | — |
| `ztna` | 🔒 | Zero Trust Network Access | Identity-driven segmented access with policy engine and ZTNA controls | — |
| `campus` | 🏢 | Campus Network | Three-tier campus: core router, distribution switches, access layer with APs and hosts | — |
| `data-center` | 🏗️ | Data Center Spine-Leaf | Spine-leaf fabric with compute servers and shared storage | — |
| `cloud-migration` | ☁️ | Cloud Migration | On-prem data center to cloud migration with hybrid VPN connectivity | — |
| `simple` | ⚡ | Simple Starter | Basic topology: router → switch → 3 hosts | — |

### Example: SD-WAN Enterprise Template

```javascript
const topo = new TopologyDesigner({ title: 'SD-WAN Deployment' });
topo.loadTemplate('sd-wan');
topo.mount('#app');
```

This generates 6 nodes (orchestrator, 2 DCs, 3 branches), 8 links (management, fabric, WAN tunnels, branch mesh), 1 act with 4 steps (Overlay Build → Branch Connect → Breakout Policy → Optimization), and 2 zones (Data Centers, Branch Sites).

---

## 55. Removal & Unsubscribe API

Topology Studio provides methods to remove elements from the topology and unsubscribe from lifecycle hooks.

### Removing Nodes

```javascript
topo.removeNode('R1');
```

Removes the node with the given ID from the topology. Also:
- Removes all links where the node is a `from` or `to` endpoint
- Removes the node ID from any step phase `show` arrays
- Returns `this` for chaining

Works on both graph-backed and standalone topologies (cleans `rawNodes` / `rawLinks` when using `TopologyGraph`, or `__nodes` / `__links` otherwise).

### Removing Links

```javascript
topo.removeLink('L1');
```

Removes the link with the given ID. Also:
- Removes the link ID from any step phase `show` arrays
- Clears the link's route cache entry
- Returns `this` for chaining

### Unsubscribing from Lifecycle Hooks

```javascript
function myHandler(data) { console.log('rendered', data); }
topo.on('afterRender', myHandler);

// Later, unsubscribe:
topo.off('afterRender', myHandler);
```

`topo.off(event, callback)` removes a previously registered callback from the specified lifecycle hook. The callback must be the same function reference passed to `topo.on()`. Returns `this` for chaining.

Supported events (same as `topo.on()`):
- `'beforeRender'` — fires before each render pass
- `'afterRender'` — fires after each render pass
- `'onStepChange'` — fires when the current step changes

### Cleanup Pattern

```javascript
// Register
const onStep = (step) => console.log('Step:', step);
topo.on('onStepChange', onStep);

// Build topology
topo.node('R1', { type: 'router' });
topo.node('SW1', { type: 'switch' });
topo.link('L1', { from: 'R1', to: 'SW1' });
topo.mount('#app');

// Tear down
topo.removeLink('L1');
topo.removeNode('SW1');
topo.removeNode('R1');
topo.off('onStepChange', onStep);
topo.render();
```

---

## 56. REST API & OpenAPI Spec

Topology Studio ships with a Node/Express server that wraps the `TopologyDesigner` engine in a RESTful API. It is intended for headless authoring pipelines, automated CI checks, and external tooling that wants to produce or analyze topologies without running a browser.

### Running the Server

```bash
npm run api              # Starts on http://localhost:3000
npm run api:dev          # Same, but with `node --watch`
npm run api:spec         # Regenerate design-system/openapi.json
```

Once running:

- **Swagger UI** — http://localhost:3000/api-docs (interactive reference generated from the OpenAPI spec)
- **Raw spec** — http://localhost:3000/openapi.json
- **Health check** — http://localhost:3000/api/health

A static copy of the Swagger UI is also served from `design-system/api-docs.html`, backed by `design-system/openapi.json`. Regenerate the static spec after editing `api/server.js` or `api/openapi-definition.js`:

```bash
node api/generate-spec.js
```

### Authentication

The server is unauthenticated by default — it is designed for local/CI use. Put it behind your own reverse proxy (nginx, Caddy, Cloudflare) if you expose it publicly.

### In-Memory Store

Topologies live in a `Map<uuid, TopologyDesigner>` inside the server process. There is no persistence layer — restarting the process clears all topologies. Import and export via JSON to persist to disk.

### Resource Model

| Resource | Base path | Methods |
|----------|-----------|---------|
| Topologies | `/api/topologies` | `GET` · `POST` |
| Topology | `/api/topologies/{id}` | `GET` · `PUT` · `DELETE` |
| Import | `/api/topologies/import` | `POST` |
| Export | `/api/topologies/{id}/export` | `GET` |
| Nodes | `/api/topologies/{id}/nodes` | `GET` |
| Node | `/api/topologies/{id}/nodes/{nodeId}` | `GET` · `PUT` · `DELETE` |
| Node neighbors | `/api/topologies/{id}/nodes/{nodeId}/neighbors` | `GET` |
| Node blast radius | `/api/topologies/{id}/nodes/{nodeId}/blast-radius` | `GET` |
| Node position keyframes | `/api/topologies/{id}/nodes/{nodeId}/positions` | `PUT` |
| Links | `/api/topologies/{id}/links` | `GET` |
| Link | `/api/topologies/{id}/links/{linkId}` | `GET` · `PUT` · `DELETE` |
| Anchors | `/api/topologies/{id}/anchors` | `GET` |
| Anchor | `/api/topologies/{id}/anchors/{anchorId}` | `GET` · `PUT` · `DELETE` |
| Acts | `/api/topologies/{id}/acts` | `GET` |
| Act | `/api/topologies/{id}/acts/{actId}` | `PUT` · `DELETE` |
| Steps | `/api/topologies/{id}/steps` | `GET` |
| Step | `/api/topologies/{id}/steps/{stepId}` | `PUT` · `DELETE` |
| Glossary | `/api/topologies/{id}/glossary` | `GET` · `PUT` |
| Analysis | `/api/topologies/{id}/analysis` | `GET` |
| Shortest path | `/api/topologies/{id}/paths?from=&to=` | `GET` |
| Connected components | `/api/topologies/{id}/components` | `GET` |
| Bridge links | `/api/topologies/{id}/bridges` | `GET` |
| Articulation points | `/api/topologies/{id}/articulation-points` | `GET` |
| BFS traversal | `/api/topologies/{id}/bfs?start=` | `GET` |
| Subgraph | `/api/topologies/{id}/subgraph` | `POST` |
| Playback state | `/api/topologies/{id}/playback` | `GET` · `PUT` |
| Layers | `/api/topologies/{id}/layers` | `GET` |
| Layer | `/api/topologies/{id}/layers/{layerId}` | `PUT` · `DELETE` |
| Flow paths | `/api/topologies/{id}/flow-paths` | `GET` |
| Flow path | `/api/topologies/{id}/flow-paths/{flowPathId}` | `PUT` · `DELETE` |
| Policy markers | `/api/topologies/{id}/policy-markers` | `GET` |
| Policy marker | `/api/topologies/{id}/policy-markers/{markerId}` | `PUT` · `DELETE` |
| Node types | `/api/meta/node-types` | `GET` |
| Link types | `/api/meta/link-types` | `GET` |
| Health | `/api/health` | `GET` |

### Request & Response Shapes

Full JSON schemas for every request and response live in `api/openapi-definition.js` and are mirrored verbatim to the static spec. The most important payloads are:

- **`TopologyConfig`** — initial constructor options: `title`, `subtitle`, `viewBox`, `phaseMs`, `drawDuration`, `fadeDuration`, `mode`, `speedMs`, `routing`
- **`TopologyExport`** — full JSON roundtrip: `nodes`, `links`, `anchors`, `acts`, `steps`, `glossary`, plus the top-level config fields
- **`NodeConfig`** — `type`, `x`, `y`, `label`, `sublabel`, `labelColor`, `color`, `variant`, `vrrp`, `managed`, `zoneIndicator`, `sideLabel`, etc.
- **`LinkConfig`** — `type`, `from`, `to`, `color`, `label`, `sublabel`, `fromLabel`, `toLabel`, `dashed`, `dots`, `reason`
- **`StepConfig`** — `act`, `name`, `goal`, `focus`, `phases[]`
- **`PlaybackState`** — `step`, `totalSteps`, `playing`, `mode`, `speedMs`

### Example: Build a Topology via REST

```bash
# 1. Create an empty topology
curl -X POST http://localhost:3000/api/topologies \
  -H 'Content-Type: application/json' \
  -d '{"title":"Demo","subtitle":"via REST","viewBox":"0 0 800 500"}'
# → { "id": "...uuid...", "title": "Demo", ... }

# 2. Add nodes
ID=<uuid-from-step-1>
curl -X PUT http://localhost:3000/api/topologies/$ID/nodes/R1 \
  -H 'Content-Type: application/json' \
  -d '{"type":"router","x":200,"y":250,"label":"R1"}'
curl -X PUT http://localhost:3000/api/topologies/$ID/nodes/S1 \
  -H 'Content-Type: application/json' \
  -d '{"type":"switch","x":500,"y":250}'

# 3. Link them
curl -X PUT http://localhost:3000/api/topologies/$ID/links/L1 \
  -H 'Content-Type: application/json' \
  -d '{"type":"line","from":"R1","to":"S1"}'

# 4. Run analysis
curl http://localhost:3000/api/topologies/$ID/analysis

# 5. Export JSON
curl http://localhost:3000/api/topologies/$ID/export > topo.json
```

### Importing an Existing Topology

```bash
curl -X POST http://localhost:3000/api/topologies/import \
  -H 'Content-Type: application/json' \
  --data @topo.json
```

### Limitations

Some engine features are inherently stateful and cannot be driven cleanly over REST:

- **Playback timing.** `GET /playback` returns the current state, and `PUT /playback` can set `step`/`mode`/`speedMs`, but continuous play/pause/animation ticks require a live DOM. The `playing` field is read-only in this context.
- **Custom render callbacks.** `step.onRender` and `phase.custom` are JavaScript functions and cannot be serialized. They are silently dropped when posted over REST.
- **Node position keyframes.** `PUT /nodes/{nodeId}/positions` accepts keyframes but they only take visible effect during DOM rendering.
- **Export formats.** PNG, SVG, PDF, GIF, and standalone-HTML exports are browser-only (they rely on the canvas and DOM). The REST `export` endpoint returns the JSON roundtrip shape; render to other formats client-side.

### Status Codes

| Code | Meaning |
|------|---------|
| `200` | Success with response body |
| `201` | Resource created |
| `204` | Success with no body (deletes) |
| `400` | Missing required query/body fields |
| `404` | Topology, node, link, anchor, act, step, layer, flow path, or policy marker not found |
| `501` | Graph module not loaded (only when the `TopologyGraph` dependency is missing) |

---

## 57. TopologyGraph — Graph Data Model

The `TopologyGraph` class (`design-system/core/graph.js`) provides graph-theoretic operations on the topology data. It replaces flat Map storage with a proper graph structure that supports adjacency queries, path finding, and topological analysis.

### Accessing the Graph

```javascript
const graph = topo.graph; // TopologyGraph instance (available after adding nodes/links)
```

### Node, Link & Anchor CRUD

```javascript
graph.addNode('R1', { type: 'router', x: 100, y: 200 });
graph.getNode('R1');          // → { id: 'R1', type: 'router', ... }
graph.hasNode('R1');          // → true
graph.removeNode('R1');       // removes node and all connected links
graph.nodeCount;              // → number of nodes

// Iteration
for (const [id, cfg] of graph.nodes()) { ... }
for (const id of graph.nodeIds()) { ... }
for (const [id, cfg] of graph.nodeEntries()) { ... }
```

Links and anchors follow the same pattern (`addLink`, `getLink`, `hasLink`, `removeLink`, `linkCount`, `links()`, `linkIds()`, `addAnchor`, `getAnchor`, `hasAnchor`, `removeAnchor`, `anchorCount`, `anchors()`).

### Adjacency Queries

```javascript
graph.getNeighbors('R1');     // → Set<nodeId> of adjacent nodes
graph.degree('R1');           // → number of connections
graph.getLinksForNode('R1');  // → Set<linkId> of all links touching R1
graph.getLinksForAnchor('mid'); // → Set<linkId> of all links touching anchor 'mid'
graph.getLinksBetween('R1', 'SW1'); // → [linkId, ...] (parallel links supported)
graph.elementType('R1');     // → 'node' | 'link' | 'anchor' | null
```

### Graph Algorithms

```javascript
graph.shortestPath('A', 'D');      // → ['A', 'B', 'C', 'D'] (BFS shortest path)
graph.bfs('A');                     // → ['A', 'B', 'C', ...] (traversal order)
graph.connectedComponents();        // → [Set<nodeId>, ...] (one per component)
graph.articulationPoints();         // → [nodeId, ...] (cut vertices)
graph.bridges();                    // → [linkId, ...] (single points of failure)
graph.blastRadius('R1');            // → { isolated: Set, affected: Set }
graph.subgraph(['A', 'B', 'C']);    // → { nodes: Map, links: Map, anchors: Map }
```

### Topology Analysis

```javascript
const findings = graph.analyze();
// → [{ type, severity, message, nodeIds, linkIds }, ...]
// Types: 'single-point-of-failure', 'bridge', 'isolated-node', 'disconnected'
```

### Serialization

```javascript
const json = graph.toJSON();   // → { nodes, links, anchors }
graph.fromJSON(json);          // restore from JSON
graph.clear();                 // remove everything
```

### Backward Compatibility

```javascript
graph.rawNodes;   // → Map (same reference as internal storage)
graph.rawLinks;   // → Map
graph.rawAnchors; // → Map
```

---

## Appendix A: Constructor Options

```javascript
new TopologyDesigner({
  title: 'My Topology',           // Top bar title
  subtitle: 'Subtitle text',      // Top bar subtitle
  viewBox: '0 0 1050 700',        // SVG viewBox dimensions
  phaseMs: 600,                    // Delay between phases in milliseconds
  drawDuration: 0.7,              // Stroke draw-in animation duration (seconds)
  fadeDuration: 0.55,             // Phase fade-in transition duration (seconds)
  mode: 'manual',                  // Initial playback mode
  speedMs: 4000,                   // Auto-play interval (milliseconds)
  routing: true,                    // Enable smart link routing (avoids crossing nodes)
  interpolationDuration: 800,       // Position interpolation animation duration (ms)
  interactiveMode: 'select',      // 'select' | 'link'
  showNodeIds: false,              // Show element ID labels
  onModeChange: (mode) => {},      // Callback when interactive mode changes
  temporalMode: 'design',          // Temporal mode: 'design' | 'operational' | 'incident'
  onTemporalModeChange: (mode) => {}, // Callback when temporal mode changes
  isometricMode: false,            // Enable isometric (pseudo-3D) tilt view
  ghosting: true,                  // Ghost previous act elements instead of hiding
  blastRadiusHops: 2,             // Default hop count for blast radius analysis
})
```

## Appendix B: Method Reference

### Registration
| Method | Description |
|--------|-------------|
| `topo.node(id, cfg)` | Register a node |
| `topo.anchor(id, { x, y, label, ... })` | Register an anchor point with optional label and offsets |
| `topo.link(id, cfg)` | Register a link |
| `topo.act(id, cfg)` | Register an act |
| `topo.addStep(id, cfg)` | Register a step within an act |
| `topo.showDuring(elementIds, { from, until })` | Declare persistent visibility for elements across a range of steps |
| `topo.glossary(terms)` | Register glossary terms |
| `topo.layer(id, cfg)` | Register or update a layer |
| `topo.setLayers(layers)` | Replace all layers at once |
| `topo.flowPath(id, cfg)` | Register a flow path overlay |
| `topo.setFlowPaths(entries)` | Replace all flow paths at once |
| `topo.policyMarker(id, cfg)` | Register a policy marker badge |
| `topo.setPolicyMarkers(entries)` | Replace all policy markers at once |

### Playback
| Method | Description |
|--------|-------------|
| `topo.mount(containerId)` | Mount into a DOM element and render |
| `topo.play()` | Start auto-play |
| `topo.pause()` | Stop auto-play |
| `topo.next()` | Advance one step |
| `topo.prev()` | Go back one step |
| `topo.reset()` | Go to step 0 |
| `topo.goTo(n)` | Jump to step n |
| `topo.render()` | Re-render the current state |

### Interactive
| Method | Description |
|--------|-------------|
| `topo.toggleNodeIds(show?)` | Toggle element ID labels |
| `topo.setMode(mode)` | Set interactive mode ('select' or 'link') |
| `topo.removeAnchor(id)` | Delete an anchor and its connected links |
| `topo.pathThrough(...ids)` | Generate a straight-line SVG path through a sequence of node/anchor IDs |
| `topo.pathBetween(fromId, toId, opts?)` | Generate a cubic bezier SVG path between two elements. `opts.bulge` controls curvature |
| `topo.graph` (getter) | Access the underlying TopologyGraph for graph-theoretic queries (neighbors, shortest path, etc.) |
| `topo.setTemporalMode(mode)` | Switch temporal mode (`'design'`, `'operational'`, `'incident'`) |
| `topo.setElementState(id, state)` | Set live metrics on a single element (`{ status, latency, jitter, loss }`) |
| `topo.setStateData(data)` | Bulk-set state data for multiple elements |
| `topo.toggleIsometric(enabled?)` | Toggle isometric tilt view; returns current state |
| `topo.toggleSecurityMode(enabled?)` | Toggle security analysis mode |
| `topo.validatePath(fromId, toId, rules?)` | Validate path between nodes; returns `{ valid, path, violations }` |
| `topo.validateTopology(rules?)` | Validate all links against rules; returns array of violations |
| `topo.getBlastRadius(nodeId, hops?)` | Get set of reachable node IDs within N hops via BFS |
| `topo.setBlastRadiusNode(nodeId\|null)` | Set or clear blast radius center node |
| `topo.setState(name, value)` | Set a state variable for conditional step logic |
| `topo.getState(name)` | Get a state variable value |

### Serialization & Import
| Method | Description |
|--------|-------------|
| `topo.toJSON()` | Export full topology state as JSON (includes zones, waypoints) |
| `topo.fromJSON(data)` | Import topology state from JSON |
| `topo.importFromText(str)` | Import from diagram-as-code text format |
| `topo.importDrawio(xml, options?)` | Import a draw.io / diagrams.net XML export |
| `topo.loadTemplate(name)` | Load a built-in template |
| `topo.applyTheme(name)` | Apply a color theme at runtime |
| `topo.autoLayout(algorithm)` | Auto-layout nodes using specified algorithm |
| `topo.setNodePositions(stepId, positions)` | Set position keyframes for choreography smoothing |

### Zone Annotations
| Method | Description |
|--------|-------------|
| `topo.zone(id, cfg)` | Register or update a zone annotation (label, nodes[], color, borderStyle, padding, description) |
| `topo.removeZone(id)` | Remove a zone annotation by ID |
| `topo.setZones(entries)` | Replace all zones from an array of [id, cfg] pairs |
| `topo.getChildZones(zoneId)` | Return child zone IDs whose `parentZone` matches the given zone |

### Link Waypoints & Routing
| Method | Description |
|--------|-------------|
| `topo.addWaypoint(linkId, pos, index?)` | Add a waypoint `{x,y}` to a link; appends unless `index` is given |
| `topo.removeWaypoint(linkId, index)` | Remove a waypoint by 0-based index |
| `topo.clearWaypoints(linkId)` | Remove all waypoints from a link |
| `topo.setLinkRouting(linkId, style)` | Set routing style: `'straight'` \| `'orthogonal'` \| `'curved'` |

### Export & Coordinate Readback
| Method | Description |
|--------|-------------|
| `topo.exportPNG()` | Export current diagram as a 2400 px PNG (async) |
| `topo.exportSVG()` | Export current diagram as a standalone SVG |
| `topo.exportPDF()` | Export current diagram as a print-ready PDF |
| `topo.exportStandaloneHTML()` | Export a fully self-contained HTML file with embedded CSS/JS (async) |
| `topo.copyEmbedCode()` | Copy an iframe embed snippet to clipboard |
| `topo.copyPositions()` | Copy all node positions to clipboard as JSON; returns Promise<object> |
| `topo.getPositionSnippet()` | Return a JS snippet recreating all node positions via `topo.node()` |

### Layout Edit Mode
| Method | Description |
|--------|-------------|
| `topo.enterLayoutEditMode()` | Enter layout edit mode — adds draggable chrome, guides, and coord tooltip |
| `topo.exitLayoutEditMode()` | Exit layout edit mode; restore original interactive mode |
| `topo.toggleLayoutEditMode()` | Flip the mode and return the new boolean state |

### Mobile Quick-Edit
| Method | Description |
|--------|-------------|
| `topo.enableMobileQuickEdit()` | Enable FAB + slide-up panel for touch editing |
| `topo.disableMobileQuickEdit()` | Disable and remove mobile quick-edit UI |

### AI-Assisted Authoring
| Method | Description |
|--------|-------------|
| `topo.showAIAssist()` | Open (or close) the AI-Assisted Topology Generation modal |
| `topo._parseAITopologyDescription(text)` | Parse natural language into `{ nodes, links }` (local, no API) |
| `topo._applyGeneratedTopology(result)` | Apply a parsed result to the current topology and re-render |

### Engagement Analytics
| Method | Description |
|--------|-------------|
| `topo.getAnalytics()` | Return engagement analytics summary (session duration, step views, completion, dwell times) |
| `topo.showAnalyticsDashboard()` | Open the glassmorphism analytics dashboard modal |

### Lifecycle Hooks
| Method | Description |
|--------|-------------|
| `topo.on('beforeRender', cb)` | Register a callback invoked before each render |
| `topo.on('afterRender', cb)` | Register a callback invoked after each render |
| `topo.on('onStepChange', cb)` | Register a callback invoked when the step changes |

### Presentation
| Method | Description |
|--------|-------------|
| `topo.present(options?)` | Enter fullscreen presentation mode. Options: `{ autoAdvance, showNotes }` |
| `topo.exitPresentation()` | Exit presentation mode, remove overlays, restore UI |

### Theme
| Method | Description |
|--------|-------------|
| `topo.setTheme(theme)` | Set theme to `'dark'` or `'light'`; persists to localStorage |
| `topo.getTheme()` | Returns current theme string (`'dark'` or `'light'`) |

### Templates
| Method | Description |
|--------|-------------|
| `topo.loadTemplate(id, options?)` | Load a built-in enterprise template by ID |

### Removal & Unsubscribe
| Method | Description |
|--------|-------------|
| `topo.removeNode(id)` | Remove a node and its connected links; returns `this` |
| `topo.removeLink(id)` | Remove a link by ID; returns `this` |
| `topo.off(event, callback)` | Unsubscribe a callback from a lifecycle hook |

### Static
| Method | Description |
|--------|-------------|
| `TopologyDesigner.registerNodeType(name, fn|plugin)` | Register a custom node renderer or plugin |
| `TopologyDesigner.registerLinkType(name, plugin)` | Register a custom link type plugin |
| `TopologyDesigner.getRegisteredNodeTypes()` | Get array of all registered node type names |
| `TopologyDesigner.getRegisteredLinkTypes()` | Get array of all registered link type names |
| `TopologyDesigner.renderEC(x, y, cfg)` | Render an EdgeConnect node |
| `TopologyDesigner.renderSwitch(x, y, cfg)` | Render a switch node |
| `TopologyDesigner.renderSwitchEnterprise(x, y, cfg)` | Render an enterprise switch node |
| `TopologyDesigner.renderCloud(x, y, cfg)` | Render a cloud node |
| `TopologyDesigner.renderHost(x, y, cfg)` | Render a host node |
| `TopologyDesigner.renderConnector(x, y, cfg)` | Render a connector/PEP node |
| `TopologyDesigner.renderApps(x, y, cfg)` | Render a private apps rack node |
| `TopologyDesigner.renderSaaS(x, y, cfg)` | Render a SaaS cloud node |
| `TopologyDesigner.renderServer(x, y, cfg)` | Render a server rack node |
| `TopologyDesigner.renderRouter(x, y, cfg)` | Render a router node |
| `TopologyDesigner.renderFirewall(x, y, cfg)` | Render a firewall node |
| `TopologyDesigner.renderDatabase(x, y, cfg)` | Render a database node |
| `TopologyDesigner.renderIdCard(x, y, cfg)` | Render an ID card badge node |
| `TopologyDesigner.renderAP(x, y, cfg)` | Render an access point node |
| `TopologyDesigner.renderOverlayCloud(x, y, cfg)` | Render an overlay cloud node |
| `TopologyDesigner.renderText(x, y, cfg)` | Render a text label node |
| `TopologyDesigner.renderShape(x, y, cfg)` | Render a geometric shape node |
| `LayoutEngine.magneticNorth(nodes, links, options?)` | Type-aware force-directed layout; returns Map of positions |
| `LayoutEngine.spineLeaf(nodes, links, options?)` | Data-center spine-leaf layout; returns Map of positions |
| `LayoutEngine.cluster(nodes, links, options?)` | Connected-component cluster layout; returns Map of positions |
| `LayoutEngine.removeOverlaps(positions, options?)` | Post-processing pass to separate overlapping nodes |
| `LayoutEngine.apply(topo, positions, animate?)` | Apply a computed positions Map to a TopologyDesigner instance |
| `TopologyDesigner.renderNodePreview(type, size?)` | Render an SVG preview of a node type at the given size |
| `TopologyDesigner.getNodeTypeCatalog()` | Return the full node type catalog grouped by category |

---

## AI Assist

The **✨ AI Assist** button in the editor toolbar lets you describe a network topology in plain English and have it generated automatically.

### How it works

1. Click **✨ AI Assist** in the toolbar.
2. Type a description (e.g. *"A firewall connects to a core switch, which connects to 3 servers and a database"*) or pick a chip suggestion.
3. Click **Generate Topology**.
4. Review the summary, then click **Apply to Canvas**.

The feature uses a two-stage pipeline:

| Stage | Condition | Description |
|-------|-----------|-------------|
| **Cloudflare Workers AI** | CF Pages deployment with AI binding configured | Calls `@cf/meta/llama-3.1-8b-instruct-fp8-fast` for high-quality results |
| **Local keyword parser** | No AI binding / local dev / network error | Falls back to built-in pattern matching — always available |

The fallback is seamless: if the AI endpoint is unreachable or returns an error, the local parser runs automatically.

### Cloudflare Pages setup

To enable the AI backend on a deployed Pages site:

1. Go to **Cloudflare Dashboard → Pages → topology-studio → Settings → Functions**.
2. Under **AI Bindings**, add a binding with variable name `AI`.
3. Redeploy (or wait for the next push to `main`).

For local development with `wrangler pages dev`, see `wrangler.toml` in the project root.

### Supported node and link types

**Nodes:** `ec`, `switch`, `switchEnterprise`, `cloud`, `host`, `connector`, `apps`, `saas`, `server`, `router`, `firewall`, `database`, `idcard`, `ap`, `text`

**Links:** `line`, `tunnel`, `wireguard`, `flow`, `packet`, `blocked`, `wifi`, `poe`, `optical`
