/* global React, ReactDOM, window */
/* signal-2.html — Signals, variation 1a.
   Same sidebar, same data, same atoms as signals-app.jsx (index.html), but the
   workbench is re-laid-out as a CARD INVENTORY with a SLIDE-OVER detail panel:

     scan tier   — a responsive grid of signal cards. Humanized names, one-line
                   gist, 30-day match count, and a recency dot that reads
                   "how alive is this rule?" at a glance.
     detail tier — a right-docked slide-over with Definition / Events tabs.
                   The grid stays put underneath; closing returns you to scan.

   Kept standalone (rather than importing from signals-app.jsx) so index.html
   and its bundle stay untouched. */

const { useState, useEffect, useRef, useMemo, useCallback } = React;
const { SIGNALS, SIGNAL_EVENTS, FAVICON } = window.SIG_DATA;

const MAX_VISIBLE_EVENTS = 50;

/* kebab-case signal id → Title Case display name (shared utils.humanizeId).
   Kept only as the FALLBACK, because humanizing the slug is what produces
   "Gtm Ai Automation" and "Series A B Funding" on the live page. */
const humanizeId = (id) => id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());

/* Display name — the authored one when it exists. */
const nameOf = (signal) => signal.name || humanizeId(signal.id);

/* The scan tier's whole job rests on this line, so it must be the authored
   one-liner. Falling back to the first sentence of `detects` is what the
   current page effectively does, and it is why scanning fails: every
   description opens "Detects when the company…", so all 27 read alike. */
const gistOf = (signal) => signal.gist || signal.detects.split(/(?<=\.)\s+/)[0] || signal.detects;

const DAY = 86400000;
const daysAgo = (iso) => Math.max(0, Math.floor((Date.now() - new Date(iso + "T12:00:00").getTime()) / DAY));

/* Activity summary per signal — the card's whole right-hand story:
   last30 (volume), total (depth), and lastDays (liveness → the recency dot). */
function activityOf(id) {
  const events = SIGNAL_EVENTS[id] || [];
  if (events.length === 0) return { last30: 0, total: 0, lastDays: null };
  const ages = events.map((e) => daysAgo(e.date));
  return {
    last30: ages.filter((d) => d <= 30).length,
    total: events.length,
    lastDays: Math.min(...ages),
  };
}

/* Recency dot — four states, deliberately coarse. The point is a glanceable
   "is this rule alive?", not a precise timestamp; the exact date lives in the
   events table one click away. */
function recencyOf(lastDays) {
  if (lastDays === null) return { tone: "never", label: "never fired" };
  if (lastDays <= 2) return { tone: "hot", label: lastDays === 0 ? "today" : lastDays + "d ago" };
  if (lastDays <= 7) return { tone: "warm", label: lastDays + "d ago" };
  if (lastDays <= 30) return { tone: "cool", label: lastDays + "d ago" };
  return { tone: "cold", label: Math.round(lastDays / 7) + "w ago" };
}

