# WUYS Map — Foundational Design (Premium Edition)

**Status**: Authoring + executing. Globe view is broken in legacy `geo.js?v=48` (line 3399 `autoFontSize` ReferenceError). We will not patch it. Instead we build the replacement.

This document is the **single source of truth** for the new map system. Decisions are codified, the API is locked, every premium touch is captured.

---

## Decisions (final)

| Q | Choice | Rationale |
|---|--------|-----------|
| **Q1: Module system** | **ES modules** (`<script type="module">`) | Cleanest source, tree-shakable, no bundler tax for v1, future-proof. |
| **Q2: Label schema** | **Premium-extended** (see Part IV.6) | Every label property a real atlas would have. No hidden lock-in. |
| **Q3: Renderer pick** | **Auto / CPU / GPU**, user-selectable | SVG for accessibility & polish, Canvas2D for CPU performance, WebGL for hot paths and games. |
| **Q4: Migration** | **Build in parallel, then swap.** Premium feel in every aspect. | Old code stays unchanged until new system is feature-complete. No risk of half-done releases. |

---

## Part I — What broke and why (kept as warning)

Line 3399 of `geo.js` references `autoFontSize` from a sibling scope where the variable was deleted mid-refactor. Globe text label rendering throws `ReferenceError` on each frame. The `requestAnimationFrame` chain silently dies.

This is a symptom of the structural disease — implicit coupling, shared mutable globals, no boundaries. Documented in detail in the inventory below.

We are not fixing this. We are replacing it.

---

## Part II — Inventory of the legacy system

