/* ============================================================================
 * COMPONENT PLAYGROUND — FIXED ENGINE
 * ----------------------------------------------------------------------------
 * Component-agnostic. Reads window.COMPONENT_SPEC and renders the 8 fixed
 * sections + the reproduce-config prompt. Token values and the WCAG contrast
 * ratio are resolved LIVE from the published stylesheet (getComputedStyle on a
 * probe placed in the current light/dark preview theme) — never hardcoded.
 * Swap pg-spec-*.js to retarget; nothing here changes.
 * ========================================================================== */
(function () {
  const SPEC = window.COMPONENT_SPEC;
  const { useState, useLayoutEffect, useMemo, useRef } = React;

  /* ── color math (for swatches + contrast) ───────────────────────────── */
  function parseColor(str) {
    if (!str) return null;
    str = str.trim();
    if (str[0] === "#") {
      let h = str.slice(1);
      if (h.length === 3) h = h.split("").map((c) => c + c).join("");
      if (h.length === 6 || h.length === 8) {
        return {
          r: parseInt(h.slice(0, 2), 16),
          g: parseInt(h.slice(2, 4), 16),
          b: parseInt(h.slice(4, 6), 16),
          a: h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1,
        };
      }
      return null;
    }
    const m = str.match(/rgba?\(([^)]+)\)/i);
    if (m) {
      const p = m[1].split(/[,\/]/).map((x) => x.trim());
      return { r: +p[0], g: +p[1], b: +p[2], a: p[3] != null ? +p[3] : 1 };
    }
    return null;
  }
  function over(top, bottom) {
    // alpha composite top over bottom (bottom assumed opaque)
    const a = top.a;
    return {
      r: top.r * a + bottom.r * (1 - a),
      g: top.g * a + bottom.g * (1 - a),
      b: top.b * a + bottom.b * (1 - a),
      a: 1,
    };
  }
  function stack(colors) {
    let base = { ...colors[0], a: 1 };
    for (let i = 1; i < colors.length; i++) if (colors[i]) base = over(colors[i], base);
    return base;
  }
  function relLum(c) {
    const f = (v) => {
      v /= 255;
      return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
    };
    return 0.2126 * f(c.r) + 0.7152 * f(c.g) + 0.0722 * f(c.b);
  }
  function ratio(c1, c2) {
    const l1 = relLum(c1), l2 = relLum(c2);
    const hi = Math.max(l1, l2), lo = Math.min(l1, l2);
    return (hi + 0.05) / (lo + 0.05);
  }
  function isColorVal(v) { return v && (v[0] === "#" || /^rgba?\(/i.test(v)); }
  function cssFromColor(c) { return `rgb(${Math.round(c.r)}, ${Math.round(c.g)}, ${Math.round(c.b)})`; }

  /* ── small chrome controls ──────────────────────────────────────────── */
  function OptRow({ options, value, onChange }) {
    return (
      <div className="pg-optrow">
        {options.map((o) => (
          <button key={o.value} type="button" aria-pressed={value === o.value} onClick={() => onChange(o.value)}>
            {o.label}
          </button>
        ))}
      </div>
    );
  }
  function SegBar({ options, value, onChange }) {
    return (
      <div className="pg-seg" role="group">
        {options.map((o) => (
          <button key={o.value} type="button" aria-pressed={value === o.value} onClick={() => onChange(o.value)}>
            {o.label}
          </button>
        ))}
      </div>
    );
  }
  function Switch({ on, onClick }) {
    return <button type="button" className="pg-sw" role="switch" aria-checked={on} onClick={onClick}></button>;
  }
  function IconPicker({ value, onChange, disabled }) {
    const names = window.vIconNames || [];
    return (
      <select className="pg-pick" value={value} disabled={disabled} onChange={(e) => onChange(e.target.value)}>
        {names.map((n) => <option key={n} value={n}>{n}</option>)}
      </select>
    );
  }

  /* ── specimen frame (states matrix + variant grid) ──────────────────── */
  function Frame({ cfg, nm, pr }) {
    return (
      <div className="pg-frame">
        <div className="pg-fstage">{SPEC.render(cfg)}</div>
        <div className="pg-fcap"><span className="nm">{nm}</span><span className="pr">{pr}</span></div>
      </div>
    );
  }

  /* ── section shell ──────────────────────────────────────────────────── */
  function Section({ num, title, slot, desc, children }) {
    return (
      <section className="pg-section">
        <div className="pg-sechead">
          <span className="num">{num}</span>
          <h2>{title}</h2>
          {slot ? <span className="pg-slot">{slot}</span> : null}
        </div>
        {desc ? <p className="pg-secdesc" dangerouslySetInnerHTML={{ __html: desc }} /> : null}
        {children}
      </section>
    );
  }

  /* ── token inspector table ──────────────────────────────────────────── */
  function TokenTable({ groups, resolved, hideValues }) {
    return (
      <div className="pg-inspector">
        <table className="pg-toktable">
          <thead><tr><th style={{ width: "30%" }}>Role</th><th style={{ width: "38%" }}>Token</th><th>{hideValues ? "Resolves to" : "Resolved value"}</th></tr></thead>
          <tbody>
            {groups.map((g) => (
              <React.Fragment key={g.group}>
                <tr className="grouprow"><td colSpan={3}>{g.group}</td></tr>
                {g.rows.map((r, i) => {
                  let val = r.token ? (resolved[r.token] || "") : "";
                  if (r.font && val) val = val.replace(/var\(--font-sans\)/, "Lexend");
                  const colorVal = r.token && isColorVal(resolved[r.token]);
                  // "Names only" mode (SPEC.hideResolvedValues): suppress raw
                  // hex/rgba for color rows — show swatch + semantic note only.
                  if (hideValues && colorVal) val = "";
                  return (
                    <tr key={i}>
                      <td className="role">{r.role}</td>
                      <td>{r.token ? <span className="tok">{r.token}</span> : <span className="tok tok--plain">{r.note || "—"}</span>}</td>
                      <td className="val">
                        <div className="pg-swrow">
                          {r.token ? (colorVal
                            ? <span className="pg-swatch" style={{ background: resolved[r.token] }}></span>
                            : <span className="pg-swatch empty"></span>) : null}
                          <span>{r.token ? (val || (hideValues && colorVal ? "" : "—")) : ""}{r.note && r.token ? <span style={{ color: "var(--pg-faint)" }}>{(val ? "  ·  " : "") + r.note}</span> : ""}</span>
                        </div>
                      </td>
                    </tr>
                  );
                })}
              </React.Fragment>
            ))}
          </tbody>
        </table>
      </div>
    );
  }

  /* ── anatomy with measured callouts ─────────────────────────────────── */
  function Anatomy({ cfg }) {
    const a = SPEC.anatomy[cfg.size];
    const specimenCfg = { ...cfg, state: "available", leading: true, trailing: false, label: cfg.label || "Label" };
    return (
      <div className="pg-anatomy">
        <div className="pg-anastage">
          <div className="pg-anabox" style={{ position: "relative", padding: "44px 64px 36px" }}>
            {/* height bracket (right) */}
            <span className="pg-guide" style={{ right: 28, top: 44, width: 1, height: a.heightPx }}></span>
            <span className="pg-dim" style={{ right: 6, top: "calc(44px + " + (a.heightPx / 2) + "px)", transform: "translateY(-50%)" }}>{a.rows[0].v}</span>
            {/* inline padding (top) */}
            <span className="pg-dim" style={{ left: 64, top: 14 }}>← {a.rows[1].v} →</span>
            {/* min target (bottom) */}
            <span className="pg-dim" style={{ left: 64, bottom: 10 }}>min target {a.rows[6].v}</span>
            {SPEC.render(specimenCfg)}
          </div>
        </div>
        <div className="pg-measure">
          {a.rows.map((r) => (
            <div className="m" key={r.k}>
              <span className="k">{r.k}</span>
              <span className="v">{r.v}</span>
              <span className="t">{r.t}</span>
            </div>
          ))}
        </div>
      </div>
    );
  }

  /* ── accessibility ──────────────────────────────────────────────────── */
  function A11y({ cfg, resolved, theme }) {
    const cp = SPEC.a11y.contrastPair(cfg);
    const AA = SPEC.a11y.aaThreshold || 4.5;       // 3.0 for non-text / graphical objects
    const AAA = SPEC.a11y.aaaThreshold || 7;
    const bgCols = cp.bgTokens.map((t) => parseColor(resolved[t])).filter(Boolean);
    const fgRaw = parseColor(resolved[cp.fgToken]);
    let result = null;
    if (bgCols.length && fgRaw) {
      const effBg = stack(bgCols);
      const fg = { ...fgRaw, a: cp.fgAlpha != null ? cp.fgAlpha : fgRaw.a };
      const effFg = fg.a < 1 ? over(fg, effBg) : fg;
      const r = ratio(effFg, effBg);
      const passAA = r >= AA, passAAA = r >= AAA;
      result = { r, passAA, passAAA, effBg, effFg };
    }
    const badge = (() => {
      if (!result) return null;
      if (cp.expected === "fail") return { cls: "warn", icon: "info", text: "Expected · disabled" };
      if (result.passAAA) return { cls: "pass", icon: "circle_check", text: "Passes AA + AAA" };
      if (result.passAA) return { cls: "pass", icon: "circle_check", text: "Passes AA" };
      return { cls: "fail", icon: "alert_circle", text: "Below AA (" + AA + ":1)" };
    })();

    return (
      <div className="pg-a11y">
        <div className="pg-card">
          <h3>Contrast · current fg / bg</h3>
          <div className="pg-ratio">
            <span className="big">{result ? result.r.toFixed(2) : "—"}</span>
            <span className="unit">: 1</span>
          </div>
          {badge ? <div className={"pg-badge " + badge.cls}><v-icon name={badge.icon}></v-icon>{badge.text}</div> : null}
          <div className="pg-pair">
            <div className="chip">
              <div className="sw" style={{ background: result ? cssFromColor(result.effFg) : "transparent" }}></div>
              <div className="lb">Foreground</div>
              <div className="tk">{cp.fgToken}{cp.fgAlpha < 1 ? " · 0.38" : ""}</div>
            </div>
            <div className="chip">
              <div className="sw" style={{ background: result ? cssFromColor(result.effBg) : "transparent" }}></div>
              <div className="lb">Background{theme === "dark" ? " · dark" : ""}</div>
              <div className="tk">{cp.bgTokens[cp.bgTokens.length - 1]}</div>
            </div>
          </div>
          {cp.expectedNote ? <p style={{ fontSize: 12, color: "var(--pg-faint)", lineHeight: 1.5, margin: "14px 0 0" }}>{cp.expectedNote}</p> : null}
          <p style={{ fontSize: 12, color: "var(--pg-faint)", lineHeight: 1.5, margin: "12px 0 0" }}
             dangerouslySetInnerHTML={{ __html: (SPEC.a11y.contrastNote || "WCAG AA for label text is 4.5:1.") + " Switch the preview to <b>dark</b> or change <b>variant</b>/<b>state</b> to watch this recompute." }} />
        </div>
        <div className="pg-card">
          <h3>Focus ring · role · keyboard</h3>
          <div className="pg-notes">
            {SPEC.a11y.notes.map((n, i) => (
              <div className="pg-note" key={i}>
                <v-icon name={n.icon}></v-icon>
                <div className="nbody" dangerouslySetInnerHTML={{ __html: n.html }} />
              </div>
            ))}
          </div>
        </div>
      </div>
    );
  }

  /* ── reproduce-config prompt ────────────────────────────────────────── */
  function PromptBlock({ cfg, theme }) {
    const text = SPEC.prompt(cfg, theme);
    const [copied, setCopied] = useState(false);
    const copy = () => {
      const done = () => { setCopied(true); setTimeout(() => setCopied(false), 1600); };
      if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(done, done);
      else done();
    };
    // highlight the value after each "• key: "
    const html = text
      .replace(/&/g, "&amp;").replace(/</g, "&lt;")
      .replace(/(• [^:]+: )(.*)/g, (m, k, v) => k + '<span class="hl">' + v + "</span>");
    return (
      <div className="pg-prompt">
        <div className="head">
          <span className="lbl">Reproduce this configuration</span>
          <span className="spacer"></span>
          <button className="pg-copybtn" type="button" onClick={copy}>
            <v-icon name={copied ? "check" : "copy"}></v-icon>{copied ? "Copied" : "Copy prompt"}
          </button>
        </div>
        <pre dangerouslySetInnerHTML={{ __html: html }} />
      </div>
    );
  }

  /* ── controls panel ─────────────────────────────────────────────────── */
  function Controls({ cfg, set }) {
    return (
      <div className="pg-controls">
        {SPEC.controls.map((c) => {
          if (c.kind === "select") {
            return (
              <div className="pg-grp" key={c.prop}>
                <div className="h"><span className="name">{c.label || c.prop}</span><span className="api">{c.api}</span></div>
                <OptRow options={c.options} value={cfg[c.prop]} onChange={(v) => set(c.prop, v)} />
              </div>
            );
          }
          if (c.kind === "text") {
            return (
              <div className="pg-grp" key={c.prop}>
                <div className="h"><span className="name">{c.label || c.prop}</span><span className="api">{c.api}: string</span></div>
                <input className="pg-txt" value={cfg[c.prop]} maxLength={40} onChange={(e) => set(c.prop, e.target.value)} />
              </div>
            );
          }
          if (c.kind === "icon") {
            return (
              <div className="pg-grp" key={c.prop}>
                <div className="h"><span className="name">{c.label || c.prop}</span><span className="api">{c.api}: glyph</span></div>
                <div className="pg-row">
                  <span className="lhs"><v-icon name={cfg[c.prop]}></v-icon>{cfg[c.prop]}</span>
                  <span className="rhs"><IconPicker value={cfg[c.prop]} onChange={(v) => set(c.prop, v)} /></span>
                </div>
              </div>
            );
          }
          // Locked icon: a non-editable glyph slot for an icon the component
          // hardwires (no prop). The picker is disabled and pinned to the fixed
          // self-hosted glyph; the row carries a "gap" flag so the missing API
          // is surfaced, never faked. Additive — only specs that set this kind
          // use it; all other specs are untouched.
          if (c.kind === "locked-icon") {
            return (
              <div className="pg-grp pg-grp--locked" key={c.prop}>
                <div className="h">
                  <span className="name">{c.label || c.prop}</span>
                  <span className="api">{c.api || "fixed · not a prop"}</span>
                </div>
                <div className="pg-row">
                  <span className="lhs"><v-icon name={c.fixed}></v-icon>{c.fixed}</span>
                  <span className="rhs">
                    <select className="pg-pick" value={c.fixed} disabled aria-disabled="true">
                      <option value={c.fixed}>{c.fixed}</option>
                    </select>
                  </span>
                </div>
                {c.note ? <div className="pg-imgnote" dangerouslySetInnerHTML={{ __html: c.note }} /> : null}
              </div>
            );
          }
          if (c.kind === "toggle") {
            return (
              <div className="pg-grp" key={c.prop}>
                <div className="h"><span className="name">{c.label}</span><span className="api">{c.api}: boolean</span></div>
                <div className="pg-row">
                  <span className="lhs">{c.iconHint ? <v-icon name={c.iconHint}></v-icon> : null}{cfg[c.prop] ? (c.onLabel || "Shown") : (c.offLabel || "Hidden")}</span>
                  <span className="rhs"><Switch on={cfg[c.prop]} onClick={() => set(c.prop, !cfg[c.prop])} /></span>
                </div>
              </div>
            );
          }
          if (c.kind === "switch") {
            return (
              <div className="pg-grp" key={c.prop}>
                <div className="h"><span className="name">{c.label}</span><span className="api">{c.api}: boolean</span></div>
                <div className="pg-row">
                  <span className="lhs"><v-icon name={cfg[c.iconProp]}></v-icon>{cfg[c.prop] ? "Shown" : "Hidden"}</span>
                  <span className="rhs">
                    <IconPicker value={cfg[c.iconProp]} disabled={!cfg[c.prop]} onChange={(v) => set(c.iconProp, v)} />
                    <Switch on={cfg[c.prop]} onClick={() => set(c.prop, !cfg[c.prop])} />
                  </span>
                </div>
              </div>
            );
          }
          // Image picker: a row of preset image swatches + an Upload tile that
          // reads a chosen file as a data URL. Generic & additive — only specs
          // that declare kind:"image" use it (e.g. Navigation's logo slot).
          if (c.kind === "image") {
            const opts = c.options || [];
            const isPreset = opts.some((o) => o.value === cfg[c.prop]);
            const isCustom = !isPreset && !!cfg[c.prop];
            return (
              <div className="pg-grp" key={c.prop}>
                <div className="h"><span className="name">{c.label || c.prop}</span><span className="api">{c.api || c.prop}: src</span></div>
                <div className="pg-imgrow">
                  {opts.map((o) => (
                    <button type="button" key={o.value}
                      className={"pg-imgopt" + (cfg[c.prop] === o.value ? " is-on" : "")}
                      title={o.label} onClick={() => set(c.prop, o.value)}>
                      <span className="pg-imgthumb"><img src={o.value} alt="" /></span>
                      <span className="pg-imglbl">{o.label}</span>
                    </button>
                  ))}
                  <label className={"pg-imgopt pg-imgopt--up" + (isCustom ? " is-on" : "")} title="Upload an image">
                    <span className="pg-imgthumb">
                      {isCustom ? <img src={cfg[c.prop]} alt="" /> : <v-icon name="plus"></v-icon>}
                    </span>
                    <span className="pg-imglbl">{isCustom ? "Custom" : "Upload"}</span>
                    <input type="file" accept={c.accept || "image/svg+xml,image/png,image/jpeg"} style={{ display: "none" }}
                      onChange={(e) => {
                        const f = e.target.files && e.target.files[0];
                        if (!f) return;
                        const rd = new FileReader();
                        rd.onload = () => set(c.prop, rd.result);
                        rd.readAsDataURL(f);
                        e.target.value = "";
                      }} />
                  </label>
                </div>
                {c.note ? <div className="pg-imgnote" dangerouslySetInnerHTML={{ __html: c.note }} /> : null}
              </div>
            );
          }
          // Per-item editor: a compact row per list/menu item with its own
          // state + leading/trailing icon slots. The spec supplies the item
          // array (cfg[c.prop]); countProp caps how many rows are editable.
          if (c.kind === "items") {
            const items = cfg[c.prop] || [];
            const count = c.countProp ? (parseInt(cfg[c.countProp], 10) || items.length) : items.length;
            const setItem = (idx, patch) =>
              set(c.prop, items.map((it, i) => (i === idx ? { ...it, ...patch } : it)));
            return (
              <div className="pg-grp" key={c.prop}>
                <div className="h"><span className="name">{c.label || c.prop}</span><span className="api">{c.api || c.prop}</span></div>
                <div className="pg-items">
                  {items.slice(0, count).map((it, idx) => (
                    <div className="pg-item" key={idx}>
                      <div className="pg-item__top">
                        <span className="pg-item__name">{it.label}</span>
                        {c.states ? (
                          <select className="pg-pick pg-pick--state" value={it.state || "live"}
                            onChange={(e) => setItem(idx, { state: e.target.value })}>
                            {c.states.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}
                          </select>
                        ) : null}
                      </div>
                      <div className="pg-item__slot">
                        <span className="pg-item__lbl">Leading</span>
                        <Switch on={!!it.leading} onClick={() => setItem(idx, { leading: !it.leading })} />
                        <IconPicker value={it.leadingIcon} disabled={!it.leading} onChange={(v) => setItem(idx, { leadingIcon: v })} />
                      </div>
                      <div className="pg-item__slot">
                        <span className="pg-item__lbl">Trailing</span>
                        <Switch on={!!it.trailing} onClick={() => setItem(idx, { trailing: !it.trailing })} />
                        <IconPicker value={it.trailingIcon} disabled={!it.trailing} onChange={(v) => setItem(idx, { trailingIcon: v })} />
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            );
          }
          // Derived glyph: a center icon the component selects from another
          // prop (no icon prop of its own). The picker is disabled and pinned
          // to the glyph c.derive(cfg) resolves to for the current config, so
          // it tracks state live. Additive — only specs that set this kind use
          // it; every other spec is untouched.
          if (c.kind === "derived-icon") {
            const glyph = c.derive ? c.derive(cfg) : c.fixed;
            return (
              <div className="pg-grp pg-grp--locked" key={c.prop || c.label}>
                <div className="h">
                  <span className="name">{c.label}</span>
                  <span className="api">{c.api || "derived · not a prop"}</span>
                </div>
                <div className="pg-row">
                  <span className="lhs">{glyph ? <v-icon name={glyph}></v-icon> : null}{glyph || "— no glyph —"}</span>
                  <span className="rhs">
                    <select className="pg-pick" value={glyph || ""} disabled aria-disabled="true">
                      <option value={glyph || ""}>{glyph || "none"}</option>
                    </select>
                  </span>
                </div>
                {c.note ? <div className="pg-imgnote" dangerouslySetInnerHTML={{ __html: c.note }} /> : null}
              </div>
            );
          }
          // Flagged gap: a property the brief asks for but the component does
          // NOT expose. Renders a dimmed, control-less row that names the
          // missing API and explains the gap — surfaced, never faked with a
          // dead control. Additive — only specs that set this kind use it.
          if (c.kind === "gap") {
            return (
              <div className="pg-grp pg-grp--locked pg-grp--gap" key={c.label}>
                <div className="h">
                  <span className="name">{c.label}</span>
                  <span className="api">{c.api || "not in API"}</span>
                </div>
                {c.note ? <div className="pg-imgnote" dangerouslySetInnerHTML={{ __html: c.note }} /> : null}
              </div>
            );
          }
          return null;
        })}
      </div>
    );
  }

  /* ── root app ───────────────────────────────────────────────────────── */
  const VP_OPTS = [
    { value: "mobile", label: "Mobile" },
    { value: "tablet", label: "Tablet" },
    { value: "desktop", label: "Desktop" },
  ];
  const VP_W = { mobile: "375px", tablet: "768px", desktop: "fill" };

  /* ── Tweaks (presentation of the playground itself — distinct from the
   * component-config controls). Edit-mode panel persists these to disk. ── */
  const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
    "accent": "#4A00BF",
    "stageSurface": "lowest",
    "specimenZoom": 100,
    "gridDots": true,
    "density": "regular"
  }/*EDITMODE-END*/;

  const STAGE_SURFACE_TOKEN = {
    lowest: "--sys-surface-container-lowest",
    surface: "--sys-surface",
    dim: "--sys-surface-dim",
  };

  function App() {
    const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
    const [cfg, setCfg] = useState({ ...SPEC.defaults });
    const [theme, setTheme] = useState("light");
    const [vp, setVp] = useState("desktop");
    const [resolved, setResolved] = useState({});
    const probeRef = useRef(null);

    const set = (k, v) => setCfg((c) => ({ ...c, [k]: v }));

    const groups = useMemo(() => SPEC.tokens(cfg), [cfg]);

    useLayoutEffect(() => {
      const probe = probeRef.current;
      if (!probe) return;
      probe.className = "pg-probe" + (theme === "dark" ? " pg-theme-dark" : "");
      const need = new Set(["--size-touch-target"]);
      groups.forEach((g) => g.rows.forEach((r) => r.token && need.add(r.token)));
      const cp = SPEC.a11y.contrastPair(cfg);
      need.add(cp.fgToken);
      cp.bgTokens.forEach((t) => need.add(t));
      const cs = getComputedStyle(probe);
      const map = {};
      need.forEach((t) => { map[t] = cs.getPropertyValue(t).trim(); });
      setResolved(map);
    }, [cfg, theme, groups]);

    const m = SPEC.meta;
    const stageSurfaceVar = `var(${STAGE_SURFACE_TOKEN[t.stageSurface] || STAGE_SURFACE_TOKEN.lowest})`;

    return (
      <div className="pg-wrap" data-density={t.density} style={{ "--pg-accent": t.accent }}>
        <span ref={probeRef} className="pg-probe" aria-hidden="true"
          style={{ position: "fixed", left: -9999, top: 0, width: 0, height: 0, pointerEvents: "none", opacity: 0 }} />

        {/* back to playground index */}
        <a className="pg-back" href="Playground Index.html">
          <v-icon name="arrow_left"></v-icon>
          <span>All playgrounds</span>
        </a>

        {/* template banner */}
        <div className="pg-banner">
          <span className="bdot"></span>
          <span className="btext">
            <b>Reusable template.</b> This page is a fixed engine; everything component-specific lives in one swappable slot —{" "}
            <code>{m.specFile || "pg-spec-button.js"}</code>. Copy that file and refill it to document another component. Each section is tagged with the spec slot that feeds it.
          </span>
        </div>

        {/* 1 · HEADER */}
        <header className="pg-header">
          <div className="pg-eyebrow">{m.eyebrow}</div>
          <div className="pg-titlerow">
            <h1 className="pg-title">{m.name}</h1>
            <span className="pg-status"><span className="ver">v{m.version}</span><span className="dot"></span>{m.status}</span>
          </div>
          <p className="pg-lede">{m.description}</p>
          <div className="pg-props">
            {m.props.map((p, i) => <span className="chip" key={i} dangerouslySetInnerHTML={{ __html: p }} />)}
          </div>
        </header>

        {/* 2 · LIVE PREVIEW + 3 · CONTROLS */}
        <Section num="02 / 03" title="Live preview + controls" slot="meta · controls"
          desc="The component on a neutral surface, centered. Toggle the surface light or dark and switch the viewport — the preview re-renders on every control change. Controls are generated one-per-prop from the component's API.">
          <div className="pg-live">
            <div className="pg-stagewrap">
              <div className="pg-stagebar">
                <span className="grouplbl">surface</span>
                <SegBar value={theme} onChange={setTheme} options={[{ value: "light", label: "Light" }, { value: "dark", label: "Dark" }]} />
                <span className="spacer"></span>
                <span className="grouplbl">viewport</span>
                <SegBar value={vp} onChange={setVp} options={VP_OPTS} />
              </div>
              <div className={"pg-stage" + (theme === "dark" ? " pg-theme-dark" : "")}
                data-grid={t.gridDots ? "on" : "off"}
                style={{ "--pg-stage-surface": stageSurfaceVar }}>
                <div className="pg-vp" data-vp={vp}>
                  <div className="pg-zoom" style={{ transform: `scale(${t.specimenZoom / 100})` }}>{SPEC.render(cfg)}</div>
                </div>
              </div>
              <div className="pg-vpmeta">
                <span>{vp} · {VP_W[vp]}</span>
                <span className={theme === "dark" ? "pg-theme-dark-readout" : ""}>{theme === "dark" ? "--sys-inverse-surface family" : "--sys-surface (light)"}</span>
              </div>
            </div>
            <Controls cfg={cfg} set={set} />
          </div>
        </Section>

        {/* 4 · STATES MATRIX */}
        <Section num="04" title="States matrix" slot="states[]"
          desc={(SPEC.copy && SPEC.copy.states) || "Every interaction state at once, in the current variant / size / label / icon config — so consistency across states is eyeballable, not just toggle-able."}>
          <div className="pg-frames states">
            {SPEC.states.map((s) => (
              <Frame key={s.key} cfg={{ ...cfg, state: s.key }} nm={s.name} pr={s.prop} />
            ))}
          </div>
        </Section>

        {/* 5 · VARIANT GRID */}
        <Section num="05" title="Variant grid" slot="variants[] × sizes[]"
          desc={(SPEC.copy && SPEC.copy.grid) || "All variants × sizes, using the current label and icon config. Icon size is coupled to button size — Small takes 20px icons, Medium 24px."}>          <div className="pg-frames grid">
            <div></div>
            {SPEC.sizes.map((s) => <div className="pg-colhead" key={s.key}>{s.name} · {s.sub}</div>)}
            {SPEC.variants.map((v) => (
              <React.Fragment key={v.key}>
                <div className="pg-rowhead">{v.name}<small>{v.prop}</small></div>
                {SPEC.sizes.map((s) => (
                  <Frame key={s.key} cfg={{ ...cfg, variant: v.key, size: s.key, state: "available" }} nm={v.name + " · " + s.name} pr={v.prop + " · Size=" + s.name} />
                ))}
              </React.Fragment>
            ))}
          </div>
        </Section>

        {/* 6 · TOKEN INSPECTOR */}
        <Section num="06" title="Token inspector" slot="tokens(config)"
          desc="Which design system tokens resolve for the current configuration, pulled by semantic name and resolved live from the published stylesheet — including the preview theme. Never hardcoded.">
          <TokenTable groups={groups} resolved={resolved} hideValues={!!SPEC.hideResolvedValues} />
        </Section>

        {/* 7 · ANATOMY & SPEC */}
        <Section num="07" title="Anatomy & spec" slot="anatomy[size]"
          desc="Spacing, padding, icon size and minimum target size for the current size, as measured callouts. Every measurement maps to a spacing or sizing token.">
          <Anatomy cfg={cfg} />
        </Section>

        {/* 8 · ACCESSIBILITY */}
        <Section num="08" title="Accessibility" slot="a11y"
          desc="Contrast ratio for the current foreground / background pair (composited with the active state layer), plus focus-ring behavior and role / aria notes.">
          <A11y cfg={cfg} resolved={resolved} theme={theme} />
        </Section>

        {/* PROMPT */}
        <Section num="↪" title="Hand this state back" slot="prompt(config)"
          desc="A copy-paste prompt that reproduces the exact configuration above — paste it back to regenerate this specific state later.">
          <PromptBlock cfg={cfg} theme={theme} />
        </Section>

        {/* ── TWEAKS — playground presentation (separate from the in-component
            controls above). Hidden until edit mode is toggled on. ── */}
        <TweaksPanel title="Tweaks">
          <TweakSection label="Specimen stage" />
          <TweakRadio label="Surface" value={t.stageSurface}
            options={["lowest", "surface", "dim"]}
            onChange={(v) => setTweak("stageSurface", v)} />
          <TweakToggle label="Grid dots" value={t.gridDots}
            onChange={(v) => setTweak("gridDots", v)} />
          <TweakSlider label="Specimen zoom" value={t.specimenZoom} min={100} max={250} step={10} unit="%"
            onChange={(v) => setTweak("specimenZoom", v)} />

          <TweakSection label="Chrome" />
          <TweakColor label="Accent" value={t.accent}
            options={["#4A00BF", "#64539B", "#790070", "#BA1A1A"]}
            onChange={(v) => setTweak("accent", v)} />
          <TweakRadio label="Density" value={t.density}
            options={["compact", "regular", "comfy"]}
            onChange={(v) => setTweak("density", v)} />
        </TweaksPanel>
      </div>
    );
  }

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