/* ---------------- Icons (shared with signals-app.jsx) ---------------- */
const I = {
  plus:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M5 12h14"/><path d="M12 5v14"/></svg>,
  sparkles:(p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/></svg>,
  trash:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>,
  chev:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="m6 9 6 6 6-6"/></svg>,
  check:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M20 6 9 17l-5-5"/></svg>,
  x:       (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>,
  bell:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></svg>,
  book:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>,
  back:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>,
  dots:    (p) => <svg viewBox="0 0 24 24" fill="currentColor" stroke="none" {...p}><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg>,
  arrow:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>,
  sort:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M4 6h13"/><path d="M4 12h9"/><path d="M4 18h5"/></svg>,
  loader:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>,
  weight:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="12" cy="5" r="3"/><path d="M6.5 8h11l1.72 12.04A2 2 0 0 1 17.24 22H6.76a2 2 0 0 1-1.98-1.96Z"/></svg>,
  sun:     (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>,
  moon:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>,

  home:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>,
  search:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>,
  list:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/></svg>,
  users:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>,
  activity:(p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"/></svg>,
  command: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"/></svg>,
  branch:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>,
  user:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>,
  gear:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>,
  buoy:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="4"/><line x1="4.93" x2="9.17" y1="4.93" y2="9.17"/><line x1="14.83" x2="19.07" y1="14.83" y2="19.07"/><line x1="14.83" x2="19.07" y1="9.17" y2="4.93"/><line x1="4.93" x2="9.17" y1="19.07" y2="14.83"/></svg>,
  logout:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/></svg>,
  chevUp:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="m18 15-6-6-6 6"/></svg>,
  chevsUD: (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>,
  burger:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M4 6h16"/><path d="M4 12h16"/><path d="M4 18h16"/></svg>,
  collapse:(p) => <svg viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="2.75" y="2.75" width="12.5" height="12.5" rx="2" ry="2" transform="translate(18) rotate(90)"/><line x1="5.75" y1="5.75" x2="5.75" y2="12.25"/><polyline points="11.5 6.75 9.25 9 11.5 11.25"/></svg>,
  expand:  (p) => <svg viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="2.75" y="2.75" width="12.5" height="12.5" rx="2" ry="2" transform="translate(18 0) rotate(90)"/><line x1="12.25" y1="5.75" x2="12.25" y2="12.25"/><polyline points="6.5 6.75 8.75 9 6.5 11.25"/></svg>,
};

/* ---------------- Atoms ---------------- */
function AcctLogo({ host, name }) {
  const [broken, setBroken] = useState(false);
  const initials = name.replace(/^The\s+/i, "").split(" ").map((s) => s[0]).slice(0, 2).join("").toUpperCase();
  if (host && !broken) {
    return <img className="sg-ev-logo" src={FAVICON(host)} alt="" loading="lazy" decoding="async" onError={() => setBroken(true)} />;
  }
  return <span className="sg-ev-logo-fb">{initials}</span>;
}

const TYPE_LABEL = { news: "public", jobs: "jobs", job_change: "career" };
function SignalTypeBadge({ type }) {
  return <span className={"sg-badge " + type}>{TYPE_LABEL[type]}</span>;
}

const Num = ({ children }) => <strong className="sg-num">{children}</strong>;

/* ---------------- Sync banner ---------------- */
function SignalSyncBanner({ isDirty, signalCount, onFinalize, isFinalizing }) {
  const [prevFinalizing, setPrevFinalizing] = useState(false);
  const [showSaved, setShowSaved] = useState(false);

  if (isFinalizing !== prevFinalizing) {
    setPrevFinalizing(isFinalizing);
    if (!isFinalizing) setShowSaved(true);
  }
  if (isDirty && showSaved) setShowSaved(false);

  const status = isFinalizing ? "finalizing" : showSaved ? "saved" : "stale";
  const visible = isDirty || status === "finalizing" || status === "saved";
  if (!visible || signalCount === 0) return null;

  const copy = {
    saved: {
      title: "Signals are up to date",
      subtitle: "Your edits are live. New discoveries will use the updated signals.",
    },
    finalizing: {
      title: "Applying signal changes…",
      subtitle: "This usually takes a few minutes. You can keep editing while it runs.",
    },
    stale: {
      title: "Signal changes pending",
      subtitle: "You've edited your signals. Finalize to apply them — takes a few minutes.",
    },
  }[status];

  return (
    <div className={"sg-banner " + status}>
      <span className="sg-banner-ico" aria-hidden>
        {status === "finalizing" ? <I.loader className="spin" />
          : status === "saved" ? <I.check />
          : <I.sparkles />}
      </span>
      <div className="sg-banner-copy">
        <span className="t">{copy.title}</span>
        <span className="s">{copy.subtitle}</span>
      </div>
      {status === "stale" && onFinalize && (
        <button className="cta sm sg-finalize" onClick={onFinalize}>Finalize <I.sparkles /></button>
      )}
    </div>
  );
}

/* ---------------- Weight select ---------------- */
const WEIGHT_OPTIONS = [
  { value: 1, name: "Supporting" },
  { value: 2, name: "Important" },
  { value: 3, name: "Critical" },
];

function SignalWeightSelect({ value, onChange }) {
  const current = value ?? 1;
  const [selected, setSelected] = useState(current);
  const [status, setStatus] = useState("idle"); // idle | saving | saved
  const savedTimer = useRef();

  useEffect(() => setSelected(current), [current]);
  useEffect(() => () => clearTimeout(savedTimer.current), []);

  const handleSelect = async (next) => {
    if (next === selected) return;
    const prev = selected;
    setSelected(next); // optimistic
    clearTimeout(savedTimer.current);
    setStatus("saving");
    try {
      await onChange(next);
      setStatus("saved");
      savedTimer.current = setTimeout(() => setStatus("idle"), 900);
    } catch {
      setSelected(prev);
      setStatus("idle");
    }
  };

  const locked = status === "saving";

  return (
    <div className="sg-weight">
      <span className={"sg-weight-lbl" + (status === "saved" ? " ok" : "")}>
        {status === "saving" ? <I.loader className="spin" />
          : status === "saved" ? <I.check />
          : <I.weight />}
        {status === "saved" ? "Saved" : "Weight"}
      </span>
      <span className="sg-weight-div" aria-hidden />
      <div className="sg-weight-scale">
        <span className="cap" aria-hidden>low</span>
        <div className="sg-weight-radios" role="radiogroup" aria-label="Weight">
          {WEIGHT_OPTIONS.map((o) => (
            <button
              key={o.value}
              type="button"
              role="radio"
              aria-checked={selected === o.value}
              aria-label={`${o.value} — ${o.name}`}
              disabled={locked}
              className={selected === o.value ? "on" : ""}
              onClick={() => handleSelect(o.value)}
            >
              {o.value}
            </button>
          ))}
        </div>
        <span className="cap" aria-hidden>high</span>
      </div>
    </div>
  );
}

/* ---------------- Matched events table ---------------- */
const CONF_BARS = { high: 3, medium: 2, low: 1 };

function SignalEvents({ events, onEventClick }) {
  if (events.length === 0) {
    return (
      <div className="v1-ev-empty">
        <p className="t">No events have matched this signal yet.</p>
        <p className="s">New signals can take a few days to start firing.</p>
      </div>
    );
  }

  const visible = [...events]
    .sort((a, b) => new Date(b.date) - new Date(a.date))
    .slice(0, MAX_VISIBLE_EVENTS);

  return (
    <div className="sg-ev-wrap">
      <div className="sg-ev-scroll">
        <table className="sg-ev">
          <colgroup><col style={{ width: "26%" }} /><col /><col style={{ width: 56 }} /><col style={{ width: 92 }} /></colgroup>
          <thead>
            <tr>
              <th>Account</th>
              <th>Event</th>
              <th className="c" title="Confidence">Conf.</th>
              <th className="r">Date</th>
            </tr>
          </thead>
          <tbody>
            {visible.map((e) => (
              <tr key={e.id}>
                <td>
                  <div className="sg-ev-acct">
                    <AcctLogo host={e.host} name={e.account} />
                    <span className="nm">{e.account}</span>
                  </div>
                </td>
                <td>
                  <button className="sg-ev-title" onClick={() => onEventClick(e)}>{e.title}</button>
                </td>
                <td>
                  <div className={"sg-conf " + e.confidence} title={`Confidence: ${e.confidence}`}>
                    {[1, 2, 3].map((bar) => (
                      <span key={bar} className={"b b" + bar + (bar <= CONF_BARS[e.confidence] ? " on" : "")} />
                    ))}
                  </div>
                </td>
                <td className="r date">{new Date(e.date).toLocaleDateString()}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

/* ---------------- Sort ----------------
   At 27 signals a flat inventory has no entry point: every card looks equally
   important, which is the exact complaint in the brief. Sorting is the cheapest
   fix — and "most active" is the right default, because the question people
   arrive with is "which of my rules are actually working?".

   Finding the DEAD signals is the other half of that job, and it is deliberately
   not a sort option: "show me the broken ones" is a filter, not an ordering, and
   burying it in a sort menu would still make you scroll past two dozen healthy
   signals to be sure you had seen every dead one. */
const SORTS = [
  {
    key: "active", label: "Most active",
    hint: "Matches in the last 30 days, high to low",
    cmp: (a, b, A) => A[b.id].last30 - A[a.id].last30 || A[b.id].total - A[a.id].total,
  },
  {
    key: "least", label: "Least active",
    hint: "Matches in the last 30 days, low to high",
    // Exact mirror of "Most active", including the all-time tie-break — so two
    // signals with no matches this month are still ordered by whether they have
    // ever fired, which is the difference between "quiet" and "broken".
    cmp: (a, b, A) => A[a.id].last30 - A[b.id].last30 || A[a.id].total - A[b.id].total,
  },
  {
    key: "weight", label: "Weight",
    hint: "Critical signals first",
    cmp: (a, b, A) => (b.weight || 1) - (a.weight || 1) || A[b.id].last30 - A[a.id].last30,
  },
  {
    key: "name", label: "Name",
    hint: "A to Z",
    cmp: (a, b) => nameOf(a).localeCompare(nameOf(b)),
  },
];

function SortMenu({ value, onChange }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  const current = SORTS.find((s) => s.key === value) || SORTS[0];

  useEffect(() => {
    if (!open) return;
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => e.key === "Escape" && setOpen(false);
    document.addEventListener("mousedown", onDown);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onDown);
      document.removeEventListener("keydown", onKey);
    };
  }, [open]);

  return (
    <div className="v1-sort" ref={ref}>
      <button
        type="button"
        className={"trg" + (open ? " on" : "")}
        aria-haspopup="menu"
        aria-expanded={open}
        onClick={() => setOpen((o) => !o)}
      >
        <I.sort width={13} height={13} />
        <span className="lb">Sort</span>
        <span className="val">{current.label}</span>
        <I.chev className={"cv" + (open ? " up" : "")} width={13} height={13} />
      </button>

      {open && (
        <div className="pop" role="menu">
          {SORTS.map((s) => (
            <button
              key={s.key}
              type="button"
              role="menuitemradio"
              aria-checked={s.key === value}
              className={s.key === value ? "on" : ""}
              onClick={() => { onChange(s.key); setOpen(false); }}
            >
              <span className="tk">{s.key === value ? <I.check width={12} height={12} /> : null}</span>
              <span className="bd">
                <span className="t">{s.label}</span>
                <span className="h">{s.hint}</span>
              </span>
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

/* ---------------- Scan tier: the card inventory ----------------
   The card is a div-with-a-role rather than a <button>, because it now hosts a
   second, nested control (Definition) — and a button inside a button is invalid
   HTML that browsers silently un-nest. Keyboard parity is restored by hand:
   Enter/Space activate, and the card is tabbable. */
function SignalCard({ signal, activity, isSelected, onOpenEvents, onOpenDefinition }) {
  const rec = recencyOf(activity.lastDays);
  const [menuOpen, setMenuOpen] = useState(false);
  const menuRef = useRef(null);

  // Click-away and Escape close the menu — otherwise a stray open menu follows
  // you around the grid.
  useEffect(() => {
    if (!menuOpen) return;
    const onDown = (e) => { if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false); };
    const onKey = (e) => e.key === "Escape" && setMenuOpen(false);
    document.addEventListener("mousedown", onDown);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onDown);
      document.removeEventListener("keydown", onKey);
    };
  }, [menuOpen]);

  const activate = (e) => {
    if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onOpenEvents(); }
  };

  return (
    <div
      role="button"
      tabIndex={0}
      onClick={onOpenEvents}
      onKeyDown={activate}
      aria-pressed={isSelected}
      className={"v1-card" + (isSelected ? " on" : "") + (activity.lastDays === null ? " idle" : "")}
    >
      <div className="v1-card-top">
        <span className="nm">{nameOf(signal)}</span>
        <SignalTypeBadge type={signal.type} />

        {/* Overflow menu, pinned to the corner. Reading the rule ITSELF is the
            deliberate, occasional act — so it lives here rather than competing
            with the card's primary target. */}
        <div className="v1-card-menu" ref={menuRef}>
          <button
            type="button"
            className={"trg" + (menuOpen ? " on" : "")}
            aria-haspopup="menu"
            aria-expanded={menuOpen}
            aria-label={"More actions for " + nameOf(signal)}
            onClick={(e) => { e.stopPropagation(); setMenuOpen((o) => !o); }}
          >
            <I.dots width={18} height={18} />
          </button>

          {menuOpen && (
            <div className="pop" role="menu">
              <button
                type="button"
                role="menuitem"
                onClick={(e) => { e.stopPropagation(); setMenuOpen(false); onOpenDefinition(); }}
              >
                <I.book width={14} height={14} /> Definition
              </button>
            </div>
          )}
        </div>
      </div>

      {/* No slug on the card. It is a lookup key, not something you scan by, and
          spending a line of type on it across 27 cards buys nothing the name
          doesn't already say. It stays in the drawer, which is where anyone
          who arrived holding a slug would end up anyway. */}
      <p className="v1-card-gist">{gistOf(signal)}</p>

      <div className="v1-card-foot">
        {/* Recency leads the footer as a bare dot: at a glance you read the
            COLOUR (alive / slowing / dead), and the exact age is one hover away.
            Spelling out "7d ago" on 27 cards spent a line of type on a precision
            nobody scanning actually needs — but it must stay reachable, hence
            the tooltip and the screen-reader text. */}
        <span
          className={"v1-rec " + rec.tone}
          title={rec.tone === "never" ? "Never fired" : "Last match " + rec.label}
        >
          <span className="dot" aria-hidden />
          <span className="sr">{rec.tone === "never" ? "Never fired" : "Last match " + rec.label}</span>
        </span>

        {/* "26 in 30d" made the reader supply the noun. Events is the object the
            whole page is about — name it. */}
        <span className="ct">
          {activity.last30 > 0
            ? <React.Fragment><strong>{activity.last30}</strong> {activity.last30 === 1 ? "event" : "events"} in 30d</React.Fragment>
            : <span className="zero">no events in 30d</span>}
        </span>

        {/* Names the card's primary target rather than leaving it implicit — the
            whole card already opens the events drawer, but a bare card gives no
            clue where a click will land. */}
        <button
          type="button"
          className="v1-card-def"
          onClick={(e) => { e.stopPropagation(); onOpenEvents(); }}
          aria-label={"View events for " + nameOf(signal)}
        >
          View events
          <I.arrow width={12} height={12} />
        </button>
      </div>
    </div>
  );
}

/* ---------------- Detail tier: TWO drawers ----------------
   The single tabbed panel is split in two, because the two tiers answer
   different questions for different people and shouldn't share a header:

     EVENTS drawer      what did this rule actually CATCH? — the activity strip
                        (30d / all-time / last match) and the matched-events
                        table. This is the common visit, so the card opens it.
     DEFINITION drawer  what IS this rule? — the gist, the long `detects` text,
                        should/should-not-match, and the signal's controls
                        (weight, refine, delete). Deliberate and occasional, so
                        it gets its own trigger on the card.

   One at a time: opening either closes the other, so the grid stays visible
   behind a single panel and 1a's claim holds — detail is a temporary overlay
   you dismiss back out of, not a second permanent column. */

/* Shared shell: scrim, right-docked panel, Escape-to-close, focus handoff.
   `wide` widens the panel for the events drawer — a table of account · headline
   · confidence · date needs the width, whereas the definition is prose and gets
   worse past a reading measure, so the two drawers are deliberately different
   sizes rather than one compromise width. */
function Drawer({ open, label, onClose, wide, children }) {
  const panelRef = useRef(null);

  useEffect(() => {
    if (!open) return;
    const onKey = (e) => e.key === "Escape" && onClose();
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [open, onClose]);

  // Focus moves into the panel on open, so Escape and Tab land here rather than
  // back in the grid behind the scrim.
  useEffect(() => {
    if (open && panelRef.current) panelRef.current.focus();
  }, [open, label]);

  return (
    <React.Fragment>
      <div className={"v1-scrim" + (open ? " on" : "")} aria-hidden onClick={onClose} />
      <aside
        className={"v1-over" + (open ? " on" : "") + (wide ? " wide" : "")}
        role="dialog"
        aria-modal="true"
        aria-label={label || "Signal detail"}
        tabIndex={-1}
        ref={panelRef}
      >
        {children}
      </aside>
    </React.Fragment>
  );
}

/* Both drawers open on the same two lines the card showed — name, badge, gist,
   slug — so switching between them never feels like landing in a new document. */
function DrawerHead({ signal, onClose }) {
  return (
    <React.Fragment>
      <div className="row1">
        <h2>{nameOf(signal)}</h2>
        <SignalTypeBadge type={signal.type} />
        <button className="v1-close" aria-label="Close" onClick={onClose}><I.x width={14} height={14} /></button>
      </div>
      <p className="v1-over-gist">{gistOf(signal)}</p>
      <code className="v1-over-slug">{signal.id}</code>
    </React.Fragment>
  );
}

function EventsDrawer({ signal, activity, events, onClose, onOpenDefinition, onEventClick }) {
  const open = !!signal;
  const eventCount = events.length;
  const shown = Math.min(eventCount, MAX_VISIBLE_EVENTS);
  const more = eventCount - shown;
  const rec = recencyOf(activity ? activity.lastDays : null);

  return (
    <Drawer open={open} label={signal ? nameOf(signal) + " — matched events" : ""} onClose={onClose} wide>
      {signal && (
        <React.Fragment>
          <header className="v1-over-hd">
            <DrawerHead signal={signal} onClose={onClose} />

            {/* The activity strip lives HERE and only here: it describes what
                the rule has caught, which is this drawer's whole subject. */}
            <div className="v1-stats">
              <span className="st">
                <strong>{activity.last30}</strong> {activity.last30 === 1 ? "event" : "events"} in 30d
              </span>
              <span className="sep" aria-hidden />
              <span className="st"><strong>{activity.total}</strong> all-time</span>
              <span className="sep" aria-hidden />
              <span className={"v1-rec " + rec.tone}>
                <span className="dot" aria-hidden />
                {rec.tone === "never" ? "never fired" : "last match " + rec.label}
              </span>
            </div>

            {/* The door to the other drawer. Named for its destination, like
                every other navigation control here — "View events" on the card,
                "View definition" from events, and back. An instruction ("Read
                the…") is a third voice for the same kind of move. */}
            <div className="v1-switch">
              <button type="button" className="cta ghost sm" onClick={onOpenDefinition}>
                View definition <I.arrow width={12} height={12} />
              </button>
            </div>
          </header>

          <div className="v1-over-body">
            <section className="v1-matched">
              <div className="v1-matched-hd">
                <span>Matched events</span>
                <span className="sum">
                  {more > 0
                    ? <React.Fragment><Num>{shown}</Num> of <Num>{eventCount}</Num> shown · <Num>{more}</Num> more</React.Fragment>
                    : <React.Fragment><Num>{eventCount}</Num> {eventCount === 1 ? "event" : "events"}</React.Fragment>}
                </span>
              </div>
              <SignalEvents events={events} onEventClick={onEventClick} />
            </section>
          </div>
        </React.Fragment>
      )}
    </Drawer>
  );
}

function DefinitionDrawer({ signal, activity, events, onClose, onOpenEvents, onRefine, onDelete, onWeightChange }) {
  const open = !!signal;
  const eventCount = events.length;

  return (
    <Drawer open={open} label={signal ? nameOf(signal) + " — definition" : ""} onClose={onClose}>
      {signal && (
        <React.Fragment>
          <header className="v1-over-hd">
            <DrawerHead signal={signal} onClose={onClose} />

            {/* The signal's own controls sit with the signal's own text —
                weight, refine, and delete all act on the RULE, not on what it
                caught, so they belong in this drawer rather than beside the
                events table. */}
            <div className="row2">
              <SignalWeightSelect value={signal.weight} onChange={(w) => onWeightChange(signal, w)} />
              <div className="acts">
                <button className="cta ghost sm" onClick={() => onRefine(signal)}>Refine <I.sparkles /></button>
                <button className="cta ghost sm danger" onClick={() => onDelete(signal)}>Delete <I.trash /></button>
              </div>
            </div>

            <div className="v1-switch">
              <button type="button" className="cta ghost sm" onClick={onOpenEvents}>
                View events <span className="n">{eventCount}</span>
                <I.arrow width={12} height={12} />
              </button>
            </div>
          </header>

          <div className="v1-over-body">
            <div className="v1-hero"><p>{signal.detects}</p></div>

            <div className="v1-bounds">
              <section>
                <div className="hd inc">
                  <span className="ic"><I.check /></span>
                  <span>Should match</span>
                  <span className="n">({signal.boundaries.includes.length})</span>
                </div>
                <ul>
                  {signal.boundaries.includes.map((item, i) => (
                    <li key={i}><span className="dot" aria-hidden />{item}</li>
                  ))}
                </ul>
              </section>
              <section>
                <div className="hd exc">
                  <span className="ic"><I.x /></span>
                  <span>Should not match</span>
                  <span className="n">({signal.boundaries.excludes.length})</span>
                </div>
                <ul>
                  {signal.boundaries.excludes.map((item, i) => (
                    <li key={i}><span className="dot" aria-hidden />{item}</li>
                  ))}
                </ul>
              </section>
            </div>
          </div>
        </React.Fragment>
      )}
    </Drawer>
  );
}

/* ---------------- Dialogs ---------------- */
function Modal({ open, onClose, children, wide }) {
  useEffect(() => {
    if (!open) return;
    const f = (e) => e.key === "Escape" && onClose();
    document.addEventListener("keydown", f);
    return () => document.removeEventListener("keydown", f);
  }, [open, onClose]);
  if (!open) return null;
  return (
    <div className="sg-overlay" onMouseDown={(e) => e.target === e.currentTarget && onClose()}>
      <div className={"sg-modal" + (wide ? " wide" : "")} role="dialog" aria-modal="true">{children}</div>
    </div>
  );
}

function DeleteDialog({ signal, onCancel, onConfirm }) {
  return (
    <Modal open={!!signal} onClose={onCancel}>
      <h3 className="sg-modal-t">Delete signal</h3>
      <p className="sg-modal-d">
        Remove “{signal ? nameOf(signal) : ""}” from this tenant's signal
        configuration? This cannot be undone.
      </p>
      <div className="sg-modal-f">
        <button className="cta ghost sm" onClick={onCancel}>Cancel</button>
        <button className="cta sm sg-destructive" onClick={onConfirm}>Delete</button>
      </div>
    </Modal>
  );
}

function EventDialog({ event, onClose }) {
  return (
    <Modal open={!!event} onClose={onClose} wide>
      {event && (
        <div className="sg-evd">
          <div className="sg-evd-hd">
            <AcctLogo host={event.host} name={event.account} />
            <div>
              <div className="a">{event.account}</div>
              <div className="m">{new Date(event.date).toLocaleDateString()} · confidence {event.confidence}</div>
            </div>
          </div>
          <h3 className="sg-evd-t">{event.title}</h3>
          <div className="sg-modal-f">
            <button className="cta ghost sm" onClick={onClose}>Close</button>
          </div>
        </div>
      )}
    </Modal>
  );
}

/* ---------------- Workbench (1a) ---------------- */
function SignalsInventory({ signals, activities, onCreate, onRefine, onDelete, onWeightChange, onFinalize, isDirty, isFinalizing, onEventClick }) {
  const [selectedId, setSelectedId] = useState(null);
  const [typeFilter, setTypeFilter] = useState("all");
  // Most active by default: the page opens on the rules that are working.
  const [sortKey, setSortKey] = useState("active");

  const allFilters = [
    { key: "all", label: "All", count: signals.length },
    { key: "news", label: "Public", count: signals.filter((s) => s.type === "news").length },
    { key: "jobs", label: "Jobs", count: signals.filter((s) => s.type === "jobs").length },
    { key: "job_change", label: "Career", count: signals.filter((s) => s.type === "job_change").length },
  ];
  const filters = allFilters.filter((f) => f.key === "all" || signals.some((s) => s.type === f.key));

  // Sort AFTER filtering, and on a copy — sorting `signals` in place would
  // reorder the caller's state.
  const sort = SORTS.find((s) => s.key === sortKey) || SORTS[0];
  const filtered = (typeFilter === "all" ? signals : signals.filter((s) => s.type === typeFilter))
    .slice()
    .sort((a, b) => sort.cmp(a, b, activities));

  // Nothing is selected by default — the grid is the resting state and the
  // drawers are opt-in. `drawer` is which of the two is showing; only ever one,
  // so opening either closes the other by construction.
  const [drawer, setDrawer] = useState(null); // null | "events" | "definition"
  const selected = signals.find((s) => s.id === selectedId) || null;

  const openEvents = useCallback((id) => { setSelectedId(id); setDrawer("events"); }, []);
  const openDefinition = useCallback((id) => { setSelectedId(id); setDrawer("definition"); }, []);
  const close = useCallback(() => { setSelectedId(null); setDrawer(null); }, []);

  // A signal that gets filtered or deleted away must not strand an open drawer
  // on a signal that is no longer there.
  const shown = selected ? drawer : null;

  return (
    <div className="v1-wb">
      <SignalSyncBanner
        isDirty={isDirty}
        signalCount={signals.length}
        onFinalize={onFinalize}
        isFinalizing={isFinalizing}
      />

      {/* Type filters left, sort right — the two halves of "narrow it down" and
          "order what's left", on one line above the grid. */}
      <div className="v1-toolbar">
        <div className="sg-filters v1-filters">
          {filters.map((f) => (
            <button
              key={f.key}
              type="button"
              /* The type key doubles as a colour hook, so a chip and the badge
                 it filters for are the same colour — "Jobs" and the `jobs` pill
                 on a card must not be two different signals to the eye. `All`
                 has no type, so it stays neutral. */
              className={"sg-filt t-" + f.key + (typeFilter === f.key ? " on" : "")}
              onClick={() => setTypeFilter(f.key)}
            >
              <span>{f.label}</span>
              <span className="n">{f.count}</span>
            </button>
          ))}
        </div>

        <SortMenu value={sortKey} onChange={setSortKey} />
      </div>

      {filtered.length > 0 ? (
        <div className="v1-grid">
          {filtered.map((s) => (
            <SignalCard
              key={s.id}
              signal={s}
              activity={activities[s.id]}
              isSelected={selectedId === s.id}
              onOpenEvents={() => openEvents(s.id)}
              onOpenDefinition={() => openDefinition(s.id)}
            />
          ))}
        </div>
      ) : (
        <div className="v1-empty">
          <p>{signals.length === 0 ? "No signals yet." : "No signals of this type."}</p>
          {signals.length === 0 && (
            <button className="cta ghost sm" onClick={onCreate}>Add your first signal <I.sparkles /></button>
          )}
        </div>
      )}

      {/* Only one is ever mounted-open: the other's `signal` is null, so it
          stays off-canvas. Switching between them is a state change, not a
          close-then-open, so the panel slides once and the header holds. */}
      <EventsDrawer
        signal={shown === "events" ? selected : null}
        activity={selected ? activities[selected.id] : null}
        events={selected ? (SIGNAL_EVENTS[selected.id] || []) : []}
        onClose={close}
        onOpenDefinition={() => setDrawer("definition")}
        onEventClick={onEventClick}
      />

      <DefinitionDrawer
        signal={shown === "definition" ? selected : null}
        activity={selected ? activities[selected.id] : null}
        events={selected ? (SIGNAL_EVENTS[selected.id] || []) : []}
        onClose={close}
        onOpenEvents={() => setDrawer("events")}
        onRefine={onRefine}
        onDelete={onDelete}
        onWeightChange={onWeightChange}
      />
    </div>
  );
}

/* ================= Sidebar (identical to index.html) ================= */
function TrayoWordmark(p) {
  return (
    <svg viewBox="0 0 698 216" xmlns="http://www.w3.org/2000/svg" fill="none" role="img" aria-label="trayo.ai" {...p}>
      <path fill="currentColor" d="M211.618 139.166V31.533h28.87v30.783h29.745v25.108h-29.745v47.813c0 6.549 3.5 10.042 10.061 10.042h19.684v26.199H243.55c-20.559 0-31.932-11.789-31.932-32.312M279.856 171.478V62.316h27.777v25.677c0 .77.625 1.396 1.396 1.396.603 0 1.136-.39 1.336-.958 5.654-16.07 18.722-26.992 41.886-27.206V91.79h-9.842c-22.528 0-33.901 9.825-33.901 36.897v42.791zM353.63 116.897c0-32.093 19.903-55.672 45.711-55.672 17.429 0 30.623 7.757 37.502 24.222a1.33 1.33 0 0 0 1.22.823c.719 0 1.303-.584 1.303-1.304v-22.65h27.777v109.162h-27.777v-23.019a.934.934 0 0 0-.934-.934h-.62a.95.95 0 0 0-.873.591c-6.851 16.624-20.088 24.454-37.598 24.454-25.808 0-45.711-23.579-45.711-55.673m28.87 0c0 17.466 10.717 31.657 27.777 31.657 16.841 0 27.995-13.972 27.995-31.657 0-17.684-11.154-31.657-27.995-31.657-17.06 0-27.777 14.192-27.777 31.657M510.42 215.798l13.23-41.89a1.868 1.868 0 0 0-1.781-2.43h-14.073L473.458 62.316h28.87l26.213 83.407c.548 1.744 3.016 1.744 3.564 0l26.213-83.407h29.089l-47.898 153.482zM581.637 116.897c0-32.748 24.276-58.074 57.739-58.074 33.682 0 57.959 25.326 57.959 58.074 0 32.749-24.277 57.856-57.959 57.856-33.463 0-57.739-25.107-57.739-57.856m27.994 0c0 18.34 12.685 31.439 29.745 31.439 17.059 0 29.745-13.099 29.745-31.439 0-18.557-12.686-31.657-29.745-31.657s-29.745 13.1-29.745 31.657M171.479 171.479h-59.504s-12.43-32.426-17.543-65.366c-10.226-66.396 77.047-39.117 77.047-39.117v49.496s-73.873-48.123-57.3-12.867c1.322 2.831 2.997 5.49 4.848 7.978 22.215 29.852 52.452 59.876 52.452 59.876M0 171.479h59.504s12.43-32.426 17.543-65.366C87.273 39.717 0 66.996 0 66.996v49.496s73.873-48.124 57.3-12.867c-1.322 2.831-2.997 5.49-4.848 7.978C30.237 141.455 0 171.479 0 171.479M119.255 32.865 85.541 0 51.827 32.865l33.714 32.864z" />
    </svg>
  );
}

function TrayoLogoMark(p) {
  return (
    <svg viewBox="0 0 171.479 171.479" xmlns="http://www.w3.org/2000/svg" fill="none" role="img" aria-label="trayo" {...p}>
      <path fill="currentColor" d="M171.479 171.479h-59.504s-12.43-32.426-17.543-65.366c-10.226-66.396 77.047-39.117 77.047-39.117v49.496s-73.873-48.123-57.3-12.867c1.322 2.831 2.997 5.49 4.848 7.978 22.215 29.852 52.452 59.876 52.452 59.876M0 171.479h59.504s12.43-32.426 17.543-65.366C87.273 39.717 0 66.996 0 66.996v49.496s73.873-48.124 57.3-12.867c-1.322 2.831-2.997 5.49-4.848 7.978C30.237 141.455 0 171.479 0 171.479M119.255 32.865 85.541 0 51.827 32.865l33.714 32.864z" />
    </svg>
  );
}

const NAV = [
  {
    label: "Home",
    collapsible: true,
    items: [
      { label: "Newsfeed", href: "#", icon: I.home },
      { label: "Find", href: "#", icon: I.search, badge: <span className="sg-beta">Beta</span> },
      { label: "Accounts", href: "#", icon: I.list },
      { label: "Contacts", href: "#", icon: I.users },
      { label: "Signals", href: "index.html", icon: I.activity, active: true },
      { label: "MCP", href: "#", icon: I.command },
    ],
  },
  {
    label: "Admin",
    collapsible: true,
    items: [
      { label: "Workflows", href: "#", icon: I.branch },
      { label: "Team members", href: "#", icon: I.user },
      { label: "Company settings", href: "#", icon: I.gear },
    ],
  },
  {
    items: [
      { label: "Plans", href: "#", icon: I.sparkles, badge: <span className="sg-pro">Pro</span> },
    ],
  },
];

function NavItem({ item, collapsed }) {
  const Icon = item.icon;
  return (
    <a
      href={item.href}
      className={"sg-nav-item" + (item.active ? " on" : "")}
      title={collapsed ? item.label : undefined}
    >
      <span className="ic"><Icon width={16} height={16} /></span>
      {!collapsed && <span className="lb">{item.label}</span>}
      {!collapsed && item.badge}
    </a>
  );
}

function NavSection({ section, collapsed }) {
  const [open, setOpen] = useState(true);
  const collapsible = Boolean(section.collapsible);
  const showItems = !collapsible || open || collapsed;

  return (
    <div className="sg-nav-sec">
      {section.label && !collapsed && (
        collapsible ? (
          <button type="button" className="sg-nav-lbl" aria-expanded={open} onClick={() => setOpen((o) => !o)}>
            <span>{section.label}</span>
            <I.chev className={"cv" + (open ? " up" : "")} width={14} height={14} />
          </button>
        ) : (
          <div className="sg-nav-lbl static">{section.label}</div>
        )
      )}
      {showItems && (
        <div className="sg-nav-items">
          {section.items.map((it) => <NavItem key={it.label} item={it} collapsed={collapsed} />)}
        </div>
      )}
    </div>
  );
}

function TenantRow({ collapsed, interactive }) {
  const name = "Notion-V2";
  return (
    <div className={"sg-tenant" + (interactive ? " act" : "")}>
      <AcctLogo host="notion.so" name={name} />
      {!collapsed && <span className="nm">{name}</span>}
      {!collapsed && interactive && <I.chevsUD className="sw" width={14} height={14} />}
    </div>
  );
}

function UserMenu({ collapsed, theme, setTheme }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    if (!open) return;
    const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => e.key === "Escape" && setOpen(false);
    document.addEventListener("mousedown", onDown);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onDown);
      document.removeEventListener("keydown", onKey);
    };
  }, [open]);

  return (
    <div className="sg-user" ref={ref}>
      <button className="sg-user-btn" onClick={() => setOpen((o) => !o)}>
        <span className="av">ME</span>
        {!collapsed && (
          <span className="id">
            <span className="nm">Maya Ellison</span>
            <span className="rl">Admin</span>
          </span>
        )}
        {!collapsed && <I.chevUp className={"cv" + (open ? " open" : "")} width={14} height={14} />}
      </button>

      {open && (
        <div className="sg-user-pop">
          <div className="hd">
            <div className="e">maya.ellison@trayo.ai</div>
            <div className="r">Admin</div>
          </div>
          <div className="th">
            <div className="eyebrow">Theme</div>
            <div className="opt-switch">
              <button className={theme === "light" ? "on" : ""} onClick={() => setTheme("light")}>
                <span className="sw-lbl"><I.sun width={13} height={13} />Light</span>
              </button>
              <button className={theme === "dark" ? "on" : ""} onClick={() => setTheme("dark")}>
                <span className="sw-lbl"><I.moon width={13} height={13} />Dark</span>
              </button>
            </div>
          </div>
          <button className="it"><I.gear width={16} height={16} /> Settings</button>
          <button className="it"><I.buoy width={16} height={16} /> Help &amp; support</button>
          <button className="it sep"><I.logout width={16} height={16} /> Sign out</button>
        </div>
      )}
    </div>
  );
}

function Sidebar({ collapsed, onCollapsedChange, open, onOpenChange, theme, setTheme }) {
  return (
    <React.Fragment>
      <div className={"sg-scrim" + (open ? " on" : "")} aria-hidden onClick={() => onOpenChange(false)} />

      <aside className={"sg-sidebar" + (collapsed ? " collapsed" : "") + (open ? " open" : "")}>
        <div className="sg-brand">
          <a href="index.html" aria-label="Home" className="lg">
            {collapsed ? <TrayoLogoMark /> : <TrayoWordmark />}
          </a>
          {!collapsed && (
            <button className="sg-toggle-btn" aria-label="Collapse sidebar" onClick={() => onCollapsedChange(true)}>
              <I.collapse />
            </button>
          )}
        </div>

        <div className="sg-sep" />

        {collapsed && (
          <button className="sg-toggle-btn ctr" aria-label="Expand sidebar" onClick={() => onCollapsedChange(false)}>
            <I.expand />
          </button>
        )}

        <TenantRow collapsed={collapsed} interactive={false} />

        <nav className="sg-nav">
          {NAV.map((section, i) => (
            <NavSection key={section.label || i} section={section} collapsed={collapsed} />
          ))}
        </nav>

        <UserMenu collapsed={collapsed} theme={theme} setTheme={setTheme} />
      </aside>
    </React.Fragment>
  );
}

/* ---------------- App ---------------- */
function App() {
  const [theme, setTheme] = useState(() => localStorage.getItem("tbl-theme") || "light");
  useEffect(() => {
    document.documentElement.setAttribute("data-theme", theme);
    localStorage.setItem("tbl-theme", theme);
  }, [theme]);

  const [collapsed, setCollapsed] = useState(() => localStorage.getItem("web-sidebar-collapsed") === "true");
  const handleCollapsed = (next) => {
    setCollapsed(next);
    localStorage.setItem("web-sidebar-collapsed", String(next));
  };
  const [mobileOpen, setMobileOpen] = useState(false);

  const [signals, setSignals] = useState(SIGNALS);
  const [isDirty, setDirty] = useState(false);
  const [isFinalizing, setFinalizing] = useState(false);
  const [confirmDelete, setConfirmDelete] = useState(null);
  const [previewEvent, setPreviewEvent] = useState(null);

  const activities = useMemo(() => {
    const a = {};
    signals.forEach((s) => { a[s.id] = activityOf(s.id); });
    return a;
  }, [signals]);

  const handleWeightChange = (signal, weight) =>
    new Promise((resolve) => {
      setTimeout(() => {
        setSignals((prev) => prev.map((s) => (s.id === signal.id ? { ...s, weight } : s)));
        resolve();
      }, 550);
    });

  const handleDelete = () => {
    setSignals((prev) => prev.filter((s) => s.id !== confirmDelete.id));
    setConfirmDelete(null);
    setDirty(true);
  };

  const handleFinalize = () => {
    if (isFinalizing) return;
    setFinalizing(true);
    setTimeout(() => {
      setFinalizing(false);
      setDirty(false);
    }, 2600);
  };

  return (
    <div className={"app sg-app v1-app" + (collapsed ? " collapsed" : "")}>
      <Sidebar
        collapsed={collapsed}
        onCollapsedChange={handleCollapsed}
        open={mobileOpen}
        onOpenChange={setMobileOpen}
        theme={theme}
        setTheme={setTheme}
      />

      <main className="main sg-main">
        <div className="sg-wrap">
          <header className="sg-header">
            <div className="sg-header-t">
              <button className="sg-burger" aria-label="Open navigation" onClick={() => setMobileOpen(true)}>
                <I.burger width={18} height={18} />
              </button>
              <h1>Signals</h1>
              <p>Signals we monitor on your behalf to surface relevant events.</p>
            </div>
            <button className="cta sm" onClick={() => {}}>New signal <I.plus /></button>
          </header>

          <SignalsInventory
            signals={signals}
            activities={activities}
            onCreate={() => {}}
            onRefine={() => {}}
            onDelete={setConfirmDelete}
            onWeightChange={handleWeightChange}
            onFinalize={handleFinalize}
            isDirty={isDirty}
            isFinalizing={isFinalizing}
            onEventClick={setPreviewEvent}
          />
        </div>
      </main>

      <DeleteDialog signal={confirmDelete} onCancel={() => setConfirmDelete(null)} onConfirm={handleDelete} />
      <EventDialog event={previewEvent} onClose={() => setPreviewEvent(null)} />
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