| File | Lines/Size | Purpose | Fate after migration |
|------|------------|---------|----------------------|
| `geo.js` | 4,444 lines | Quiz + map + editor + audio in one IIFE | Quiz only (~2,500 lines) |
| `world-map.svg` | 244 lines / 165KB | Pre-projected Equal Earth flat map | **Deleted** |
| `world-map-data.js` | 7.6 MB | SVG wrapped as JS string (file:// fallback) | **Deleted** |
| `world-topo-data.js` | 3.6 MB | Natural Earth 10m topojson | **Moved to** `map/data/topology-10m.json` |
| `flags-data.js` | 1.25 MB | 255 country flags base64 PNG @ w640 | **Moved to** `map/data/flags.js` |
| `styles.css` map section | ~400 lines | Map CSS scattered with other features | **Moved to** `map/themes/*.css` |
| `/tmp/build-*.js` | uncommitted | Manual build scripts | **Committed** at `map/build/` |

**LocalStorage keys** map writes:
- `wuys-map-theme`, `wuys-map-flags`, `wuys-label-overrides-v3` → migrated into namespaced `wuys-map:*` keys, then removed from legacy
- Quiz keys (`wuys-geo-best-*`, `wuys-geo-size`, `wuys-geo-country`) stay where they are

---

## Part III — Design principles

### 1. Pure data, pluggable renderer
The map's truth is data + math. Rendering plugs in. Same data drives SVG, Canvas, WebGL.

### 2. ONE coordinate system: geographic
All user-facing API is `(lon, lat)` in degrees. Projections convert on demand. Never store view-space coordinates anywhere.

### 3. Observable state
State is in one store per map instance. Mutations go through commands. Subscribers get notified. Zero shared globals.

### 4. Build-then-patch render lifecycle
Each renderer: `init(target, data)` once → `patch(state)` per frame. Never `innerHTML` in the hot path. 60fps floor.

### 5. Pay-for-what-you-use
Layers, renderers, premium features are individually importable. A page that only needs a flat map shouldn't pull WebGL code. ES module tree-shake makes this free.

### 6. Premium-first defaults
The defaults look beautiful. The user doesn't have to opt in to good rendering, smooth animations, sensible interactions. They have to opt OUT for performance modes.

---

## Part IV — Architecture

### 1. Module structure

```
emotions/map/
  README.md                       # Quick start, examples, license
  PLAN.md                         # This file
  CHANGELOG.md                    # Versioned
  
  src/
    index.js                      # Public API: createMap, projections, themes, layers
    
    core/
      state.js                    # Single store per instance
      events.js                   # EventBus (on/off/emit, typed)
      animation.js                # rAF loop, easing curves, target chase
      nav.js                      # Commands: setCenter, flyTo, panBy, zoomBy, fitBounds
      labels.js                   # Canonical label model + auto-sizing
      editor.js                   # Drag/resize/rotate state machine (pure)
      hash.js                     # URL hash sync (#map/...)
      perf.js                     # Dev HUD: frame timer, draw count
    
    math/
      projections/
        equal-earth.js            # Forward + inverse
        orthographic.js           # Forward + inverse
        mercator.js               # Forward + inverse (for game-friendly mode)
        robinson.js               # Forward + inverse (atlas-classic)
        winkel-tripel.js          # Forward + inverse (NatGeo)
      geodesy.js                  # Great-circle distance, bearing, midpoint
      antimeridian.js             # Polygon split on dateline crossing
      horizon.js                  # Sphere-side clipping
      bezier.js                   # Smooth arc paths (for refugee flows etc.)
    
    data/
      topology.js                 # Decode topojson, normalize to GeoJSON-lite
      centroids.js                # Largest-sub-polygon shoelace centroid
      iso-codes.js                # Numeric ↔ alpha2 mapping (full ISO 3166)
      country-meta.js             # Builds cache: { bbox, centroid, area } per iso
      loader.js                   # Lazy fetcher with cache, progress events
    
    layers/
      base.js                     # Layer interface + lifecycle
      countries.js                # Country polygons (data-iso paths)
      flags.js                    # Flag images, clip-pathed
      labels.js                   # Text labels per country
      spotlights.js               # Microstates, sub-national landmarks
      contested.js                # Tibet, Xinjiang, Crimea, Kurdistan, Tigray
      hover.js                    # Tooltip overlay
      atmosphere.js               # Globe-edge glow + space gradient
      day-night.js                # Solar terminator overlay
      mini-map.js                 # Corner overview rectangle
      heatmap.js                  # Generic per-country value coloring
      markers.js                  # Generic point markers
      arcs.js                     # Great-circle arcs between points
      grid.js                     # Lat/lon grid lines (configurable density)
    
    renderers/
      base.js                     # Renderer interface
      svg.js                      # Primary (web, accessibility)
      canvas.js                   # CPU performance + games
      webgl.js                    # GPU performance + 3D scenes
      detect.js                   # Auto-pick: GPU if available, else CPU
    
    interactions/
      mouse.js                    # Drag, wheel, hover, click
      touch.js                    # Pan, pinch, two-finger rotate
      keyboard.js                 # Arrow keys, modifiers, shortcuts
      gamepad.js                  # Analog stick (for game integration)
    
    themes/
      vector.js                   # Modern, clean, dark
      antique.js                  # Parchment + sea-teal + ink labels
      toon.js                     # Cel-shaded
      pixel.js                    # 8-bit retro
      satellite.js                # Realistic ocean + terrain tint (future)
      print.js                    # B&W high-contrast (for export)
    
    integrations/
      web-component.js            # <wuys-map> custom element
      vanilla.js                  # createMap() function
      react.js                    # <Map /> + hooks (later)
      tauri.js                    # Native desktop integration helper
    
    plugins/
      url-hash.js                 # Auto-restore from #map/...
      search.js                   # Country search box
      keyboard-help.js            # ? key → shortcut overlay
      screenshot.js               # Export current view as PNG/SVG
      label-editor.js             # ✎ button → drag/resize/rotate labels
      stats.js                    # Live country statistics overlay (pluggable data)

  data/                           # Static assets, loaded lazily
    topology-10m.json             # ONE source of truth (3.6 MB)
    centroids.json                # Pre-computed per-country centroids (~30 KB)
    flags/                        # PNG files (regenerated by build)
      ar.png, br.png, ...
    label-positions.json          # Curated per-country canonical labels
    contested-regions.json        # Hand-traced polygons
    spotlights.json               # Microstate markers + sub-national landmarks
    iso-codes.json                # Numeric ↔ alpha2 ↔ alpha3
  
  build/
    bundle.js                     # ES modules → optionally one file
    rebuild-topology.js           # Fetch + project + simplify
    rebuild-flags.js              # Fetch from flagcdn, optimize, pack
    rebuild-centroids.js          # Pre-compute centroids
    package-data.js               # JSON → base64-JS for file:// support
    deploy.sh                     # One command: refresh everything
  
  examples/
    01-minimal.html               # 30-line map mount
    02-quiz-page.html             # Replicates current geo tab
    03-heatmap.html               # Corruption-by-country overlay
    04-flyto-search.html          # Search + animated fly-to
    05-game-overlay.html          # Canvas renderer, manual tick
    06-webgl-globe.html           # 3D globe with atmosphere
    07-custom-theme.html          # Define + apply a new theme
    08-screenshot.html            # Export PNG of current view
    09-plugin-recipe.html         # How to write a custom layer
    10-server-render.html         # Node-side SVG (for thumbnails)
  
  test/                           # When we want them
    projections.test.js
    centroids.test.js
    labels.test.js
    nav.test.js
    layers/*.test.js
```

### 2. Public API surface

```js
// Import what you need (tree-shakable)
import { createMap, projections, themes, layers, renderers } from "/map/src/index.js";

// Create an instance
const map = await createMap({
  target: document.getElementById("geo-hub-grid"),
  
  // What to show
  view: "flat",                              // "flat" | "globe" | "split" (both side-by-side)
  projection: projections.equalEarth,        // or .orthographic, .mercator, .robinson, .winkel-tripel
  
  // How to render
  renderer: "auto",                          // "auto" | "svg" | "canvas" | "webgl"
  pixelRatio: window.devicePixelRatio,       // crisp on retina
  
  // Appearance
  theme: themes.vector,                      // or .antique, .toon, .pixel, .satellite, .print
  layers: ["countries", "labels", "spotlights", "contested"],
  
  // Data
  data: {
    topology: "./map/data/topology-10m.json",
    flags: "./map/data/flags.js",
    labels: "./map/data/label-positions.json",
    contested: "./map/data/contested-regions.json",
    spotlights: "./map/data/spotlights.json",
  },
  
  // Initial position
  position: { lon: 0, lat: 0, zoom: 1 },
  
  // Behavior
  interactions: ["mouse", "touch", "keyboard"],  // or false to disable
  autoTick: true,                                // false for manual loop (games)
  autoRestore: true,                             // restore from URL hash
  
  // i18n
  getCountryName: iso => t("page.geo.country."+iso+".name", iso.toUpperCase()),
  
  // Performance
  fps: 60,                                       // target frame rate
  qualityMode: "auto",                           // "auto" | "performance" | "quality"
});

// Navigation API
map.nav.setCenter({ lon, lat });
map.nav.setZoom(4);
map.nav.flyTo({ lon, lat, zoom, duration: 800, easing: "easeOutCubic" });
map.nav.panBy({ dx, dy });                       // pixels
map.nav.fitCountry("ru", { padding: 50 });
map.nav.fitBounds({ lonMin, lonMax, latMin, latMax });
map.nav.reset({ animated: true });

// View + theme
map.view.set("globe", { animated: true });       // smooth crossfade
map.theme.set(themes.antique);
map.theme.flagsVisible = false;

// Layers — pluggable, queryable
map.layers.toggle("labels");
map.layers.get("countries").setFill(iso, color);  // for heatmaps
map.layers.get("countries").setStroke(iso, color);
map.layers.add(myCustomLayer);
map.layers.remove("labels");
map.layers.reorder(["countries", "heatmap", "labels"]);  // z-order

// Events (typed)
map.on("country:click",     (iso, ev) => {});
map.on("country:hover",     (iso) => {});
map.on("country:leave",     (iso) => {});
map.on("nav:start",         (state) => {});
map.on("nav:change",        (state) => {});
map.on("nav:end",           (state) => {});
map.on("view:change",       (mode) => {});
map.on("theme:change",      (theme) => {});
map.on("layer:add",         (layer) => {});
map.on("layer:remove",      (layer) => {});
map.on("ready",             () => {});
map.on("destroy",           () => {});

// Game integration (manual control)
map.autoTick = false;
gameLoop.onUpdate(dt => map.tick(dt));      // advance animations
gameLoop.onRender(() => map.render());      // request a render

// Direct coordinate helpers
const { x, y } = map.worldToScreen({ lon, lat });
const { lon, lat } = map.screenToWorld({ x, y });
const viewport = map.getViewport();           // { lonMin, lonMax, latMin, latMax }
const center = map.getCenter();               // { lon, lat, zoom }

// Snapshot / export
const dataUrl = await map.exportPNG({ width: 2000, height: 1000 });
const svgString = await map.exportSVG();
const config = map.serialize();
map.restore(config);

// Plugin install
map.use(plugin);                              // calls plugin(map)

// Cleanup
map.destroy();
```

### 3. Renderer contract

```typescript
interface Renderer {
  // Lifecycle
  init(target: Element, data: MapData, opts: RendererOpts): Promise<void>;
  destroy(): void;
  
  // Hot path (called per frame when state changes)
  patch(state: MapState): void;
  
  // Manual control (for games)
  render(): void;                              // explicit one-shot
  tick(dt: number): void;                      // step internal animations
  
  // Queries
  hitTest(x: number, y: number): HitResult | null;
  worldToScreen(point: GeoPoint): ScreenPoint;
  screenToWorld(point: ScreenPoint): GeoPoint;
  getViewport(): Bounds;
  
  // Export
  exportPNG(opts?: ExportOpts): Promise<string>;     // canvas/webgl native
  exportSVG(opts?: ExportOpts): Promise<string>;     // svg native; others rasterize
  
  // Performance hints
  setQualityMode(mode: "performance" | "quality"): void;
  getStats(): { fps, drawCalls, frameTime };
}
```

Each renderer implements this. Core orchestrates.

**Auto-detect rule for `renderer: "auto"`**: WebGL2 if available & `(country_count > 200 || zoom > 6)`, else SVG. Falls back gracefully on context loss.

### 4. State store (one per instance)

```typescript
type MapState = {
  data: DataState;             // topology, countries, flags, labels, etc.
  nav: NavState;               // lon, lat, zoom (current + target)
  view: { mode, projection };
  theme: { id, flagsVisible, customColors };
  layers: { enabled, config, order };
  editor: { mode, selectedIso, drag };
  interactions: { isPointerDown, isAnimating, hoveredIso };
  perf: { fps, frameTime, drawCalls };       // dev HUD
}
```

Single store. Subscribers via `EventBus`. No globals.

### 5. Canonical label schema (PREMIUM)

Every label, in every storage location, has this shape:

```typescript
type Label = {
  iso: string;                          // "ca", "_us_dc", "_tibet"
  
  // POSITION
  lon: number;                          // absolute degrees
  lat: number;                          // absolute degrees
  anchor: "center" | "topleft" | "topright" | "bottomleft" | "bottomright" | "top" | "bottom" | "left" | "right";
                                        // where label "x,y" attaches to text
  
  // SIZE (view-agnostic)
  sizeRatio: number;                    // label height ÷ country projected height
  minSizePx?: number;                   // never render smaller than N screen pixels (readable floor)
  maxSizePx?: number;                   // never larger than M screen pixels
  
  // VISIBILITY (LOD — level of detail)
  visibleZoom?: [number, number];       // [minZoom, maxZoom] — hide outside range
  visibleViews?: ("flat" | "globe")[];  // hide in some views
  
  // TYPOGRAPHY
  text?: string;                        // override auto i18n name
  shortText?: string;                   // for low-zoom; e.g. "USA" vs "United States"
  textTransform?: "none" | "uppercase" | "smallcaps";
  fontFamily?: string;                  // override theme default
  fontWeight?: number;                  // 100–900
  fontStyle?: "normal" | "italic";
  letterSpacing?: number;               // em
  
  // ROTATION
  rotate: number;                       // degrees, screen-space
  rotateMode?: "screen" | "geographic"; // geographic rotates with view
  
  // COLOR / DECORATION
  fill?: string;                        // text fill (CSS color)
  stroke?: string;                      // halo
  strokeWidth?: number;
  glow?: { color: string; radius: number; }; // soft glow
  background?: {                        // pill behind text
    fill: string;
    stroke?: string;
    padding: number;
    radius: number;
  };
  
  // ICON PREFIX
  icon?: {                              // small graphic before text
    type: "flag" | "pin" | "circle" | "custom";
    src?: string;                       // for "custom"
    size?: number;                      // multiplier
  };
  
  // ANIMATION
  enter?: "fade" | "scale" | "drop" | "none";  // appear animation
  exit?: "fade" | "scale" | "rise" | "none";   // disappear animation
  enterDuration?: number;               // ms
  
  // PER-VIEW OVERRIDES (rare; some labels look better positioned differently)
  flatOverride?: Partial<Label>;
  globeOverride?: Partial<Label>;
}
```

**Defaults** are smart so a label entry can be as terse as:
```js
{ iso: "ca", lon: -106.3, lat: 56.1 }   // everything else inferred
```

### 6. Theme schema (PREMIUM)

```typescript
type Theme = {
  id: string;
  name: string;                         // user-facing
  
  // Background (around the map)
  background: string | CSSGradient;
  
  // Ocean
  ocean: {
    fill: string;
    stroke?: string;
    strokeWidth?: number;
    pattern?: "none" | "waves" | "stipple"; // SVG <pattern>
  };
  
  // Countries
  countries: {
    fill: string | "flag" | ((iso) => string);  // "flag" = use flag pattern
    stroke: string;
    strokeWidth: number;
    hoverFill?: string;
    activeFill?: string;
    pendingFilter?: string;             // CSS filter for un-quizzed countries
  };
  
  // Borders (between countries)
  borders: {
    color: string;
    width: number;
    style?: "solid" | "dashed" | "dotted";
  };
  
  // Labels
  labels: {
    fill: string;
    stroke?: string;
    strokeWidth?: number;
    fontFamily: string;
    fontWeight?: number;
    fontStyle?: string;
  };
  
  // Contested regions
  contested: {
    fill: string;
    stroke: string;
    strokeWidth: number;
    strokeDasharray: string;
  };
  
  // Spotlights
  spotlights: {
    fill: string;
    border: string;
    glow?: string;
  };
  
  // Globe extras
  globe?: {
    atmosphere?: { color: string; intensity: number; };
    dayNightTerminator?: { tint: string; opacity: number; };
  };
  
  // CSS variables exposed for SVG renderer
  cssVars: Record<string, string>;
}
```

### 7. Layer interface

```typescript
type Layer = {
  id: string;
  enabled: boolean;
  order: number;
  
  // Lifecycle
  init(map: MapInstance): void;
  destroy(): void;
  
  // Render hook (called by renderer at appropriate z-order)
  render(ctx: RenderContext): void;
  
  // Optional
  hitTest?(point: ScreenPoint): HitResult | null;
  onStateChange?(state: MapState): void;
  
  // For data-driven layers
  setData?(data: any): void;
}
```

Built-in layers: `countries`, `flags`, `labels`, `spotlights`, `contested`, `hover`, `atmosphere`, `day-night`, `mini-map`, `heatmap`, `markers`, `arcs`, `grid`.

---

## Part V — Premium feature set

The "next-gen feel" delivered in the foundation, not as add-ons.

### Core navigation
- **Smooth animations**: cubic-bezier easing on every transition (not linear).
- **Inertia / momentum** on drag release.
- **Snap-to-bounds** when panning out of map.
- **Smart zoom-to-fit**: clicking a country gently flies + zooms to fit.
- **Pinch + rotate gestures** on touch (two-finger rotate spins the globe).
- **Velocity-based zoom**: wheel speed determines zoom magnitude.
- **Multi-touch precision** on retina/touch displays.

### Visual polish
- **Subpixel antialiasing** via correct SVG `shape-rendering` attributes per zoom.
- **Smooth color transitions** between themes (CSS variable transitions, 200ms).
- **Atmosphere glow** on globe: subtle radial gradient at the limb.
- **Day/night terminator**: optional overlay, solar-position-aware, updates in real-time.
- **Star field** background option for globe mode.
- **Ocean waves** texture pattern in antique mode.
- **Country shadows** subtle drop-shadow on hover.
- **Smooth crossfade** when toggling views (flat ↔ globe).
- **Loading shimmer**: world-map silhouette filling in instead of spinner.

### Label intelligence (premium-tier)
- **Automatic LOD**: small countries get short names at low zoom, full names at high zoom.
- **Collision avoidance**: labels reposition slightly to avoid overlapping neighbors.
- **Edge-of-globe culling**: labels near the visible horizon fade smoothly out.
- **Per-view overrides**: a label can position differently in flat vs globe if natural.
- **Editable in-place**: ✎ tool with drag/resize/rotate.
- **Halo & glow**: configurable text shadow / outline.

### Interaction & UX
- **Country search**: type "Rus" → filter list → arrow nav → Enter to fly-to.
- **URL hash sync**: `#map/view=globe,lon=12.3,lat=20.5,z=4.2,theme=antique`.
- **Keyboard shortcuts**:
  - Arrows = pan/rotate
  - Shift+arrows = fast (3×)
  - Ctrl+arrows = warp (6×)
  - `+`/`-` = zoom
  - `Space` = reset
  - `G` = toggle globe
  - `F` = toggle flags
  - `T` = cycle theme
  - `S` = open search
  - `?` = help overlay
- **`?` overlay** with all shortcuts shown.
- **Screenshot export** to PNG or SVG.
- **Right-click context menu**: "Copy lat/lon", "Center here", "Open quiz", etc.
- **Touch gestures** documented and consistent across views.

### Performance
- **Build-once-patch-per-frame** rendering. No `innerHTML` in animation hot path.
- **DOM element cache** per layer, reused across frames.
- **Topology decoded once**, projected per frame from raw arcs.
- **Back-hemisphere culling** on globe (skip ~50% of countries each frame).
- **Adaptive LOD**: simplify polygons at low zoom (Douglas-Peucker).
- **WebGL renderer** for power users + games.
- **Web Workers** for projection math on large overlays (heatmap, etc.).
- **`requestIdleCallback`** for non-critical work (label collision resolution, etc.).
- **rAF throttling**: never more than 60 frames/sec.

### Developer experience
- **Dev HUD**: press Ctrl+Shift+M → see FPS, frame time, draw calls.
- **Type definitions** via JSDoc comments (works in VSCode without TS).
- **Source maps** when bundled.
- **Verbose error messages** with hints ("Did you mean ...?").
- **Console API**: `wuysMap.gotoCountry("us")`, `wuysMap.serialize()`.
- **Example for every feature** in `/examples`.

### Data efficiency
- **Lazy loading** of topology + flags + labels.
- **Progressive enhancement**: 110m loads first (300 KB), 10m streams in if zoomed.
- **Compressed JSON** (topojson is already delta-encoded).
- **Differential updates**: changing a single country's color updates one path attribute, not the whole layer.

### Game integration
- **Manual loop mode**: `autoTick: false` → game controls timing.
- **Render-to-texture**: WebGL renderer can output to an `OffscreenCanvas` for use as a texture.
- **Event-driven**: every action emits an event, easy for game logic to react.
- **Serializable state**: save/load games include map state trivially.
- **No DOM required** for canvas/webgl renderer — runs in worker, headless, anywhere.

### Accessibility
- **ARIA labels** on every country path (SVG renderer).
- **Keyboard navigation** through countries (Tab cycles).
- **Screen reader** announces hovered country.
- **High-contrast theme** (`print`) for export & accessibility.
- **Respects `prefers-reduced-motion`** — disables animations.
- **Respects `prefers-color-scheme`** — auto-pick theme.

---

## Part VI — Migration roadmap

### Phase A: Build new module in parallel (3–4 sessions)

`geo.js` untouched. New code lives in `emotions/map/`.

**Session A1** — `core/` foundation:
- `state.js`, `events.js`, `animation.js`, `nav.js`, `hash.js`, `perf.js`
- `examples/01-minimal.html` shows nav events firing (no rendering yet)

**Session A2** — `math/` + `data/`:
- All five projections + inverses, antimeridian, horizon, geodesy, bezier
- Topology decoder, centroid computation, ISO codes
- Unit tests for projection round-trips

**Session A3** — `renderers/svg.js` + core layers:
- SVG renderer implementing full contract
- `layers/countries.js`, `layers/flags.js`, `layers/labels.js`
- `examples/02-quiz-page.html` matches geo tab pixel-close

**Session A4** — premium layers + interactions:
- `layers/spotlights.js`, `layers/contested.js`, `layers/hover.js`, `layers/atmosphere.js`
- `interactions/mouse.js`, `touch.js`, `keyboard.js`
- `themes/vector.js`, `antique.js`, `toon.js`, `pixel.js`
- `examples/03-heatmap.html` and `04-flyto-search.html`

### Phase B: Data baking (1 session)

- Migrate `LABEL_OFFSETS` from `geo.js` → `data/label-positions.json` with canonical absolute lon/lat + sizeRatio.
- Move `world-topo-data.js` → `data/topology-10m.json`.
- Move `flags-data.js` → `data/flags.js` (or PNG files in `data/flags/`).
- Build script committed: `npm run build:map` regenerates everything.
- Delete redundant `world-map.svg` and `world-map-data.js` (-7.6 MB).

### Phase C: Swap geo.js (1 session)

- Replace ~2,000 lines of map code in `geo.js` with:
  ```js
  import { createMap } from "/map/src/index.js";
  const map = await createMap({ target: hubGrid, ... });
  map.on("country:click", iso => showStage(iso));
  ```
- Quiz code stays.
- `geo.js` shrinks to ~2,500 lines (just quiz + glue).

### Phase D: Premium polish layers (per-session, in any order)

Each is a tiny, additive change with zero foundation risk:
- Country search + flyTo
- Rich hover tooltips
- Atmosphere glow + day/night
- Theme crossfade
- Mini-map
- Screenshot export
- Tax-haven overlay
- Power-hub overlay
- Conflict timeline

### Phase E: Additional renderers (when needed)

- **Canvas2D** when a game/dashboard needs it
- **WebGL** when a 3D scene needs it

Both implement the same `Renderer` contract. Adding doesn't touch core.

### Phase F: Public release (the dream)

When stable + documented, publish `@wuys/map` to npm. Other people can use it.

---

## Part VII — Data strategy

### Single source of truth

`map/data/topology-10m.json` (Natural Earth 10m, ~3.6 MB).

Everything derived: country polygons (any projection), centroids, bboxes, areas.

### File formats

| Asset | Format | Size | Strategy |
|-------|--------|------|----------|
| Topology | TopoJSON | 3.6 MB | Lazy-load on first mount |
| Flags | PNG @ w640 | 1.25 MB | Lazy-load on first mount |
| Centroids | JSON | 30 KB | Eager (always needed) |
| Labels | JSON | 50 KB | Eager |
| Contested regions | JSON | 5 KB | Eager |
| Spotlights | JSON | 10 KB | Eager |
| ISO codes | JSON | 20 KB | Eager |

**Total lazy payload: ~4.9 MB** (vs current ~12 MB).
**Total eager payload: ~115 KB** (instant init).

### Build pipeline

```
naturalearth 10m + npm package
              │
              ▼
build/rebuild-topology.js
              │
              ▼
data/topology-10m.json + data/centroids.json
              │
              ▼
build/package-data.js (optional, for file:// support)
              │
              ▼
data/topology-10m.data.js  (base64-inline)
```

`npm run build:map` regenerates everything. Committed scripts replace the lost `/tmp/build-*.js`.

---

## Part VIII — Open questions resolved

| Q | Resolution |
|---|------------|
| ES modules vs IIFE? | **ES modules.** `<script type="module" src="map/src/index.js">`. |
| Plugin registration? | `map.use(plugin)` calls `plugin(map)`. Plugins register layers, listeners, commands. |
| Themes for non-SVG renderers? | Theme = JS object. SVG renderer emits CSS variables. Canvas/WebGL reads raw values. |
| i18n? | `createMap({ getCountryName: iso => t(...) })`. Map module doesn't know about i18n; app supplies. |
| Service worker? | `fetch()` for data; SW caches transparently. No special integration. |
| Type hints? | JSDoc comments throughout. VSCode IntelliSense works without TS step. |

---

## Part IX — Performance targets

Measured with `performance.mark`. Dev HUD displays live.

| Operation | Target | Hard cap |
|-----------|--------|----------|
| First mount (cold) | < 1500 ms | 3000 ms |
| First mount (cached) | < 300 ms | 600 ms |
| Pan frame (SVG) | < 6 ms | 16 ms |
| Pan frame (Canvas) | < 3 ms | 16 ms |
| Pan frame (WebGL) | < 2 ms | 16 ms |
| View toggle (animated) | 800 ms total | — |
| Theme change | 200 ms crossfade | — |
| Label editor drag | < 16 ms / frame | — |
| Screenshot export | < 500 ms | 2000 ms |

---

## Part X — Status & execution log

This section is updated as phases ship.

### ✅ Plan written and locked
This document.

### ▶ Phase A — Foundation in parallel
*Currently in flight.*

**A1 — Core (in progress, this session)**:
- [x] Directory structure created
- [x] `core/state.js` — single store with subscribe/notify
- [x] `core/events.js` — typed EventBus
- [x] `core/animation.js` — rAF + easing curves + target chase
- [x] `core/nav.js` — high-level commands
- [x] `math/projections/equal-earth.js`, `orthographic.js`
- [x] `data/topology.js` — decoder
- [x] `index.js` — public API entry stub
- [ ] (next session) `renderers/svg.js`, layers, examples

**Legacy `geo.js` remains untouched** with its globe-text bug. Users currently visiting the geo tab see a broken globe label rendering. This is accepted technical debt during the parallel build.

---

*This plan is the contract. No more patching the legacy IIFE.*
*All future map work happens in `emotions/map/`.*
