/* Common Works — pixel scene engine (home page hero + CTA)
   A pixel-art industrial landscape: five portfolio businesses arranged
   around a compact Common Works operating hub. On load, the hub sends
   pulses down the cables and each business comes online in sequence —
   crews walk out, lights come up, machinery starts. The scene is drawn
   on a low-resolution canvas upscaled with image-rendering:pixelated
   (one logical unit = one pixel), so the art stays crisp and the whole
   world costs a single element. The art targets medium-density early-90s
   simulation-game detail: fine window grids, rooflines, docks, pipes and
   conveyors, with small but legible people and vehicles. Sprites elsewhere
   on the page are tiny run-length-encoded SVGs from ASCII maps. */
const { useState, useEffect, useRef } = React;

const PX_RM = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);

/* ============================================================
   SCENE RENDERER
   ============================================================ */

/* deterministic per-pixel hash — stable window patterns across frames */
function pxh(a, b, c = 0) {
  const n = Math.sin(a * 127.1 + b * 311.7 + c * 74.7) * 43758.5453;
  return n - Math.floor(n);
}

function rct(ctx, c, x, y, w, h) { ctx.fillStyle = c; ctx.fillRect(x | 0, y | 0, w, h); }

const PXC = {
  ridge: "#1A2B20", ridge2: "#152218",
  walk: "#212D22", road: "#171F18", below: "#182219",
  edge: "#11170F",
  bodyA: "#2E382C", bodyB: "#354136", bodyC: "#2F3A2F", bodyD: "#333F32",
  pilaster: "#273025",
  parapet: "#3D493A", coping: "#48543F",
  roof: "#232C22",
  metal: "#49553F", metalHi: "#5F6B54",
  winOff: "#1E2620", winWarm: "#E6D9AE", winPre: "#6E6748",
  winCool: "#DDE7C6", glass: "#1D2B21", frame: "#141B14",
  sill: "#4A5443",
  mintHi: "#C9EED8", mint: "#9FD3B4", mintLo: "#6FA88C",
  pulse: "#CBEFD9",
  amber: "#D9A662", amberLo: "#8F7A2E",
  crate: "#4A4032", crate2: "#5C5140", crate3: "#332C22",
  tree: "#233022", treeHi: "#2E3D33", treeLo: "#1B261B",
};

/* sky: near-black space at the top, blooming into the brand forest green
   toward the lower atmosphere — a dawn read rather than a flat night sky.
   Colors are re-blended every frame (see sunrise below), so parseColor
   accepts both "#rrggbb" and the "rgb(r,g,b)" strings lerpColor returns,
   letting a second blend chain on top of the first. */
