Home

Internal tool

Rahnotate

A zero-dependency, single-file visual-feedback overlay for HTML prototypes. Click an element to leave a note, or tweak its radius, size, padding, and alignment live, then copy structured markdown your AI agent can use to find the exact element. No build, no npm, no network, just one inline <script>.

Easiest path: Copy install prompt + script, paste into Claude Code / Cursor / Copilot in the repo of the HTML you want to annotate, name the file when it asks. Done.

Try it live

A real accordion prototype with Rahnotate inlined. Open it, click the round button (bottom-right), pick Tweak, then click the accordion or a row and drag the radius, size, and padding fields, or set the alignment. Changes apply live; hit Copy to see the before → after CSS an agent would receive.

What it does

  • Click the floating round button in the prototype, click any element, type a note, copy structured markdown. The markdown carries the element identity, the path, the surrounding text, enough for an AI agent to find the exact thing you meant.
  • Tweak mode: click an element and adjust its radius, size, and padding with draggable fields, plus alignment via a Figma-style 3×3 packer (both axes) with a space-between toggle, in a compact inspector you can drag out of the way. Changes apply live, and each one is captured as a precise before → after CSS diff in the copied markdown, so the agent gets the exact change, not a vague description.
  • Resting state is a small round button at the bottom-right. Drag it anywhere; it remembers where you put it. Click to expand the toolbar.
  • Keyboard: A annotate · T tweak · C copy · X clear all · Esc stop / close.
  • Self-contained on purpose: the prototype stays one shareable file you can email, publish, or open offline. The tool travels with it.

Install

Option A: hand it to your AI agent (recommended)

Use Copy install prompt + script at the top. Paste into Claude Code, Cursor, or Copilot in the repo where the HTML lives. Tell it which file to wire into when it asks.

Or download the .md and drop it into the agent's context, it contains the prompt and the script in one file.

The prompt

Add the JavaScript below to my HTML file <PUT-YOUR-FILE-PATH-HERE>. Inline it as a `<script id="rahnotate">…</script>` block placed immediately before the closing `</body>` tag. Paste the code inline, do not reference it with a src attribute, because the file must stay self-contained and shareable. If a `<script id="rahnotate">` block already exists, replace it. Do not change anything else in the file.

Option B: paste by hand

  1. Open your prototype's .html file.
  2. Just before </body>, paste a <script id="rahnotate">…</script> block with the script below inside it.
  3. Open the file in a browser. Click the floating button.

The script

Paste this inside a <script id="rahnotate"> block. Inline only, never load via src=, the prototype must stay self-contained.

rahnotate.js

/*
 * Rahnotate — a zero-dependency, single-file visual-feedback overlay for
 * standalone HTML prototypes. A vanilla port of the `agentation` npm package
 * (https://agentation.com) for pages where React isn't mounted.
 *
 * TWO MODES:
 *   • Annotate — click elements, add notes, copy structured markdown an AI
 *     coding agent can use to locate the exact element. Output matches
 *     Agentation's Compact / Standard / Detailed markdown byte-for-byte.
 *   • Tweak — click an element and adjust radius / size / padding / alignment
 *     with draggable (Figma-style) fields. Changes apply live to the element's
 *     inline styles; each is captured as a before→after CSS diff in the copied
 *     markdown so the agent gets the exact change, not a description.
 *
 * UI mirrors Agentation:
 *   • Resting state = a floating round button (bottom-right). Click to expand.
 *   • Expanded = a toolbar (Annotate · Tweak · count · Copy · Clear · settings
 *     · collapse).
 *   • On-page numbered markers; click a marker to edit / delete its note or
 *     tweak. Tweak markers are teal, note markers are indigo.
 *   • Collapse returns to the floating button; state persists per session.
 * Icons are inline Phosphor (regular weight) plus a few hand-drawn SVGs — no
 * icon font, no CDN.
 *
 * Keyboard: A annotate · T tweak · C copy · X clear all · Esc stop / close
 * (ignored while typing).
 *
 * SHAREABLE BY DESIGN: no build step, no imports, no network. Inline this whole
 * file as a <script> block before </body> and the prototype stays a single
 * self-contained file you can email or publish — the tool travels with it.
 *
 * Canonical source lives at prototypes/tools/rahnotate.js.
 */
