// Board canvas — the main wall. Pan + zoom on a dotted infinite-feel
// surface; notes float at world coords. There is no white "paper" frame —
// the dot grid extends past the viewport, so the canvas reads as endless.
//
// Visibility model (see server/api.js):
//   - Notes default to `self`; only their author + admins see content.
//   - Other viewers see a redacted placeholder via StickyNote (geometry +
//     author chip only).
// Ownership: only owners or admins can drag / rotate / resize / edit a note.

// World extent used by the auto-fit calc and as a coord-space anchor for
// notes. There's no visible boundary; this is just a hint for "where most
// of the action is" so first-load centers something useful.
const WORLD_W = 2200;
const WORLD_H = 1100;

function BoardScreen({
  palette, role, locked, notes, recipientName, density,
  myUserId, isAdmin,
  onUpdateNoteLocal, onCommitNote, onDeleteNote, onBringForward, onReorderNote, onEditNote,
  onSetNoteVisibility,
  onCompose, onStartDoodle, onCreateDoodle, onOpenAdmin, onReveal,
}) {
  const [selectedId, setSelectedId] = React.useState(null);
  const [hoveringId, setHoveringId] = React.useState(null);
  const [zoom, setZoom] = React.useState(0.55);
  const [pan, setPan] = React.useState({ x: 0, y: 0 });
  const stageRef = React.useRef(null);
  const panRef = React.useRef(null);

  // Drawing mode — when active, the user sketches directly on the board
  // paper. `drawing.paths` holds finalized strokes; the in-progress one
  // lives in currentStrokeRef so pointermove doesn't re-render the world.
  const [drawing, setDrawing] = React.useState(null);
  const currentStrokeRef = React.useRef(null);
  const [, setStrokeTick] = React.useState(0);
  const bumpStroke = () => setStrokeTick((t) => (t + 1) | 0);
  // Cancel drawing mode if the board gets locked under us mid-sketch.
  React.useEffect(() => { if (locked) setDrawing(null); }, [locked]);
  // Mirror current pan/zoom into a ref so the wheel handler (registered
  // once with a non-passive listener) reads fresh values without React
  // re-binding it on every state change.
  const viewRef = React.useRef({ pan, zoom });
  viewRef.current = { pan, zoom };

  const ZOOM_MIN = 0.2;
  const ZOOM_MAX = 1.4;

  // Fit on first mount and on resize. Centers the WORLD_W×WORLD_H envelope
  // in the viewport so first paint shows roughly where the notes live.
  React.useEffect(() => {
    const fit = () => {
      const el = stageRef.current; if (!el) return;
      const W = el.clientWidth, H = el.clientHeight;
      const z = Math.min(W / WORLD_W, H / WORLD_H) * 0.94;
      setZoom(z);
      setPan({
        x: (W - WORLD_W * z) / 2,
        y: (H - WORLD_H * z) / 2,
      });
    };
    fit();
    window.addEventListener("resize", fit);
    return () => window.removeEventListener("resize", fit);
  }, []);

  // Fit-to-all: snap pan/zoom so every note is visible at once. Falls back
  // to the world envelope when the wall is empty. Triggered by the new
  // "fit" affordance in ZoomControls — useful after a user has panned/
  // zoomed far away and lost track of the board.
  const fitAll = React.useCallback(() => {
    const el = stageRef.current; if (!el) return;
    const W = el.clientWidth, H = el.clientHeight;
    const bbox = notesBBox(notes);
    const targetW = Math.max(bbox.w, 400);
    const targetH = Math.max(bbox.h, 300);
    const z = Math.min(
      ZOOM_MAX,
      Math.max(ZOOM_MIN, Math.min(W / targetW, H / targetH) * 0.9),
    );
    setZoom(z);
    setPan({
      x: W / 2 - bbox.cx * z,
      y: H / 2 - bbox.cy * z,
    });
  }, [notes]);

  // Native trackpad pinch / Cmd+wheel zoom toward the cursor; two-finger
  // scroll pans. Suppress the browser's default page zoom + scroll. Has to
  // be a non-passive listener (React's onWheel is passive in some setups).
  React.useEffect(() => {
    const el = stageRef.current; if (!el) return;
    const onWheel = (e) => {
      e.preventDefault();
      const { pan: p, zoom: z } = viewRef.current;
      const rect = el.getBoundingClientRect();
      const cx = e.clientX - rect.left;
      const cy = e.clientY - rect.top;
      // Mac trackpad pinch and Ctrl/Cmd + wheel both surface as ctrlKey.
      if (e.ctrlKey || e.metaKey) {
        const factor = Math.exp(-e.deltaY * 0.01);
        const newZ = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, z * factor));
        if (newZ === z) return;
        const ratio = newZ / z;
        // Keep the world point under the cursor anchored.
        setZoom(newZ);
        setPan({
          x: cx - (cx - p.x) * ratio,
          y: cy - (cy - p.y) * ratio,
        });
      } else {
        // Two-finger trackpad pan (or shift+wheel for horizontal mice).
        setPan({ x: p.x - e.deltaX, y: p.y - e.deltaY });
      }
    };
    el.addEventListener("wheel", onWheel, { passive: false });
    return () => el.removeEventListener("wheel", onWheel);
  }, []);

  // Two-finger pinch on touch devices: tracks the midpoint and the distance
  // between the two touches, then maps changes onto pan + zoom so the world
  // point that was under the initial midpoint stays anchored to wherever
  // the user's midpoint is now. While pinching, a window-level flag tells
  // single-pointer drag handlers (board pan, note drag/rotate/resize,
  // doodle strokes) to bail so they don't fight the pinch.
  React.useEffect(() => {
    const el = stageRef.current; if (!el) return;
    const pinch = { active: false, d0: 0, m0: null, z0: 1, w0: null };
    const dist = (a, b) => Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
    const mid = (a, b, rect) => ({
      x: (a.clientX + b.clientX) / 2 - rect.left,
      y: (a.clientY + b.clientY) / 2 - rect.top,
    });

    const onStart = (e) => {
      if (e.touches.length < 2) return;
      e.preventDefault();
      const rect = el.getBoundingClientRect();
      const m0 = mid(e.touches[0], e.touches[1], rect);
      const { pan: p, zoom: z } = viewRef.current;
      pinch.active = true;
      pinch.d0 = dist(e.touches[0], e.touches[1]) || 1;
      pinch.m0 = m0;
      pinch.z0 = z;
      // World coord under the starting midpoint — kept invariant under
      // the gesture so the wall feels anchored to the user's fingers.
      pinch.w0 = { x: (m0.x - p.x) / z, y: (m0.y - p.y) / z };
      window.__fwPinching = true;
    };
    const onMove = (e) => {
      if (!pinch.active || e.touches.length < 2) return;
      e.preventDefault();
      const rect = el.getBoundingClientRect();
      const m = mid(e.touches[0], e.touches[1], rect);
      const d = dist(e.touches[0], e.touches[1]);
      const newZ = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, pinch.z0 * (d / pinch.d0)));
      setZoom(newZ);
      setPan({
        x: m.x - pinch.w0.x * newZ,
        y: m.y - pinch.w0.y * newZ,
      });
    };
    const onEnd = (e) => {
      if (e.touches.length < 2) {
        pinch.active = false;
        window.__fwPinching = false;
      }
    };

    el.addEventListener("touchstart", onStart, { passive: false });
    el.addEventListener("touchmove", onMove, { passive: false });
    el.addEventListener("touchend", onEnd);
    el.addEventListener("touchcancel", onEnd);
    return () => {
      el.removeEventListener("touchstart", onStart);
      el.removeEventListener("touchmove", onMove);
      el.removeEventListener("touchend", onEnd);
      el.removeEventListener("touchcancel", onEnd);
      window.__fwPinching = false;
    };
  }, []);

  // Spacebar/middle-mouse pan
  const onStagePointerDown = (e) => {
    if (e.target !== stageRef.current && e.target !== panRef.current) return;
    setSelectedId(null);
    if (e.button !== 0 && e.button !== 1) return;
    const x0 = e.clientX, y0 = e.clientY;
    const p0 = pan;
    const move = (ev) => {
      // Yield to a pinch in progress — otherwise the first finger's pan
      // fights the two-finger pinch and the wall jitters.
      if (window.__fwPinching) return;
      setPan({ x: p0.x + (ev.clientX - x0), y: p0.y + (ev.clientY - y0) });
    };
    const up = () => {
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", up);
    };
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
  };

  // Translate a screen-space point to world coords (panRef-local). Just
  // undoes the stage's pan/zoom — there's no paper transform to worry about.
  const clientToWorld = (clientX, clientY) => {
    const rect = stageRef.current.getBoundingClientRect();
    return [
      (clientX - rect.left - pan.x) / zoom,
      (clientY - rect.top  - pan.y) / zoom,
    ];
  };

  // ── drawing mode ─────────────────────────────────────────────────────────
  // Each sampled point is [worldX, worldY, screenDistFromPrev]. The screen-
  // space distance becomes a per-point speed proxy that `pointsToInkPath`
  // turns into a variable stroke width (slow → thick, fast → thin).
  const beginStroke = (e) => {
    if (!drawing) return;
    if (e.button !== undefined && e.button !== 0) return;
    e.preventDefault();
    e.stopPropagation();
    const [wx0, wy0] = clientToWorld(e.clientX, e.clientY);
    let lastSX = e.clientX, lastSY = e.clientY;
    currentStrokeRef.current = {
      points: [[wx0, wy0, 0]],
      color: drawing.brushColor,
      width: drawing.brushWidth,
    };
    bumpStroke();
    const move = (ev) => {
      if (!currentStrokeRef.current) return;
      if (window.__fwPinching) return;   // let pinch take over
      const sd = Math.hypot(ev.clientX - lastSX, ev.clientY - lastSY);
      lastSX = ev.clientX; lastSY = ev.clientY;
      const [wx, wy] = clientToWorld(ev.clientX, ev.clientY);
      currentStrokeRef.current.points.push([wx, wy, sd]);
      bumpStroke();
    };
    const up = () => {
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", up);
      const cur = currentStrokeRef.current;
      currentStrokeRef.current = null;
      if (!cur || cur.points.length < 2) { bumpStroke(); return; }
      setDrawing((d) => d ? {
        ...d,
        paths: [...d.paths, {
          d: pointsToInkPath(cur.points, cur.width),
          color: cur.color,
          width: cur.width,
          points: cur.points,
          kind: "ink",
        }],
      } : d);
    };
    window.addEventListener("pointermove", move);
    window.addEventListener("pointerup", up);
  };

  const undoStroke = () => setDrawing((d) => d ? { ...d, paths: d.paths.slice(0, -1) } : d);
  const clearStrokes = () => setDrawing((d) => d ? { ...d, paths: [] } : d);
  const cancelDrawing = () => { currentStrokeRef.current = null; setDrawing(null); };
  const saveDrawing = () => {
    if (!drawing || drawing.paths.length === 0) { cancelDrawing(); return; }
    // Compute bbox of all stroke points; pad so strokes don't touch the edge.
    // Pad is generous enough for the ink polygon's peak half-width.
    const PAD = 18;
    const allPoints = drawing.paths.flatMap((p) => p.points);
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    for (const pt of allPoints) {
      const x = pt[0], y = pt[1];
      if (x < minX) minX = x; if (x > maxX) maxX = x;
      if (y < minY) minY = y; if (y > maxY) maxY = y;
    }
    minX -= PAD; minY -= PAD; maxX += PAD; maxY += PAD;
    const w = Math.max(80, maxX - minX);
    const h = Math.max(80, maxY - minY);
    const normalized = drawing.paths.map((p) => ({
      d: pointsToInkPath(p.points.map(([x, y, sd]) => [x - minX, y - minY, sd || 0]), p.width),
      color: p.color,
      kind: "ink",
    }));
    onCreateDoodle?.({
      paths: normalized,
      doodleVW: w, doodleVH: h,
      x: minX, y: minY, w, h,
    });
    setDrawing(null);
  };

  const startDrawing = () => {
    if (locked) return;
    // Parent can veto (e.g. user has no name yet); falsy return aborts.
    if (onStartDoodle && onStartDoodle() === false) return;
    setSelectedId(null);
    setDrawing({ paths: [], brushColor: "var(--ink)", brushWidth: 3 });
  };
  const setBrushColor = (color) => setDrawing((d) => d ? { ...d, brushColor: color } : d);
  const setBrushWidth = (width) => setDrawing((d) => d ? { ...d, brushWidth: width } : d);

  const visibleNotes = React.useMemo(() => {
    if (density === "regular") return notes;
    const factor = density === "sparse" ? 1.15 : 0.86;
    return notes.map((n) => ({
      ...n,
      x: 1100 + (n.x - 1100) * factor,
      y: 540 + (n.y - 540) * factor,
    }));
  }, [notes, density]);

  // Local-only updates fire many times per gesture; the server PATCH happens
  // once on commit (pointerup) via onCommitNote.
  const updateNote = (id, patch) => onUpdateNoteLocal(id, patch);

  const deleteNote = (id) => {
    onDeleteNote(id);
    setSelectedId(null);
  };

  const bringForward = (id) => onBringForward(id);

  const selected = visibleNotes.find((n) => n.id === selectedId);
  const authorOf = (note) => authorForNote(note);

  // Per-note edit gate: admin can touch anything; otherwise only the author
  // of a non-redacted note. Redacted notes are inherently read-only.
  const canEditNote = (n) => {
    if (n.redacted) return false;
    if (locked && !isAdmin) return false;
    if (isAdmin) return true;
    return !!(myUserId && n.authorId === myUserId);
  };

  return (
    <div style={{ position: "absolute", inset: 0, background: palette.bg, overflow: "hidden" }}>
      <div className="paper-noise" />

      {/* Top action bar — replaced by the drawing toolbar while sketching. */}
      {!drawing && (
        <BoardTopBar
          palette={palette}
          role={role}
          locked={locked}
          contributorCount={new Set(notes.map((n) => n.authorId)).size}
          recipientName={recipientName}
          zoom={zoom}
          setZoom={setZoom}
          onCompose={onCompose}
          onStartDoodle={startDrawing}
          onOpenAdmin={onOpenAdmin}
          onReveal={onReveal}
        />
      )}
      {drawing && (
        <DrawBar
          palette={palette}
          paths={drawing.paths}
          brushColor={drawing.brushColor}
          brushWidth={drawing.brushWidth}
          setBrushColor={setBrushColor}
          setBrushWidth={setBrushWidth}
          onUndo={undoStroke}
          onClear={clearStrokes}
          onCancel={cancelDrawing}
          onSave={saveDrawing}
        />
      )}

      {/* The stage — pan/zoom container */}
      <div
        ref={stageRef}
        onPointerDown={onStagePointerDown}
        style={{
          position: "absolute", inset: 0, top: 78,
          overflow: "hidden", cursor: selectedId ? "default" : "grab",
          // Disable native browser pinch/scroll on touch so our handlers
          // own the gesture (otherwise iOS Safari hijacks pinch as page zoom).
          touchAction: "none",
        }}
        onClick={(e) => { if (e.target === stageRef.current) setSelectedId(null); }}
      >
        {/* Background grid */}
        <CanvasGrid pan={pan} zoom={zoom} />

        <div
          ref={panRef}
          style={{
            position: "absolute", left: 0, top: 0,
            width: WORLD_W, height: WORLD_H,
            transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
            transformOrigin: "0 0",
          }}
        >
          {/* Notes — placed in world coords (note.x, note.y) directly on
              the dotted canvas. No paper backdrop. */}
          {visibleNotes.map((n) => (
            <div key={n.id}
                 onPointerEnter={() => setHoveringId(n.id)}
                 onPointerLeave={() => setHoveringId(null)}>
              <StickyNote
                note={n}
                author={authorOf(n)}
                selected={selectedId === n.id}
                hovering={hoveringId === n.id}
                scale={zoom}
                readOnly={!canEditNote(n) || !!drawing}
                onSelect={(id) => { setSelectedId(id); bringForward(id); }}
                onChange={updateNote}
                onCommit={onCommitNote}
              />
            </div>
          ))}

          {/* In-progress drawing — committed strokes plus the live one. The
              SVG covers the WORLD_W×WORLD_H envelope; strokes outside it
              still render because SVGs don't clip by default. */}
          {drawing && (
            <svg
              viewBox={`0 0 ${WORLD_W} ${WORLD_H}`}
              preserveAspectRatio="none"
              style={{
                position: "absolute", left: 0, top: 0,
                width: WORLD_W, height: WORLD_H,
                pointerEvents: "none", overflow: "visible",
              }}
            >
              {drawing.paths.map((p, i) => (
                <path key={i} d={p.d}
                      fill={p.color} stroke={p.color} strokeWidth={0.5}
                      strokeLinejoin="round" />
              ))}
              {currentStrokeRef.current && currentStrokeRef.current.points.length > 0 && (
                <path d={pointsToInkPath(currentStrokeRef.current.points, currentStrokeRef.current.width)}
                      fill={currentStrokeRef.current.color}
                      stroke={currentStrokeRef.current.color}
                      strokeWidth={0.5}
                      strokeLinejoin="round" />
              )}
            </svg>
          )}
        </div>

        {/* Drawing overlay — captures pointer events on top of notes so
            sketches go to the canvas instead of dragging notes around.
            Sits inside the stage so its rect aligns with stageRef. */}
        {drawing && (
          <div
            onPointerDown={beginStroke}
            style={{
              position: "absolute", inset: 0,
              cursor: "crosshair",
              touchAction: "none",
              zIndex: 25,
            }}
          />
        )}

        {/* Selection toolbar */}
        {!drawing && selected && !selected.redacted && canEditNote(selected) && (
          <SelectionToolbar
            note={selected}
            zoom={zoom}
            pan={pan}
            isAdmin={isAdmin}
            onDelete={() => deleteNote(selected.id)}
            onChange={(patch) => {
              updateNote(selected.id, patch);
              onCommitNote(selected.id);
            }}
            onEdit={onEditNote ? () => onEditNote(selected.id) : undefined}
            onVisibilityChange={onSetNoteVisibility
              ? (v) => onSetNoteVisibility(selected.id, v)
              : undefined}
            onReorder={onReorderNote}
            palette={palette}
          />
        )}

        {/* Zoom controls bottom-left */}
        <ZoomControls zoom={zoom} setZoom={setZoom} onFit={fitAll} />

        {/* Help hint */}
        <div style={{
          position: "absolute", left: 16, bottom: 16,
          fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--ink-3)",
          letterSpacing: "0.06em", textTransform: "uppercase",
        }} />

        {/* Lock badge */}
        {locked && (
          <div style={{
            position: "absolute", right: 22, bottom: 22,
            padding: "10px 14px", background: "var(--ink)", color: "#fff",
            borderRadius: 999, display: "flex", alignItems: "center", gap: 8,
            boxShadow: "var(--shadow-2)", fontSize: 12.5,
          }}>
            <IconLock size={13} /> Locked — no new notes until reveal
          </div>
        )}
      </div>
    </div>
  );
}