function hexToRgb(hex) {
  const n = parseInt(hex.slice(1), 16);
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
function parseColor(c) {
  if (c[0] === "#") return hexToRgb(c);
  const m = c.match(/[\d.]+/g).map(Number);
  return [m[0], m[1], m[2]];
}
function lerpColor(a, b, t) {
  const [r1, g1, b1] = parseColor(a), [r2, g2, b2] = parseColor(b);
  return "rgb(" + Math.round(r1 + (r2 - r1) * t) + "," + Math.round(g1 + (g2 - g1) * t) + "," + Math.round(b1 + (b2 - b1) * t) + ")";
}
const SKY_TOP = "#0A0B12";
const SKY_BOTTOM = "#1B3A2F";
/* the brighter target the sky slowly blends toward as the sunrise progresses */
const SKY_DAWN_TOP = "#26402F";
const SKY_DAWN_BOTTOM = "#699478";
const SUNRISE_DUR = 150; // seconds to reach full morning brightness
const SKY_BANDS = 28;
function buildSkyBands(skyH) {
  const bands = [];
  for (let i = 0; i < SKY_BANDS; i++) {
    const eased = (i / (SKY_BANDS - 1)) ** 1.5; // hold the near-black longer, bloom smoothly toward the horizon
    bands.push({
      y0: Math.round((i / SKY_BANDS) * skyH),
      y1: Math.round(((i + 1) / SKY_BANDS) * skyH),
      eased,
    });
  }
  return bands;
}
function skyBandColor(band, dawnP) {
  const base = lerpColor(SKY_TOP, SKY_BOTTOM, band.eased);
  if (dawnP <= 0) return base;
  const dawn = lerpColor(SKY_DAWN_TOP, SKY_DAWN_BOTTOM, band.eased);
  return lerpColor(base, dawn, dawnP);
}

/* Building registry. keep = priority when the viewport can't fit everyone
   (hub always survives). act = seconds until the hub brings it online. */
const PX_SPECS = [
  { id: "factory",   w: 76, keep: 3, act: 6.5 },
  { id: "logistics", w: 88, keep: 2, act: 3.0 },
  { id: "hub",       w: 52, keep: 0, act: 0 },
  { id: "software",  w: 44, keep: 1, act: 10.0 },
  { id: "health",    w: 52, keep: 4, act: 13.5 },
  { id: "consumer",  w: 54, keep: 5, act: 17.0 },
];

const PX_ANCHOR = {
  factory:   (p, G) => [p.x + 18, G - 46],
  logistics: (p, G) => [p.x + 48, G - 28],
  software:  (p, G) => [p.x + 10, G - 92],
  health:    (p, G) => [p.x + 29, G - 49],
  consumer:  (p, G) => [p.x + 8,  G - 32],
  hub:       (p, G) => [p.x + 40, G - 56],
};

function buildScene(W, H) {
  const G = H - 14;
  const byKeep = [...PX_SPECS].sort((a, b) => a.keep - b.keep);
  const chosen = new Set();
  let total = 0;
  for (const s of byKeep) {
    const cost = s.w + (chosen.size ? 8 : 0);
    if (total + cost <= W - 12) { chosen.add(s.id); total += cost; }
  }
  const specs = PX_SPECS.filter(s => chosen.has(s.id));
  const n = specs.length;
  const sumW = specs.reduce((a, s) => a + s.w, 0);
  const gap = Math.max(6, Math.min(26, Math.floor((W - sumW - 16) / Math.max(1, n - 1))));
  let cx = Math.floor((W - (sumW + gap * (n - 1))) / 2);
  const placed = [];
  for (const s of specs) {
    const p = { ...s, x: cx };
    p.anchor = PX_ANCHOR[s.id](p, G);
    placed.push(p);
    cx += s.w + gap;
  }
  const byId = {};
  placed.forEach(p => { byId[p.id] = p; });

  /* street furniture in the gaps and margins */
  const lamps = [], trees = [];
  for (let i = 0; i < placed.length - 1; i++) {
    const c = placed[i].x + placed[i].w + gap / 2;
    if (gap >= 10) lamps.push(Math.round(c));
    if (gap >= 20) trees.push(Math.round(c) + 7);
  }
  const m0 = placed[0].x, m1 = W - (placed[n - 1].x + placed[n - 1].w);
  if (m0 > 24) { trees.push(m0 - 12); lamps.push(m0 - 21); }
  if (m1 > 24) { trees.push(W - m1 + 12); lamps.push(W - m1 + 21); }

  return { W, H, G, placed, byId, lamps, trees, skyBands: buildSkyBands(G), src: byId.hub ? byId.hub.anchor : [W >> 1, 40] };
}

/* stepped cable with a slight sag; pulses travel the same path */
function cableSag(a, b) { return Math.min(9, Math.hypot(b[0] - a[0], b[1] - a[1]) * 0.07); }
function cablePoint(a, b, p) {
  return [
    Math.round(a[0] + (b[0] - a[0]) * p),
    Math.round(a[1] + (b[1] - a[1]) * p + Math.sin(Math.PI * p) * cableSag(a, b)),
  ];
}
function drawCable(ctx, a, b, alpha) {
  const steps = Math.max(Math.abs(b[0] - a[0]), Math.abs(b[1] - a[1]));
  ctx.fillStyle = "rgba(143,181,166," + alpha + ")";
  for (let s = 0; s <= steps; s += 2) {
    const [x, y] = cablePoint(a, b, s / steps);
    ctx.fillRect(x, y, 1, 1);
  }
}

/* window grid: bottom floors light first, then a slow ambient flicker */
function drawWindowGrid(ctx, x0, y0, cols, rows, sx, sy, w, h, T, on, act, litColor) {
  for (let c = 0; c < cols; c++) for (let r = 0; r < rows; r++) {
    const wx = x0 + c * sx, wy = y0 + r * sy;
    const key = pxh(wx, wy);
    let color = PXC.winOff;
    if (on) {
      const f = Math.min(1, (T - act) / 3);
      const score = ((rows - 1 - r) / Math.max(1, rows)) * 0.5 + key * 0.5;
      let lit = score < 0.9 * f + 0.06;
      if (lit && pxh(wx, wy, Math.floor(T / 1.3)) < 0.04) lit = false;
      else if (!lit && f >= 1 && pxh(wx, wy, Math.floor(T / 1.7)) < 0.02) lit = true;
      if (lit) color = litColor || PXC.winWarm;
    } else if (key < 0.07) {
      color = PXC.winPre;
    }
    rct(ctx, color, wx, wy, w, h);
  }
}

/* industrial multi-pane window: pw × ph panes of 2×2 glass in a 1px frame */
function drawPaned(ctx, x0, y0, pw, ph, T, on, act, litColor) {
  rct(ctx, PXC.frame, x0, y0, pw * 3 + 1, ph * 3 + 1);
  drawWindowGrid(ctx, x0 + 1, y0 + 1, pw, ph, 3, 3, 2, 2, T, on, act, litColor);
}

/* rooftop HVAC unit sitting behind a roofline at roofY */
function drawHVAC(ctx, x, roofY) {
  rct(ctx, PXC.metal, x, roofY - 3, 6, 3);
  rct(ctx, PXC.metalHi, x, roofY - 3, 6, 1);
  rct(ctx, PXC.edge, x + 1, roofY - 2, 1, 1);
  rct(ctx, PXC.edge, x + 4, roofY - 2, 1, 1);
}

function drawPost(ctx, ax, ay, roofY, on) {
  rct(ctx, PXC.edge, ax, ay, 1, roofY - ay);
  rct(ctx, on ? PXC.mintHi : "#3F4A3D", ax, ay - 1, 1, 1);
}

/* ---------------- buildings ---------------- */

function drawFactory(ctx, x, G, T, on, b) {
  const bodyY = G - 32;
  /* brick chimney: banding, accent course, iron cap, service ladder */
  rct(ctx, "#20271F", x + 2, G - 60, 8, 60);
  rct(ctx, "#2A322A", x + 4, G - 58, 2, 58);
  for (let yy = G - 52; yy < G - 8; yy += 6) rct(ctx, "#1B221A", x + 2, yy, 8, 1);
  rct(ctx, "#463A30", x + 2, G - 46, 8, 2);
  rct(ctx, PXC.edge, x + 1, G - 61, 10, 2);
  rct(ctx, PXC.metalHi, x + 2, G - 62, 8, 1);
  for (let yy = G - 42; yy < G - 4; yy += 3) rct(ctx, PXC.edge, x + 10, yy, 1, 2);
  drawPost(ctx, b.anchor[0], b.anchor[1], G - 40, on);
  /* main hall */
  rct(ctx, PXC.bodyA, x + 14, bodyY, 62, 32);
  /* sawtooth roof — glazed vertical faces, slopes falling right */
  for (let i = 0; i < 5; i++) {
    const tx = x + 15 + i * 12;
    for (let s = 0; s < 8; s++) {
      const w = Math.max(2, 12 - Math.floor(s * 1.5));
      rct(ctx, PXC.roof, tx, bodyY - 1 - s, w, 1);
      rct(ctx, "#303B2B", tx + w - 1, bodyY - 1 - s, 1, 1);
    }
    rct(ctx, on ? PXC.winCool : PXC.glass, tx + 1, bodyY - 8, 2, 7);
    rct(ctx, PXC.frame, tx + 1, bodyY - 5, 2, 1);
  }
  /* pilasters between bays + string course */
  for (let i = 1; i < 5; i++) rct(ctx, PXC.pilaster, x + 14 + i * 12, bodyY, 1, 32);
  rct(ctx, PXC.pilaster, x + 14, G - 16, 62, 1);
  /* clerestory paned windows, one per bay */
  for (let i = 0; i < 5; i++) drawPaned(ctx, x + 17 + i * 12, bodyY + 4, 2, 2, T, on, b.act);
  /* personnel door */
  rct(ctx, PXC.frame, x + 18, G - 9, 6, 9);
  rct(ctx, on ? "#B99C55" : "#11170F", x + 19, G - 8, 4, 8);
  /* small shop window with sill */
  rct(ctx, PXC.frame, x + 30, G - 12, 7, 6);
  drawWindowGrid(ctx, x + 31, G - 11, 2, 1, 3, 4, 2, 4, T, on, b.act);
  rct(ctx, PXC.sill, x + 30, G - 6, 7, 1);
  /* roller shutter door — opens onto a warm interior once online */
  rct(ctx, PXC.frame, x + 40, G - 12, 12, 12);
  rct(ctx, "#151C14", x + 41, G - 11, 10, 11);
  for (let r = 0; r < 5; r++) rct(ctx, "#212921", x + 41, G - 10 + r * 2, 10, 1);
  if (on) {
    const gapH = Math.min(8, Math.max(0, (T - b.act) * 3));
    if (gapH > 0) rct(ctx, PXC.winWarm, x + 41, G - gapH, 10, gapH);
    if (gapH > 5) { rct(ctx, PXC.crate3, x + 43, G - 4, 3, 4); rct(ctx, PXC.crate3, x + 47, G - 3, 2, 3); }
  }
  /* conveyor line out of the hall, with product moving once running */
  rct(ctx, PXC.metalHi, x + 54, G - 5, 15, 1);
  rct(ctx, PXC.edge, x + 54, G - 4, 15, 1);
  for (const lx of [55, 61, 67]) rct(ctx, PXC.metal, x + lx, G - 3, 1, 3);
  if (on) {
    const cx = x + 54 + Math.floor(((T - b.act) * 7) % 13);
    rct(ctx, PXC.crate, cx, G - 8, 3, 3);
    rct(ctx, PXC.crate2, cx, G - 8, 3, 1);
  }
  /* pallet stack past the end of the line */
  rct(ctx, PXC.crate, x + 70, G - 4, 5, 4);
  rct(ctx, PXC.crate2, x + 71, G - 7, 3, 3);
}

function drawLogistics(ctx, x, G, T, on, b) {
  /* office block on the left end */
  rct(ctx, PXC.bodyD, x, G - 26, 16, 26);
  rct(ctx, PXC.edge, x, G - 26, 16, 1);
  rct(ctx, PXC.coping, x + 1, G - 27, 14, 1);
  drawWindowGrid(ctx, x + 2, G - 23, 3, 3, 4, 5, 3, 3, T, on, b.act);
  rct(ctx, PXC.frame, x + 5, G - 7, 6, 7);
  rct(ctx, on ? "#B99C55" : "#141A13", x + 6, G - 6, 4, 6);
  /* warehouse hall: coped parapet, ribbed cladding band */
  rct(ctx, PXC.bodyA, x + 16, G - 20, 72, 20);
  rct(ctx, PXC.edge, x + 16, G - 20, 72, 1);
  rct(ctx, PXC.coping, x + 16, G - 21, 72, 1);
  for (let rx = x + 18; rx < x + 87; rx += 3) rct(ctx, "#333E31", rx, G - 18, 1, 5);
  drawPost(ctx, b.anchor[0], b.anchor[1], G - 21, on);
  /* sign band — letters warm up once online */
  rct(ctx, PXC.frame, x + 20, G - 18, 16, 5);
  for (let i = 0; i < 4; i++) {
    const lit = on && (T - b.act) * 4 > i;
    rct(ctx, lit ? PXC.winWarm : "#242C22", x + 22 + i * 3, G - 16, 2, 2);
  }
  /* dock doors: bumpers, lamps; roll open to warm interiors and freight */
  for (let i = 0; i < 4; i++) {
    const dx = x + 24 + i * 16;
    rct(ctx, PXC.frame, dx - 1, G - 12, 12, 12);
    rct(ctx, "#141A13", dx, G - 11, 10, 11);
    for (let r = 0; r < 5; r++) rct(ctx, "#1F271E", dx, G - 10 + r * 2, 10, 1);
    rct(ctx, PXC.edge, dx - 1, G - 3, 1, 3);
    rct(ctx, PXC.edge, dx + 10, G - 3, 1, 3);
    rct(ctx, on ? PXC.amber : "#39433A", dx + 4, G - 14, 2, 1);
    if (on) {
      const gapH = Math.min(8, Math.max(0, (T - b.act - i * 0.5) * 3));
      if (gapH > 0) {
        rct(ctx, PXC.winWarm, dx, G - gapH, 10, gapH);
        if (gapH > 5) { rct(ctx, PXC.crate3, dx + 1, G - 4, 3, 4); rct(ctx, PXC.crate3, dx + 6, G - 3, 3, 3); }
      }
    }
  }
  /* rooftop plant */
  drawHVAC(ctx, x + 38, G - 21);
  drawHVAC(ctx, x + 60, G - 21);
  rct(ctx, PXC.metal, x + 80, G - 25, 1, 4);
  rct(ctx, PXC.metalHi, x + 79, G - 25, 3, 1);
  /* staged freight between docks and at the far end */
  rct(ctx, PXC.crate, x + 36, G - 4, 3, 4);
  rct(ctx, PXC.crate2, x + 36, G - 6, 3, 2);
  rct(ctx, PXC.crate, x + 84, G - 4, 4, 4);
  rct(ctx, PXC.crate2, x + 85, G - 7, 3, 3);
}

function drawForklift(ctx, sc, b, T) {
  const G = sc.G, x1 = b.x + 18, x2 = b.x + 82;
  const u = ((T - b.act) * 0.09) % 2;
  const tri = u < 1 ? u : 2 - u;
  const fx = Math.round(x1 + (x2 - x1) * tri);
  const d = u < 1 ? 1 : -1;
  const y = G - 7;
  /* chassis, counterweight, overhead guard, driver */
  rct(ctx, PXC.amberLo, fx, y + 3, 5, 2);
  rct(ctx, "#7A6A28", d > 0 ? fx : fx + 4, y + 2, 1, 1);
  rct(ctx, "#3A423A", fx + 1, y, 3, 1);
  rct(ctx, "#3A423A", fx + 1, y + 1, 1, 2);
  rct(ctx, "#3A423A", fx + 3, y + 1, 1, 2);
  rct(ctx, "#D8CDAF", fx + 2, y + 1, 1, 1);
  rct(ctx, PXC.edge, fx, y + 5, 2, 2);
  rct(ctx, PXC.edge, fx + 3, y + 5, 2, 2);
  /* mast, forks, and a pallet coming back */
  const mx = d > 0 ? fx + 5 : fx - 1;
  rct(ctx, "#3A423A", mx, y - 1, 1, 7);
  rct(ctx, "#3A423A", d > 0 ? mx + 1 : mx - 2, y + 5, 2, 1);
  if (d < 0) { rct(ctx, PXC.crate2, mx - 3, y + 2, 3, 3); rct(ctx, PXC.crate, mx - 3, y + 4, 3, 1); }
}

function drawSoftware(ctx, x, G, T, on, b) {
  const topY = G - 80;
  /* tower shaft with corner piers and coped parapet */
  rct(ctx, PXC.bodyC, x, topY, 44, 80);
  rct(ctx, "#28322A", x, topY, 2, 80);
  rct(ctx, "#28322A", x + 42, topY, 2, 80);
  rct(ctx, PXC.parapet, x, topY - 2, 44, 2);
  rct(ctx, PXC.coping, x, topY - 2, 44, 1);
  /* curtain-wall grid */
  drawWindowGrid(ctx, x + 4, topY + 4, 9, 11, 4, 5, 3, 3, T, on, b.act, PXC.winCool);
  /* rooftop plant, comms mast, microwave dish */
  drawHVAC(ctx, x + 26, topY - 2);
  rct(ctx, PXC.edge, x + 10, G - 92, 1, 10);
  rct(ctx, PXC.edge, x + 9, G - 89, 3, 1);
  rct(ctx, PXC.edge, x + 8, G - 86, 5, 1);
  const blink = !PX_RM && (T % 2.6) < 0.18;
  rct(ctx, blink ? PXC.amber : "#4A4639", x + 10, G - 93, 1, 1);
  rct(ctx, PXC.edge, x + 34, G - 87, 1, 5);
  rct(ctx, PXC.metalHi, x + 33, G - 86, 2, 2);
  /* server room band — chasing status lights when online */
  rct(ctx, PXC.frame, x + 3, G - 20, 38, 6);
  for (let r = 0; r < 2; r++) for (let k = 0; k < 12; k++) {
    const litK = on && (((k + r * 3 - Math.floor(T * 5)) % 7) + 7) % 7 === 0;
    rct(ctx, litK ? PXC.mintHi : "#22301F", x + 5 + k * 3, G - 19 + r * 2, 2, 1);
  }
  /* double-height glass lobby with revolving door */
  rct(ctx, PXC.metalHi, x + 1, G - 13, 42, 1);
  rct(ctx, on ? "#2E4234" : PXC.glass, x + 2, G - 12, 40, 12);
  for (let k = 0; k <= 8; k++) rct(ctx, PXC.frame, x + 2 + k * 5, G - 12, 1, 12);
  if (on) for (let k = 0; k < 8; k++) {
    if (pxh(k, 11) < 0.35) rct(ctx, "#1E3226", x + 4 + k * 5, G - 4, 1, 4);
  }
  rct(ctx, PXC.edge, x + 19, G - 10, 6, 10);
  rct(ctx, on ? PXC.winWarm : "#1F2B22", x + 21, G - 9, 2, 9);
}

function drawHealth(ctx, x, G, T, on, b) {
  /* main block + low entrance wing */
  rct(ctx, PXC.bodyD, x, G - 40, 34, 40);
  rct(ctx, PXC.edge, x, G - 40, 34, 1);
  rct(ctx, PXC.parapet, x + 1, G - 41, 32, 1);
  rct(ctx, PXC.bodyC, x + 34, G - 22, 18, 22);
  rct(ctx, PXC.edge, x + 34, G - 22, 18, 1);
  rct(ctx, PXC.coping, x + 34, G - 23, 18, 1);
  /* mech penthouse carries the cable post */
  rct(ctx, PXC.bodyA, x + 26, G - 45, 6, 4);
  rct(ctx, PXC.edge, x + 26, G - 45, 6, 1);
  drawPost(ctx, b.anchor[0], b.anchor[1], G - 45, on);
  /* rooftop cross sign on supports */
  rct(ctx, PXC.edge, x + 13, G - 43, 1, 2);
  rct(ctx, PXC.edge, x + 21, G - 43, 1, 2);
  rct(ctx, PXC.frame, x + 11, G - 55, 13, 12);
  rct(ctx, "#161D16", x + 12, G - 54, 11, 10);
  const cc = on ? "#DFF0E2" : "#39433A";
  rct(ctx, cc, x + 16, G - 53, 3, 8);
  rct(ctx, cc, x + 14, G - 51, 7, 3);
  /* ward windows with sills */
  drawWindowGrid(ctx, x + 4, G - 36, 4, 3, 7, 8, 4, 4, T, on, b.act);
  for (let c = 0; c < 4; c++) for (let r = 0; r < 3; r++)
    rct(ctx, PXC.sill, x + 4 + c * 7, G - 32 + r * 8, 4, 1);
  /* ambulance bay with marker light */
  rct(ctx, PXC.frame, x + 4, G - 11, 11, 11);
  rct(ctx, "#161D16", x + 5, G - 10, 9, 10);
  for (let r = 0; r < 4; r++) rct(ctx, "#20281F", x + 5, G - 9 + r * 2, 9, 1);
  rct(ctx, on ? PXC.amber : "#39433A", x + 8, G - 13, 3, 1);
  /* wing: lit sign, canopy on posts, glass double door */
  rct(ctx, PXC.metal, x + 44, G - 26, 2, 3);
  rct(ctx, PXC.frame, x + 37, G - 19, 12, 4);
  if (on) for (let i = 0; i < 3; i++) rct(ctx, "#DFF0E2", x + 39 + i * 3, G - 18, 2, 2);
  rct(ctx, PXC.metalHi, x + 36, G - 13, 15, 1);
  rct(ctx, PXC.edge, x + 36, G - 12, 1, 12);
  rct(ctx, PXC.edge, x + 50, G - 12, 1, 12);
  rct(ctx, on ? "#CFE2D2" : PXC.glass, x + 39, G - 11, 9, 11);
  rct(ctx, PXC.frame, x + 43, G - 11, 1, 11);
}

function drawConsumer(ctx, x, G, T, on, b) {
  /* two-storey mixed-use block with cornice */
  rct(ctx, PXC.bodyC, x, G - 26, 54, 26);
  rct(ctx, PXC.coping, x, G - 27, 54, 1);
  rct(ctx, PXC.edge, x, G - 26, 54, 1);
  rct(ctx, PXC.pilaster, x, G - 25, 54, 1);
  drawPost(ctx, b.anchor[0], b.anchor[1], G - 27, on);
  /* upper-floor windows with sills */
  drawWindowGrid(ctx, x + 5, G - 23, 5, 1, 10, 7, 4, 5, T, on, b.act);
  for (let c = 0; c < 5; c++) rct(ctx, PXC.sill, x + 5 + c * 10, G - 18, 4, 1);
  /* string course + sign band — letters warm up one at a time */
  rct(ctx, PXC.parapet, x, G - 16, 54, 1);
  rct(ctx, PXC.frame, x + 5, G - 15, 44, 4);
  const litN = on ? Math.min(12, Math.floor((T - b.act) * 5)) : 0;
  for (let i = 0; i < 12; i++) rct(ctx, i < litN ? PXC.winWarm : "#242C22", x + 8 + i * 3, G - 14, 2, 2);
  /* striped awning with scalloped edge */
  for (let i = 0; i < 25; i++) {
    const cA = i % 2 ? "#44503F" : "#A69B7B";
    rct(ctx, cA, x + 2 + i * 2, G - 11, 2, 2);
    if (i % 2 === 0) rct(ctx, cA, x + 2 + i * 2, G - 9, 2, 1);
  }
  /* storefront: framed display bays, goods in the glass, recessed door */
  rct(ctx, on ? "#E2D3A0" : PXC.glass, x + 3, G - 8, 48, 8);
  for (const k of [0, 12, 24, 36, 47]) rct(ctx, PXC.frame, x + 3 + k, G - 8, 1, 8);
  rct(ctx, PXC.crate3, x + 7, G - 5, 3, 5);
  rct(ctx, PXC.crate3, x + 12, G - 4, 2, 4);
  rct(ctx, PXC.crate3, x + 19, G - 5, 2, 5);
  rct(ctx, PXC.crate3, x + 42, G - 5, 3, 5);
  rct(ctx, PXC.frame, x + 29, G - 8, 8, 8);
  rct(ctx, "#11170F", x + 30, G - 7, 6, 7);
  if (on) rct(ctx, "#B99C55", x + 32, G - 7, 2, 7);
  /* sidewalk planters */
  rct(ctx, PXC.crate, x, G - 2, 3, 2);
  rct(ctx, PXC.treeHi, x, G - 3, 3, 1);
  rct(ctx, PXC.crate, x + 51, G - 2, 3, 2);
  rct(ctx, PXC.treeHi, x + 51, G - 3, 3, 1);
}

/* tiny 4/5-wide pixel font — just enough glyphs for the hub's rooftop sign */
const PX_FONT = {
  " ": { w: 2, rows: ["", "", "", "", ""] },
  C: { w: 4, rows: [".##.", "#...", "#...", "#...", ".##."] },
  O: { w: 4, rows: [".##.", "#..#", "#..#", "#..#", ".##."] },
  M: { w: 5, rows: ["#...#", "##.##", "#.#.#", "#...#", "#...#"] },
  N: { w: 4, rows: ["#..#", "##.#", "#.##", "#..#", "#..#"] },
  W: { w: 5, rows: ["#...#", "#...#", "#.#.#", "##.##", "#...#"] },
  R: { w: 4, rows: ["###.", "#..#", "###.", "#.#.", "#..#"] },
  K: { w: 4, rows: ["#..#", "#.#.", "##..", "#.#.", "#..#"] },
  S: { w: 4, rows: [".###", "#...", ".##.", "...#", "###."] },
};
function drawPixelText(ctx, str, x, y, color) {
  let cx = x;
  for (const ch of str) {
    const g = PX_FONT[ch];
    if (!g) { cx += 5; continue; }
    for (let r = 0; r < g.rows.length; r++) {
      const row = g.rows[r];
      for (let c = 0; c < row.length; c++) if (row[c] === "#") rct(ctx, color, cx + c, y + r, 1, 1);
    }
    cx += g.w + 1;
  }
}

function drawHub(ctx, x, G, T, on, b) {
  const hallY = G - 30;
  rct(ctx, PXC.bodyB, x, hallY, 52, 30);
  rct(ctx, PXC.edge, x, hallY, 52, 1);
  rct(ctx, PXC.parapet, x + 1, hallY - 1, 30, 1);
  /* rooftop vents on the hall */
  rct(ctx, PXC.metal, x + 10, hallY - 4, 3, 3);
  rct(ctx, PXC.metalHi, x + 10, hallY - 4, 3, 1);
  rct(ctx, PXC.metal, x + 20, hallY - 4, 3, 3);
  rct(ctx, PXC.metalHi, x + 20, hallY - 4, 3, 1);
  /* brand mark — four squares, shaded like the logo: solid (darker) on the
     TL/BR diagonal, faded (lighter) on TR/BL. Centered between the roofline
     and the top of the workshop glazing, as is the monogram. */
  rct(ctx, PXC.mintLo, x + 5, hallY + 3, 3, 3);
  rct(ctx, PXC.mintHi, x + 9, hallY + 3, 3, 3);
  rct(ctx, PXC.mintHi, x + 5, hallY + 7, 3, 3);
  rct(ctx, PXC.mintLo, x + 9, hallY + 7, 3, 3);

  /* discreet monogram on the main hall face */
  drawPixelText(ctx, "CW", x + 36, hallY + 4, on ? PXC.mintHi : "#3F4A3D");

  /* glazed workshop band — lit from second zero: the hub never sleeps */
  rct(ctx, PXC.frame, x + 3, G - 17, 46, 9);
  for (let i = 0; i < 11; i++) rct(ctx, i % 3 ? PXC.mint : PXC.mintHi, x + 4 + i * 4, G - 16, 3, 7);
  rct(ctx, "#2A4A38", x + 13, G - 13, 1, 4);
  rct(ctx, "#2A4A38", x + 33, G - 12, 1, 3);
  /* entrance: canopy, door, bollard lights */
  rct(ctx, PXC.metalHi, x + 21, G - 9, 10, 1);
  rct(ctx, PXC.frame, x + 23, G - 8, 6, 8);
  rct(ctx, "#E8E0C2", x + 24, G - 7, 4, 7);
  rct(ctx, PXC.mintLo, x + 19, G - 2, 1, 2);
  rct(ctx, PXC.mintLo, x + 32, G - 2, 1, 2);
  /* control room penthouse with a bank of screens */
  rct(ctx, PXC.bodyA, x + 32, hallY - 11, 18, 11);
  rct(ctx, PXC.edge, x + 32, hallY - 11, 18, 1);
  rct(ctx, PXC.parapet, x + 33, hallY - 12, 16, 1);
  rct(ctx, PXC.frame, x + 34, hallY - 9, 14, 7);
  for (let i = 0; i < 4; i++) {
    const flick = !PX_RM && pxh(i, Math.floor(T / 0.7)) < 0.18;
    rct(ctx, flick ? PXC.mintHi : PXC.mint, x + 35 + i * 3, hallY - 8, 2, 5);
  }

  /* comms mast on the penthouse roof — where the cables to every other
     building actually terminate, so the wire network reads as connected
     to the hub structure rather than floating above it */
  const mastX = x + 40, mastTop = hallY - 26;
  rct(ctx, PXC.edge, mastX, mastTop, 1, 14);
  rct(ctx, PXC.edge, mastX - 1, mastTop + 2, 3, 1);
  rct(ctx, PXC.edge, mastX - 2, mastTop + 6, 5, 1);
  rct(ctx, PXC.metalHi, mastX + 2, mastTop + 9, 2, 2);
  const blink = PX_RM || (T % 1.8) < 0.14;
  rct(ctx, blink ? PXC.mintHi : "#4A6152", mastX, mastTop - 1, 1, 1);
  if (blink && !PX_RM) {
    ctx.fillStyle = "rgba(201,238,216,0.25)";
    ctx.fillRect(mastX - 1, mastTop - 2, 3, 3);
  }
}

const PX_DRAW = {
  factory: drawFactory, logistics: drawLogistics, hub: drawHub,
  software: drawSoftware, health: drawHealth, consumer: drawConsumer,
};

/* ---------------- ambient actors ---------------- */

function drawTree(ctx, tx, G) {
  const h = pxh(tx, 33) > 0.5 ? 12 : 10;
  rct(ctx, "#141A12", tx, G - 4, 1, 4);
  rct(ctx, PXC.treeLo, tx - 3, G - h + 3, 7, 5);
  rct(ctx, PXC.tree, tx - 2, G - h + 1, 5, 5);
  rct(ctx, PXC.tree, tx - 1, G - h, 3, 2);
  rct(ctx, PXC.treeHi, tx - 1, G - h + 1, 2, 2);
  rct(ctx, PXC.treeHi, tx + 1, G - h + 4, 1, 1);
}

function drawLamp(ctx, lx, G) {
  rct(ctx, PXC.edge, lx, G - 11, 1, 11);
  rct(ctx, PXC.edge, lx + 1, G - 11, 3, 1);
  rct(ctx, PXC.metal, lx + 3, G - 10, 2, 1);
  rct(ctx, PXC.amber, lx + 3, G - 9, 2, 1);
  ctx.fillStyle = "rgba(217,166,98,0.10)";
  ctx.fillRect(lx + 2, G - 8, 4, 5);
  ctx.fillRect(lx + 1, G - 3, 6, 3);
}

function drawSmoke(ctx, b, G, T) {
  const cx = b.x + 5, top = G - 62;
  for (let i = 0; i < 6; i++) {
    const age = (T * 0.4 + i * 0.31) % 1;
    const y = top - 2 - age * 18;
    const x = cx + Math.sin((T * 0.35 + i) * 2 + age * 5) * (1 + age * 2);
    const s = age < 0.4 ? 1 : 2;
    ctx.fillStyle = "rgba(180,185,175," + ((1 - age) * 0.15).toFixed(2) + ")";
    ctx.fillRect(Math.round(x), Math.round(y), s + 1, s);
  }
}

function drawFigure(ctx, fx, G, c, bob) {
  rct(ctx, "#D8CDAF", fx, G - 5 - bob, 1, 1);
  rct(ctx, c, fx, G - 4 - bob, 1, 2);
  rct(ctx, "#11170F", fx, G - 2, 1, 2);
  if (bob) rct(ctx, "#11170F", fx + 1, G - 1, 1, 1);
}

/* ---------------- morning fog ---------------- */
/* A voluminous bank of fog sits over the skyline at rest, then drifts slowly
   apart and fades over the first ~45s — clarity arriving over the portfolio
   the same way it arrives over a vague AI theme. Deterministic in T, so it
   never resets: once cleared, it stays clear. */
const PX_FOG_DUR = 45;
const PX_CLOUDS = [
  { cx0: 0.30, cx1: 0.04, cy: 0.24, w: 108, h: 17, a: 0.17 },
  { cx0: 0.66, cx1: 0.93, cy: 0.19, w: 92,  h: 14, a: 0.15 },
  { cx0: 0.46, cx1: 0.20, cy: 0.42, w: 138, h: 22, a: 0.14 },
  { cx0: 0.58, cx1: 0.86, cy: 0.48, w: 118, h: 18, a: 0.12 },
  { cx0: 0.18, cx1: -0.02, cy: 0.63, w: 96,  h: 14, a: 0.11 },
];
/* a small cluster of overlapping lobes, not a single blurred rectangle */
const PX_FOG_LOBES = [
  { dx: -0.34, dy: 0.16, wf: 0.58, hf: 0.8 },
  { dx: 0.36, dy: 0.2, wf: 0.52, hf: 0.72 },
  { dx: 0.08, dy: -0.24, wf: 0.46, hf: 0.66 },
  { dx: -0.1, dy: 0.05, wf: 0.7, hf: 0.9 },
];
function drawFogPatch(ctx, cx, cy, w, h, alpha) {
  if (alpha <= 0.004) return;
  for (const L of PX_FOG_LOBES) {
    const lw = w * L.wf, lh = h * L.hf;
    const lx = cx + w * L.dx, ly = cy + h * L.dy;
    ctx.fillStyle = "rgba(233,238,224," + (alpha * 0.28).toFixed(3) + ")";
    ctx.fillRect(Math.round(lx - lw / 2), Math.round(ly - lh / 2), Math.round(lw), Math.round(lh));
  }
  ctx.fillStyle = "rgba(233,238,224," + (alpha * 0.4).toFixed(3) + ")";
  ctx.fillRect(Math.round(cx - w * 0.24), Math.round(cy - h * 0.24), Math.round(w * 0.48), Math.round(h * 0.48));
}
function drawClouds(ctx, sc, T, skyOff = 0) {
  const p = Math.min(1, Math.max(0, T / PX_FOG_DUR));
  const eased = 1 - Math.pow(1 - p, 1.3);
  const alphaMul = 1 - eased;
  if (alphaMul <= 0.004) return;
  const { W, G } = sc;
  for (const c of PX_CLOUDS) {
    const cx = (c.cx0 + (c.cx1 - c.cx0) * eased) * W;
    drawFogPatch(ctx, cx, c.cy * G + skyOff, c.w, c.h, c.a * alphaMul);
  }
}

function drawHorizonHaze(ctx, W, G, dawnP) {
  const hazeH = Math.min(98, G * 0.7);
  const baseAlpha = 0.085 + dawnP * 0.085;
  for (let yy = 0; yy < hazeH; yy += 2) {
    const p = yy / hazeH;
    const a = baseAlpha * Math.pow(p, 1.65);
    if (a <= 0.003) continue;
    ctx.fillStyle = "rgba(120,175,145," + a.toFixed(3) + ")";
    ctx.fillRect(0, Math.round(G - hazeH + yy), W, 2);
  }
  for (let yy = 0; yy < hazeH * 0.42; yy += 2) {
    const p = yy / (hazeH * 0.42);
    const a = (0.018 + dawnP * 0.016) * Math.pow(1 - p, 1.4);
    ctx.fillStyle = "rgba(233,238,224," + a.toFixed(3) + ")";
    ctx.fillRect(0, Math.round(G - hazeH * 0.44 + yy), W, 2);
  }
}

const PX_CREW_COLORS = [PXC.mintHi, "#C8BE9E", PXC.mintLo];

function drawCrews(ctx, sc, T) {
  const hub = sc.byId.hub;
  if (!hub) return;
  const doorX = hub.x + 26;
  for (const b of sc.placed) {
    if (b.id === "hub") continue;
    const start = b.act - 4.5, end = b.act - 0.2;
    if (T < start || T > end) continue;
    const p = (T - start) / (end - start);
    const target = b.x + Math.floor(b.w / 2);
    for (let i = 0; i < 3; i++) {
      const fx = Math.round(doorX + (target - doorX) * p) - i * 4 * Math.sign(target - doorX || 1);
      const bob = Math.floor(T * 4 + i) % 2;
      drawFigure(ctx, fx, sc.G, PX_CREW_COLORS[i], bob);
    }
  }
}

function drawTruckAt(ctx, W, G, T, period, phase, kind, onlyDir = 0) {
  const dur = (W + 60) / 34;
  const Tp = T + phase;
  const k = Math.floor(Tp / period), c = Tp % period;
  if (c > dur) return;
  const dir = k % 2 === 0 ? 1 : -1;
  if (onlyDir && dir !== onlyDir) return;
  const tx = Math.round(dir > 0 ? -24 + (W + 48) * (c / dur) : W + 24 - (W + 48) * (c / dur));
  const y = dir > 0 ? G + 4 : G - 1;
  if (kind === 1) {
    /* panel van */
    rct(ctx, "#3A4437", tx, y + 1, 9, 5);
    rct(ctx, "#4A5646", tx, y + 1, 9, 1);
    rct(ctx, "#9FB6A4", dir > 0 ? tx + 7 : tx + 1, y + 2, 1, 2);
    rct(ctx, PXC.amber, dir > 0 ? tx + 8 : tx, y + 4, 1, 1);
    rct(ctx, "#060907", tx + 1, y + 6, 2, 1);
    rct(ctx, "#060907", tx + 6, y + 6, 2, 1);
    return;
  }
  /* semi: ribbed trailer, cab with stack and lit windshield */
  const cab = dir > 0 ? tx + 14 : tx - 6;
  rct(ctx, "#313B2F", tx, y, 14, 6);
  rct(ctx, "#3F4A3B", tx, y, 14, 1);
  for (let i = 2; i < 13; i += 3) rct(ctx, "#2A342A", tx + i, y + 1, 1, 4);
  rct(ctx, "#242E23", dir > 0 ? tx : tx + 13, y + 1, 1, 4);
  rct(ctx, PXC.bodyA, cab, y + 1, 6, 5);
  rct(ctx, "#3F4A3B", cab + (dir > 0 ? 0 : 1), y + 1, 5, 1);
  rct(ctx, PXC.metalHi, dir > 0 ? cab : cab + 5, y, 1, 1);
  rct(ctx, "#9FB6A4", dir > 0 ? cab + 4 : cab + 1, y + 2, 1, 2);
  rct(ctx, PXC.amber, dir > 0 ? cab + 5 : cab, y + 4, 1, 1);
  ctx.fillStyle = "rgba(217,166,98,0.12)";
  ctx.fillRect(dir > 0 ? cab + 6 : cab - 3, y + 4, 3, 1);
  rct(ctx, "#060907", tx + 1, y + 6, 2, 1);
  rct(ctx, "#060907", tx + 4, y + 6, 2, 1);
  rct(ctx, "#060907", tx + 11, y + 6, 2, 1);
  rct(ctx, "#060907", cab + (dir > 0 ? 3 : 1), y + 6, 2, 1);
}
/* several vehicles in rotation, staggered evenly across a cycle sized to the
   crossing duration (so the rest gap never lets one overtake the next) */
function drawTruck(ctx, sc, T) {
  const { W, G } = sc;
  if (W < 260) return;
  const dur = (W + 60) / 34;
  const period = dur + 6;
  for (const dir of [-1, 1]) {
    for (let i = 0; i < 3; i++) drawTruckAt(ctx, W, G, T, period, (i * period) / 3, i, dir);
  }
}

function drawPulse(ctx, a, b, p) {
  const [x, y] = cablePoint(a, b, p);
  ctx.fillStyle = PXC.pulse; ctx.fillRect(x, y - 1, 2, 2);
  const [tx, ty] = cablePoint(a, b, Math.max(0, p - 0.06));
  ctx.fillStyle = "rgba(143,181,166,0.5)"; ctx.fillRect(tx, ty, 1, 1);
}

function drawPulses(ctx, sc, T, anim, travel = true, flashes = true) {
  if (!sc.byId.hub) return;
  const tars = sc.placed.filter(p => p.id !== "hub");
  if (!tars.length) return;
  for (const b of tars) {
    if (travel && T >= b.act - 1.4 && T < b.act) drawPulse(ctx, sc.src, b.anchor, (T - (b.act - 1.4)) / 1.4);
    if (flashes && T >= b.act && T < b.act + 0.6) {
      ctx.fillStyle = "rgba(203,239,217," + (0.9 * (1 - (T - b.act) / 0.6)).toFixed(2) + ")";
      ctx.fillRect(b.anchor[0] - 1, b.anchor[1] - 2, 3, 3);
    }
  }
  if (!travel || !anim) return;
  if (T > 18) {
    const k = Math.floor((T - 18) / 2.6), ph = ((T - 18) % 2.6) / 1.3;
    if (ph <= 1) {
      const b = tars[k % tars.length];
      drawPulse(ctx, sc.src, b.anchor, k % 3 === 2 ? 1 - ph : ph);
    }
  }
}

/* a rare streak across the upper sky — deterministic per time-cycle so every
   viewer sees the same show, not a per-frame random flicker */
function drawShootingStar(ctx, W, G, T, skyOff = 0) {
  const period = 7.5;
  const cyc = Math.floor(T / period);
  if (pxh(cyc, 41) > 0.62) return;
  const localT = T - cyc * period;
  const dur = 0.8;
  if (localT < 0 || localT > dur) return;
  const p = localT / dur;
  const x0 = W * (0.08 + pxh(cyc, 7) * 0.55);
  const y0 = 2 + pxh(cyc, 13) * (G * 0.28) + skyOff;
  const dx = 34 + pxh(cyc, 19) * 18, dy = dx * 0.42;
  const fade = 1 - p;
  for (let i = 0; i < 7; i++) {
    const tp = Math.max(0, p - i * 0.045);
    const head = i === 0;
    ctx.fillStyle = "rgba(236,242,230," + (fade * (1 - i * 0.14) * (head ? 1 : 0.85)).toFixed(2) + ")";
    const px = Math.round(x0 + dx * tp), py = Math.round(y0 + dy * tp);
    ctx.fillRect(px, py, head ? 2 : 1, head ? 2 : 1);
  }
}

/* ---------------- master draw ---------------- */

function drawScene(ctx, sc, T, anim, skyOff = 0) {
  const { W, H, G } = sc;
  const dawnP = Math.min(1, Math.max(0, T / SUNRISE_DUR));
  /* skyOff is the scroll-parallax offset (logical px): the starry background
     and distant ridges sink relative to the pinned foreground as the page
     scrolls, giving the scene depth without moving the world or the copy */
  for (const band of sc.skyBands) rct(ctx, skyBandColor(band, dawnP), 0, band.y0 + skyOff, W, band.y1 - band.y0);
  if (skyOff > 0) rct(ctx, skyBandColor(sc.skyBands[0], dawnP), 0, 0, W, skyOff);
  /* stars — thin out and wash away toward the green dawn band near the horizon,
     then dim further (never fully gone) as the slow sunrise brightens the sky */
  const starDim = 1 - dawnP * 0.75;
  for (let i = 0; i < 46; i++) {
    const x = Math.floor(pxh(i, 3) * W), y = Math.floor(pxh(i, 9) * G * 0.62) + 2 + skyOff;
    const depth = Math.max(0, 1 - y / (G * 0.72));
    const tw = 0.4 + 0.4 * pxh(i, 5) + (anim ? 0.25 * Math.sin(T * 0.8 + i * 2.1) : 0.15);
    ctx.fillStyle = "rgba(224,232,218," + Math.max(0, tw * 0.75 * depth * starDim).toFixed(2) + ")";
    const big = pxh(i, 23) > 0.82;
    ctx.fillRect(x, y, big ? 2 : 1, big ? 2 : 1);
  }
  if (anim) drawShootingStar(ctx, W, G, T, skyOff);
  /* distant ridges, two depths, with the odd far-off homestead light —
     each tracks the background at part of the sky's parallax rate */
  const ridgeOff2 = Math.round(skyOff * 0.6), ridgeOff = Math.round(skyOff * 0.35);
  for (let x = 0; x < W; x += 4) {
    const h = 5 + Math.floor(pxh(x, 17) * 8);
    rct(ctx, PXC.ridge2, x, G - h + ridgeOff2, 4, h);
  }
  for (let x = 0; x < W; x += 3) {
    const h = 2 + Math.floor(pxh(x, 1) * 6);
    rct(ctx, PXC.ridge, x, G - h + ridgeOff, 3, h);
    if (pxh(x, 29) < 0.045) rct(ctx, "#57684C", x + 1, G - h + 1 + ridgeOff, 1, 1);
  }
  /* ground: curbed sidewalk with joints, road with lane dashes and drains */
  rct(ctx, PXC.walk, 0, G, W, 3);
  rct(ctx, "#2A3728", 0, G, W, 1);
  for (let x = 6; x < W; x += 14) rct(ctx, "#1B241B", x, G + 1, 1, 2);
  rct(ctx, PXC.road, 0, G + 3, W, 8);
  for (let x = 0; x < W; x += 10) rct(ctx, "rgba(190,190,170,0.12)", x, G + 7, 4, 1);
  for (let x = 30; x < W; x += 110) {
    const mx = x + Math.floor(pxh(x, 2) * 40);
    rct(ctx, "#101710", mx, G + 5, 3, 1);
  }
  rct(ctx, PXC.below, 0, G + 11, W, H - G - 11);

  sc.trees.forEach(tx => drawTree(ctx, tx, G));
  sc.lamps.forEach(lx => drawLamp(ctx, lx, G));

  /* cables */
  for (const b of sc.placed) {
    if (b.id === "hub") continue;
    drawCable(ctx, sc.src, b.anchor, T >= b.act ? 0.3 : 0.14);
  }
  drawPulses(ctx, sc, T, anim, true, false);
  /* buildings */
  for (const b of sc.placed) PX_DRAW[b.id](ctx, b.x, G, T, T >= b.act, b);

  /* ambient dawn haze rising off the horizon onto the skyline — stepped so
     the sky/building transition stays pixelated without a hard rectangle */
  drawHorizonHaze(ctx, W, G, dawnP);

  drawClouds(ctx, sc, T, skyOff);

  if (anim) {
    if (sc.byId.logistics && T >= sc.byId.logistics.act + 2) drawForklift(ctx, sc, sc.byId.logistics, T);
    drawCrews(ctx, sc, T);
    drawTruck(ctx, sc, T);
    if (sc.byId.factory) drawSmoke(ctx, sc.byId.factory, sc.G, T);
  } else if (sc.byId.logistics) {
    /* static frame still shows a parked forklift */
    const b = sc.byId.logistics;
    rct(ctx, PXC.amberLo, b.x + 40, sc.G - 5, 5, 3);
    rct(ctx, "#3A423A", b.x + 45, sc.G - 8, 1, 7);
    rct(ctx, PXC.edge, b.x + 40, sc.G - 2, 2, 2);
    rct(ctx, PXC.edge, b.x + 43, sc.G - 2, 2, 2);
  }
  drawPulses(ctx, sc, T, anim, false, true);
}

/* ---------------- component ---------------- */

const PX_MIN_H = 120;

function PxScene({ className = "", parallax = false }) {
  const wrapRef = useRef(null);
  const cvsRef = useRef(null);

  useEffect(() => {
    const wrap = wrapRef.current, cvs = cvsRef.current;
    if (!wrap || !cvs) return;
    const ctx = cvs.getContext("2d");
    let scene = null, raf = null, running = false, started = false, t0 = 0, last = 0, disposed = false;
    let scale = 2;
    const offset = 0;

    function setup() {
      const wpx = wrap.clientWidth || 600;
      const hpx = wrap.clientHeight || 400;
      /* keep the logical canvas near the 384–480-wide simulation-game range */
      const s = wpx >= 2200 ? 4 : wpx >= 1400 ? 3 : 2;
      scale = s;
      const W = Math.max(150, Math.floor(wpx / s));
      const H = Math.max(PX_MIN_H, Math.floor(hpx / s));
      cvs.width = W; cvs.height = H;
      cvs.style.width = (W * s) + "px";
      cvs.style.height = (H * s) + "px";
      scene = buildScene(W, H);
    }

    /* scroll parallax, layered inside the scene: the starfield (and, at
       partial rates, the distant ridges and fog) sinks relative to the
       pinned world as the page scrolls, so the skyline rises past the
       stars. The world, the copy, and the hero's bottom seam never move.
       The offset is in whole logical pixels, so the grid stays crisp. */
    let skyOff = 0, scrollPending = false;
    function sceneT() { return started ? (performance.now() - t0) / 1000 + offset : offset; }
    function applyParallax() {
      scrollPending = false;
      if (disposed) return;
      const sy = window.scrollY || 0;
      if (sy > window.innerHeight * 1.6) return; // hero is long out of view
      const next = Math.round((sy * 0.3) / scale);
      if (next === skyOff) return;
      skyOff = next;
      /* repaint immediately so the background tracks the scroll, not the
         12fps ambient cadence */
      drawScene(ctx, scene, sceneT(), true, skyOff);
    }
    function onScroll() {
      if (!scrollPending) { scrollPending = true; requestAnimationFrame(applyParallax); }
    }

    function frame(now) {
      if (!running || disposed) return;
      if (now - last >= 85) {
        last = now;
        drawScene(ctx, scene, (now - t0) / 1000 + offset, true, skyOff);
      }
      raf = requestAnimationFrame(frame);
    }
    function start() {
      if (running || disposed) return;
      if (!started) { started = true; t0 = performance.now(); }
      running = true;
      raf = requestAnimationFrame(frame);
    }
    function stop() { running = false; if (raf) cancelAnimationFrame(raf); raf = null; }

    setup();

    if (PX_RM) {
      drawScene(ctx, scene, 140, false);
      const onResizeStatic = () => { setup(); drawScene(ctx, scene, 140, false); };
      window.addEventListener("resize", onResizeStatic);
      return () => { disposed = true; window.removeEventListener("resize", onResizeStatic); };
    }

    /* first frame immediately so the world exists before the loop starts */
    drawScene(ctx, scene, offset, true);

    if (parallax) {
      window.addEventListener("scroll", onScroll, { passive: true });
      applyParallax();
    }

    let io = null;
    if ("IntersectionObserver" in window) {
      io = new IntersectionObserver((es) => {
        es.forEach(e => { e.isIntersecting ? start() : stop(); });
      }, { threshold: 0.1 });
      io.observe(wrap);
    } else start();

    const onVis = () => { document.hidden ? stop() : start(); };
    const onResize = () => {
      /* setting canvas.width clears it — always repaint synchronously so a
         resize never leaves a blank frame */
      setup();
      drawScene(ctx, scene, sceneT(), true, skyOff);
    };
    document.addEventListener("visibilitychange", onVis);
    window.addEventListener("resize", onResize);
    return () => {
      disposed = true; stop();
      if (io) io.disconnect();
      document.removeEventListener("visibilitychange", onVis);
      window.removeEventListener("resize", onResize);
      if (parallax) window.removeEventListener("scroll", onScroll);
    };
  }, []);

  return (
    <div className={"px-scene " + className} ref={wrapRef} aria-hidden="true">
      <canvas ref={cvsRef} />
    </div>
  );
}

/* ============================================================
   SPRITES — ASCII maps compiled to run-length SVG rects
   ============================================================ */

const SPR_PAL = {
  "#": "#16140F", x: "#4A4639", s: "#837C68",
  g: "#1B3A2F", G: "#4E7D68", m: "#8FB5A6",
  w: "#C99B45", c: "#EEE9DD", d: "#2D352D",
  r: "#3A4437", k: "#232B22", a: "#D9A662",
};

function Sprite({ map, scale = 4, className = "", pal = SPR_PAL, trim = false }) {
  const rows = map.trim().split("\n").map(r => r.trim()).filter(Boolean);
  const w = Math.max(...rows.map(r => r.length));
  const h = rows.length;
  const rects = [];
  let minX = w, minY = h, maxX = -1, maxY = -1;
  rows.forEach((row, y) => {
    let x = 0;
    while (x < row.length) {
      const ch = row[x];
      if (ch === "." || !pal[ch]) { x++; continue; }
      let run = 1;
      while (x + run < row.length && row[x + run] === ch) run++;
      minX = Math.min(minX, x);
      minY = Math.min(minY, y);
      maxX = Math.max(maxX, x + run - 1);
      maxY = Math.max(maxY, y);
      rects.push(<rect key={x + "-" + y} x={x} y={y} width={run} height={1} fill={pal[ch]} />);
      x += run;
    }
  });
  const cropped = trim && maxX >= 0;
  const viewX = cropped ? minX : 0;
  const viewY = cropped ? minY : 0;
  const viewW = cropped ? maxX - minX + 1 : w;
  const viewH = cropped ? maxY - minY + 1 : h;
  return (
    <svg className={"px-sprite " + className} width={viewW * scale} height={viewH * scale}
         viewBox={viewX + " " + viewY + " " + viewW + " " + viewH} shapeRendering="crispEdges" aria-hidden="true">
      {rects}
    </svg>
  );
}

/* One small factory, three states: surveyed, planned, running. */
const SPR_UNDERWRITE = `
gg................................gg
g..................................g
..#####.............................
...#x#..............................
...#x#..............................
...#x#...k.......k.......k..........
...#x#...dkk.....dkk.....dkk........
...#x#...dkkkk...dkkkk...dkkkk......
...#x#...dkkkkkk.dkkkkkk.dkkkkkk....
...#x#..##########################..
...#x#..#rrrrrrrrrrrrrrrrrrrrrrrr#..
...#x#..#rrddddrddddrddddrddddrrr#..
mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
...#x#..#rrddddrddddrddddrddddrrr#..
...#x#..#rrrrrrrrrrrrrrrrrrrrrrrr#..
...#x#..#rrddddrr########rrddddrr#..
...#x#..#rrddddrrxxxxxxxxrrddddrr#..
...#x#..#rrddddrr########rrddddrr#..
...#x#..#rrrrrrrrxxxxxxxxrrrrrrrr#..
...###############################..
....g.....g......g......g.....g.....
....................................
g..................................g
gg................................gg
`;

const SPR_PLAN = `
....................................
....................................
..ggggg.............................
...g.g..............................
...g.g..............................
...g.g...g.......g.......g..........
...g.g...ggg.....ggg.....ggg........
...g.g...g..gg...g..gg...g..gg......
...g.g...g....gg.g....gg.g....gg....
...g.g..gggggggggggggggggggggggggg..
...g.g..g........................g.m
...g.g..g..mm....mm....mm....mm..g..
...g.g..g........................g.m
...g.g..g..mm....mm....mm....mm..g..
...g.g..g........................g.m
...g.g..g........gggggggg........g..
...g.g..g........g......g........g.m
...g.g..g........g......g........g..
...g.g..g........g......g........g.m
...ggggggggggggggggggggggggggggggg..
.g.g.g.g.g.g.g.g.g.g.g.g.g.g.g.g.g..
....................................
....................................
....................................
`;

const SPR_DEPLOY = `
.....ss.............................
....ss..............................
..#####.............................
...#x#..............................
...#x#..............................
...#x#...k.......k.......k..........
...#x#...ckk.....ckk.....ckk........
...#x#...ckkkk...ckkkk...ckkkk......
...#x#...ckkkkkk.ckkkkkk.ckkkkkk....
...#x#..##########################..
...#x#..#rrrrrrrrrrrrrrrrrrrrrrrr#..
...#x#..#rrwwwwrwwwwrwwwwrwwwwrrr#..
...#x#..#rrwwwwrwwwwrwwwwrwwwwrrr#..
...#x#..#rrwwwwrwwwwrwwwwrwwwwrrr#..
...#x#..#rrrrrrrrrrrrrrrrrrrrrrrr#..
...#x#..#rrwwwwrrwwwwwwwwrrwwwwrr#..
...#x#..#rrwwwwrrwwwwwwwwrrwwwwrr#..
...#x#..#rrwwwwrrww#ww#wwrrwwwwrr#..
...#x#..#rrrrrrrrww#ww#wwrrrrrrrr#..
...###############################..
....................................
....................................
....................................
....................................
`;

const SPR_WORK_PORTCO = `
.....................................
.....................................
....#####################............
....#rrwwrrwwrrwwrrwwrr#............
....#rrrrrrrrrrrrrrrrrrr#............
....#rrwwrrwwrrwwrrwwrr#.....a......
....#rrrrrrrrrrrrrrrrrrr#.....#......
....#rrwwrrwwrrwwrrwwrr#...#####....
....#rrrrrrrrrrrrrrrrrrr#...#rrr#....
....#rrrrrrrr#ww#rrrrrrr#...#rwr#....
....#rrrrrrrr#ww#rrrrrrr#...#rrr#....
....#####################...#####....
mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
`;

const SPR_WORK_DILIGENCE = `
.....................................
..............a......................
..............#......................
.........###############.............
.........#mmmmmmmmmmmmm#.............
....#########################........
....#rrwwrrwwrrwwrrwwrrwwr#........
....#rrrrrrrrrrrrrrrrrrrrr#........
....#rrwwrrwwrrwwrrwwrrwwr#........
....#rrrrrrrrrrrrrrrrrrrrr#........
....#rrrrrrrrr#ww#rrrrrrrr#........
....#########################........
mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
`;

const SPR_WORK_GP_INFRA = `
.....................................
....###########......................
....#ccccccccc#......############...
....#csssssssc#......#rrrrrrrrrr#...
....#ccccccccc#......#rmmmmmmmrr#...
.....###########.....#rmwwmwwmrr#...
......###########....#rmmmmmmmrr#...
......#ccccccccc#....#rrwwwwwwrr#...
......#csssssssc#....#rrrrrrrrrr#...
......#ccccccccc#....############...
......###########........####........
........................######.......
mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm
`;

/* board thumbnails — the sector list's businesses in miniature */
const SPR_T_FACTORY = `
##......................
##......................
##.k.....k.....k........
##.wkk...wkk...wkk......
##.wkkkk.wkkkk.wkkkk....
######################..
#rrwwrrwwrrwwrrwwrrrr#..
#rrwwrrwwrrwwrrwwrrrr#..
#rrrrrrrrrrrrrrrrrrrr#..
#rrwwrrrr######rrwwrr#..
#rrwwrrrr#wwww#rrwwrr#..
######################..
`;

const SPR_T_HEALTH = `
......#######...........
......#ddGdd#...........
......#dGGGd#...........
......#ddGdd#...........
......#######...........
.####################...
.#rrwwrrwwrrwwrrwwrr#...
.#rrrrrrrrrrrrrrrrrr#...
.#rrwwrrwwrrwwrrwwrr#...
.#rrrrrrr######rrrrr#...
.#rrwwrrr#cccc#rrwwr#...
.#rrwwrrr#cccc#rrwwr#...
.####################...
`;

const SPR_T_SOFTWARE = `
......a.................
......#.................
....############........
....#rmmrmmrmmr#........
....#rrrrrrrrrr#........
....#rmmrmmrmmr#........
....#rrrrrrrrrr#........
....#rmmrmmrmmr#........
....#rrrrrrrrrr#........
....#rmmrmmrmmr#........
....#rrrrrrrrrr#........
....#rr##ww##rr#........
....############........
`;

const SPR_T_CONSUMER = `
.####################...
.#rrwwrrwwrrwwrrwwrr#...
.#rdwdwdwdwdwdwdwdrr#...
.#cgcgcgcgcgcgcgcgcg#...
.#wwwwwwww####wwwwww#...
.#wwswwsww#ww#wwswww#...
.####################...
`;

const SPR_T_BANK = `
.........######.........
.....##############.....
..####################..
..#.cc.cc.cc.cc.cc.cc#..
..#.cc.cc.cc.cc.cc.cc#..
..#.cc.cc.cc.cc.cc.cc#..
..#.cc.cc.cc.cc.cc.cc#..
..####################..
..#rrrrrrr#ww#rrrrrrr#..
..#rrrrrrr#ww#rrrrrrr#..
.######################.
########################
`;

const SPR_T_PE = `
........a...............
........#...............
.....##########.........
.....#mmmmmmmm#.........
.....##########.........
.....#wwrwwrww#.........
.....#rrrrrrrr#.........
.....#wwrwwrww#.........
.....#rrrrrrrr#.........
.....#wwrwwrww#.........
.....#rrrrrrrr#.........
.....#rr#ww#rr#.........
.....##########.........
`;

const SPR_T_CTECH = `
....##############......
....#dddddddddddd#......
....#dmmmmmmmmmmd#......
....#dddddddddddd#......
....#dmmddmmddmmd#......
....#dmmddmmddmmd#......
....#dddddddddddd#......
....##############......
.......########.........
.....############.......
`;

const SPR_T_BIZSVC = `
..####################..
..#xsrxsrxsrxsrxsrxsr#..
..#rrrrrrrrrrrrrrrrrr#..
..#xsrxsrxsrxsrxsrxsr#..
..#rrrrrrrrrrrrrrrrrr#..
..#xsrxsrxsrxsrxsrxsr#..
..#rrrrrrrrrrrrrrrrrr#..
..#rrrrrrr#ww#rrrrrrr#..
..#rrrrrrr#ww#rrrrrrr#..
..####################..
`;

/* the network — a small crew of builders */
const SPR_CREW = `
..cc........cc........cc........cc........cc........cc....
..ss........ss........ss........ss........ss........ss....
.gggg..x...xxxx......GGGG..c...gggg..s...xxxx......GGGG...
..gg...x....xx...s....GG...c....gg..s.....xx........GG..s.
..gg..x.....xx..s.....GG..cc....gg........xx...##...GG.s..
..##........##........##........##........##...##...##....
..#.#......#.#........#.#......#.#........#.#......#.#....
`;

const PX_ARC_SPRITES = [SPR_WORK_DILIGENCE, SPR_WORK_PORTCO, SPR_WORK_GP_INFRA];

Object.assign(window, {
  PxScene, Sprite, PX_ARC_SPRITES, SPR_CREW,
  SPR_T_FACTORY, SPR_T_HEALTH, SPR_T_SOFTWARE, SPR_T_CONSUMER,
  SPR_T_BANK, SPR_T_PE, SPR_T_CTECH, SPR_T_BIZSVC,
});
