{"kind":"Skill","metadata":{"namespace":"community","name":"napkin","version":"0.1.0"},"spec":{"description":"Visual whiteboard collaboration for Copilot CLI. Creates an interactive whiteboard that opens in your browser — draw, sketch, add sticky notes, then share everything back with Copilot. Copilot sees your drawings and text, and responds with analysis, suggestions, and ideas.","files":{"SKILL.md":"---\nname: napkin\ndescription: 'Visual whiteboard collaboration for Copilot CLI. Creates an interactive whiteboard that opens in your browser — draw, sketch, add sticky notes, then share everything back with Copilot. Copilot sees your drawings and text, and responds with analysis, suggestions, and ideas.'\n---\n\n# Napkin — Visual Whiteboard for Copilot CLI\n\nNapkin gives users a browser-based whiteboard where they can draw, sketch, and add sticky notes to think through ideas visually. The agent reads back the whiteboard contents (via a PNG snapshot and optional JSON data) and responds conversationally with analysis, suggestions, and next steps.\n\nThe target audience is lawyers, PMs, and business stakeholders — not software developers. Keep everything approachable and jargon-free.\n\n---\n\n## Activation\n\nWhen the user invokes this skill — saying things like \"let's napkin,\" \"open a napkin,\" \"start a whiteboard,\" or using the slash command — do the following:\n\n1. **Copy the bundled HTML template** from the skill assets to the user's Desktop.\n   - The template lives at `assets/napkin.html` relative to this SKILL.md file.\n   - Copy it to `~/Desktop/napkin.html`.\n   - If `~/Desktop/napkin.html` already exists, ask the user whether they want to open the existing one or start fresh before overwriting.\n\n2. **Open it in the default browser:**\n   - macOS: `open ~/Desktop/napkin.html`\n   - Linux: `xdg-open ~/Desktop/napkin.html`\n   - Windows: `start ~/Desktop/napkin.html`\n\n3. **Tell the user what to do next.** Say something warm and simple:\n\n   ```\n   Your napkin is open in your browser!\n\n   Draw, sketch, or add sticky notes — whatever helps you think through your idea.\n\n   When you're ready for my input, click the green \"Share with Copilot\" button on the whiteboard, then come back here and say \"check the napkin.\"\n   ```\n\n---\n\n## Reading the Napkin\n\nWhen the user says \"check the napkin,\" \"look at the napkin,\" \"what do you think,\" \"read my napkin,\" or anything similar, follow these steps:\n\n### Step 1 — Read the PNG snapshot (primary)\n\nLook for a PNG file called `napkin-snapshot.png`. Check these locations in order (the browser saves it to the user's default download folder, which varies):\n\n1. `~/Downloads/napkin-snapshot.png`\n2. `~/Desktop/napkin-snapshot.png`\n\nUse the `view` tool to read the PNG. This sends the image as base64-encoded data to the model, which can visually interpret it. The PNG is the **primary** way the agent understands what the user drew — it captures freehand sketches, arrows, spatial layout, annotations, circled or crossed-out items, and anything else on the canvas.\n\nIf the PNG is not found in either location, do NOT silently skip it. Instead, tell the user:\n\n```\nI don't see a snapshot from your napkin yet. Here's what to do:\n\n1. Go to your whiteboard in the browser\n2. Click the green \"Share with Copilot\" button\n3. Come back here and say \"check the napkin\" again\n\nThe button saves a screenshot that I can look at.\n```\n\n### Step 2 — Read the clipboard for structured JSON (supplementary)\n\nAlso try to grab structured JSON data from the system clipboard. The whiteboard copies this automatically alongside the PNG.\n\n- macOS: `pbpaste`\n- Linux: `xclip -selection clipboard -o`\n- Windows: `powershell -command \"Get-Clipboard\"`\n\nThe JSON contains the exact text content of sticky notes and text labels, their positions, and their colors. This supplements the PNG by giving you precise text that might be hard to read from a screenshot.\n\nIf the clipboard doesn't contain JSON data, that's fine — the PNG alone gives the model plenty to work with. Do not treat a missing clipboard as an error.\n\n### Step 3 — Interpret both sources together\n\nSynthesize the visual snapshot and the structured text into a coherent understanding of what the user is thinking or planning:\n\n- **From the PNG:** Describe what you see — sketches, diagrams, flowcharts, groupings, arrows, spatial layout, annotations, circled items, crossed-out items, emphasis marks.\n- **From the JSON:** Read the exact text content of sticky notes and labels, noting their positions and colors.\n- **Combine both** into a single, conversational interpretation.\n\n### Step 4 — Respond conversationally\n\nDo not dump raw data or a technical summary. Respond as a collaborator who looked at someone's whiteboard sketch. Examples:\n\n- \"I can see you've sketched out a three-stage process — it looks like you're thinking about [X] flowing into [Y] and then [Z]. The sticky note in the corner says '[text]' — is that a concern you want me to address?\"\n- \"It looks like you've grouped these four ideas together on the left side and separated them from the two items on the right. Are you thinking of these as two different categories?\"\n- \"I see you drew arrows connecting [A] to [B] to [C] — is this the workflow you're envisioning?\"\n\n### Step 5 — Ask what's next\n\nAlways end by offering a next step:\n\n- \"Want me to build on this?\"\n- \"Should I turn this into a structured document?\"\n- \"Want me to add my suggestions to the napkin?\"\n\n---\n\n## Responding on the Napkin\n\nWhen the user wants the agent to add content back to the whiteboard:\n\n- The agent **cannot** directly modify the HTML file's canvas state — that's managed by JavaScript running in the browser.\n- Instead, offer practical alternatives:\n  - Provide the response right here in the CLI, and suggest the user add it to the napkin manually.\n  - Offer to create a separate document (markdown, memo, checklist, etc.) based on what was interpreted from the napkin.\n  - If it makes sense, create an updated copy of `napkin.html` with pre-loaded content.\n\n---\n\n## Tone and Style\n\n- Use the same approachable, non-technical tone as the noob-mode skill.\n- Never use developer jargon without explaining it in plain English.\n- Treat the napkin as a creative, collaborative space — not a formal input mechanism.\n- Be encouraging about the user's sketches regardless of artistic quality.\n- Frame responses as \"building on your thinking,\" not \"analyzing your input.\"\n\n---\n\n## Error Handling\n\n**PNG snapshot not found:**\n\n```\nI don't see a snapshot from your napkin yet. Here's what to do:\n\n1. Go to your whiteboard in the browser\n2. Click the green \"Share with Copilot\" button\n3. Come back here and say \"check the napkin\" again\n\nThe button saves a screenshot that I can look at.\n```\n\n**Whiteboard file doesn't exist on Desktop:**\n\n```\nIt looks like we haven't started a napkin yet. Want me to open one for you?\n```\n\n---\n\n## Important Notes\n\n- The PNG interpretation is the **primary** channel. Multimodal models can read and interpret the base64 image data returned by the `view` tool.\n- The JSON clipboard data is **supplementary** — it provides precise text but does not capture freehand drawings.\n- Always check for the PNG first. If it isn't found, prompt the user to click \"Share with Copilot.\"\n- If the clipboard doesn't have JSON data, proceed with the PNG alone.\n- The HTML template is located at `assets/napkin.html` relative to this SKILL.md file.\n- If the noob-mode skill is also active, use its risk indicator format (green/yellow/red) when requesting file or bash permissions.\n","assets/napkin.html":"\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n\u003cmeta charset=\"UTF-8\"\u003e\n\u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n\u003ctitle\u003eNapkin — Whiteboard for Copilot\u003c/title\u003e\n\u003cstyle\u003e\n*, *::before, *::after {\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n}\n\nhtml, body {\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n  background: #f5f5f5;\n  user-select: none;\n  -webkit-user-select: none;\n}\n\n/* ── Toolbar ───────────────────────────────────────────────────── */\n#toolbar {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  height: 72px;\n  background: #fafafa;\n  border-bottom: 1px solid #e0e0e0;\n  display: flex;\n  align-items: center;\n  padding: 0 12px;\n  gap: 4px;\n  z-index: 1000;\n  box-shadow: 0 1px 3px rgba(0,0,0,0.06);\n}\n\n.toolbar-group {\n  display: flex;\n  align-items: center;\n  gap: 2px;\n  padding: 0 6px;\n}\n\n.toolbar-group + .toolbar-group {\n  border-left: 1px solid #e0e0e0;\n  margin-left: 4px;\n  padding-left: 10px;\n}\n\n.tool-btn {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  width: 56px;\n  height: 56px;\n  border: 2px solid transparent;\n  border-radius: 10px;\n  background: transparent;\n  cursor: pointer;\n  transition: all 0.15s ease;\n  padding: 4px 2px 2px;\n}\n\n.tool-btn:hover {\n  background: #eee;\n}\n\n.tool-btn.active {\n  background: #e3f2fd;\n  border-color: #1e88e5;\n}\n\n.tool-btn .icon {\n  font-size: 20px;\n  line-height: 1;\n  height: 24px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.tool-btn .label {\n  font-size: 9px;\n  color: #666;\n  margin-top: 2px;\n  white-space: nowrap;\n  font-weight: 500;\n  letter-spacing: 0.02em;\n}\n\n.tool-btn.active .label {\n  color: #1e88e5;\n}\n\n/* Color picker */\n.color-picker {\n  display: flex;\n  gap: 3px;\n  align-items: center;\n  padding: 0 4px;\n}\n\n.color-swatch {\n  width: 22px;\n  height: 22px;\n  border-radius: 50%;\n  border: 2px solid #ddd;\n  cursor: pointer;\n  transition: transform 0.1s ease;\n}\n\n.color-swatch:hover {\n  transform: scale(1.15);\n}\n\n.color-swatch.active {\n  border-color: #333;\n  box-shadow: 0 0 0 2px #fff, 0 0 0 4px #333;\n}\n\n/* Stroke width buttons */\n.stroke-btn {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  width: 40px;\n  height: 40px;\n  border: 2px solid transparent;\n  border-radius: 8px;\n  background: transparent;\n  cursor: pointer;\n}\n\n.stroke-btn:hover {\n  background: #eee;\n}\n\n.stroke-btn.active {\n  background: #e3f2fd;\n  border-color: #1e88e5;\n}\n\n.stroke-btn .stroke-line {\n  background: #333;\n  border-radius: 4px;\n  width: 20px;\n}\n\n.stroke-btn .label {\n  font-size: 8px;\n  color: #888;\n  margin-top: 2px;\n}\n\n/* Share button */\n.share-btn {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  padding: 10px 20px;\n  background: #0d9488;\n  color: #fff;\n  border: none;\n  border-radius: 10px;\n  font-size: 14px;\n  font-weight: 600;\n  cursor: pointer;\n  transition: background 0.15s ease, transform 0.1s ease;\n  margin-left: auto;\n  white-space: nowrap;\n  box-shadow: 0 2px 8px rgba(13,148,136,0.3);\n  font-family: inherit;\n}\n\n.share-btn:hover {\n  background: #0f766e;\n  transform: translateY(-1px);\n}\n\n.share-btn:active {\n  transform: translateY(0);\n}\n\n.share-btn .icon {\n  font-size: 18px;\n}\n\n/* Help button */\n.help-btn {\n  width: 36px;\n  height: 36px;\n  border-radius: 50%;\n  border: 2px solid #ccc;\n  background: #fff;\n  color: #888;\n  font-size: 16px;\n  font-weight: 700;\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  margin-left: 8px;\n  flex-shrink: 0;\n  font-family: inherit;\n}\n\n.help-btn:hover {\n  border-color: #999;\n  color: #555;\n}\n\n/* ── Canvas area ───────────────────────────────────────────────── */\n#canvas-container {\n  position: fixed;\n  top: 72px;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  overflow: hidden;\n  background: #f0f0f0;\n  cursor: crosshair;\n}\n\n#canvas-container.panning {\n  cursor: grab;\n}\n\n#canvas-container.panning:active {\n  cursor: grabbing;\n}\n\n#drawing-canvas {\n  position: absolute;\n  background: #fff;\n  box-shadow: 0 2px 20px rgba(0,0,0,0.08);\n}\n\n/* ── Sticky notes ──────────────────────────────────────────────── */\n.sticky-note {\n  position: absolute;\n  min-width: 140px;\n  min-height: 100px;\n  border-radius: 4px;\n  box-shadow: 2px 3px 12px rgba(0,0,0,0.12), 0 1px 4px rgba(0,0,0,0.06);\n  display: flex;\n  flex-direction: column;\n  z-index: 500;\n  font-family: inherit;\n}\n\n.sticky-note .note-header {\n  height: 24px;\n  border-radius: 4px 4px 0 0;\n  cursor: move;\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n  padding: 0 4px;\n  flex-shrink: 0;\n  opacity: 0.8;\n}\n\n.sticky-note .note-delete {\n  width: 18px;\n  height: 18px;\n  border: none;\n  background: rgba(0,0,0,0.15);\n  color: rgba(0,0,0,0.5);\n  border-radius: 50%;\n  font-size: 12px;\n  line-height: 1;\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  opacity: 0;\n  transition: opacity 0.15s;\n  font-family: inherit;\n}\n\n.sticky-note:hover .note-delete {\n  opacity: 1;\n}\n\n.sticky-note .note-delete:hover {\n  background: rgba(0,0,0,0.3);\n  color: rgba(0,0,0,0.8);\n}\n\n.sticky-note .note-body {\n  flex: 1;\n  padding: 8px 12px 12px;\n  font-size: 14px;\n  line-height: 1.4;\n  outline: none;\n  cursor: text;\n  overflow-wrap: break-word;\n  word-break: break-word;\n  white-space: pre-wrap;\n  border-radius: 0 0 4px 4px;\n  min-height: 76px;\n}\n\n.sticky-note .note-resize {\n  position: absolute;\n  bottom: 0;\n  right: 0;\n  width: 16px;\n  height: 16px;\n  cursor: nwse-resize;\n  opacity: 0;\n  transition: opacity 0.15s;\n}\n\n.sticky-note:hover .note-resize {\n  opacity: 0.4;\n}\n\n.sticky-note .note-resize::after {\n  content: '';\n  position: absolute;\n  bottom: 3px;\n  right: 3px;\n  width: 8px;\n  height: 8px;\n  border-right: 2px solid rgba(0,0,0,0.3);\n  border-bottom: 2px solid rgba(0,0,0,0.3);\n}\n\n/* Sticky note colors */\n.sticky-yellow { background: #fff9c4; }\n.sticky-yellow .note-header { background: #fff176; }\n.sticky-pink { background: #fce4ec; }\n.sticky-pink .note-header { background: #f48fb1; }\n.sticky-blue { background: #e3f2fd; }\n.sticky-blue .note-header { background: #90caf9; }\n.sticky-green { background: #e8f5e9; }\n.sticky-green .note-header { background: #a5d6a7; }\n\n/* Sticky note color picker in toolbar */\n.note-color-picker {\n  display: none;\n  position: absolute;\n  top: 60px;\n  background: #fff;\n  border-radius: 10px;\n  padding: 8px;\n  box-shadow: 0 4px 16px rgba(0,0,0,0.15);\n  gap: 6px;\n  z-index: 1001;\n}\n\n.note-color-picker.show {\n  display: flex;\n}\n\n.note-color-opt {\n  width: 30px;\n  height: 30px;\n  border-radius: 6px;\n  border: 2px solid #ddd;\n  cursor: pointer;\n}\n\n.note-color-opt:hover {\n  border-color: #999;\n}\n\n/* ── Text labels on canvas ─────────────────────────────────────── */\n.canvas-text-label {\n  position: absolute;\n  font-size: 16px;\n  color: #333;\n  outline: none;\n  cursor: text;\n  padding: 2px 4px;\n  min-width: 20px;\n  min-height: 20px;\n  white-space: pre-wrap;\n  z-index: 400;\n  border: 1px dashed transparent;\n  border-radius: 3px;\n  font-family: inherit;\n  background: transparent;\n}\n\n.canvas-text-label:focus {\n  border-color: #90caf9;\n  background: rgba(255,255,255,0.85);\n}\n\n/* ── Overlays ──────────────────────────────────────────────────── */\n.overlay-backdrop {\n  position: fixed;\n  inset: 0;\n  background: rgba(0,0,0,0.45);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  z-index: 9999;\n}\n\n.overlay-backdrop.hidden {\n  display: none;\n}\n\n.overlay-card {\n  background: #fff;\n  border-radius: 16px;\n  padding: 40px 44px;\n  max-width: 500px;\n  width: 90%;\n  box-shadow: 0 16px 48px rgba(0,0,0,0.18);\n  text-align: center;\n}\n\n.overlay-card h1 {\n  font-size: 26px;\n  font-weight: 700;\n  color: #222;\n  margin-bottom: 8px;\n}\n\n.overlay-card .subtitle {\n  font-size: 15px;\n  color: #666;\n  margin-bottom: 24px;\n}\n\n.overlay-card .steps {\n  text-align: left;\n  margin: 0 auto 28px;\n  max-width: 380px;\n}\n\n.overlay-card .steps .step {\n  display: flex;\n  gap: 12px;\n  margin-bottom: 14px;\n  font-size: 14px;\n  line-height: 1.5;\n  color: #444;\n}\n\n.overlay-card .steps .step-num {\n  flex-shrink: 0;\n  width: 26px;\n  height: 26px;\n  background: #0d9488;\n  color: #fff;\n  border-radius: 50%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 13px;\n  font-weight: 700;\n}\n\n.overlay-card .cta-btn {\n  display: inline-block;\n  padding: 14px 32px;\n  background: #0d9488;\n  color: #fff;\n  border: none;\n  border-radius: 10px;\n  font-size: 16px;\n  font-weight: 600;\n  cursor: pointer;\n  transition: background 0.15s ease;\n  font-family: inherit;\n}\n\n.overlay-card .cta-btn:hover {\n  background: #0f766e;\n}\n\n/* Share confirmation */\n.overlay-card .confirm-icon {\n  font-size: 48px;\n  margin-bottom: 12px;\n}\n\n.overlay-card .confirm-detail {\n  text-align: left;\n  background: #f5f5f5;\n  border-radius: 10px;\n  padding: 16px 20px;\n  margin: 16px 0 24px;\n  font-size: 13px;\n  line-height: 1.7;\n  color: #555;\n}\n\n.overlay-card .confirm-detail .clipboard-hint {\n  display: inline-block;\n  background: #e8f5e9;\n  color: #2e7d32;\n  padding: 2px 8px;\n  border-radius: 4px;\n  font-family: monospace;\n  font-size: 13px;\n  margin-top: 4px;\n}\n\n/* ── Keyboard shortcuts panel ──────────────────────────────────── */\n.shortcuts-panel {\n  position: fixed;\n  bottom: 16px;\n  right: 16px;\n  background: #fff;\n  border-radius: 12px;\n  padding: 16px 20px;\n  box-shadow: 0 4px 20px rgba(0,0,0,0.12);\n  z-index: 1001;\n  font-size: 12px;\n  display: none;\n  min-width: 220px;\n}\n\n.shortcuts-panel.show {\n  display: block;\n}\n\n.shortcuts-panel h3 {\n  font-size: 13px;\n  font-weight: 700;\n  margin-bottom: 10px;\n  color: #333;\n}\n\n.shortcuts-panel .shortcut-row {\n  display: flex;\n  justify-content: space-between;\n  padding: 3px 0;\n  color: #555;\n}\n\n.shortcuts-panel .shortcut-row kbd {\n  background: #f0f0f0;\n  border: 1px solid #ddd;\n  border-radius: 4px;\n  padding: 1px 6px;\n  font-family: monospace;\n  font-size: 11px;\n  color: #444;\n}\n\n.shortcuts-panel .close-shortcuts {\n  position: absolute;\n  top: 8px;\n  right: 10px;\n  border: none;\n  background: none;\n  cursor: pointer;\n  font-size: 16px;\n  color: #999;\n}\n\n/* ── Zoom indicator ────────────────────────────────────────────── */\n.zoom-indicator {\n  position: fixed;\n  bottom: 16px;\n  left: 16px;\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  background: #fff;\n  border-radius: 8px;\n  padding: 6px 12px;\n  box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n  font-size: 12px;\n  color: #555;\n  z-index: 1001;\n}\n\n.zoom-indicator button {\n  width: 26px;\n  height: 26px;\n  border: 1px solid #ddd;\n  border-radius: 6px;\n  background: #fff;\n  cursor: pointer;\n  font-size: 14px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  color: #555;\n  font-family: inherit;\n}\n\n.zoom-indicator button:hover {\n  background: #f5f5f5;\n}\n\n/* ── Toast notification ────────────────────────────────────────── */\n.toast {\n  position: fixed;\n  bottom: 60px;\n  left: 50%;\n  transform: translateX(-50%) translateY(20px);\n  background: #333;\n  color: #fff;\n  padding: 10px 20px;\n  border-radius: 8px;\n  font-size: 13px;\n  opacity: 0;\n  transition: all 0.3s ease;\n  z-index: 9998;\n  pointer-events: none;\n}\n\n.toast.show {\n  opacity: 1;\n  transform: translateX(-50%) translateY(0);\n}\n\u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\n\u003c!-- ── Toolbar ──────────────────────────────────────────────────── --\u003e\n\u003cdiv id=\"toolbar\"\u003e\n  \u003c!-- Drawing tools --\u003e\n  \u003cdiv class=\"toolbar-group\"\u003e\n    \u003cbutton class=\"tool-btn active\" data-tool=\"select\" title=\"Select / Move (V)\"\u003e\n      \u003cspan class=\"icon\"\u003e\n        \u003csvg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M5 3l14 9-7 2-4 7z\"/\u003e\u003c/svg\u003e\n      \u003c/span\u003e\n      \u003cspan class=\"label\"\u003eSelect\u003c/span\u003e\n    \u003c/button\u003e\n    \u003cbutton class=\"tool-btn\" data-tool=\"pen\" title=\"Pen (P)\"\u003e\n      \u003cspan class=\"icon\"\u003e\n        \u003csvg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M12 20h9\"/\u003e\u003cpath d=\"M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z\"/\u003e\u003c/svg\u003e\n      \u003c/span\u003e\n      \u003cspan class=\"label\"\u003ePen\u003c/span\u003e\n    \u003c/button\u003e\n    \u003cbutton class=\"tool-btn\" data-tool=\"line\" title=\"Line (L)\"\u003e\n      \u003cspan class=\"icon\"\u003e\n        \u003csvg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cline x1=\"5\" y1=\"19\" x2=\"19\" y2=\"5\"/\u003e\u003c/svg\u003e\n      \u003c/span\u003e\n      \u003cspan class=\"label\"\u003eLine\u003c/span\u003e\n    \u003c/button\u003e\n    \u003cbutton class=\"tool-btn\" data-tool=\"arrow\" title=\"Arrow (A)\"\u003e\n      \u003cspan class=\"icon\"\u003e\n        \u003csvg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cline x1=\"5\" y1=\"19\" x2=\"19\" y2=\"5\"/\u003e\u003cpolyline points=\"10 5 19 5 19 14\"/\u003e\u003c/svg\u003e\n      \u003c/span\u003e\n      \u003cspan class=\"label\"\u003eArrow\u003c/span\u003e\n    \u003c/button\u003e\n    \u003cbutton class=\"tool-btn\" data-tool=\"rect\" title=\"Rectangle (R)\"\u003e\n      \u003cspan class=\"icon\"\u003e\n        \u003csvg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003crect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/\u003e\u003c/svg\u003e\n      \u003c/span\u003e\n      \u003cspan class=\"label\"\u003eRect\u003c/span\u003e\n    \u003c/button\u003e\n    \u003cbutton class=\"tool-btn\" data-tool=\"ellipse\" title=\"Circle (C)\"\u003e\n      \u003cspan class=\"icon\"\u003e\n        \u003csvg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cellipse cx=\"12\" cy=\"12\" rx=\"10\" ry=\"8\"/\u003e\u003c/svg\u003e\n      \u003c/span\u003e\n      \u003cspan class=\"label\"\u003eCircle\u003c/span\u003e\n    \u003c/button\u003e\n    \u003cbutton class=\"tool-btn\" data-tool=\"eraser\" title=\"Eraser (E)\"\u003e\n      \u003cspan class=\"icon\"\u003e\n        \u003csvg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpath d=\"M20 20H7L3 16l9-9 8 8-4 5z\"/\u003e\u003cpath d=\"M6 11l8 8\"/\u003e\u003c/svg\u003e\n      \u003c/span\u003e\n      \u003cspan class=\"label\"\u003eEraser\u003c/span\u003e\n    \u003c/button\u003e\n  \u003c/div\u003e\n\n  \u003c!-- Text \u0026 Notes --\u003e\n  \u003cdiv class=\"toolbar-group\"\u003e\n    \u003cbutton class=\"tool-btn\" data-tool=\"text\" title=\"Text (T)\"\u003e\n      \u003cspan class=\"icon\"\u003e\n        \u003csvg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpolyline points=\"4 7 4 4 20 4 20 7\"/\u003e\u003cline x1=\"12\" y1=\"4\" x2=\"12\" y2=\"20\"/\u003e\u003cline x1=\"8\" y1=\"20\" x2=\"16\" y2=\"20\"/\u003e\u003c/svg\u003e\n      \u003c/span\u003e\n      \u003cspan class=\"label\"\u003eText\u003c/span\u003e\n    \u003c/button\u003e\n    \u003cbutton class=\"tool-btn\" data-tool=\"note\" id=\"note-tool-btn\" title=\"Sticky Note (N)\"\u003e\n      \u003cspan class=\"icon\"\u003e\n        \u003csvg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003crect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/\u003e\u003cpath d=\"M14 3v8h8\"/\u003e\u003c/svg\u003e\n      \u003c/span\u003e\n      \u003cspan class=\"label\"\u003eNote\u003c/span\u003e\n    \u003c/button\u003e\n    \u003cdiv class=\"note-color-picker\" id=\"note-color-picker\"\u003e\n      \u003cdiv class=\"note-color-opt\" data-note-color=\"yellow\" style=\"background:#fff9c4;\" title=\"Yellow\"\u003e\u003c/div\u003e\n      \u003cdiv class=\"note-color-opt\" data-note-color=\"pink\" style=\"background:#fce4ec;\" title=\"Pink\"\u003e\u003c/div\u003e\n      \u003cdiv class=\"note-color-opt\" data-note-color=\"blue\" style=\"background:#e3f2fd;\" title=\"Blue\"\u003e\u003c/div\u003e\n      \u003cdiv class=\"note-color-opt\" data-note-color=\"green\" style=\"background:#e8f5e9;\" title=\"Green\"\u003e\u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\n  \u003c!-- Color \u0026 stroke --\u003e\n  \u003cdiv class=\"toolbar-group\"\u003e\n    \u003cdiv class=\"color-picker\"\u003e\n      \u003cdiv class=\"color-swatch active\" data-color=\"#222222\" style=\"background:#222222;\" title=\"Black\"\u003e\u003c/div\u003e\n      \u003cdiv class=\"color-swatch\" data-color=\"#e53935\" style=\"background:#e53935;\" title=\"Red\"\u003e\u003c/div\u003e\n      \u003cdiv class=\"color-swatch\" data-color=\"#1e88e5\" style=\"background:#1e88e5;\" title=\"Blue\"\u003e\u003c/div\u003e\n      \u003cdiv class=\"color-swatch\" data-color=\"#43a047\" style=\"background:#43a047;\" title=\"Green\"\u003e\u003c/div\u003e\n      \u003cdiv class=\"color-swatch\" data-color=\"#fb8c00\" style=\"background:#fb8c00;\" title=\"Orange\"\u003e\u003c/div\u003e\n      \u003cdiv class=\"color-swatch\" data-color=\"#8e24aa\" style=\"background:#8e24aa;\" title=\"Purple\"\u003e\u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\n  \u003cdiv class=\"toolbar-group\"\u003e\n    \u003cbutton class=\"stroke-btn active\" data-stroke=\"2\" title=\"Thin\"\u003e\n      \u003cdiv class=\"stroke-line\" style=\"height:2px;\"\u003e\u003c/div\u003e\n      \u003cspan class=\"label\"\u003eThin\u003c/span\u003e\n    \u003c/button\u003e\n    \u003cbutton class=\"stroke-btn\" data-stroke=\"4\" title=\"Medium\"\u003e\n      \u003cdiv class=\"stroke-line\" style=\"height:4px;\"\u003e\u003c/div\u003e\n      \u003cspan class=\"label\"\u003eMed\u003c/span\u003e\n    \u003c/button\u003e\n    \u003cbutton class=\"stroke-btn\" data-stroke=\"7\" title=\"Thick\"\u003e\n      \u003cdiv class=\"stroke-line\" style=\"height:7px;\"\u003e\u003c/div\u003e\n      \u003cspan class=\"label\"\u003eThick\u003c/span\u003e\n    \u003c/button\u003e\n  \u003c/div\u003e\n\n  \u003c!-- Undo / Redo --\u003e\n  \u003cdiv class=\"toolbar-group\"\u003e\n    \u003cbutton class=\"tool-btn\" id=\"undo-btn\" title=\"Undo (Ctrl/Cmd+Z)\"\u003e\n      \u003cspan class=\"icon\"\u003e\n        \u003csvg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpolyline points=\"1 4 1 10 7 10\"/\u003e\u003cpath d=\"M3.51 15a9 9 0 105.64-10.36L1 10\"/\u003e\u003c/svg\u003e\n      \u003c/span\u003e\n      \u003cspan class=\"label\"\u003eUndo\u003c/span\u003e\n    \u003c/button\u003e\n    \u003cbutton class=\"tool-btn\" id=\"redo-btn\" title=\"Redo (Ctrl/Cmd+Shift+Z)\"\u003e\n      \u003cspan class=\"icon\"\u003e\n        \u003csvg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"\u003e\u003cpolyline points=\"23 4 23 10 17 10\"/\u003e\u003cpath d=\"M20.49 15a9 9 0 11-5.64-10.36L23 10\"/\u003e\u003c/svg\u003e\n      \u003c/span\u003e\n      \u003cspan class=\"label\"\u003eRedo\u003c/span\u003e\n    \u003c/button\u003e\n  \u003c/div\u003e\n\n  \u003c!-- Share button --\u003e\n  \u003cbutton class=\"share-btn\" id=\"share-btn\"\u003e\n    \u003cspan class=\"icon\"\u003e\u0026#9993;\u003c/span\u003e\n    Share with Copilot\n  \u003c/button\u003e\n\n  \u003c!-- Help --\u003e\n  \u003cbutton class=\"help-btn\" id=\"help-btn\" title=\"Help\"\u003e?\u003c/button\u003e\n\u003c/div\u003e\n\n\u003c!-- ── Canvas ──────────────────────────────────────────────────── --\u003e\n\u003cdiv id=\"canvas-container\"\u003e\n  \u003ccanvas id=\"drawing-canvas\"\u003e\u003c/canvas\u003e\n\u003c/div\u003e\n\n\u003c!-- ── Onboarding overlay ──────────────────────────────────────── --\u003e\n\u003cdiv class=\"overlay-backdrop\" id=\"onboarding-overlay\"\u003e\n  \u003cdiv class=\"overlay-card\"\u003e\n    \u003ch1\u003eWelcome to Napkin!\u003c/h1\u003e\n    \u003cp class=\"subtitle\"\u003eYour whiteboard for brainstorming with Copilot.\u003c/p\u003e\n    \u003cdiv class=\"steps\"\u003e\n      \u003cdiv class=\"step\"\u003e\n        \u003cdiv class=\"step-num\"\u003e1\u003c/div\u003e\n        \u003cdiv\u003eDraw, sketch, or add sticky notes \u0026mdash; whatever helps you think\u003c/div\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"step\"\u003e\n        \u003cdiv class=\"step-num\"\u003e2\u003c/div\u003e\n        \u003cdiv\u003eWhen you're ready, click \u003cstrong\u003e\"Share with Copilot\"\u003c/strong\u003e (the green button)\u003c/div\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"step\"\u003e\n        \u003cdiv class=\"step-num\"\u003e3\u003c/div\u003e\n        \u003cdiv\u003eGo back to your terminal and say \u003cstrong\u003e\"check the napkin\"\u003c/strong\u003e\u003c/div\u003e\n      \u003c/div\u003e\n      \u003cdiv class=\"step\"\u003e\n        \u003cdiv class=\"step-num\"\u003e4\u003c/div\u003e\n        \u003cdiv\u003eCopilot will look at your whiteboard and respond\u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n    \u003cp style=\"font-size:14px;color:#888;margin-bottom:20px;\"\u003eThat's it. Let's go!\u003c/p\u003e\n    \u003cbutton class=\"cta-btn\" id=\"onboarding-dismiss\"\u003eGot it \u0026mdash; start drawing\u003c/button\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\n\u003c!-- ── Share confirmation overlay ──────────────────────────────── --\u003e\n\u003cdiv class=\"overlay-backdrop hidden\" id=\"share-overlay\"\u003e\n  \u003cdiv class=\"overlay-card\"\u003e\n    \u003cdiv class=\"confirm-icon\"\u003e\u0026#10004;\u0026#65039;\u003c/div\u003e\n    \u003ch1\u003eShared with Copilot!\u003c/h1\u003e\n    \u003cdiv class=\"confirm-detail\"\u003e\n      \u0026#128190; A screenshot was saved (check your Downloads or Desktop).\u003cbr\u003e\n      \u0026#128203; The text content was copied to your clipboard.\u003cbr\u003e\u003cbr\u003e\n      Go back to Copilot CLI and say:\u003cbr\u003e\n      \u003cspan class=\"clipboard-hint\"\u003e\"check the napkin\"\u003c/span\u003e\n    \u003c/div\u003e\n    \u003cbutton class=\"cta-btn\" id=\"share-overlay-close\"\u003eGot it\u003c/button\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\n\u003c!-- ── Keyboard shortcuts panel ────────────────────────────────── --\u003e\n\u003cdiv class=\"shortcuts-panel\" id=\"shortcuts-panel\"\u003e\n  \u003cbutton class=\"close-shortcuts\" id=\"close-shortcuts\"\u003e\u0026times;\u003c/button\u003e\n  \u003ch3\u003eKeyboard Shortcuts\u003c/h3\u003e\n  \u003cdiv class=\"shortcut-row\"\u003e\u003cspan\u003eSelect / Move\u003c/span\u003e\u003ckbd\u003eV\u003c/kbd\u003e\u003c/div\u003e\n  \u003cdiv class=\"shortcut-row\"\u003e\u003cspan\u003ePen\u003c/span\u003e\u003ckbd\u003eP\u003c/kbd\u003e\u003c/div\u003e\n  \u003cdiv class=\"shortcut-row\"\u003e\u003cspan\u003eRectangle\u003c/span\u003e\u003ckbd\u003eR\u003c/kbd\u003e\u003c/div\u003e\n  \u003cdiv class=\"shortcut-row\"\u003e\u003cspan\u003eCircle\u003c/span\u003e\u003ckbd\u003eC\u003c/kbd\u003e\u003c/div\u003e\n  \u003cdiv class=\"shortcut-row\"\u003e\u003cspan\u003eArrow\u003c/span\u003e\u003ckbd\u003eA\u003c/kbd\u003e\u003c/div\u003e\n  \u003cdiv class=\"shortcut-row\"\u003e\u003cspan\u003eLine\u003c/span\u003e\u003ckbd\u003eL\u003c/kbd\u003e\u003c/div\u003e\n  \u003cdiv class=\"shortcut-row\"\u003e\u003cspan\u003eText\u003c/span\u003e\u003ckbd\u003eT\u003c/kbd\u003e\u003c/div\u003e\n  \u003cdiv class=\"shortcut-row\"\u003e\u003cspan\u003eSticky Note\u003c/span\u003e\u003ckbd\u003eN\u003c/kbd\u003e\u003c/div\u003e\n  \u003cdiv class=\"shortcut-row\"\u003e\u003cspan\u003eEraser\u003c/span\u003e\u003ckbd\u003eE\u003c/kbd\u003e\u003c/div\u003e\n  \u003cdiv class=\"shortcut-row\"\u003e\u003cspan\u003eUndo\u003c/span\u003e\u003ckbd\u003eCtrl/Cmd+Z\u003c/kbd\u003e\u003c/div\u003e\n  \u003cdiv class=\"shortcut-row\"\u003e\u003cspan\u003eRedo\u003c/span\u003e\u003ckbd\u003eCtrl/Cmd+Shift+Z\u003c/kbd\u003e\u003c/div\u003e\n  \u003cdiv class=\"shortcut-row\"\u003e\u003cspan\u003ePan canvas\u003c/span\u003e\u003ckbd\u003eSpace+Drag\u003c/kbd\u003e\u003c/div\u003e\n\u003c/div\u003e\n\n\u003c!-- ── Zoom indicator ──────────────────────────────────────────── --\u003e\n\u003cdiv class=\"zoom-indicator\"\u003e\n  \u003cbutton id=\"zoom-out-btn\" title=\"Zoom out\"\u003e\u0026minus;\u003c/button\u003e\n  \u003cspan id=\"zoom-level\"\u003e100%\u003c/span\u003e\n  \u003cbutton id=\"zoom-in-btn\" title=\"Zoom in\"\u003e+\u003c/button\u003e\n  \u003cbutton id=\"fit-btn\" title=\"Fit to content\" style=\"font-size:11px;width:auto;padding:0 8px;\"\u003eFit\u003c/button\u003e\n\u003c/div\u003e\n\n\u003c!-- ── Toast ───────────────────────────────────────────────────── --\u003e\n\u003cdiv class=\"toast\" id=\"toast\"\u003e\u003c/div\u003e\n\n\u003cscript\u003e\n// ═══════════════════════════════════════════════════════════════════\n//  NAPKIN — Self-contained whiteboard for Copilot collaboration\n// ═══════════════════════════════════════════════════════════════════\n\n(function () {\n  'use strict';\n\n  // ── DOM references ───────────────────────────────────────────────\n  const container    = document.getElementById('canvas-container');\n  const canvas       = document.getElementById('drawing-canvas');\n  const ctx          = canvas.getContext('2d');\n  const toolbar      = document.getElementById('toolbar');\n  const toastEl      = document.getElementById('toast');\n  const onboarding   = document.getElementById('onboarding-overlay');\n  const shareOverlay = document.getElementById('share-overlay');\n  const noteColorPicker = document.getElementById('note-color-picker');\n\n  // ── State ────────────────────────────────────────────────────────\n  const CANVAS_W = 3840;\n  const CANVAS_H = 2160;\n\n  let currentTool   = 'select';\n  let currentColor   = '#222222';\n  let currentStroke  = 2;\n  let noteColor      = 'yellow';\n\n  // View transform\n  let viewX = 0, viewY = 0, viewScale = 1;\n\n  // Drawing state\n  let isDrawing = false;\n  let isPanning = false;\n  let spaceHeld = false;\n  let eraserDidErase = false;\n  let panStartX = 0, panStartY = 0;\n  let panViewStartX = 0, panViewStartY = 0;\n\n  // Objects\n  let drawingObjects = [];  // { type, points?, x?, y?, ... }\n  let stickyNotes    = [];  // { id, text, x, y, w, h, color }\n  let textLabels     = [];  // { id, text, x, y, fontSize }\n\n  // Current in-progress drawing\n  let currentPath = null;\n\n  // Undo/redo stacks\n  let undoStack = [];\n  let redoStack = [];\n\n  // Unique ID counter\n  let idCounter = Date.now();\n  function uid() { return 'n' + (idCounter++); }\n\n  // ── Utility ──────────────────────────────────────────────────────\n  function screenToCanvas(sx, sy) {\n    const rect = container.getBoundingClientRect();\n    return {\n      x: (sx - rect.left - viewX) / viewScale,\n      y: (sy - rect.top - viewY) / viewScale\n    };\n  }\n\n  function showToast(msg, duration) {\n    toastEl.textContent = msg;\n    toastEl.classList.add('show');\n    clearTimeout(showToast._t);\n    showToast._t = setTimeout(() =\u003e toastEl.classList.remove('show'), duration || 2500);\n  }\n\n  // ── Onboarding ───────────────────────────────────────────────────\n  function initOnboarding() {\n    if (localStorage.getItem('napkin_onboarded')) {\n      onboarding.classList.add('hidden');\n    }\n    document.getElementById('onboarding-dismiss').addEventListener('click', () =\u003e {\n      onboarding.classList.add('hidden');\n      localStorage.setItem('napkin_onboarded', '1');\n    });\n    document.getElementById('help-btn').addEventListener('click', () =\u003e {\n      onboarding.classList.remove('hidden');\n    });\n  }\n\n  // ── Canvas setup ─────────────────────────────────────────────────\n  function initCanvas() {\n    canvas.width = CANVAS_W;\n    canvas.height = CANVAS_H;\n    centerView();\n    render();\n  }\n\n  function centerView() {\n    const cw = container.clientWidth;\n    const ch = container.clientHeight;\n    viewScale = Math.min(cw / CANVAS_W, ch / CANVAS_H, 1) * 0.9;\n    viewX = (cw - CANVAS_W * viewScale) / 2;\n    viewY = (ch - CANVAS_H * viewScale) / 2;\n    updateCanvasTransform();\n  }\n\n  function updateCanvasTransform() {\n    canvas.style.left = viewX + 'px';\n    canvas.style.top = viewY + 'px';\n    canvas.style.width = (CANVAS_W * viewScale) + 'px';\n    canvas.style.height = (CANVAS_H * viewScale) + 'px';\n    document.getElementById('zoom-level').textContent = Math.round(viewScale * 100) + '%';\n\n    // Reposition sticky notes and text labels\n    repositionOverlays();\n  }\n\n  function repositionOverlays() {\n    document.querySelectorAll('.sticky-note').forEach(el =\u003e {\n      const note = stickyNotes.find(n =\u003e n.id === el.dataset.noteId);\n      if (!note) return;\n      el.style.left = (viewX + note.x * viewScale) + 'px';\n      el.style.top  = (viewY + note.y * viewScale) + 'px';\n      el.style.width  = (note.w * viewScale) + 'px';\n      el.style.height = (note.h * viewScale) + 'px';\n      el.style.fontSize = (14 * viewScale) + 'px';\n    });\n    document.querySelectorAll('.canvas-text-label').forEach(el =\u003e {\n      const lbl = textLabels.find(l =\u003e l.id === el.dataset.labelId);\n      if (!lbl) return;\n      el.style.left = (viewX + lbl.x * viewScale) + 'px';\n      el.style.top  = (viewY + lbl.y * viewScale) + 'px';\n      el.style.fontSize = (lbl.fontSize * viewScale) + 'px';\n    });\n  }\n\n  // ── Render canvas objects ────────────────────────────────────────\n  function render() {\n    ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);\n    ctx.fillStyle = '#ffffff';\n    ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);\n\n    // Draw grid (very subtle)\n    ctx.strokeStyle = '#f0f0f0';\n    ctx.lineWidth = 1;\n    for (let x = 0; x \u003c CANVAS_W; x += 40) {\n      ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, CANVAS_H); ctx.stroke();\n    }\n    for (let y = 0; y \u003c CANVAS_H; y += 40) {\n      ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(CANVAS_W, y); ctx.stroke();\n    }\n\n    // Draw all objects\n    drawingObjects.forEach(obj =\u003e drawObject(ctx, obj));\n\n    // Draw current in-progress path\n    if (currentPath) {\n      drawObject(ctx, currentPath);\n    }\n  }\n\n  function drawObject(c, obj) {\n    c.lineCap = 'round';\n    c.lineJoin = 'round';\n\n    switch (obj.type) {\n      case 'pen': {\n        if (obj.points.length \u003c 2) return;\n        c.strokeStyle = obj.color;\n        c.lineWidth = obj.stroke;\n        c.beginPath();\n        c.moveTo(obj.points[0].x, obj.points[0].y);\n        for (let i = 1; i \u003c obj.points.length; i++) {\n          c.lineTo(obj.points[i].x, obj.points[i].y);\n        }\n        c.stroke();\n        break;\n      }\n      case 'line': {\n        c.strokeStyle = obj.color;\n        c.lineWidth = obj.stroke;\n        c.beginPath();\n        c.moveTo(obj.x1, obj.y1);\n        c.lineTo(obj.x2, obj.y2);\n        c.stroke();\n        break;\n      }\n      case 'arrow': {\n        c.strokeStyle = obj.color;\n        c.lineWidth = obj.stroke;\n        c.fillStyle = obj.color;\n        c.beginPath();\n        c.moveTo(obj.x1, obj.y1);\n        c.lineTo(obj.x2, obj.y2);\n        c.stroke();\n        // Arrowhead\n        const angle = Math.atan2(obj.y2 - obj.y1, obj.x2 - obj.x1);\n        const headLen = 12 + obj.stroke * 2;\n        c.beginPath();\n        c.moveTo(obj.x2, obj.y2);\n        c.lineTo(obj.x2 - headLen * Math.cos(angle - 0.4), obj.y2 - headLen * Math.sin(angle - 0.4));\n        c.lineTo(obj.x2 - headLen * Math.cos(angle + 0.4), obj.y2 - headLen * Math.sin(angle + 0.4));\n        c.closePath();\n        c.fill();\n        break;\n      }\n      case 'rect': {\n        c.strokeStyle = obj.color;\n        c.lineWidth = obj.stroke;\n        c.beginPath();\n        c.rect(obj.x, obj.y, obj.w, obj.h);\n        c.stroke();\n        break;\n      }\n      case 'ellipse': {\n        c.strokeStyle = obj.color;\n        c.lineWidth = obj.stroke;\n        c.beginPath();\n        const cx = obj.x + obj.w / 2;\n        const cy = obj.y + obj.h / 2;\n        c.ellipse(cx, cy, Math.abs(obj.w / 2), Math.abs(obj.h / 2), 0, 0, Math.PI * 2);\n        c.stroke();\n        break;\n      }\n    }\n  }\n\n  // ── Shape recognition ────────────────────────────────────────────\n  function recognizeShape(points) {\n    if (points.length \u003c 10) return null;\n\n    const first = points[0];\n    const last  = points[points.length - 1];\n    const dist  = Math.hypot(last.x - first.x, last.y - first.y);\n\n    // Bounding box\n    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n    points.forEach(p =\u003e {\n      if (p.x \u003c minX) minX = p.x;\n      if (p.y \u003c minY) minY = p.y;\n      if (p.x \u003e maxX) maxX = p.x;\n      if (p.y \u003e maxY) maxY = p.y;\n    });\n    const bw = maxX - minX;\n    const bh = maxY - minY;\n    const diagonal = Math.hypot(bw, bh);\n\n    // Check if path closes (endpoints near each other relative to size)\n    if (dist \u003e diagonal * 0.25) return null;\n\n    // Compute total path length\n    let pathLen = 0;\n    for (let i = 1; i \u003c points.length; i++) {\n      pathLen += Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);\n    }\n\n    // Skip tiny shapes\n    if (bw \u003c 20 || bh \u003c 20) return null;\n\n    // Check rectangularity by analyzing corner angles\n    const cx = (minX + maxX) / 2;\n    const cy = (minY + maxY) / 2;\n\n    // Measure how well points fit an ellipse vs a rectangle\n    let ellipseError = 0;\n    let rectError = 0;\n    const rx = bw / 2;\n    const ry = bh / 2;\n\n    points.forEach(p =\u003e {\n      // Ellipse error: distance from ellipse boundary\n      const dx = (p.x - cx) / rx;\n      const dy = (p.y - cy) / ry;\n      const r = Math.sqrt(dx * dx + dy * dy);\n      ellipseError += Math.abs(r - 1);\n\n      // Rectangle error: distance from nearest rectangle edge\n      const distToLeft   = Math.abs(p.x - minX);\n      const distToRight  = Math.abs(p.x - maxX);\n      const distToTop    = Math.abs(p.y - minY);\n      const distToBottom = Math.abs(p.y - maxY);\n      rectError += Math.min(distToLeft, distToRight, distToTop, distToBottom);\n    });\n\n    ellipseError /= points.length;\n    rectError /= points.length;\n\n    // Normalize errors\n    const normEllipse = ellipseError;\n    const normRect = rectError / Math.max(bw, bh) * 4;\n\n    if (normEllipse \u003c 0.35 \u0026\u0026 normEllipse \u003c normRect) {\n      return { type: 'ellipse', x: minX, y: minY, w: bw, h: bh };\n    }\n\n    if (normRect \u003c 0.25) {\n      return { type: 'rect', x: minX, y: minY, w: bw, h: bh };\n    }\n\n    return null;\n  }\n\n  // ── roundRect fallback for older browsers ──────────────────────\n  function safeRoundRect(ctx, x, y, w, h, radii) {\n    if (typeof ctx.roundRect === 'function') {\n      ctx.roundRect(x, y, w, h, radii);\n      return;\n    }\n    const r = Array.isArray(radii) ? radii : [radii, radii, radii, radii];\n    const [tl, tr, br, bl] = r.length === 4 ? r : r.length === 2 ? [r[0], r[1], r[0], r[1]] : [r[0], r[0], r[0], r[0]];\n    ctx.moveTo(x + tl, y);\n    ctx.lineTo(x + w - tr, y);\n    ctx.arcTo(x + w, y, x + w, y + tr, tr);\n    ctx.lineTo(x + w, y + h - br);\n    ctx.arcTo(x + w, y + h, x + w - br, y + h, br);\n    ctx.lineTo(x + bl, y + h);\n    ctx.arcTo(x, y + h, x, y + h - bl, bl);\n    ctx.lineTo(x, y + tl);\n    ctx.arcTo(x, y, x + tl, y, tl);\n    ctx.closePath();\n  }\n\n  // ── Eraser ───────────────────────────────────────────────────────\n  function eraseAt(cx, cy, radius) {\n    const r2 = radius * radius;\n    const before = drawingObjects.length;\n\n    drawingObjects = drawingObjects.filter(obj =\u003e {\n      switch (obj.type) {\n        case 'pen':\n          return !obj.points.some(p =\u003e (p.x - cx) ** 2 + (p.y - cy) ** 2 \u003c r2);\n        case 'line':\n        case 'arrow':\n          return distToSegment(cx, cy, obj.x1, obj.y1, obj.x2, obj.y2) \u003e radius;\n        case 'rect':\n          return !(cx \u003e obj.x - radius \u0026\u0026 cx \u003c obj.x + obj.w + radius \u0026\u0026\n                   cy \u003e obj.y - radius \u0026\u0026 cy \u003c obj.y + obj.h + radius);\n        case 'ellipse': {\n          const ecx = obj.x + obj.w / 2;\n          const ecy = obj.y + obj.h / 2;\n          const dx = (cx - ecx) / (Math.abs(obj.w) / 2 + radius);\n          const dy = (cy - ecy) / (Math.abs(obj.h) / 2 + radius);\n          return (dx * dx + dy * dy) \u003e 1.0;\n        }\n        default: return true;\n      }\n    });\n\n    if (drawingObjects.length !== before) {\n      eraserDidErase = true;\n      render();\n      return true;\n    }\n    return false;\n  }\n\n  function distToSegment(px, py, x1, y1, x2, y2) {\n    const dx = x2 - x1, dy = y2 - y1;\n    const lenSq = dx * dx + dy * dy;\n    if (lenSq === 0) return Math.hypot(px - x1, py - y1);\n    let t = ((px - x1) * dx + (py - y1) * dy) / lenSq;\n    t = Math.max(0, Math.min(1, t));\n    return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy));\n  }\n\n  // ── Undo / Redo ──────────────────────────────────────────────────\n  function saveState() {\n    undoStack.push({\n      objects: JSON.parse(JSON.stringify(drawingObjects)),\n      notes:   JSON.parse(JSON.stringify(stickyNotes)),\n      labels:  JSON.parse(JSON.stringify(textLabels))\n    });\n    if (undoStack.length \u003e 60) undoStack.shift();\n    redoStack = [];\n    scheduleAutoSave();\n  }\n\n  function undo() {\n    if (undoStack.length === 0) return;\n    redoStack.push({\n      objects: JSON.parse(JSON.stringify(drawingObjects)),\n      notes:   JSON.parse(JSON.stringify(stickyNotes)),\n      labels:  JSON.parse(JSON.stringify(textLabels))\n    });\n    const state = undoStack.pop();\n    drawingObjects = state.objects;\n    stickyNotes = state.notes;\n    textLabels = state.labels;\n    rebuildOverlays();\n    render();\n    scheduleAutoSave();\n  }\n\n  function redo() {\n    if (redoStack.length === 0) return;\n    undoStack.push({\n      objects: JSON.parse(JSON.stringify(drawingObjects)),\n      notes:   JSON.parse(JSON.stringify(stickyNotes)),\n      labels:  JSON.parse(JSON.stringify(textLabels))\n    });\n    const state = redoStack.pop();\n    drawingObjects = state.objects;\n    stickyNotes = state.notes;\n    textLabels = state.labels;\n    rebuildOverlays();\n    render();\n    scheduleAutoSave();\n  }\n\n  document.getElementById('undo-btn').addEventListener('click', undo);\n  document.getElementById('redo-btn').addEventListener('click', redo);\n\n  // ── Tool selection ───────────────────────────────────────────────\n  function setTool(tool) {\n    currentTool = tool;\n    document.querySelectorAll('.tool-btn[data-tool]').forEach(btn =\u003e {\n      btn.classList.toggle('active', btn.dataset.tool === tool);\n    });\n    container.style.cursor = tool === 'select' ? 'default' :\n                             tool === 'eraser' ? 'cell' : 'crosshair';\n    noteColorPicker.classList.remove('show');\n  }\n\n  toolbar.addEventListener('click', e =\u003e {\n    const btn = e.target.closest('.tool-btn[data-tool]');\n    if (!btn) return;\n    const tool = btn.dataset.tool;\n\n    if (tool === 'note') {\n      noteColorPicker.classList.toggle('show');\n      const rect = btn.getBoundingClientRect();\n      noteColorPicker.style.left = rect.left + 'px';\n    } else {\n      setTool(tool);\n    }\n  });\n\n  // Note color picker\n  noteColorPicker.addEventListener('click', e =\u003e {\n    const opt = e.target.closest('.note-color-opt');\n    if (!opt) return;\n    noteColor = opt.dataset.noteColor;\n    noteColorPicker.classList.remove('show');\n    setTool('note');\n  });\n\n  // Color swatches\n  document.querySelectorAll('.color-swatch').forEach(s =\u003e {\n    s.addEventListener('click', () =\u003e {\n      document.querySelectorAll('.color-swatch').forEach(el =\u003e el.classList.remove('active'));\n      s.classList.add('active');\n      currentColor = s.dataset.color;\n    });\n  });\n\n  // Stroke buttons\n  document.querySelectorAll('.stroke-btn').forEach(s =\u003e {\n    s.addEventListener('click', () =\u003e {\n      document.querySelectorAll('.stroke-btn').forEach(el =\u003e el.classList.remove('active'));\n      s.classList.add('active');\n      currentStroke = parseInt(s.dataset.stroke, 10);\n    });\n  });\n\n  // ── Mouse / pointer events on canvas ─────────────────────────────\n  let drawStartX, drawStartY;\n\n  container.addEventListener('pointerdown', e =\u003e {\n    if (e.target.closest('#toolbar') || e.target.closest('.sticky-note') ||\n        e.target.closest('.canvas-text-label') || e.target.closest('.overlay-backdrop') ||\n        e.target.closest('.shortcuts-panel') || e.target.closest('.zoom-indicator')) return;\n\n    const pt = screenToCanvas(e.clientX, e.clientY);\n\n    // Pan with space or middle button\n    if (spaceHeld || e.button === 1) {\n      isPanning = true;\n      panStartX = e.clientX;\n      panStartY = e.clientY;\n      panViewStartX = viewX;\n      panViewStartY = viewY;\n      container.classList.add('panning');\n      e.preventDefault();\n      return;\n    }\n\n    if (e.button !== 0) return;\n\n    switch (currentTool) {\n      case 'pen':\n      case 'eraser': {\n        isDrawing = true;\n        if (currentTool === 'pen') {\n          currentPath = { type: 'pen', points: [pt], color: currentColor, stroke: currentStroke };\n        } else {\n          eraserDidErase = false;\n          const redoStackBeforeEraser = redoStack.slice();\n          saveState();\n          eraseAt(pt.x, pt.y, 16);\n          if (!eraserDidErase) {\n            redoStack = redoStackBeforeEraser;\n          }\n        }\n        break;\n      }\n      case 'line':\n      case 'arrow':\n      case 'rect':\n      case 'ellipse': {\n        isDrawing = true;\n        drawStartX = pt.x;\n        drawStartY = pt.y;\n        if (currentTool === 'line' || currentTool === 'arrow') {\n          currentPath = { type: currentTool, x1: pt.x, y1: pt.y, x2: pt.x, y2: pt.y, color: currentColor, stroke: currentStroke };\n        } else {\n          currentPath = { type: currentTool, x: pt.x, y: pt.y, w: 0, h: 0, color: currentColor, stroke: currentStroke };\n        }\n        break;\n      }\n      case 'text': {\n        createTextLabel(pt.x, pt.y);\n        break;\n      }\n      case 'note': {\n        createStickyNote(pt.x, pt.y);\n        setTool('select');\n        break;\n      }\n      case 'select': {\n        // In select mode, clicking empty canvas does nothing special\n        break;\n      }\n    }\n  });\n\n  container.addEventListener('pointermove', e =\u003e {\n    if (isPanning) {\n      viewX = panViewStartX + (e.clientX - panStartX);\n      viewY = panViewStartY + (e.clientY - panStartY);\n      updateCanvasTransform();\n      return;\n    }\n\n    if (!isDrawing) return;\n    const pt = screenToCanvas(e.clientX, e.clientY);\n\n    switch (currentTool) {\n      case 'pen': {\n        if (currentPath) {\n          currentPath.points.push(pt);\n          render();\n        }\n        break;\n      }\n      case 'eraser': {\n        eraseAt(pt.x, pt.y, 16);\n        break;\n      }\n      case 'line':\n      case 'arrow': {\n        if (currentPath) {\n          currentPath.x2 = pt.x;\n          currentPath.y2 = pt.y;\n          render();\n        }\n        break;\n      }\n      case 'rect':\n      case 'ellipse': {\n        if (currentPath) {\n          currentPath.x = Math.min(drawStartX, pt.x);\n          currentPath.y = Math.min(drawStartY, pt.y);\n          currentPath.w = Math.abs(pt.x - drawStartX);\n          currentPath.h = Math.abs(pt.y - drawStartY);\n          render();\n        }\n        break;\n      }\n    }\n  });\n\n  function finishDrawing() {\n    if (isPanning) {\n      isPanning = false;\n      container.classList.remove('panning');\n      return;\n    }\n\n    if (!isDrawing) return;\n    isDrawing = false;\n\n    if (currentTool === 'eraser') {\n      if (!eraserDidErase) {\n        // Nothing was erased — pop the pre-erase state we saved on pointerdown\n        undoStack.pop();\n      }\n      return;\n    }\n\n    if (!currentPath) return;\n\n    // Shape recognition for pen tool\n    if (currentPath.type === 'pen') {\n      const recognized = recognizeShape(currentPath.points);\n      if (recognized) {\n        currentPath = {\n          ...recognized,\n          color: currentPath.color,\n          stroke: currentPath.stroke\n        };\n      }\n    }\n\n    // Don't save degenerate shapes\n    if (currentPath.type === 'pen' \u0026\u0026 currentPath.points.length \u003c 2) {\n      currentPath = null;\n      return;\n    }\n    if ((currentPath.type === 'rect' || currentPath.type === 'ellipse') \u0026\u0026\n        (Math.abs(currentPath.w) \u003c 3 \u0026\u0026 Math.abs(currentPath.h) \u003c 3)) {\n      currentPath = null;\n      render();\n      return;\n    }\n    if ((currentPath.type === 'line' || currentPath.type === 'arrow') \u0026\u0026\n        Math.hypot(currentPath.x2 - currentPath.x1, currentPath.y2 - currentPath.y1) \u003c 3) {\n      currentPath = null;\n      render();\n      return;\n    }\n\n    saveState();\n    drawingObjects.push(currentPath);\n    currentPath = null;\n    render();\n    scheduleAutoSave();\n  }\n\n  container.addEventListener('pointerup', finishDrawing);\n  container.addEventListener('pointerleave', finishDrawing);\n\n  // ── Zoom ─────────────────────────────────────────────────────────\n  container.addEventListener('wheel', e =\u003e {\n    e.preventDefault();\n    const delta = e.deltaY \u003e 0 ? 0.92 : 1.08;\n    zoomAt(e.clientX, e.clientY, delta);\n  }, { passive: false });\n\n  function zoomAt(sx, sy, factor) {\n    const rect = container.getBoundingClientRect();\n    const mx = sx - rect.left;\n    const my = sy - rect.top;\n\n    const newScale = Math.min(Math.max(viewScale * factor, 0.1), 5);\n    const scaleRatio = newScale / viewScale;\n\n    viewX = mx - (mx - viewX) * scaleRatio;\n    viewY = my - (my - viewY) * scaleRatio;\n    viewScale = newScale;\n    updateCanvasTransform();\n  }\n\n  document.getElementById('zoom-in-btn').addEventListener('click', () =\u003e {\n    const rect = container.getBoundingClientRect();\n    zoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2, 1.2);\n  });\n  document.getElementById('zoom-out-btn').addEventListener('click', () =\u003e {\n    const rect = container.getBoundingClientRect();\n    zoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2, 0.8);\n  });\n  document.getElementById('fit-btn').addEventListener('click', fitToContent);\n\n  function fitToContent() {\n    // Find bounding box of all content\n    let minX = CANVAS_W, minY = CANVAS_H, maxX = 0, maxY = 0;\n    let hasContent = false;\n\n    drawingObjects.forEach(obj =\u003e {\n      hasContent = true;\n      switch (obj.type) {\n        case 'pen':\n          obj.points.forEach(p =\u003e {\n            minX = Math.min(minX, p.x); minY = Math.min(minY, p.y);\n            maxX = Math.max(maxX, p.x); maxY = Math.max(maxY, p.y);\n          });\n          break;\n        case 'line': case 'arrow':\n          minX = Math.min(minX, obj.x1, obj.x2); minY = Math.min(minY, obj.y1, obj.y2);\n          maxX = Math.max(maxX, obj.x1, obj.x2); maxY = Math.max(maxY, obj.y1, obj.y2);\n          break;\n        case 'rect': case 'ellipse':\n          minX = Math.min(minX, obj.x); minY = Math.min(minY, obj.y);\n          maxX = Math.max(maxX, obj.x + obj.w); maxY = Math.max(maxY, obj.y + obj.h);\n          break;\n      }\n    });\n    stickyNotes.forEach(n =\u003e {\n      hasContent = true;\n      minX = Math.min(minX, n.x); minY = Math.min(minY, n.y);\n      maxX = Math.max(maxX, n.x + n.w); maxY = Math.max(maxY, n.y + n.h);\n    });\n    textLabels.forEach(l =\u003e {\n      hasContent = true;\n      minX = Math.min(minX, l.x); minY = Math.min(minY, l.y);\n      maxX = Math.max(maxX, l.x + 200); maxY = Math.max(maxY, l.y + 30);\n    });\n\n    if (!hasContent) {\n      centerView();\n      return;\n    }\n\n    const pad = 80;\n    minX -= pad; minY -= pad; maxX += pad; maxY += pad;\n    const cw = container.clientWidth;\n    const ch = container.clientHeight;\n    viewScale = Math.min(cw / (maxX - minX), ch / (maxY - minY), 2);\n    viewX = (cw - (maxX - minX) * viewScale) / 2 - minX * viewScale;\n    viewY = (ch - (maxY - minY) * viewScale) / 2 - minY * viewScale;\n    updateCanvasTransform();\n  }\n\n  // ── Keyboard ─────────────────────────────────────────────────────\n  document.addEventListener('keydown', e =\u003e {\n    // Don't capture when typing in inputs\n    if (e.target.isContentEditable || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {\n      if (e.key === 'Escape') e.target.blur();\n      return;\n    }\n\n    if (e.key === ' ') {\n      e.preventDefault();\n      spaceHeld = true;\n      container.classList.add('panning');\n    }\n\n    // Ctrl/Cmd shortcuts\n    if (e.metaKey || e.ctrlKey) {\n      if (e.key === 'z' \u0026\u0026 !e.shiftKey) { e.preventDefault(); undo(); }\n      if (e.key === 'z' \u0026\u0026 e.shiftKey)  { e.preventDefault(); redo(); }\n      if (e.key === 'Z')               { e.preventDefault(); redo(); }\n      return;\n    }\n\n    if (e.key === 'Delete' || e.key === 'Backspace') {\n      // No specific selection handling in v1 beyond sticky notes\n      return;\n    }\n\n    const keyMap = { v: 'select', p: 'pen', r: 'rect', c: 'ellipse', a: 'arrow', l: 'line', t: 'text', n: 'note', e: 'eraser' };\n    if (keyMap[e.key]) {\n      if (e.key === 'n') {\n        noteColorPicker.classList.toggle('show');\n        const btn = document.getElementById('note-tool-btn');\n        const rect = btn.getBoundingClientRect();\n        noteColorPicker.style.left = rect.left + 'px';\n      } else {\n        setTool(keyMap[e.key]);\n      }\n    }\n  });\n\n  document.addEventListener('keyup', e =\u003e {\n    if (e.key === ' ') {\n      spaceHeld = false;\n      if (!isPanning) container.classList.remove('panning');\n    }\n  });\n\n  // Shortcuts panel\n  document.getElementById('close-shortcuts').addEventListener('click', () =\u003e {\n    document.getElementById('shortcuts-panel').classList.remove('show');\n  });\n\n  // Show shortcuts with ? key when not typing\n  document.addEventListener('keydown', e =\u003e {\n    if (e.target.isContentEditable || e.target.tagName === 'INPUT') return;\n    if (e.key === '?') {\n      document.getElementById('shortcuts-panel').classList.toggle('show');\n    }\n  });\n\n  // ── Sticky notes ─────────────────────────────────────────────────\n  function createStickyNote(x, y) {\n    const note = {\n      id: uid(),\n      text: '',\n      x: x,\n      y: y,\n      w: 200,\n      h: 160,\n      color: noteColor\n    };\n    saveState();\n    stickyNotes.push(note);\n    renderStickyNote(note, true);\n    scheduleAutoSave();\n  }\n\n  function renderStickyNote(note, focusAfter) {\n    const el = document.createElement('div');\n    el.className = 'sticky-note sticky-' + note.color;\n    el.dataset.noteId = note.id;\n    el.style.left = (viewX + note.x * viewScale) + 'px';\n    el.style.top  = (viewY + note.y * viewScale) + 'px';\n    el.style.width  = (note.w * viewScale) + 'px';\n    el.style.height = (note.h * viewScale) + 'px';\n    el.style.fontSize = (14 * viewScale) + 'px';\n\n    const header = document.createElement('div');\n    header.className = 'note-header';\n\n    const del = document.createElement('button');\n    del.className = 'note-delete';\n    del.textContent = '\\u00d7';\n    del.addEventListener('click', () =\u003e {\n      saveState();\n      stickyNotes = stickyNotes.filter(n =\u003e n.id !== note.id);\n      el.remove();\n      scheduleAutoSave();\n    });\n    header.appendChild(del);\n\n    const body = document.createElement('div');\n    body.className = 'note-body';\n    body.contentEditable = 'true';\n    body.textContent = note.text;\n    body.addEventListener('input', () =\u003e {\n      note.text = body.textContent;\n      scheduleAutoSave();\n    });\n    body.addEventListener('blur', () =\u003e {\n      note.text = body.textContent;\n      scheduleAutoSave();\n    });\n\n    const resize = document.createElement('div');\n    resize.className = 'note-resize';\n\n    el.appendChild(header);\n    el.appendChild(body);\n    el.appendChild(resize);\n    container.appendChild(el);\n\n    // Drag header to move\n    let dragOffX, dragOffY, isDragging = false;\n    header.addEventListener('pointerdown', e =\u003e {\n      isDragging = true;\n      const rect = el.getBoundingClientRect();\n      dragOffX = e.clientX - rect.left;\n      dragOffY = e.clientY - rect.top;\n      e.preventDefault();\n      header.setPointerCapture(e.pointerId);\n    });\n    header.addEventListener('pointermove', e =\u003e {\n      if (!isDragging) return;\n      const cRect = container.getBoundingClientRect();\n      const newLeft = e.clientX - cRect.left - dragOffX;\n      const newTop  = e.clientY - cRect.top - dragOffY;\n      note.x = (newLeft - viewX) / viewScale;\n      note.y = (newTop - viewY) / viewScale;\n      el.style.left = newLeft + 'px';\n      el.style.top  = newTop + 'px';\n    });\n    header.addEventListener('pointerup', () =\u003e {\n      if (isDragging) scheduleAutoSave();\n      isDragging = false;\n    });\n\n    // Resize handle\n    let isResizing = false, resizeStartW, resizeStartH, resizeStartMx, resizeStartMy;\n    resize.addEventListener('pointerdown', e =\u003e {\n      isResizing = true;\n      resizeStartW = note.w;\n      resizeStartH = note.h;\n      resizeStartMx = e.clientX;\n      resizeStartMy = e.clientY;\n      e.preventDefault();\n      e.stopPropagation();\n      resize.setPointerCapture(e.pointerId);\n    });\n    resize.addEventListener('pointermove', e =\u003e {\n      if (!isResizing) return;\n      const dw = (e.clientX - resizeStartMx) / viewScale;\n      const dh = (e.clientY - resizeStartMy) / viewScale;\n      note.w = Math.max(120, resizeStartW + dw);\n      note.h = Math.max(80, resizeStartH + dh);\n      el.style.width  = (note.w * viewScale) + 'px';\n      el.style.height = (note.h * viewScale) + 'px';\n    });\n    resize.addEventListener('pointerup', () =\u003e {\n      if (isResizing) scheduleAutoSave();\n      isResizing = false;\n    });\n\n    if (focusAfter) {\n      setTimeout(() =\u003e body.focus(), 50);\n    }\n  }\n\n  // ── Text labels ──────────────────────────────────────────────────\n  function createTextLabel(x, y) {\n    const lbl = {\n      id: uid(),\n      text: '',\n      x: x,\n      y: y,\n      fontSize: 16\n    };\n    saveState();\n    textLabels.push(lbl);\n    renderTextLabel(lbl, true);\n    setTool('select');\n    scheduleAutoSave();\n  }\n\n  function renderTextLabel(lbl, focusAfter) {\n    const el = document.createElement('div');\n    el.className = 'canvas-text-label';\n    el.dataset.labelId = lbl.id;\n    el.contentEditable = 'true';\n    el.style.left = (viewX + lbl.x * viewScale) + 'px';\n    el.style.top  = (viewY + lbl.y * viewScale) + 'px';\n    el.style.fontSize = (lbl.fontSize * viewScale) + 'px';\n    el.textContent = lbl.text;\n\n    el.addEventListener('input', () =\u003e {\n      lbl.text = el.textContent;\n      scheduleAutoSave();\n    });\n    el.addEventListener('blur', () =\u003e {\n      lbl.text = el.textContent;\n      if (!lbl.text.trim()) {\n        textLabels = textLabels.filter(l =\u003e l.id !== lbl.id);\n        el.remove();\n      }\n      scheduleAutoSave();\n    });\n\n    container.appendChild(el);\n    if (focusAfter) {\n      setTimeout(() =\u003e el.focus(), 50);\n    }\n  }\n\n  // ── Rebuild overlays from data (for undo/redo) ───────────────────\n  function rebuildOverlays() {\n    document.querySelectorAll('.sticky-note').forEach(el =\u003e el.remove());\n    document.querySelectorAll('.canvas-text-label').forEach(el =\u003e el.remove());\n    stickyNotes.forEach(n =\u003e renderStickyNote(n, false));\n    textLabels.forEach(l =\u003e renderTextLabel(l, false));\n  }\n\n  // ── Auto-save to localStorage ────────────────────────────────────\n  let autoSaveTimer = null;\n\n  function scheduleAutoSave() {\n    clearTimeout(autoSaveTimer);\n    autoSaveTimer = setTimeout(autoSave, 2000);\n  }\n\n  function autoSave() {\n    try {\n      const state = {\n        objects: drawingObjects,\n        notes:   stickyNotes,\n        labels:  textLabels\n      };\n      localStorage.setItem('napkin_state', JSON.stringify(state));\n    } catch (e) {\n      // localStorage might be full; silently ignore\n    }\n  }\n\n  // Periodic save every 10 seconds\n  setInterval(autoSave, 10000);\n\n  function loadState() {\n    try {\n      const raw = localStorage.getItem('napkin_state');\n      if (!raw) return;\n      const state = JSON.parse(raw);\n      if (state.objects) drawingObjects = state.objects;\n      if (state.notes)   stickyNotes = state.notes;\n      if (state.labels)  textLabels = state.labels;\n      rebuildOverlays();\n      render();\n    } catch (e) {\n      // corrupted state, ignore\n    }\n  }\n\n  // ── Share with Copilot ───────────────────────────────────────────\n  document.getElementById('share-btn').addEventListener('click', async () =\u003e {\n    try {\n      // Create an offscreen canvas for export\n      const exportCanvas = document.createElement('canvas');\n      exportCanvas.width = CANVAS_W;\n      exportCanvas.height = CANVAS_H;\n      const ectx = exportCanvas.getContext('2d');\n\n      // White background\n      ectx.fillStyle = '#fff';\n      ectx.fillRect(0, 0, CANVAS_W, CANVAS_H);\n\n      // Draw all drawing objects\n      drawingObjects.forEach(obj =\u003e drawObject(ectx, obj));\n\n      // Draw sticky notes onto export canvas\n      stickyNotes.forEach(note =\u003e {\n        const colors = {\n          yellow: { bg: '#fff9c4', header: '#fff176' },\n          pink:   { bg: '#fce4ec', header: '#f48fb1' },\n          blue:   { bg: '#e3f2fd', header: '#90caf9' },\n          green:  { bg: '#e8f5e9', header: '#a5d6a7' }\n        };\n        const c = colors[note.color] || colors.yellow;\n\n        // Shadow\n        ectx.shadowColor = 'rgba(0,0,0,0.12)';\n        ectx.shadowBlur = 12;\n        ectx.shadowOffsetX = 2;\n        ectx.shadowOffsetY = 3;\n\n        // Body\n        ectx.fillStyle = c.bg;\n        ectx.beginPath();\n        safeRoundRect(ectx, note.x, note.y, note.w, note.h, 4);\n        ectx.fill();\n\n        // Reset shadow\n        ectx.shadowColor = 'transparent';\n        ectx.shadowBlur = 0;\n        ectx.shadowOffsetX = 0;\n        ectx.shadowOffsetY = 0;\n\n        // Header\n        ectx.fillStyle = c.header;\n        ectx.beginPath();\n        safeRoundRect(ectx, note.x, note.y, note.w, 24, [4, 4, 0, 0]);\n        ectx.fill();\n\n        // Text\n        if (note.text) {\n          ectx.fillStyle = '#333';\n          ectx.font = '14px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif';\n          const lines = wrapText(ectx, note.text, note.w - 24);\n          lines.forEach((line, i) =\u003e {\n            ectx.fillText(line, note.x + 12, note.y + 44 + i * 20);\n          });\n        }\n      });\n\n      // Draw text labels\n      textLabels.forEach(lbl =\u003e {\n        if (!lbl.text) return;\n        ectx.fillStyle = '#333';\n        ectx.font = lbl.fontSize + 'px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif';\n        ectx.fillText(lbl.text, lbl.x, lbl.y + lbl.fontSize);\n      });\n\n      // Export PNG\n      const dataUrl = exportCanvas.toDataURL('image/png');\n      const link = document.createElement('a');\n      link.download = 'napkin-snapshot.png';\n      link.href = dataUrl;\n      document.body.appendChild(link);\n      link.click();\n      document.body.removeChild(link);\n\n      // Build JSON\n      const json = {\n        version: 1,\n        timestamp: new Date().toISOString(),\n        notes: stickyNotes.map(n =\u003e ({\n          id: n.id, text: n.text, x: n.x, y: n.y, color: n.color, width: n.w, height: n.h\n        })),\n        textLabels: textLabels.map(l =\u003e ({\n          id: l.id, text: l.text, x: l.x, y: l.y, fontSize: l.fontSize\n        })),\n        canvasSize: { width: CANVAS_W, height: CANVAS_H }\n      };\n\n      // Copy JSON to clipboard\n      try {\n        await navigator.clipboard.writeText(JSON.stringify(json, null, 2));\n      } catch (clipErr) {\n        // Fallback for file:// protocol or older browsers\n        const ta = document.createElement('textarea');\n        ta.value = JSON.stringify(json, null, 2);\n        ta.style.position = 'fixed';\n        ta.style.left = '-9999px';\n        document.body.appendChild(ta);\n        ta.select();\n        document.execCommand('copy');\n        document.body.removeChild(ta);\n      }\n\n      // Show confirmation\n      shareOverlay.classList.remove('hidden');\n\n    } catch (err) {\n      showToast('Export failed: ' + err.message, 4000);\n    }\n  });\n\n  document.getElementById('share-overlay-close').addEventListener('click', () =\u003e {\n    shareOverlay.classList.add('hidden');\n  });\n\n  function wrapText(c, text, maxWidth) {\n    const words = text.split(/\\s+/);\n    const lines = [];\n    let currentLine = '';\n    words.forEach(word =\u003e {\n      const test = currentLine ? currentLine + ' ' + word : word;\n      if (c.measureText(test).width \u003e maxWidth \u0026\u0026 currentLine) {\n        lines.push(currentLine);\n        currentLine = word;\n      } else {\n        currentLine = test;\n      }\n    });\n    if (currentLine) lines.push(currentLine);\n    return lines;\n  }\n\n  // ── Touch support for pinch zoom ─────────────────────────────────\n  let lastPinchDist = 0;\n  let lastPinchCX = 0, lastPinchCY = 0;\n\n  container.addEventListener('touchstart', e =\u003e {\n    if (e.touches.length === 2) {\n      const dx = e.touches[0].clientX - e.touches[1].clientX;\n      const dy = e.touches[0].clientY - e.touches[1].clientY;\n      lastPinchDist = Math.hypot(dx, dy);\n      lastPinchCX = (e.touches[0].clientX + e.touches[1].clientX) / 2;\n      lastPinchCY = (e.touches[0].clientY + e.touches[1].clientY) / 2;\n    }\n  }, { passive: true });\n\n  container.addEventListener('touchmove', e =\u003e {\n    if (e.touches.length === 2) {\n      e.preventDefault();\n      const dx = e.touches[0].clientX - e.touches[1].clientX;\n      const dy = e.touches[0].clientY - e.touches[1].clientY;\n      const dist = Math.hypot(dx, dy);\n      const cx = (e.touches[0].clientX + e.touches[1].clientX) / 2;\n      const cy = (e.touches[0].clientY + e.touches[1].clientY) / 2;\n\n      if (lastPinchDist \u003e 0) {\n        const factor = dist / lastPinchDist;\n        zoomAt(cx, cy, factor);\n        viewX += cx - lastPinchCX;\n        viewY += cy - lastPinchCY;\n        updateCanvasTransform();\n      }\n\n      lastPinchDist = dist;\n      lastPinchCX = cx;\n      lastPinchCY = cy;\n    }\n  }, { passive: false });\n\n  container.addEventListener('touchend', () =\u003e {\n    lastPinchDist = 0;\n  }, { passive: true });\n\n  // ── Close overlays on escape ─────────────────────────────────────\n  document.addEventListener('keydown', e =\u003e {\n    if (e.key === 'Escape') {\n      onboarding.classList.add('hidden');\n      shareOverlay.classList.add('hidden');\n      noteColorPicker.classList.remove('show');\n      document.getElementById('shortcuts-panel').classList.remove('show');\n    }\n  });\n\n  // Close note color picker on outside click\n  document.addEventListener('pointerdown', e =\u003e {\n    if (!e.target.closest('#note-color-picker') \u0026\u0026 !e.target.closest('#note-tool-btn')) {\n      noteColorPicker.classList.remove('show');\n    }\n  });\n\n  // ── Window resize ────────────────────────────────────────────────\n  window.addEventListener('resize', () =\u003e {\n    updateCanvasTransform();\n  });\n\n  // ── Init ─────────────────────────────────────────────────────────\n  initOnboarding();\n  initCanvas();\n  loadState();\n\n})();\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n"},"import":{"commit_sha":"541b7819d8c3545c6df122491af4fa1eae415779","imported_at":"2026-05-18T20:05:35Z","license_text":"MIT License\n\nCopyright GitHub, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.","owner":"github","repo":"github/awesome-copilot","source_url":"https://github.com/github/awesome-copilot/tree/541b7819d8c3545c6df122491af4fa1eae415779/plugins/napkin/skills/napkin"}},"content_hash":[53,233,130,115,143,191,90,13,138,230,161,6,152,154,163,134,81,7,145,129,110,1,233,202,196,188,195,149,147,38,115,213],"trust_level":"unsigned","yanked":false}