(function () {
  "use strict";
  if (window.__RAHNOTATE__) return;
  window.__RAHNOTATE__ = true;

  /* ============================== icons =============================== */
  /* Phosphor (regular, viewBox 0 0 256 256), extracted from @phosphor-icons/react. */
  const ICONS = {
    crosshair:
      "M232,120h-8.34A96.14,96.14,0,0,0,136,32.34V24a8,8,0,0,0-16,0v8.34A96.14,96.14,0,0,0,32.34,120H24a8,8,0,0,0,0,16h8.34A96.14,96.14,0,0,0,120,223.66V232a8,8,0,0,0,16,0v-8.34A96.14,96.14,0,0,0,223.66,136H232a8,8,0,0,0,0-16Zm-96,87.6V200a8,8,0,0,0-16,0v7.6A80.15,80.15,0,0,1,48.4,136H56a8,8,0,0,0,0-16H48.4A80.15,80.15,0,0,1,120,48.4V56a8,8,0,0,0,16,0V48.4A80.15,80.15,0,0,1,207.6,120H200a8,8,0,0,0,0,16h7.6A80.15,80.15,0,0,1,136,207.6ZM128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Z",
    mappin:
      "M128,64a40,40,0,1,0,40,40A40,40,0,0,0,128,64Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,128Zm0-112a88.1,88.1,0,0,0-88,88c0,31.4,14.51,64.68,42,96.25a254.19,254.19,0,0,0,41.45,38.3,8,8,0,0,0,9.18,0A254.19,254.19,0,0,0,174,200.25c27.45-31.57,42-64.85,42-96.25A88.1,88.1,0,0,0,128,16Zm0,206c-16.53-13-72-60.75-72-118a72,72,0,0,1,144,0C200,161.23,144.53,209,128,222Z",
    copy:
      "M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z",
    trash:
      "M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z",
    gear:
      "M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Zm109.94-52.79a8,8,0,0,0-3.89-5.4l-29.83-17-.12-33.62a8,8,0,0,0-2.83-6.08,111.91,111.91,0,0,0-36.72-20.67,8,8,0,0,0-6.46.59L128,41.85,97.88,25a8,8,0,0,0-6.47-.6A112.1,112.1,0,0,0,54.73,45.15a8,8,0,0,0-2.83,6.07l-.15,33.65-29.83,17a8,8,0,0,0-3.89,5.4,106.47,106.47,0,0,0,0,41.56,8,8,0,0,0,3.89,5.4l29.83,17,.12,33.62a8,8,0,0,0,2.83,6.08,111.91,111.91,0,0,0,36.72,20.67,8,8,0,0,0,6.46-.59L128,214.15,158.12,231a7.91,7.91,0,0,0,3.9,1,8.09,8.09,0,0,0,2.57-.42,112.1,112.1,0,0,0,36.68-20.73,8,8,0,0,0,2.83-6.07l.15-33.65,29.83-17a8,8,0,0,0,3.89-5.4A106.47,106.47,0,0,0,237.94,107.21Zm-15,34.91-28.57,16.25a8,8,0,0,0-3,3c-.58,1-1.19,2.06-1.81,3.06a7.94,7.94,0,0,0-1.22,4.21l-.15,32.25a95.89,95.89,0,0,1-25.37,14.3L134,199.13a8,8,0,0,0-3.91-1h-.19c-1.21,0-2.43,0-3.64,0a8.08,8.08,0,0,0-4.1,1l-28.84,16.1A96,96,0,0,1,67.88,201l-.11-32.2a8,8,0,0,0-1.22-4.22c-.62-1-1.23-2-1.8-3.06a8.09,8.09,0,0,0-3-3.06l-28.6-16.29a90.49,90.49,0,0,1,0-28.26L61.67,97.63a8,8,0,0,0,3-3c.58-1,1.19-2.06,1.81-3.06a7.94,7.94,0,0,0,1.22-4.21l.15-32.25a95.89,95.89,0,0,1,25.37-14.3L122,56.87a8,8,0,0,0,4.1,1c1.21,0,2.43,0,3.64,0a8.08,8.08,0,0,0,4.1-1l28.84-16.1A96,96,0,0,1,188.12,55l.11,32.2a8,8,0,0,0,1.22,4.22c.62,1,1.23,2,1.8,3.06a8.09,8.09,0,0,0,3,3.06l28.6,16.29A90.49,90.49,0,0,1,222.9,142.12Z",
    caret:
      "M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z",
  };
  function svg(name, size) {
    size = size || 18;
    return (
      '<svg viewBox="0 0 256 256" width="' +
      size +
      '" height="' +
      size +
      '" fill="currentColor" aria-hidden="true"><path d="' +
      ICONS[name] +
      '"/></svg>'
    );
  }

  /* Hand-drawn SVGs (viewBox 0 0 24 24) for the Tweak toolbar + alignment row. */
  const SLIDERS_SVG =
    '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><line x1="4" y1="8" x2="20" y2="8"/><line x1="4" y1="16" x2="20" y2="16"/><circle cx="9" cy="8" r="2.4" fill="currentColor" stroke="none"/><circle cx="15" cy="16" r="2.4" fill="currentColor" stroke="none"/></svg>';
  const ALIGN_SVGS = {
    left:
      '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><rect x="3" y="6" width="11" height="4" rx="1.5"/><rect x="3" y="14" width="7" height="4" rx="1.5"/></svg>',
    center:
      '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><rect x="6.5" y="6" width="11" height="4" rx="1.5"/><rect x="8.5" y="14" width="7" height="4" rx="1.5"/></svg>',
    right:
      '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><rect x="10" y="6" width="11" height="4" rx="1.5"/><rect x="14" y="14" width="7" height="4" rx="1.5"/></svg>',
    between:
      '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><rect x="3" y="8" width="6" height="8" rx="1.5"/><rect x="15" y="8" width="6" height="8" rx="1.5"/></svg>',
  };
  // Drag handle: a 2x3 dot grip.
  const GRIP_SVG =
    '<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/></svg>';
  // Field-prefix glyphs (Figma-style): a corner for radius, a nested box for padding.
  const RADIUS_ICON =
    '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 19V11a6 6 0 0 1 6-6h8"/></svg>';
  const PADDING_ICON =
    '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="8.5" y="8.5" width="7" height="7" rx="1"/></svg>';

  /* ============================ capture helpers ============================ */
  /* Ported faithfully from agentation@3.0.2 so captured fields + selectors match. */

  function getElementPath(target, maxDepth) {
    maxDepth = maxDepth || 4;
    const parts = [];
    let current = target;
    let depth = 0;
    while (current && depth < maxDepth) {
      const tag = current.tagName.toLowerCase();
      if (tag === "html" || tag === "body") break;
      let identifier = tag;
      if (current.id) {
        identifier = "#" + current.id;
      } else if (current.className && typeof current.className === "string") {
        const meaningful = current.className
          .split(/\s+/)
          .find((c) => c.length > 2 && !c.match(/^[a-z]{1,2}$/) && !c.match(/[A-Z0-9]{5,}/));
        if (meaningful) identifier = "." + meaningful.split("_")[0];
      }
      parts.unshift(identifier);
      current = current.parentElement;
      depth++;
    }
    return parts.join(" > ");
  }

  function identifyElement(target) {
    const tag = target.tagName.toLowerCase();
    if (target.dataset && target.dataset.element) return target.dataset.element;
    if (["path", "circle", "rect", "line", "g"].includes(tag)) {
      const svgEl = target.closest("svg");
      if (svgEl && svgEl.parentElement) return "graphic in " + identifyElement(svgEl.parentElement);
      return "graphic element";
    }
    if (tag === "svg") {
      const p = target.parentElement;
      if (p && p.tagName.toLowerCase() === "button") {
        const t = (p.textContent || "").trim();
        return t ? 'icon in "' + t + '" button' : "button icon";
      }
      return "icon";
    }
    if (tag === "button") {
      const text = (target.textContent || "").trim();
      const aria = target.getAttribute("aria-label");
      if (aria) return "button [" + aria + "]";
      return text ? 'button "' + text.slice(0, 25) + '"' : "button";
    }
    if (tag === "a") {
      const text = (target.textContent || "").trim();
      const href = target.getAttribute("href");
      if (text) return 'link "' + text.slice(0, 25) + '"';
      if (href) return "link to " + href.slice(0, 30);
      return "link";
    }
    if (tag === "input") {
      const type = target.getAttribute("type") || "text";
      const placeholder = target.getAttribute("placeholder");
      const name = target.getAttribute("name");
      if (placeholder) return 'input "' + placeholder + '"';
      if (name) return "input [" + name + "]";
      return type + " input";
    }
    if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag)) {
      const text = (target.textContent || "").trim();
      return text ? tag + ' "' + text.slice(0, 35) + '"' : tag;
    }
    if (tag === "p") {
      const text = (target.textContent || "").trim();
      if (text) return 'paragraph: "' + text.slice(0, 40) + (text.length > 40 ? "..." : "") + '"';
      return "paragraph";
    }
    if (tag === "span" || tag === "label") {
      const text = (target.textContent || "").trim();
      if (text && text.length < 40) return '"' + text + '"';
      return tag;
    }
    if (tag === "li") {
      const text = (target.textContent || "").trim();
      if (text && text.length < 40) return 'list item: "' + text.slice(0, 35) + '"';
      return "list item";
    }
    if (tag === "blockquote") return "blockquote";
    if (tag === "code") {
      const text = (target.textContent || "").trim();
      if (text && text.length < 30) return "code: `" + text + "`";
      return "code";
    }
    if (tag === "pre") return "code block";
    if (tag === "img") {
      const alt = target.getAttribute("alt");
      return alt ? 'image "' + alt.slice(0, 30) + '"' : "image";
    }
    if (tag === "video") return "video";
    if (["div", "section", "article", "nav", "header", "footer", "aside", "main"].includes(tag)) {
      const className = target.className;
      const role = target.getAttribute("role");
      const aria = target.getAttribute("aria-label");
      if (aria) return tag + " [" + aria + "]";
      if (role) return role;
      if (typeof className === "string" && className) {
        const words = className
          .split(/[\s_-]+/)
          .map((c) => c.replace(/[A-Z0-9]{5,}.*$/, ""))
          .filter((c) => c.length > 2 && !/^[a-z]{1,2}$/.test(c))
          .slice(0, 2);
        if (words.length > 0) return words.join(" ");
      }
      return tag === "div" ? "container" : tag;
    }
    return tag;
  }

  function getNearbyText(element) {
    const texts = [];
    const ownText = (element.textContent || "").trim();
    if (ownText && ownText.length < 100) texts.push(ownText);
    const prev = element.previousElementSibling;
    if (prev) {
      const t = (prev.textContent || "").trim();
      if (t && t.length < 50) texts.unshift('[before: "' + t.slice(0, 40) + '"]');
    }
    const next = element.nextElementSibling;
    if (next) {
      const t = (next.textContent || "").trim();
      if (t && t.length < 50) texts.push('[after: "' + t.slice(0, 40) + '"]');
    }
    return texts.join(" ");
  }

  function getAccessibilityInfo(target) {
    const parts = [];
    const role = target.getAttribute("role");
    const ariaLabel = target.getAttribute("aria-label");
    const ariaDescribedBy = target.getAttribute("aria-describedby");
    const tabIndex = target.getAttribute("tabindex");
    const ariaHidden = target.getAttribute("aria-hidden");
    if (role) parts.push('role="' + role + '"');
    if (ariaLabel) parts.push('aria-label="' + ariaLabel + '"');
    if (ariaDescribedBy) parts.push('aria-describedby="' + ariaDescribedBy + '"');
    if (tabIndex) parts.push("tabindex=" + tabIndex);
    if (ariaHidden === "true") parts.push("aria-hidden");
    if (target.matches("a, button, input, select, textarea, [tabindex]")) parts.push("focusable");
    return parts.join(", ");
  }

  function getElementClasses(target) {
    const className = target.className;
    if (typeof className !== "string" || !className) return "";
    return className
      .split(/\s+/)
      .filter((c) => c.length > 0)
      .map((c) => {
        const m = c.match(/^([a-zA-Z][a-zA-Z0-9_-]*?)(?:_[a-zA-Z0-9]{5,})?$/);
        return m ? m[1] : c;
      })
      .filter((c, i, arr) => arr.indexOf(c) === i)
      .join(", ");
  }

  let idSeq = 0;
  function captureAnnotation(el) {
    const rect = el.getBoundingClientRect();
    const sx = window.scrollX;
    const sy = window.scrollY;
    return {
      id: "rn-" + ++idSeq,
      type: "note",
      comment: "",
      element: identifyElement(el),
      elementPath: getElementPath(el),
      x: (rect.left / window.innerWidth) * 100,
      y: rect.top + sy,
      boundingBox: { x: rect.left, y: rect.top + sy, width: rect.width, height: rect.height },
      cssClasses: getElementClasses(el),
      nearbyText: getNearbyText(el),
      accessibility: getAccessibilityInfo(el),
      pageX: rect.left + sx, // page coords for marker placement
      pageY: rect.top + sy,
    };
  }

  /* ============================ style helpers ============================ */
  /* Used by Tweak mode to normalise + diff before/after CSS values. */

  function normVal(v) {
    return String(v == null ? "" : v).trim().replace(/\s+/g, " ").toLowerCase();
  }
  function collapsePad(shorthand) {
    const parts = String(shorthand == null ? "" : shorthand).trim().split(/\s+/);
    if (parts.length && parts.every((p) => p === parts[0])) return parts[0];
    return String(shorthand == null ? "" : shorthand).trim();
  }
  function restoreStyle(el, attr) {
    if (attr == null) el.removeAttribute("style");
    else el.setAttribute("style", attr);
  }

  /* ============================ markdown output ============================ */
  /* Matches agentation's generateMarkdown() — compact / standard / detailed.
   * Tweak items add a "Style changes" block of before → after CSS diffs. */

  function tweakSummary(a) {
    return (a.changes || []).map((c) => c.css + " " + c.from + "→" + c.to).join("; ");
  }

  function toMarkdown(list, level) {
    const pathname = window.location.pathname || "/";
    const viewport = window.innerWidth + "×" + window.innerHeight;
    let out = "## Page Feedback: " + pathname + "\n";
    if (level !== "compact") out += "**Viewport:** " + viewport + "\n";
    out += "\n";
    list.forEach((a, i) => {
      if (level === "compact") {
        if (a.type === "tweak") {
          const cs = tweakSummary(a);
          out += i + 1 + ". **" + a.element + "**: " + cs + (a.comment ? " — " + a.comment : "") + "\n";
        } else {
          out += i + 1 + ". **" + a.element + "**: " + a.comment + "\n";
        }
        return;
      }
      out += "### " + (i + 1) + ". " + a.element + "\n";
      out += "**Location:** " + a.elementPath + "\n";
      if (level === "detailed") {
        if (a.cssClasses) out += "**Classes:** " + a.cssClasses + "\n";
        if (a.boundingBox) {
          out +=
            "**Position:** " +
            Math.round(a.boundingBox.x) +
            "px, " +
            Math.round(a.boundingBox.y) +
            "px (" +
            Math.round(a.boundingBox.width) +
            "×" +
            Math.round(a.boundingBox.height) +
            "px)\n";
        }
        if (a.nearbyText) out += "**Context:** " + a.nearbyText.slice(0, 100) + "\n";
      }
      if (a.type === "tweak") {
        if (a.changes && a.changes.length) {
          out += "**Style changes:**\n";
          a.changes.forEach((c) => {
            out += "- " + c.css + ": " + c.from + " → " + c.to + "\n";
          });
        }
        if (a.comment) out += "**Feedback:** " + a.comment + "\n";
        out += "\n";
      } else {
        out += "**Feedback:** " + a.comment + "\n\n";
      }
    });
    return out.trim();
  }

  /* ============================== state =============================== */

  const STORE_KEY = "rn-collapsed";
  const annotations = [];
  let captureMode = null; // null | "note" | "tweak"
  let hideMarkers = false;
  let detail = "standard"; // "compact" | "standard" | "detailed"
  let settingsOpen = false;
  let popup = null; // active note / tweak popover
  let collapsed = (function () {
    try { return sessionStorage.getItem(STORE_KEY) !== "0"; } catch { return true; }
  })(); // default collapsed (floating button), like Agentation

  const POS_KEY = "rn-pos";
  let pos = (function () {
    try { return JSON.parse(sessionStorage.getItem(POS_KEY)) || { right: 16, bottom: 16 }; }
    catch { return { right: 16, bottom: 16 }; }
  })(); // anchored from the bottom-right corner so FAB/bar stay put across sizes
  let justDragged = false;

  /* ============================== styles ============================== */

  const Z = 2147483600;
  const style = document.createElement("style");
  style.textContent = `
  .rn-root, .rn-root * { box-sizing: border-box; font-family: ui-sans-serif, system-ui, -apple-system, "Geist", sans-serif; }
  .rn-root svg { display: block; flex: none; }
  .rn-fab {
    position: fixed; bottom: 16px; right: 16px; z-index: ${Z};
    width: 44px; height: 44px; border-radius: 50%; border: 1px solid #2e2e35;
    background: #18181b; color: #fafafa; cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    box-shadow: 0 8px 30px rgba(0,0,0,.4), 0 1px 0 rgba(255,255,255,.04) inset;
    transition: transform .12s ease, background .12s ease;
    cursor: grab; touch-action: none;
  }
  .rn-fab:hover { background: #202028; transform: translateY(-1px); }
  .rn-fab:active { transform: scale(.95); }
  .rn-fab .rn-fab-badge {
    position: absolute; top: -4px; right: -4px; min-width: 18px; height: 18px; padding: 0 5px;
    border-radius: 9px; background: #4f46e5; color: #fff; border: 2px solid #18181b;
    font-size: 10px; font-weight: 600; font-variant-numeric: tabular-nums;
    display: flex; align-items: center; justify-content: center;
  }
  .rn-bar {
    position: fixed; bottom: 16px; right: 16px; z-index: ${Z};
    display: inline-flex; align-items: center; gap: 2px; height: 40px; padding: 4px;
    background: #18181b; color: #fafafa; border: 1px solid #2e2e35; border-radius: 12px;
    box-shadow: 0 8px 30px rgba(0,0,0,.4), 0 1px 0 rgba(255,255,255,.04) inset;
    user-select: none; font-size: 13px; cursor: grab; touch-action: none;
    animation: rn-pop-in .16s cubic-bezier(.2,.8,.2,1);
  }
  .rn-bar.rn-dragging, .rn-fab.rn-dragging { cursor: grabbing; }
  .rn-act {
    display: inline-flex; align-items: center; gap: 7px; height: 32px; padding: 0 12px;
    border: none; border-radius: 8px; background: transparent; color: #fafafa;
    font-size: 13px; font-weight: 500; cursor: pointer; transition: background .12s ease;
  }
  .rn-act:hover { background: #26262e; }
  .rn-act[data-on="true"] { background: #4f46e5; }
  .rn-act[data-on="true"] svg { animation: rn-pulse 1.4s ease-in-out infinite; }
  @keyframes rn-pulse { 0%,100% { opacity: 1; } 50% { opacity: .45; } }
  .rn-count {
    display: inline-flex; align-items: center; gap: 4px; height: 24px; padding: 0 8px; margin: 0 2px;
    border-radius: 7px; background: #101013; color: #a1a1aa; font-size: 12px; font-variant-numeric: tabular-nums;
  }
  .rn-div { width: 1px; height: 22px; background: #2e2e35; margin: 0 3px; }
  .rn-ic {
    display: inline-flex; align-items: center; justify-content: center;
    width: 32px; height: 32px; border: none; border-radius: 8px;
    background: transparent; color: #d4d4d8; cursor: pointer;
    transition: background .12s ease, color .12s ease;
  }
  .rn-ic:hover:not(:disabled) { background: #26262e; color: #fafafa; }
  .rn-ic:disabled { opacity: .35; cursor: default; }
  .rn-outline {
    position: fixed; z-index: ${Z - 2}; pointer-events: none;
    border: 1.5px solid #6366f1; border-radius: 4px; background: rgba(99,102,241,.08);
  }
  .rn-label {
    position: fixed; z-index: ${Z - 1}; pointer-events: none; transform: translateY(-100%);
    padding: 2px 7px; border-radius: 5px; background: #4f46e5; color: #fff;
    font-size: 11px; font-weight: 500; white-space: nowrap; max-width: 320px; overflow: hidden; text-overflow: ellipsis;
  }
  .rn-marker {
    position: absolute; z-index: ${Z - 3}; width: 22px; height: 22px;
    border-radius: 50% 50% 50% 2px; background: #4f46e5; color: #fff; border: 2px solid #fff;
    display: flex; align-items: center; justify-content: center;
    font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
    cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,.4);
    transform: translate(-4px, -4px); animation: rn-pin-in .18s cubic-bezier(.2,.8,.2,1);
  }
  .rn-marker.rn-tw { background: #0d9488; }
  @keyframes rn-pin-in { from { opacity: 0; transform: translate(-4px,-4px) scale(.4); } to { opacity: 1; transform: translate(-4px,-4px) scale(1); } }
  .rn-popup {
    position: fixed; z-index: ${Z + 1}; width: 300px;
    background: #18181b; color: #fafafa; border: 1px solid #2e2e35; border-radius: 12px;
    padding: 12px; box-shadow: 0 16px 48px rgba(0,0,0,.5);
    animation: rn-pop-in .14s cubic-bezier(.2,.8,.2,1);
  }
  @keyframes rn-pop-in { from { opacity: 0; transform: translateY(4px) scale(.96); } to { opacity: 1; transform: none; } }
  .rn-sel {
    font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11px; color: #a1a1aa;
    background: #101013; border: 1px solid #2e2e35; border-radius: 6px; padding: 5px 8px;
    margin-bottom: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .rn-popup textarea {
    width: 100%; min-height: 64px; resize: vertical; background: #101013; color: #fafafa;
    border: 1px solid #2e2e35; border-radius: 8px; padding: 8px; font-size: 13px; outline: none;
  }
  .rn-popup textarea:focus { border-color: #6366f1; }
  .rn-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
  .rn-btn {
    height: 30px; padding: 0 12px; border-radius: 8px; font-size: 12px; font-weight: 500;
    cursor: pointer; border: 1px solid #2e2e35; background: #202028; color: #fafafa;
  }
  .rn-btn:hover { background: #26262e; }
  .rn-btn.rn-primary { background: #4f46e5; border-color: #6366f1; }
  .rn-btn.rn-primary:hover { background: #5b52ee; }
  .rn-btn.rn-del { color: #f87171; border-color: transparent; background: transparent; padding: 0 6px; }
  .rn-btn.rn-del:hover { background: rgba(248,113,113,.12); }
  .rn-btn.rn-ghost { background: transparent; border-color: transparent; color: #a1a1aa; padding: 0 6px; }
  .rn-btn.rn-ghost:hover { background: #202028; color: #fafafa; }
  .rn-settings {
    position: fixed; z-index: ${Z + 1}; width: 220px; padding: 10px;
    background: #18181b; color: #fafafa; border: 1px solid #2e2e35; border-radius: 12px;
    box-shadow: 0 16px 48px rgba(0,0,0,.5); animation: rn-pop-in .14s cubic-bezier(.2,.8,.2,1);
  }
  .rn-settings h4 { margin: 2px 4px 6px; font-size: 11px; font-weight: 600; color: #a1a1aa; text-transform: uppercase; letter-spacing: .04em; }
  .rn-opt { display: flex; align-items: center; gap: 8px; padding: 6px 6px; border-radius: 7px; font-size: 13px; cursor: pointer; }
  .rn-opt:hover { background: #202028; }
  .rn-opt input { accent-color: #6366f1; }
  .rn-sep { height: 1px; background: #2e2e35; margin: 6px 2px; }
  .rn-tweak {
    position: fixed; z-index: ${Z + 1}; width: 280px;
    background: #18181b; color: #fafafa; border: 1px solid #2e2e35; border-radius: 12px;
    padding: 12px; box-shadow: 0 16px 48px rgba(0,0,0,.5);
    animation: rn-pop-in .14s cubic-bezier(.2,.8,.2,1);
  }
  .rn-sub { font-size: 10px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: #71717a; margin: 12px 2px 6px; }
  .rn-sub:first-of-type { margin-top: 2px; }
  .rn-field {
    display: flex; align-items: center; height: 30px; overflow: hidden;
    background: #101013; border: 1px solid #2e2e35; border-radius: 8px;
  }
  .rn-field-label {
    flex: none; min-width: 34px; padding: 0 8px; height: 100%; display: inline-flex; align-items: center;
    font-size: 11px; color: #a1a1aa; cursor: ew-resize; user-select: none;
    background: #1c1c22; border-right: 1px solid #2e2e35; touch-action: none;
  }
  .rn-field-label.rn-scrubbing { color: #a5b4fc; background: #24242c; }
  .rn-field-input {
    flex: 1; min-width: 0; width: 100%; background: transparent; border: none; outline: none;
    color: #fafafa; font-size: 13px; font-variant-numeric: tabular-nums; padding: 0 8px;
  }
  .rn-fieldrow { display: flex; gap: 8px; }
  .rn-fieldrow .rn-field { flex: 1; min-width: 0; }
  .rn-field-ic { padding: 0 6px; color: #71717a; }
  .rn-field-ic svg { display: block; }
  .rn-tw-div { height: 1px; background: #2e2e35; margin: 12px 0 2px; }
  .rn-align-row { display: flex; gap: 10px; align-items: flex-start; }
  .rn-packer {
    position: relative; width: 78px; height: 78px; flex: none;
    border: 1px solid #2e2e35; border-radius: 8px; background: #101013; overflow: hidden;
  }
  .rn-packer-preview { position: absolute; inset: 0; display: flex; gap: 4px; padding: 13px; pointer-events: none; }
  .rn-pbar { width: 5px; border-radius: 2px; background: #6b6b76; }
  .rn-pbar:nth-child(1) { height: 42%; }
  .rn-pbar:nth-child(2) { height: 60%; }
  .rn-pbar:nth-child(3) { height: 34%; }
  .rn-packer:hover .rn-pbar { background: #818cf8; }
  .rn-packer-hit {
    position: absolute; inset: 0; display: grid;
    grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr);
  }
  .rn-cell { border: none; background: transparent; padding: 0; cursor: pointer; transition: background .1s ease; }
  .rn-cell:hover { background: rgba(255,255,255,.05); }
  .rn-cell[data-on="true"] { background: rgba(99,102,241,.16); box-shadow: inset 0 0 0 1px rgba(129,140,248,.55); }
  .rn-align-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
  .rn-dist {
    height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 7px;
    border: 1px solid #2e2e35; border-radius: 8px; background: #101013; color: #a1a1aa;
    font-size: 11.5px; font-weight: 500; cursor: pointer;
    transition: background .12s ease, color .12s ease, border-color .12s ease;
  }
  .rn-dist:hover { background: #202028; color: #fafafa; }
  .rn-dist[data-on="true"] { background: #4f46e5; border-color: #6366f1; color: #fff; }
  .rn-tweak textarea {
    width: 100%; min-height: 38px; resize: vertical; background: #101013; color: #fafafa;
    border: 1px solid #2e2e35; border-radius: 8px; padding: 7px 8px; font-size: 12px; outline: none;
  }
  .rn-tweak textarea:focus { border-color: #6366f1; }
  .rn-tw-head {
    display: flex; align-items: center; gap: 8px; margin: -2px 0 8px; padding-bottom: 8px;
    border-bottom: 1px solid #2e2e35; cursor: grab; touch-action: none; user-select: none;
  }
  .rn-tw-head.rn-dragging { cursor: grabbing; }
  .rn-grip { color: #52525b; flex: none; display: inline-flex; }
  .rn-tw-head:hover .rn-grip { color: #a1a1aa; }
  .rn-sel-head { margin-bottom: 0; flex: 1; min-width: 0; }
  `;
  document.head.appendChild(style);

  /* ============================ DOM scaffold ============================ */

  const root = document.createElement("div");
  root.className = "rn-root";
  document.body.appendChild(root);

  const outline = document.createElement("div");
  outline.className = "rn-outline";
  outline.style.display = "none";
  root.appendChild(outline);

  const label = document.createElement("div");
  label.className = "rn-label";
  label.style.display = "none";
  root.appendChild(label);

  const markerLayer = document.createElement("div");
  markerLayer.className = "rn-markers";
  document.body.appendChild(markerLayer);

  // floating button (collapsed resting state)
  const fab = document.createElement("button");
  fab.className = "rn-fab";
  fab.title = "Open Rahnotate";
  root.appendChild(fab);

  // toolbar (expanded state)
  const bar = document.createElement("div");
  bar.className = "rn-bar";
  const actBtn = document.createElement("button");
  actBtn.className = "rn-act";
  const tweakBtn = document.createElement("button");
  tweakBtn.className = "rn-act";
  const countEl = document.createElement("span");
  countEl.className = "rn-count";
  const div1 = document.createElement("span");
  div1.className = "rn-div";
  const copyBtn = document.createElement("button");
  copyBtn.className = "rn-ic";
  copyBtn.innerHTML = svg("copy");
  copyBtn.title = "Copy feedback (C)";
  const clearBtn = document.createElement("button");
  clearBtn.className = "rn-ic";
  clearBtn.innerHTML = svg("trash");
  clearBtn.title = "Clear all (X)";
  const div2 = document.createElement("span");
  div2.className = "rn-div";
  const gearBtn = document.createElement("button");
  gearBtn.className = "rn-ic";
  gearBtn.innerHTML = svg("gear");
  gearBtn.title = "Settings";
  const collapseBtn = document.createElement("button");
  collapseBtn.className = "rn-ic";
  collapseBtn.innerHTML = svg("caret");
  collapseBtn.title = "Collapse";
  bar.append(actBtn, tweakBtn, countEl, div1, copyBtn, clearBtn, div2, gearBtn, collapseBtn);
  root.appendChild(bar);

  function isOurNode(node) {
    return node && (node.closest(".rn-root") || node.closest(".rn-markers"));
  }

  /* ============================== render ============================== */

  function renderCollapse() {
    fab.style.display = collapsed ? "" : "none";
    bar.style.display = collapsed ? "none" : "";
    try { sessionStorage.setItem(STORE_KEY, collapsed ? "1" : "0"); } catch {}
    applyPos();
  }

  function applyPos() {
    clampPos();
    [fab, bar].forEach((el) => {
      el.style.left = "auto";
      el.style.top = "auto";
      el.style.right = pos.right + "px";
      el.style.bottom = pos.bottom + "px";
    });
  }

  function clampPos() {
    const el = collapsed ? fab : bar;
    const w = el.offsetWidth || 44;
    const h = el.offsetHeight || 40;
    pos.right = Math.max(8, Math.min(pos.right, window.innerWidth - w - 8));
    pos.bottom = Math.max(8, Math.min(pos.bottom, window.innerHeight - h - 8));
  }

  function renderFab() {
    fab.innerHTML = svg("crosshair", 22);
    if (annotations.length) {
      const b = document.createElement("span");
      b.className = "rn-fab-badge";
      b.textContent = String(annotations.length);
      fab.appendChild(b);
    }
  }

  function renderBar() {
    actBtn.setAttribute("data-on", String(captureMode === "note"));
    actBtn.innerHTML = svg("crosshair") + "<span>" + (captureMode === "note" ? "Recording…" : "Annotate") + "</span>";
    tweakBtn.setAttribute("data-on", String(captureMode === "tweak"));
    tweakBtn.innerHTML = SLIDERS_SVG + "<span>" + (captureMode === "tweak" ? "Picking…" : "Tweak") + "</span>";
    countEl.innerHTML = svg("mappin", 14) + "<span>" + annotations.length + "</span>";
    copyBtn.disabled = annotations.length === 0;
    clearBtn.disabled = annotations.length === 0;
    renderFab();
  }

  function setCollapsed(v) {
    collapsed = v;
    if (v) { stopCapture(); closeSettings(); closePopup(); }
    renderCollapse();
  }

  fab.addEventListener("click", (e) => {
    e.stopPropagation();
    if (justDragged) return; // a drag, not a tap
    setCollapsed(false);
  });
  collapseBtn.addEventListener("click", (e) => { e.stopPropagation(); setCollapsed(true); });

  /* ============================== drag to move ============================== */

  let drag = null;
  function onDragStart(e, el) {
    // let toolbar buttons work — drag from the bar's non-button areas (count, dividers, padding)
    if (el === bar && e.target.closest("button")) return;
    drag = { px: e.clientX, py: e.clientY, right: pos.right, bottom: pos.bottom, moved: false, el };
    try { el.setPointerCapture(e.pointerId); } catch {}
  }
  function onDragMove(e) {
    if (!drag) return;
    const dx = e.clientX - drag.px;
    const dy = e.clientY - drag.py;
    if (!drag.moved && Math.abs(dx) + Math.abs(dy) < 4) return; // tap threshold
    if (!drag.moved) { drag.moved = true; drag.el.classList.add("rn-dragging"); }
    pos.right = drag.right - dx; // bottom-right anchored: moving right shrinks the offset
    pos.bottom = drag.bottom - dy;
    applyPos();
  }
  function onDragEnd() {
    if (!drag) return;
    drag.el.classList.remove("rn-dragging");
    if (drag.moved) {
      justDragged = true;
      try { sessionStorage.setItem(POS_KEY, JSON.stringify(pos)); } catch {}
      setTimeout(() => { justDragged = false; }, 0); // swallow the click that follows pointerup
    }
    drag = null;
  }
  fab.addEventListener("pointerdown", (e) => onDragStart(e, fab));
  bar.addEventListener("pointerdown", (e) => onDragStart(e, bar));
  window.addEventListener("pointermove", onDragMove);
  window.addEventListener("pointerup", onDragEnd);

  /* ============================== capture modes ============================== */

  function setMode(mode) {
    if (collapsed) return;
    captureMode = captureMode === mode ? null : mode;
    const on = captureMode !== null;
    document.body.style.cursor = on ? "crosshair" : "";
    if (!on) hideHover();
    if (on) { closeSettings(); closePopup(); }
    renderBar();
  }

  function stopCapture() {
    captureMode = null;
    document.body.style.cursor = "";
    hideHover();
    renderBar();
  }

  function hideHover() {
    outline.style.display = "none";
    label.style.display = "none";
  }

  actBtn.addEventListener("click", (e) => { e.stopPropagation(); setMode("note"); });
  tweakBtn.addEventListener("click", (e) => { e.stopPropagation(); setMode("tweak"); });

  function onMove(e) {
    if (!captureMode) return;
    const el = e.target;
    if (!el || isOurNode(el)) { hideHover(); return; }
    const r = el.getBoundingClientRect();
    outline.style.display = "block";
    outline.style.left = r.left + "px";
    outline.style.top = r.top + "px";
    outline.style.width = r.width + "px";
    outline.style.height = r.height + "px";
    label.style.display = "block";
    label.style.left = r.left + "px";
    label.style.top = Math.max(r.top, 14) + "px";
    label.textContent = identifyElement(el);
  }

  function onClick(e) {
    if (!captureMode) return;
    const el = e.target;
    if (isOurNode(el)) return;
    e.preventDefault();
    e.stopPropagation();
    const mode = captureMode;
    stopCapture();
    if (mode === "tweak") openTweak(captureAnnotation(el), true, el, el.getBoundingClientRect());
    else openNote(captureAnnotation(el), true, el.getBoundingClientRect());
  }

  document.addEventListener("mousemove", onMove, true);
  document.addEventListener("click", onClick, true);
  window.addEventListener("scroll", () => { if (captureMode) hideHover(); }, true);

  /* ============================== note popover ============================== */

  function openNote(a, isNew, anchorRect) {
    closePopup();
    popup = document.createElement("div");
    popup.className = "rn-popup";

    const sel = document.createElement("div");
    sel.className = "rn-sel";
    sel.textContent = a.elementPath;
    popup.appendChild(sel);

    const ta = document.createElement("textarea");
    ta.placeholder = "What should change?";
    ta.value = a.comment || "";
    popup.appendChild(ta);

    const row = document.createElement("div");
    row.className = "rn-row";
    if (!isNew) {
      const del = document.createElement("button");
      del.className = "rn-btn rn-del";
      del.textContent = "Delete";
      del.style.marginRight = "auto";
      del.onclick = () => {
        const i = annotations.indexOf(a);
        if (i >= 0) annotations.splice(i, 1);
        closePopup();
        renderBar();
        renderMarkers();
      };
      row.appendChild(del);
    } else {
      row.appendChild(Object.assign(document.createElement("span"), { style: "margin-right:auto" }));
    }
    const cancel = document.createElement("button");
    cancel.className = "rn-btn";
    cancel.textContent = "Cancel";
    cancel.onclick = () => closePopup();
    const save = document.createElement("button");
    save.className = "rn-btn rn-primary";
    save.textContent = "Save";
    save.onclick = () => {
      const text = ta.value.trim();
      if (!text) { ta.focus(); return; }
      a.comment = text;
      if (isNew) annotations.push(a);
      closePopup();
      renderBar();
      renderMarkers();
    };
    row.append(cancel, save);
    popup.appendChild(row);
    root.appendChild(popup);

    positionFloating(popup, anchorRect, 300);
    ta.focus();
    ta.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); save.click(); }
    });
  }

  /* ============================== tweak panel ============================== */

  // A Figma-style draggable number field. Drag the label horizontally to scrub,
  // or type a value directly. Returns { wrap, set, get }.
  function makeScrub(labelText, initial, opts) {
    opts = opts || {};
    const min = opts.min != null ? opts.min : 0;
    const max = opts.max != null ? opts.max : 9999;
    const step = opts.step || 1;
    const onChange = opts.onChange || function () {};

    function clamp(v) { v = Math.round(v); return Math.max(min, Math.min(max, v)); }

    const wrap = document.createElement("div");
    wrap.className = "rn-field";
    const lab = document.createElement("span");
    lab.className = "rn-field-label";
    if (opts.icon) {
      lab.innerHTML = opts.icon;
      lab.title = labelText;
      lab.classList.add("rn-field-ic");
    } else {
      lab.textContent = labelText;
    }
    const input = document.createElement("input");
    input.className = "rn-field-input";
    input.type = "text";
    input.inputMode = "numeric";

    let val = clamp(initial);
    input.value = String(val);

    function set(v, fire) {
      val = clamp(v);
      input.value = String(val);
      if (fire !== false) onChange(val);
    }

    input.addEventListener("input", () => {
      const n = parseFloat(input.value);
      if (!isNaN(n)) { val = clamp(n); onChange(val); }
    });
    input.addEventListener("blur", () => { input.value = String(val); });

    let sc = null;
    lab.addEventListener("pointerdown", (e) => {
      e.preventDefault();
      e.stopPropagation();
      sc = { x: e.clientX, v: val };
      try { lab.setPointerCapture(e.pointerId); } catch {}
      lab.classList.add("rn-scrubbing");
    });
    lab.addEventListener("pointermove", (e) => {
      if (!sc) return;
      const dx = e.clientX - sc.x;
      set(sc.v + Math.round(dx) * step, true);
    });
    function endScrub(e) {
      if (!sc) return;
      sc = null;
      lab.classList.remove("rn-scrubbing");
      try { lab.releasePointerCapture(e.pointerId); } catch {}
    }
    lab.addEventListener("pointerup", endScrub);
    lab.addEventListener("pointercancel", endScrub);

    wrap.append(lab, input);
    return { wrap, set, get: () => val };
  }

  function openTweak(item, isNew, el, anchorRect) {
    closePopup();
    closeSettings();

    const cs = getComputedStyle(el);
    const rect0 = el.getBoundingClientRect();

    // Baseline = the ORIGINAL values (before any Rahnotate tweak). Stored on the
    // item so from→to stays stable even when a tweak is re-opened and edited.
    const baseline = item.baseline || {
      radius: cs.borderTopLeftRadius,
      width: Math.round(rect0.width) + "px",
      height: Math.round(rect0.height) + "px",
      padding: [cs.paddingTop, cs.paddingRight, cs.paddingBottom, cs.paddingLeft].join(" "),
      display: cs.display,
      justify: cs.justifyContent,
      alignItems: cs.alignItems,
    };
    if (isNew) {
      item.baseline = baseline;
      item.styleAttrBefore = el.getAttribute("style"); // null if none
    }
    const styleAtOpen = el.getAttribute("style"); // for Cancel (revert to this open state)

    // Seed working values from the element's CURRENT state.
    const seedRadius = parseFloat(cs.borderTopLeftRadius) || 0;
    const seedW = Math.round(rect0.width);
    const seedH = Math.round(rect0.height);
    const seedPad = parseFloat(cs.paddingTop) || 0;
    let chosenH = item.alignH || null; // justify-content (horizontal)
    let chosenV = item.alignV || null; // align-items (vertical)

    popup = document.createElement("div");
    popup.className = "rn-tweak";

    // Draggable header — grab it to move the panel off the element you're editing.
    const head = document.createElement("div");
    head.className = "rn-tw-head";
    const grip = document.createElement("span");
    grip.className = "rn-grip";
    grip.innerHTML = GRIP_SVG;
    const sel = document.createElement("div");
    sel.className = "rn-sel rn-sel-head";
    sel.textContent = item.elementPath;
    head.append(grip, sel);
    popup.appendChild(head);

    // -- Size + corner radius --
    popup.appendChild(subLabel("Size"));
    const sizeRow = document.createElement("div");
    sizeRow.className = "rn-fieldrow";
    const w = makeScrub("W", seedW, { min: 0, max: 4000, onChange: (v) => { el.style.width = v + "px"; } });
    const h = makeScrub("H", seedH, { min: 0, max: 4000, onChange: (v) => { el.style.height = v + "px"; } });
    sizeRow.append(w.wrap, h.wrap);
    popup.appendChild(sizeRow);

    popup.appendChild(subLabel("Corner radius"));
    const rad = makeScrub("Radius", seedRadius, { icon: RADIUS_ICON, min: 0, max: 200, onChange: (v) => { el.style.borderRadius = v + "px"; } });
    popup.appendChild(rad.wrap);

    popup.appendChild(divider());

    // -- Auto layout: a Figma-style alignment box that shows a LIVE PREVIEW of
    //    the element's content (3 bars) sitting exactly where justify-content
    //    (horizontal) and align-items (vertical) put them. Nine hit-zones set
    //    both axes at once; hovering previews. Padding + a space-between toggle
    //    sit beside it, as in Figma auto-layout. Picks promote to display:flex.
    function ensureFlex() {
      if (!/flex|grid/.test(getComputedStyle(el).display)) el.style.display = "flex";
    }
    const HMAP = { left: "flex-start", center: "center", right: "flex-end", between: "space-between" };
    const VMAP = { top: "flex-start", middle: "center", bottom: "flex-end" };
    const HKEYS = ["left", "center", "right"];
    const VKEYS = ["top", "middle", "bottom"];
    const initJustify = cs.justifyContent;
    const initAlign = cs.alignItems;

    function renderPreview(hoverH, hoverV) {
      const jH = hoverH != null ? hoverH : chosenH;
      const jV = hoverV != null ? hoverV : chosenV;
      preview.style.justifyContent = jH === "between" ? "space-between" : (HMAP[jH] || initJustify);
      preview.style.alignItems = VMAP[jV] || initAlign;
    }
    function paintPacker() {
      const colI = { left: 0, center: 1, right: 2 }[chosenH];
      const rowI = { top: 0, middle: 1, bottom: 2 }[chosenV];
      const dist = chosenH === "between";
      cells.forEach((x) => {
        const xr = +x.dataset.r;
        const xc = +x.dataset.c;
        const on = dist
          ? rowI != null && xr === rowI // whole row lights up while distributing
          : colI != null && rowI != null && xc === colI && xr === rowI;
        x.setAttribute("data-on", String(on));
      });
      distBtn.setAttribute("data-on", String(dist));
      renderPreview();
    }

    popup.appendChild(subLabel("Auto layout"));
    const alignBox = document.createElement("div");
    alignBox.className = "rn-align-row";

    const packer = document.createElement("div");
    packer.className = "rn-packer";
    const preview = document.createElement("div");
    preview.className = "rn-packer-preview";
    preview.innerHTML = '<span class="rn-pbar"></span><span class="rn-pbar"></span><span class="rn-pbar"></span>';
    const hit = document.createElement("div");
    hit.className = "rn-packer-hit";
    const cells = [];
    for (let r = 0; r < 3; r++) {
      for (let c = 0; c < 3; c++) {
        const cell = document.createElement("button");
        cell.className = "rn-cell";
        cell.dataset.r = String(r);
        cell.dataset.c = String(c);
        cell.title = VKEYS[r] + " " + HKEYS[c];
        cell.onmouseenter = () => {
          if (chosenH === "between") renderPreview("between", VKEYS[+cell.dataset.r]);
          else renderPreview(HKEYS[+cell.dataset.c], VKEYS[+cell.dataset.r]);
        };
        cell.onclick = (e) => {
          e.stopPropagation();
          ensureFlex();
          if (chosenH === "between") {
            chosenV = VKEYS[+cell.dataset.r]; // distribute mode: pick vertical only
            el.style.alignItems = VMAP[chosenV];
          } else {
            chosenH = HKEYS[+cell.dataset.c];
            chosenV = VKEYS[+cell.dataset.r];
            el.style.justifyContent = HMAP[chosenH];
            el.style.alignItems = VMAP[chosenV];
          }
          paintPacker();
        };
        hit.appendChild(cell);
        cells.push(cell);
      }
    }
    packer.append(preview, hit);
    packer.addEventListener("mouseleave", () => renderPreview());
    alignBox.appendChild(packer);

    const col = document.createElement("div");
    col.className = "rn-align-col";
    const pad = makeScrub("Padding", seedPad, { icon: PADDING_ICON, min: 0, max: 400, onChange: (v) => { el.style.padding = v + "px"; } });
    const distBtn = document.createElement("button");
    distBtn.className = "rn-dist";
    distBtn.title = "Space between";
    distBtn.innerHTML = ALIGN_SVGS.between + "<span>Space between</span>";
    distBtn.onclick = (e) => {
      e.stopPropagation();
      if (chosenH === "between") {
        chosenH = null;
        el.style.justifyContent = ""; // revert to the stylesheet value
      } else {
        chosenH = "between";
        ensureFlex();
        el.style.justifyContent = "space-between";
      }
      paintPacker();
    };
    col.append(pad.wrap, distBtn);
    alignBox.appendChild(col);
    popup.appendChild(alignBox);
    paintPacker();

    // -- Optional note --
    popup.appendChild(subLabel("Note (optional)"));
    const note = document.createElement("textarea");
    note.placeholder = "Why this change?";
    note.value = item.comment || "";
    popup.appendChild(note);

    // -- Footer --
    const row = document.createElement("div");
    row.className = "rn-row";

    if (!isNew) {
      const del = document.createElement("button");
      del.className = "rn-btn rn-del";
      del.textContent = "Delete";
      del.onclick = () => {
        restoreStyle(el, item.styleAttrBefore); // undo the live changes entirely
        const i = annotations.indexOf(item);
        if (i >= 0) annotations.splice(i, 1);
        closePopup();
        renderBar();
        renderMarkers();
      };
      row.appendChild(del);
    }

    const reset = document.createElement("button");
    reset.className = "rn-btn rn-ghost";
    reset.textContent = "Reset";
    reset.style.marginRight = "auto"; // pushes Cancel / Save to the right
    reset.onclick = () => {
      restoreStyle(el, item.styleAttrBefore); // back to original, pre-tweak visuals
      rad.set(parseFloat(baseline.radius) || 0, false);
      w.set(parseFloat(baseline.width) || 0, false);
      h.set(parseFloat(baseline.height) || 0, false);
      pad.set(parseFloat(baseline.padding) || 0, false);
      chosenH = null;
      chosenV = null;
      paintPacker();
    };
    row.appendChild(reset);

    const cancel = document.createElement("button");
    cancel.className = "rn-btn";
    cancel.textContent = "Cancel";
    cancel.onclick = () => {
      restoreStyle(el, styleAtOpen); // revert to how it looked when the panel opened
      closePopup();
    };

    const save = document.createElement("button");
    save.className = "rn-btn rn-primary";
    save.textContent = "Save";
    save.onclick = () => {
      const changes = [];
      const add = (css, from, to) => {
        if (normVal(from) !== normVal(to)) changes.push({ css, from: String(from), to: String(to) });
      };
      add("border-radius", baseline.radius, el.style.borderRadius || getComputedStyle(el).borderTopLeftRadius);
      // Only report W/H the user explicitly scrubbed — never the measured box,
      // which drifts as a side-effect of padding/alignment reflow.
      if (el.style.width) add("width", baseline.width, el.style.width);
      if (el.style.height) add("height", baseline.height, el.style.height);
      if (el.style.padding) add("padding", collapsePad(baseline.padding), el.style.padding);
      if (chosenH || chosenV) {
        if (el.style.display && normVal(el.style.display) !== normVal(baseline.display)) {
          add("display", baseline.display, el.style.display);
        }
        if (chosenH) add("justify-content", baseline.justify, el.style.justifyContent);
        if (chosenV) add("align-items", baseline.alignItems, el.style.alignItems);
      }

      const comment = note.value.trim();
      if (!changes.length && !comment) {
        // nothing to record — treat Save as Cancel
        if (isNew) restoreStyle(el, styleAtOpen);
        closePopup();
        return;
      }

      item.type = "tweak";
      item.changes = changes;
      item.alignH = chosenH;
      item.alignV = chosenV;
      item.comment = comment;
      item.el = el;
      item.element = item.element || identifyElement(el);
      if (isNew) annotations.push(item);
      closePopup();
      renderBar();
      renderMarkers();
    };

    row.append(cancel, save);
    popup.appendChild(row);
    root.appendChild(popup);

    positionPanel(popup, anchorRect, 280); // placed beside the element, not over it
    makeDraggable(popup, head); // drag the header to move it anywhere
    note.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); save.click(); }
    });
  }

  // Drag a floating panel by a handle element, clamped to the viewport.
  function makeDraggable(node, handle) {
    let d = null;
    handle.addEventListener("pointerdown", (e) => {
      if (e.target.closest("input, textarea, button")) return;
      e.preventDefault();
      const r = node.getBoundingClientRect();
      d = { px: e.clientX, py: e.clientY, left: r.left, top: r.top };
      try { handle.setPointerCapture(e.pointerId); } catch {}
      handle.classList.add("rn-dragging");
    });
    handle.addEventListener("pointermove", (e) => {
      if (!d) return;
      const w = node.offsetWidth;
      const h = node.offsetHeight;
      let left = d.left + (e.clientX - d.px);
      let top = d.top + (e.clientY - d.py);
      left = Math.max(8, Math.min(left, window.innerWidth - w - 8));
      top = Math.max(8, Math.min(top, window.innerHeight - h - 8));
      node.style.left = left + "px";
      node.style.top = top + "px";
    });
    function up(e) {
      if (!d) return;
      d = null;
      handle.classList.remove("rn-dragging");
      try { handle.releasePointerCapture(e.pointerId); } catch {}
    }
    handle.addEventListener("pointerup", up);
    handle.addEventListener("pointercancel", up);
  }

  // Prefer placing the panel to the side of the anchored element so it doesn't
  // cover what you're editing; fall back to below / above when there's no room.
  function positionPanel(node, anchor, width) {
    const w = width || 268;
    const h = node.offsetHeight || 320;
    const gap = 10;
    const vw = window.innerWidth;
    const vh = window.innerHeight;
    // Keep the panel top near the element, but always fully on-screen.
    const top = Math.max(8, Math.min(anchor.top, vh - h - 8));
    let left;
    if (anchor.right + gap + w <= vw - 8) {
      left = anchor.right + gap; // fits cleanly to the right
    } else if (anchor.left - gap - w >= 8) {
      left = anchor.left - gap - w; // fits cleanly to the left
    } else if (vw - anchor.right >= anchor.left) {
      left = vw - w - 8; // no clean gap: hug whichever viewport edge has more room
    } else {
      left = 8;
    }
    node.style.left = left + "px";
    node.style.top = top + "px";
  }

  function subLabel(text) {
    const s = document.createElement("div");
    s.className = "rn-sub";
    s.textContent = text;
    return s;
  }

  function divider() {
    const d = document.createElement("div");
    d.className = "rn-tw-div";
    return d;
  }

  function closePopup() {
    if (popup) { popup.remove(); popup = null; }
  }

  function positionFloating(node, anchor, w) {
    const width = w || 300;
    const h = node.offsetHeight || 150;
    const gap = 8;
    let left = Math.min(Math.max(8, anchor.left), window.innerWidth - width - 8);
    let top = anchor.bottom + gap;
    if (top + h > window.innerHeight - 8) top = Math.max(8, anchor.top - h - gap);
    node.style.left = left + "px";
    node.style.top = top + "px";
  }

  /* ============================== settings ============================== */

  function openSettings() {
    closeSettings();
    settingsOpen = true;
    const s = document.createElement("div");
    s.className = "rn-settings";
    s.id = "rn-settings";

    const h = document.createElement("h4");
    h.textContent = "Output detail";
    s.appendChild(h);
    [["compact", "Compact"], ["standard", "Standard"], ["detailed", "Detailed"]].forEach(([val, lbl]) => {
      const opt = document.createElement("label");
      opt.className = "rn-opt";
      const r = document.createElement("input");
      r.type = "radio";
      r.name = "rn-detail";
      r.checked = detail === val;
      r.onchange = () => { detail = val; };
      opt.append(r, document.createTextNode(lbl));
      s.appendChild(opt);
    });

    s.appendChild(Object.assign(document.createElement("div"), { className: "rn-sep" }));

    const mk = document.createElement("label");
    mk.className = "rn-opt";
    const cb = document.createElement("input");
    cb.type = "checkbox";
    cb.checked = hideMarkers;
    cb.onchange = () => { hideMarkers = cb.checked; renderMarkers(); };
    mk.append(cb, document.createTextNode("Hide markers"));
    s.appendChild(mk);

    root.appendChild(s);
    const r = gearBtn.getBoundingClientRect();
    s.style.top = Math.max(8, r.top - s.offsetHeight - 8) + "px";
    s.style.left = Math.min(r.left, window.innerWidth - 220 - 8) + "px";
  }

  function closeSettings() {
    settingsOpen = false;
    const s = document.getElementById("rn-settings");
    if (s) s.remove();
  }

  gearBtn.addEventListener("click", (e) => { e.stopPropagation(); settingsOpen ? closeSettings() : openSettings(); });
  document.addEventListener("click", (e) => {
    if (settingsOpen && !e.target.closest("#rn-settings") && e.target !== gearBtn) closeSettings();
  });

  /* ============================== markers ============================== */

  function renderMarkers() {
    markerLayer.innerHTML = "";
    markerLayer.style.display = hideMarkers ? "none" : "";
    if (hideMarkers) return;
    annotations.forEach((a, i) => {
      const m = document.createElement("div");
      m.className = "rn-marker" + (a.type === "tweak" ? " rn-tw" : "");
      m.textContent = String(i + 1);
      m.style.left = a.pageX + "px";
      m.style.top = a.pageY + "px";
      m.title = a.type === "tweak" ? tweakSummary(a) : a.comment;
      m.onclick = (e) => {
        e.stopPropagation();
        if (a.type === "tweak") {
          openTweak(a, false, a.el, m.getBoundingClientRect());
        } else {
          openNote(a, false, m.getBoundingClientRect());
        }
      };
      markerLayer.appendChild(m);
    });
  }

  /* ============================== actions ============================== */

  function copyFeedback() {
    if (!annotations.length) return;
    navigator.clipboard.writeText(toMarkdown(annotations, detail)).then(() => {
      copyBtn.title = "Copied ✓";
      setTimeout(() => (copyBtn.title = "Copy feedback (C)"), 1200);
    });
  }
  function clearAll() {
    if (!annotations.length) return;
    annotations.length = 0;
    renderBar();
    renderMarkers();
  }
  copyBtn.addEventListener("click", (e) => { e.stopPropagation(); copyFeedback(); });
  clearBtn.addEventListener("click", (e) => { e.stopPropagation(); clearAll(); });

  /* ============================== keyboard ============================== */

  function typingInField() {
    const a = document.activeElement;
    return a && (a.tagName === "INPUT" || a.tagName === "TEXTAREA" || a.tagName === "SELECT" || a.isContentEditable);
  }
  document.addEventListener("keydown", (e) => {
    if (e.key === "Escape") {
      if (popup) closePopup();
      else if (settingsOpen) closeSettings();
      else if (captureMode) stopCapture();
      return;
    }
    if (collapsed || typingInField() || e.metaKey || e.ctrlKey || e.altKey) return;
    if (e.key === "a" || e.key === "A") setMode("note");
    else if (e.key === "t" || e.key === "T") setMode("tweak");
    else if (e.key === "c" || e.key === "C") copyFeedback();
    else if (e.key === "x" || e.key === "X") clearAll();
  });

  window.addEventListener("resize", () => { closePopup(); closeSettings(); applyPos(); });

  /* ============================== boot =============================== */

  renderBar();
  renderMarkers();
  renderCollapse();
  console.info("[rahnotate] ready — click the floating button (bottom-right).");
})();