function CanvasGrid({ pan, zoom }) {
  const dot = 24 * zoom;
  return (
    <div style={{
      position: "absolute", inset: 0,
      backgroundImage: `radial-gradient(rgba(31,27,23,0.07) 1px, transparent 1px)`,
      backgroundSize: `${dot}px ${dot}px`,
      backgroundPosition: `${pan.x % dot}px ${pan.y % dot}px`,
      pointerEvents: "none",
    }} />
  );
}

function ZoomControls({ zoom, setZoom, onFit }) {
  const pct = Math.round(zoom * 100);
  return (
    <div style={{
      position: "absolute", right: 16, bottom: 16,
      display: "flex", gap: 4, alignItems: "center",
      background: "#fff", border: "1px solid var(--line)",
      borderRadius: 12, padding: 4, boxShadow: "var(--shadow-1)",
    }}>
      <button onClick={() => setZoom((z) => Math.max(0.2, z - 0.1))}
        style={zBtn} title="Zoom out">−</button>
      <span style={{ fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--ink-2)", padding: "0 8px", minWidth: 40, textAlign: "center" }}>{pct}%</span>
      <button onClick={() => setZoom((z) => Math.min(1.4, z + 0.1))}
        style={zBtn} title="Zoom in">+</button>
      {onFit && (
        <>
          <span style={{ width: 1, height: 18, background: "var(--line)", margin: "0 2px" }} />
          <button onClick={onFit} style={zBtn} title="Fit all notes in view"
                  aria-label="Fit all notes in view">
            <IconResize size={13} />
          </button>
        </>
      )}
    </div>
  );
}

// World-space bounding box of all notes (accounting for each note's own
// rotation). Returns the world envelope as a fallback when the wall is
// empty so fit-to-all still has something sensible to center on.
function notesBBox(notes) {
  let minX =  Infinity, minY =  Infinity;
  let maxX = -Infinity, maxY = -Infinity;
  for (const n of notes || []) {
    if (typeof n.x !== "number" || typeof n.y !== "number") continue;
    const w = n.w || 240, h = n.h || 200;
    const rad = (n.rot || 0) * Math.PI / 180;
    const c = Math.abs(Math.cos(rad));
    const s = Math.abs(Math.sin(rad));
    const rw = w * c + h * s;
    const rh = w * s + h * c;
    const cx = n.x + w / 2;
    const cy = n.y + h / 2;
    if (cx - rw / 2 < minX) minX = cx - rw / 2;
    if (cy - rh / 2 < minY) minY = cy - rh / 2;
    if (cx + rw / 2 > maxX) maxX = cx + rw / 2;
    if (cy + rh / 2 > maxY) maxY = cy + rh / 2;
  }
  if (!isFinite(minX)) {
    minX = 0; minY = 0; maxX = WORLD_W; maxY = WORLD_H;
  }
  return {
    minX, minY, maxX, maxY,
    w: maxX - minX,
    h: maxY - minY,
    cx: (minX + maxX) / 2,
    cy: (minY + maxY) / 2,
  };
}
const zBtn = {
  appearance: "none", border: 0, background: "transparent",
  width: 28, height: 28, borderRadius: 7, cursor: "pointer",
  color: "var(--ink)", fontSize: 15,
};

// Variable-width ink stroke: each `point` is [x, y, screenSpeed]. Output is
// a closed filled polygon (SVG `d`) whose half-thickness at each sample
// follows a slow→thick / fast→thin curve, smoothed across neighbors and
// tapered hard at the very end (pen-lift) and softer at the start. The
// renderer just fills this shape — no `strokeWidth`.
function computeInkWidths(points, base) {
  const N = points.length;
  if (N === 0) return [];
  if (N === 1) return [base];
  const minW = base * 0.30;
  const maxW = base * 1.50;
  const raw = new Array(N);
  for (let i = 0; i < N; i++) {
    // Screen-space distance from previous sample (stored as third coord).
    // Average with the next sample so isolated spikes don't pop.
    const s0 = points[i][2] || 0;
    const s1 = points[Math.min(N - 1, i + 1)][2] || 0;
    const speed = (s0 + s1) / 2;
    let w = base * (1.50 - speed * 0.040);
    if (w < minW) w = minW;
    else if (w > maxW) w = maxW;
    raw[i] = w;
  }
  const widths = new Array(N);
  const WIN = 3;
  for (let i = 0; i < N; i++) {
    let sum = 0, count = 0;
    const lo = Math.max(0, i - WIN);
    const hi = Math.min(N - 1, i + WIN);
    for (let j = lo; j <= hi; j++) { sum += raw[j]; count++; }
    widths[i] = sum / count;
  }
  const taperStart = Math.min(5, Math.floor(N * 0.18));
  for (let i = 0; i < taperStart; i++) {
    const t = i / taperStart;
    widths[i] *= 0.25 + 0.75 * t;
  }
  const taperEnd = Math.min(8, Math.floor(N * 0.30));
  for (let i = 0; i < taperEnd; i++) {
    const t = i / taperEnd;
    widths[N - 1 - i] *= 0.10 + 0.90 * Math.pow(t, 1.6);
  }
  return widths;
}

function pointsToInkPath(points, base) {
  const N = points.length;
  if (N === 0) return "";
  if (N === 1) {
    const [x, y] = points[0];
    const r = Math.max(0.6, base * 0.45);
    // Two arcs = a circle. Acts as a tiny dot for taps.
    return `M ${(x - r).toFixed(2)} ${y.toFixed(2)} a ${r} ${r} 0 1 0 ${(r * 2).toFixed(2)} 0 a ${r} ${r} 0 1 0 ${(-r * 2).toFixed(2)} 0 Z`;
  }
  const widths = computeInkWidths(points, base);
  const left = new Array(N);
  const right = new Array(N);
  for (let i = 0; i < N; i++) {
    const [x, y] = points[i];
    let tx, ty;
    if (i === 0) {
      tx = points[1][0] - x; ty = points[1][1] - y;
    } else if (i === N - 1) {
      tx = x - points[i - 1][0]; ty = y - points[i - 1][1];
    } else {
      tx = points[i + 1][0] - points[i - 1][0];
      ty = points[i + 1][1] - points[i - 1][1];
    }
    const len = Math.hypot(tx, ty) || 1;
    const nx = -ty / len;
    const ny =  tx / len;
    const h = widths[i] / 2;
    left[i]  = [x + nx * h, y + ny * h];
    right[i] = [x - nx * h, y - ny * h];
  }
  const fmt = (n) => n.toFixed(2);
  let d = `M ${fmt(left[0][0])} ${fmt(left[0][1])}`;
  // Quadratic smoothing along the left edge: control point at left[i],
  // anchor at the midpoint between left[i] and left[i+1].
  for (let i = 1; i < N - 1; i++) {
    const mx = (left[i][0] + left[i + 1][0]) / 2;
    const my = (left[i][1] + left[i + 1][1]) / 2;
    d += ` Q ${fmt(left[i][0])} ${fmt(left[i][1])} ${fmt(mx)} ${fmt(my)}`;
  }
  d += ` L ${fmt(left[N - 1][0])} ${fmt(left[N - 1][1])}`;
  d += ` L ${fmt(right[N - 1][0])} ${fmt(right[N - 1][1])}`;
  for (let i = N - 2; i > 0; i--) {
    const mx = (right[i][0] + right[i - 1][0]) / 2;
    const my = (right[i][1] + right[i - 1][1]) / 2;
    d += ` Q ${fmt(right[i][0])} ${fmt(right[i][1])} ${fmt(mx)} ${fmt(my)}`;
  }
  d += ` L ${fmt(right[0][0])} ${fmt(right[0][1])} Z`;
  return d;
}

// Replaces the BoardTopBar while a sketch is in progress. Lives in the same
// top-bar slot so the spatial "where the controls live" stays predictable.
function DrawBar({
  palette, paths, brushColor, brushWidth,
  setBrushColor, setBrushWidth, onUndo, onClear, onCancel, onSave,
}) {
  const isMobile = useIsMobile();
  const COLORS = [
    { v: "var(--ink)",   bg: "#1F1B17" },
    { v: "var(--coral)", bg: palette?.accent || "#E87E5A" },
    { v: "#3F8FA8",      bg: "#3F8FA8" },
    { v: "#5C8B6B",      bg: "#5C8B6B" },
  ];
  const SIZES = [2, 3, 5, 8];
  const canSave = paths.length > 0;
  return (
    <div style={{
      position: "absolute",
      top: isMobile ? 60 : 14,
      left: isMobile ? 8 : 0,
      right: isMobile ? 8 : 14,
      height: isMobile ? "auto" : 56,
      paddingLeft: isMobile ? 0 : 660,
      display: "flex", alignItems: "center", gap: isMobile ? 8 : 14,
      flexWrap: "wrap", justifyContent: "flex-end",
      zIndex: 30,
    }}>
      <div style={{
        display: "flex", alignItems: "center", gap: 12,
        padding: "8px 14px",
        background: "rgba(255,255,255,0.92)",
        border: "1px solid var(--line)",
        borderRadius: 14, boxShadow: "var(--shadow-1)",
        backdropFilter: "blur(14px) saturate(140%)",
        WebkitBackdropFilter: "blur(14px) saturate(140%)",
      }}>
        <span style={{
          fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--ink-3)",
          letterSpacing: "0.1em", textTransform: "uppercase",
        }}>Drawing</span>
        <span style={{ width: 1, height: 18, background: "var(--line)" }} />
        <div style={{ display: "flex", gap: 4 }}>
          {COLORS.map((c) => (
            <button key={c.v} onClick={() => setBrushColor(c.v)} title="Brush color"
                    style={{
                      width: 22, height: 22, borderRadius: "50%",
                      background: c.bg,
                      border: brushColor === c.v ? "1.5px solid var(--ink)" : "1px solid rgba(31,27,23,0.15)",
                      cursor: "pointer", padding: 0,
                    }} />
          ))}
          <CustomColorButton
            value={brushColor}
            onPick={(hex) => setBrushColor(hex)}
            style={{ width: 22, height: 22, borderRadius: "50%" }}
          />
        </div>
        <span style={{ width: 1, height: 18, background: "var(--line)" }} />
        <div style={{ display: "flex", gap: 4, alignItems: "center" }}>
          {SIZES.map((s) => (
            <button key={s} onClick={() => setBrushWidth(s)} title={`Brush size ${s}`}
                    style={{
                      width: 26, height: 24, borderRadius: 6, border: 0,
                      background: brushWidth === s ? "rgba(31,27,23,0.08)" : "transparent",
                      cursor: "pointer", display: "grid", placeItems: "center",
                    }}>
              <span style={{
                width: s * 1.6 + 2, height: s * 1.6 + 2, borderRadius: "50%",
                background: "var(--ink)", display: "block",
              }} />
            </button>
          ))}
        </div>
        <span style={{ width: 1, height: 18, background: "var(--line)" }} />
        <button onClick={onUndo} disabled={!canSave}
                className="btn btn-ghost"
                style={{ padding: "5px 10px", fontSize: 12, opacity: canSave ? 1 : 0.4 }}>
          Undo
        </button>
        <button onClick={onClear} disabled={!canSave}
                className="btn btn-ghost"
                style={{ padding: "5px 10px", fontSize: 12, opacity: canSave ? 1 : 0.4 }}>
          Clear
        </button>
      </div>

      <div style={{ flex: 1 }} />

      <div style={{ display: "flex", gap: 8 }}>
        <button className="btn btn-ghost" onClick={onCancel}>Cancel</button>
        <button className="btn btn-coral" onClick={onSave} disabled={!canSave}
                style={{ opacity: canSave ? 1 : 0.45, cursor: canSave ? "pointer" : "not-allowed" }}>
          <IconCheck size={13} /> Stick it on
        </button>
      </div>
    </div>
  );
}

Object.assign(window, { BoardScreen, DrawBar });
