// MostlyQR — QR builder (core tool). Fully interactive: pick a type, edit
// content, style the live preview, export PNG / SVG / PDF.
const I18N = (typeof window !== 'undefined' && window.ForgeI18n) || { t: (k) => k, has: () => false, locale: 'en', dir: 'ltr', fmt: { number: (n)=>String(n??''), currency:(n)=>String(n??''), percent:(n)=>String(n??''), date:(d)=>String(d??''), time:(d)=>String(d??''), relative:(d)=>String(d??'') } };
const tr = I18N.t, fmt = I18N.fmt;
const { Icon, MQMark, MTMark, Wordmark, QR, Barcode, bareUrl, exportPNG, exportSVG, exportJPG, exportPDF, minModuleContrast, contrastRatio } = window.MQR;
// Below this module-vs-background contrast ratio a QR risks not scanning. Mirrors
// the logo↔ECC guard — a warning, surfaced before export (CONSTITUTION P7).
const MIN_SCAN_CONTRAST = 3;
const F = window.ForgeDesignSystem_e40d74;
const { Button, Badge, Field, Input, Select, Switch } = F;
const { useTweaks, TweaksPanel, TweakSection, TweakRadio, TweakColor, TweakToggle } = window;
const { UpgradeDialog } = window.ForgeKit || {};
const Api = window.MQRApi;

const ACCENTS = { indigo: "#7c69e0", blue: "#5b8def", green: "#2f9e6f", pink: "#cc5f97", amber: "#c9762f" };

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "light",
  "accent": "blue",
  "preset": "rounded"
}/*EDITMODE-END*/;

/* ── shape preview glyphs ──────────────────────────────── */
function BodyGlyph({ shape, c = "currentColor" }) {
  // Merging shapes (square + smooth): SAME silhouette, drawn with no gaps between
  // modules. `square` keeps sharp corners; `smooth` rounds every exposed outer
  // corner (same neighbour rule as the real QR), so they match in shape.
  if (shape === "square" || shape === "smooth") {
    const S = 6, cells = [[5, 5], [11, 5], [5, 11], [11, 11], [17, 11]];
    const has = (x, y) => cells.some(([a, b]) => a === x && b === y);
    const rad = shape === "smooth" ? 3 : 0;
    const rr = (x, y, w, h, tl, tr, br, bl) => [
      `M${x + tl},${y}`,
      `H${x + w - tr}`, tr ? `A${tr},${tr} 0 0 1 ${x + w},${y + tr}` : `L${x + w},${y}`,
      `V${y + h - br}`, br ? `A${br},${br} 0 0 1 ${x + w - br},${y + h}` : `L${x + w},${y + h}`,
      `H${x + bl}`, bl ? `A${bl},${bl} 0 0 1 ${x},${y + h - bl}` : `L${x},${y + h}`,
      `V${y + tl}`, tl ? `A${tl},${tl} 0 0 1 ${x + tl},${y}` : `L${x},${y}`, 'Z',
    ].join(' ');
    return (
      <svg width="22" height="22" viewBox="0 0 22 22">
        {cells.map(([x, y], i) => {
          const up = has(x, y - S), dn = has(x, y + S), lf = has(x - S, y), rt = has(x + S, y);
          const tl = !up && !lf ? rad : 0, tr = !up && !rt ? rad : 0, br = !dn && !rt ? rad : 0, bl = !dn && !lf ? rad : 0;
          const w = S + (rt ? 0.5 : 0), h = S + (dn ? 0.5 : 0);
          return <path key={i} d={rr(x, y, w, h, tl, tr, br, bl)} fill={c} shapeRendering={rad ? undefined : "crispEdges"} />;
        })}
      </svg>
    );
  }
  // separated tiles: dots / diamond / rounded / classy
  const pts = [[5, 5], [11, 5], [5, 11], [17, 11], [11, 17], [17, 17]];
  const r = shape === "dots" ? 50 : shape === "classy" ? 45 : 30;
  return (
    <svg width="22" height="22" viewBox="0 0 22 22">
      {pts.map(([x, y], i) => {
        if (shape === "dots") return <circle key={i} cx={x + 2} cy={y + 2} r="2.3" fill={c} />;
        if (shape === "diamond") return <path key={i} d={`M${x + 2} ${y - 0.3}L${x + 4.3} ${y + 2}L${x + 2} ${y + 4.3}L${x - 0.3} ${y + 2}Z`} fill={c} />;
        return <rect key={i} x={x} y={y} width="4.6" height="4.6" rx={(4.6 * r) / 100} fill={c} />;
      })}
    </svg>
  );
}
function EyeFrameGlyph({ shape, c = "currentColor" }) {
  if (shape === "circle") return <svg width="22" height="22" viewBox="0 0 22 22"><circle cx="11" cy="11" r="7.5" fill="none" stroke={c} strokeWidth="3" /></svg>;
  const rx = shape === "rounded" ? 5 : 1.5;
  return <svg width="22" height="22" viewBox="0 0 22 22"><rect x="3.5" y="3.5" width="15" height="15" rx={rx} fill="none" stroke={c} strokeWidth="3" /></svg>;
}
function EyeBallGlyph({ shape, c = "currentColor" }) {
  if (shape === "circle") return <svg width="22" height="22" viewBox="0 0 22 22"><circle cx="11" cy="11" r="4.4" fill={c} /></svg>;
  const rx = shape === "rounded" ? 2.4 : 0.8;
  return <svg width="22" height="22" viewBox="0 0 22 22"><rect x="6.6" y="6.6" width="8.8" height="8.8" rx={rx} fill={c} /></svg>;
}

/* ── content types ─────────────────────────────────────── */
const TYPES = [
  { key: "url", name: tr('mqr.builder.type.url.name'), sub: tr('mqr.builder.type.url.sub'), icon: "link" },
  { key: "vcard", name: tr('mqr.builder.type.vcard.name'), sub: tr('mqr.builder.type.vcard.sub'), icon: "user" },
  { key: "wifi", name: tr('mqr.builder.type.wifi.name'), sub: tr('mqr.builder.type.wifi.sub'), icon: "wifi" },
  { key: "menu", name: tr('mqr.builder.type.menu.name'), sub: tr('mqr.builder.type.menu.sub'), icon: "utensils" },
  { key: "pdf", name: tr('mqr.builder.type.pdf.name'), sub: tr('mqr.builder.type.pdf.sub'), icon: "file" },
  { key: "social", name: tr('mqr.builder.type.social.name'), sub: tr('mqr.builder.type.social.sub'), icon: "share" },
  { key: "email", name: tr('mqr.builder.type.email.name'), sub: tr('mqr.builder.type.email.sub'), icon: "mail" },
  { key: "sms", name: tr('mqr.builder.type.sms.name'), sub: tr('mqr.builder.type.sms.sub'), icon: "chat" },
  { key: "phone", name: tr('mqr.builder.type.phone.name'), sub: tr('mqr.builder.type.phone.sub'), icon: "phone" },
  { key: "text", name: tr('mqr.builder.type.text.name'), sub: tr('mqr.builder.type.text.sub'), icon: "type" },
];

const DEFAULT_FORMS = {
  url: { url: "https://northbrew.co/spring-menu" },
  vcard: { name: "Ada Vega", org: "Northbrew", phone: "+1 555 0142", email: "ada@northbrew.co", website: "northbrew.co" },
  wifi: { ssid: "Northbrew Guest", password: "pourover2026", enc: "WPA", hidden: false },
  menu: { venue: "Northbrew Café", dest: "northbrew.co/menu" },
  pdf: { title: "Spring catalogue", dest: "northbrew.co/catalogue.pdf" },
  social: { handle: "northbrew", dest: "linktr.ee/northbrew" },
  email: { to: "hello@northbrew.co", subject: "Hello Northbrew", body: "" },
  sms: { phone: "+15550142", message: "Table for two?" },
  phone: { phone: "+15550142" },
  text: { text: "Northbrew — pour-over, daily 7–4." },
};
const DEFAULT_STYLE = {
  fg: "#1d1d1f", bg: "#ffffff", eyeColor: "#5b8def", ballColor: "match",
  bgImage: null, bgImagePath: null,
  // gradient: { on, type:"linear"|"radial", from, to, direction } → body fill.
  gradient: { on: false, type: "linear", from: "#1d1d1f", to: "#5b8def", direction: "diagonal" },
  bodyShape: "rounded", frameShape: "rounded", ballShape: "rounded",
  logo: null, logoPath: null, logoStyle: "hug", logoSize: 0.24,
  frame: "none", frameText: "", frameColor: null, ecc: "M",
  // How the code is drawn. "qr" (default) → the styled QR matrix; "barcode" → a 1-D
  // Code 128 of the SAME mqr.sh short link (dynamic: still repointable + analytics).
  // barcodeFormat is code128 for dynamic links; the static "barcode" type may pick ean13.
  renderMode: "qr", barcodeFormat: "code128",
};

function shortId(name) {
  let h = 5381;
  for (let i = 0; i < name.length; i++) h = ((h << 5) + h + name.charCodeAt(i)) >>> 0;
  return h.toString(36).slice(0, 5).toUpperCase(); // uppercase base36 — matches the redirect code alphabet (QR alphanumeric)
}
// The PRINTED code always encodes a permanent short link — that's what makes
// it dynamic. The destination below is what it currently points to.
// Permanent short link = `${baseUrl}/${code}` (matches @mostly-tiny/redirect).
const BASE_URL = (window.MQR_CONFIG && window.MQR_CONFIG.baseUrl) || "https://mqr.sh";
function codeUrl(id) { return `${BASE_URL}/${id}`; }
function destinationOf(type, f) {
  const strip = (u) => (u || "—").replace(/^https?:\/\//, "");
  if (type === "url") return strip(f.url);
  if (type === "vcard") return `${f.name} · ${f.org}`;
  if (type === "wifi") return tr('mqr.builder.dest.wifi', { ssid: f.ssid });
  if (type === "menu") return strip(f.dest);
  if (type === "pdf") return strip(f.dest);
  if (type === "social") return strip(f.dest);
  if (type === "email") return f.to || "—";
  if (type === "sms" || type === "phone") return f.phone || "—";
  if (type === "text") return (f.text || "—").slice(0, 42);
  return "—";
}
// The REAL destination URL the code points to (what becomes link.destination_url
// and the flow's default). vcard/wifi encode data inline, so they have no URL.
function destinationUrlOf(type, f) {
  const withScheme = (u) => (u ? (/^https?:\/\//.test(u) ? u : "https://" + u) : "");
  if (type === "url") return withScheme(f.url);
  if (type === "menu" || type === "pdf" || type === "social") return withScheme(f.dest);
  return "";
}

// Inherently-static content types (F011): the QR encodes the data DIRECTLY (tel:/mailto:/SMSTO:/
// plain text) — these don't redirect, so they work offline and need no short link. (URL/menu/pdf/
// social stay dynamic — they encode the repointable mqr.sh short link.)
const STATIC_INLINE = new Set(["email", "sms", "phone", "text"]);
function inlineEncode(type, f) {
  f = f || {};
  if (type === "email") {
    const to = String(f.to || "").trim();
    const parts = [];
    if (f.subject) parts.push("subject=" + encodeURIComponent(f.subject));
    if (f.body) parts.push("body=" + encodeURIComponent(f.body));
    return `mailto:${to}${parts.length ? "?" + parts.join("&") : ""}`;
  }
  if (type === "sms") {
    const num = String(f.phone || "").trim();
    const msg = String(f.message || "").trim();
    return msg ? `SMSTO:${num}:${msg}` : `SMSTO:${num}`;
  }
  if (type === "phone") return `tel:${String(f.phone || "").trim()}`;
  if (type === "text") return String(f.text || "");
  return "";
}

/* ── forms ─────────────────────────────────────────────── */
function FormFields({ type, f, set }) {
  const upd = (k) => (e) => set({ ...f, [k]: e.target ? (e.target.type === "checkbox" ? e.target.checked : e.target.value) : e });
  if (type === "url")
    return (
      <Field label={tr('mqr.builder.form.url.label')} hint={tr('mqr.builder.form.url.hint')}>
        <Input value={f.url} onChange={upd("url")} placeholder="https://…" icon={<Icon name="link" size={15} />} />
      </Field>
    );
  if (type === "vcard")
    return (
      <React.Fragment>
        <div className="bld-row2">
          <Field label={tr('mqr.builder.form.fullname')}><Input value={f.name} onChange={upd("name")} /></Field>
          <Field label={tr('mqr.builder.form.company')}><Input value={f.org} onChange={upd("org")} /></Field>
        </div>
        <Field label={tr('mqr.builder.form.phone')}><Input value={f.phone} onChange={upd("phone")} /></Field>
        <Field label={tr('mqr.builder.form.email')}><Input value={f.email} onChange={upd("email")} /></Field>
        <Field label={tr('mqr.builder.form.website')}><Input value={f.website} onChange={upd("website")} /></Field>
      </React.Fragment>
    );
  if (type === "wifi")
    return (
      <React.Fragment>
        <Field label={tr('mqr.builder.form.ssid')}><Input value={f.ssid} onChange={upd("ssid")} icon={<Icon name="wifi" size={15} />} /></Field>
        <Field label={tr('mqr.builder.form.password')}><Input value={f.password} onChange={upd("password")} /></Field>
        <div className="bld-row2">
          <Field label={tr('mqr.builder.form.security')}><Select value={f.enc} onChange={upd("enc")} options={["WPA", "WEP", "None"]} /></Field>
          <Field label={tr('mqr.builder.form.hidden')}>
            <div style={{ height: 34, display: "flex", alignItems: "center" }}>
              <Switch checked={f.hidden} onChange={upd("hidden")} label={f.hidden ? tr('mqr.builder.form.yes') : tr('mqr.builder.form.no')} />
            </div>
          </Field>
        </div>
      </React.Fragment>
    );
  if (type === "menu")
    return (
      <React.Fragment>
        <Field label={tr('mqr.builder.form.venue')}><Input value={f.venue} onChange={upd("venue")} icon={<Icon name="utensils" size={15} />} /></Field>
        <Field label={tr('mqr.builder.form.menulink')} hint={tr('mqr.builder.form.menulink.hint')}><Input value={f.dest} onChange={upd("dest")} /></Field>
      </React.Fragment>
    );
  if (type === "pdf")
    return (
      <React.Fragment>
        <Field label={tr('mqr.builder.form.doctitle')}><Input value={f.title} onChange={upd("title")} icon={<Icon name="file" size={15} />} /></Field>
        <Field label={tr('mqr.builder.form.filelink')} hint={tr('mqr.builder.form.filelink.hint')}><Input value={f.dest} onChange={upd("dest")} /></Field>
      </React.Fragment>
    );
  if (type === "social")
    return (
      <React.Fragment>
        <Field label={tr('mqr.builder.form.handle')}><Input value={f.handle} onChange={upd("handle")} icon={<Icon name="share" size={15} />} /></Field>
        <Field label={tr('mqr.builder.form.social.dest')}><Input value={f.dest} onChange={upd("dest")} /></Field>
      </React.Fragment>
    );
  if (type === "email")
    return (
      <React.Fragment>
        <Field label={tr('mqr.builder.form.email.to')}><Input value={f.to} onChange={upd("to")} icon={<Icon name="mail" size={15} />} placeholder="hello@brand.com" /></Field>
        <Field label={tr('mqr.builder.form.email.subject')}><Input value={f.subject} onChange={upd("subject")} /></Field>
        <Field label={tr('mqr.builder.form.email.body')}><Input value={f.body} onChange={upd("body")} /></Field>
      </React.Fragment>
    );
  if (type === "sms")
    return (
      <React.Fragment>
        <Field label={tr('mqr.builder.form.phone')}><Input value={f.phone} onChange={upd("phone")} icon={<Icon name="chat" size={15} />} placeholder="+15550142" /></Field>
        <Field label={tr('mqr.builder.form.sms.message')}><Input value={f.message} onChange={upd("message")} /></Field>
      </React.Fragment>
    );
  if (type === "phone")
    return <Field label={tr('mqr.builder.form.phone')}><Input value={f.phone} onChange={upd("phone")} icon={<Icon name="phone" size={15} />} placeholder="+15550142" /></Field>;
  if (type === "text")
    return <Field label={tr('mqr.builder.form.text')} hint={tr('mqr.builder.form.text.hint')}><Input value={f.text} onChange={upd("text")} icon={<Icon name="type" size={15} />} /></Field>;
  return null;
}

/* ── style panel ───────────────────────────────────────── */
const SHAPES_BODY = ["square", "smooth", "rounded", "classy", "dots", "diamond"];
const SHAPES_EYE = ["square", "rounded", "circle"];
// Frame / shape options for the code, shown as an icon row above the colours.
const FRAMES = ["none", "border", "scanme", "scanme-top", "circle"];
const FRAME_LABEL = {
  none: "mqr.builder.frame.none", border: "mqr.builder.frame.border",
  scanme: "mqr.builder.frame.scanme", "scanme-top": "mqr.builder.frame.scanmetop",
  circle: "mqr.builder.frame.circle",
};
const ECC = [["L", "7%"], ["M", "15%"], ["Q", "25%"], ["H", "30%"]];
// Plain-language hint per error-correction level. Higher = more damage tolerated,
// but a denser (larger) matrix. M is the default: smallest code that still recovers
// from everyday print smudges.
const ECC_HINT = {
  L: tr('mqr.builder.ecc.hint.L'),
  M: tr('mqr.builder.ecc.hint.M'),
  Q: tr('mqr.builder.ecc.hint.Q'),
  H: tr('mqr.builder.ecc.hint.H'),
};

// One compact colour: a swatch that opens the OS colour picker on click, plus the
// live hex. `matched` (eyes only) follows the foreground until you pick a colour.
function ColorCell({ label, value, display, matched, onChange, onMatch }) {
  return (
    <div className="bld-colorcell">
      <div className="bld-colorcell__top">
        <span className="bld-colorcell__lbl">{label}</span>
        {onMatch && (
          <button className="bld-colorcell__match" data-on={matched} onClick={onMatch}
            title={tr('mqr.builder.style.matchbody')}><Icon name="link" size={11} sw={2.2} /></button>
        )}
      </div>
      <label className="bld-colorpick">
        <span className="bld-colorpick__chip" style={{ background: display }} />
        <input type="color" value={display} onChange={(e) => onChange(e.target.value)} aria-label={label} />
        <span className="bld-colorpick__hex">{matched ? tr('mqr.builder.style.matchbody') : (value || "").toUpperCase()}</span>
      </label>
    </div>
  );
}

// Gradient control for the body fill — toggle on, pick two stops, choose linear
// (with a direction) or radial. When off the body uses the solid foreground colour.
function GradientControl({ g, onChange }) {
  const G = g || { on: false, type: "linear", from: "#1d1d1f", to: "#5b8def", direction: "diagonal" };
  const upd = (patch) => onChange({ ...G, ...patch });
  return (
    <div className="bld-gradient">
      <div className="bld-colorcell__top">
        <span className="bld-colorcell__lbl">{tr('mqr.builder.style.gradient')}</span>
        <button className="bld-colorcell__match" data-on={G.on} onClick={() => upd({ on: !G.on })}
          title={tr('mqr.builder.style.gradient.toggle')} aria-label={tr('mqr.builder.style.gradient.toggle')}>
          <Icon name="palette" size={11} sw={2.2} />
        </button>
      </div>
      {G.on && (
        <React.Fragment>
          <div className="bld-gradient__stops">
            <label className="bld-colorpick">
              <span className="bld-colorpick__chip" style={{ background: G.from }} />
              <input type="color" value={G.from} onChange={(e) => upd({ from: e.target.value })} aria-label={tr('mqr.builder.style.gradient.from')} />
              <span className="bld-colorpick__hex">{(G.from || "").toUpperCase()}</span>
            </label>
            <label className="bld-colorpick">
              <span className="bld-colorpick__chip" style={{ background: G.to }} />
              <input type="color" value={G.to} onChange={(e) => upd({ to: e.target.value })} aria-label={tr('mqr.builder.style.gradient.to')} />
              <span className="bld-colorpick__hex">{(G.to || "").toUpperCase()}</span>
            </label>
          </div>
          <div className="bld-segs" style={{ marginTop: 8 }}>
            {[["linear", 'mqr.builder.style.gradient.linear'], ["radial", 'mqr.builder.style.gradient.radial']].map(([v, k]) => (
              <button key={v} className="bld-eccbtn" data-on={G.type === v} onClick={() => upd({ type: v })} style={{ flex: 1 }}>
                <span className="bld-eccbtn__k" style={{ fontSize: 11, fontWeight: 600 }}>{tr(k)}</span>
              </button>
            ))}
          </div>
          {G.type === "linear" && (
            <div className="bld-segs" style={{ marginTop: 6 }}>
              {[["horizontal", "→"], ["vertical", "↓"], ["diagonal", "↘"]].map(([v, gl]) => (
                <button key={v} className="bld-eccbtn" data-on={G.direction === v} onClick={() => upd({ direction: v })} style={{ flex: 1 }}
                  title={tr('mqr.builder.style.gradient.' + v)} aria-label={tr('mqr.builder.style.gradient.' + v)}>
                  <span className="bld-eccbtn__k" style={{ fontSize: 14 }}>{gl}</span>
                </button>
              ))}
            </div>
          )}
        </React.Fragment>
      )}
    </div>
  );
}

// Icon for each frame option: the code (a rounded square / circle) plus, for the
// label frames, a filled CTA bar above or below.
function FrameGlyph({ type }) {
  const c = "currentColor";
  const common = { width: 22, height: 22, viewBox: "0 0 24 24", fill: "none", stroke: c, strokeWidth: 1.7, strokeLinejoin: "round" };
  if (type === "border")
    return <svg {...common}><rect x="3" y="3" width="18" height="18" rx="3.5" /><rect x="7.5" y="7.5" width="9" height="9" rx="1.5" /></svg>;
  if (type === "scanme")
    return <svg {...common}><rect x="5" y="3.5" width="14" height="11" rx="2" /><rect x="5" y="17" width="14" height="3.6" rx="1.8" fill={c} stroke="none" /></svg>;
  if (type === "scanme-top")
    return <svg {...common}><rect x="5" y="3.4" width="14" height="3.6" rx="1.8" fill={c} stroke="none" /><rect x="5" y="9.5" width="14" height="11" rx="2" /></svg>;
  if (type === "circle")
    return <svg {...common}><circle cx="12" cy="12" r="9" /></svg>;
  // none — just the code, no surround
  return <svg {...common}><rect x="5.5" y="5.5" width="13" height="13" rx="2.5" /></svg>;
}

// Tiny glyph for the logo-fit buttons (the shape of the logo's backing).
function LogoFitGlyph({ shape }) {
  const c = "currentColor";
  if (shape === "circle") return (<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke={c} strokeWidth="1.6" /></svg>);
  if (shape === "card") return (<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="2" width="12" height="12" rx="3.5" stroke={c} strokeWidth="1.6" /></svg>);
  return (<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2.4" y="2.4" width="11.2" height="11.2" rx="0.6" stroke={c} strokeWidth="1.6" strokeDasharray="2.4 2" /></svg>);
}

/* ── saved brand styles ────────────────────────────────── */
// A reusable-style strip atop the Style step: apply a saved style with one click,
// or save the current one as a named brand kit. Free feature (it strengthens the
// "genuinely usable free tier" wedge vs GetQR, whose reviewers cite no preset
// saving). Persists per-account via the brand-presets callables.
function StylePresetBar({ presets, busy, onApply, onSave, onDelete }) {
  const [naming, setNaming] = React.useState(false);
  const [name, setName] = React.useState("");
  const submit = async () => { const n = name.trim(); if (!n) return; await onSave(n); setName(""); setNaming(false); };
  const eyeOf = (st) => (st && (st.eyeColor === "match" ? st.fg : st.eyeColor)) || "#1d1d1f";
  return (
    <div className="bld-presets">
      <div className="bld-grouplabel" style={{ marginTop: 0 }}>{tr('mqr.builder.presets.label')}</div>
      <div className="bld-presetrow">
        {presets.map((p) => (
          <div className="bld-preset" key={p.id}>
            <button className="bld-preset__apply" onClick={() => onApply(p.style)} title={tr('mqr.builder.presets.apply', { name: p.name })}>
              <span className="bld-preset__sw" style={{ background: (p.style && p.style.bg) || "#fff" }}>
                <i style={{ background: (p.style && p.style.fg) || "#1d1d1f" }} />
                <i style={{ background: eyeOf(p.style) }} />
              </span>
              <span className="bld-preset__nm">{p.name}</span>
            </button>
            <button className="bld-preset__rm" onClick={() => onDelete(p.id)} title={tr('mqr.builder.presets.delete')} aria-label={tr('mqr.builder.presets.delete')}><Icon name="x" size={11} sw={2.4} /></button>
          </div>
        ))}
        {naming ? (
          <span className="bld-preset__new">
            <input autoFocus className="bld-preset__nameinput" value={name} maxLength={60}
              placeholder={tr('mqr.builder.presets.nameph')}
              onChange={(e) => setName(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") submit(); if (e.key === "Escape") { setNaming(false); setName(""); } }} />
            <button className="bld-preset__confirm" disabled={busy || !name.trim()} onClick={submit} aria-label={tr('mqr.builder.presets.save')}><Icon name="check" size={13} sw={2.6} /></button>
          </span>
        ) : (
          <button className="bld-preset__add" onClick={() => setNaming(true)} disabled={busy}>
            <Icon name="plus" size={13} sw={2.2} /> {tr('mqr.builder.presets.save')}
          </button>
        )}
      </div>
    </div>
  );
}

// Barcode mode has a much smaller style surface than the QR matrix: no eyes, body
// shapes, logo, or error-correction — just the bar/background colours. The human-
// readable value is always printed beneath the bars (standard for 1-D codes).
function BarcodeStyle({ s, set }) {
  return (
    <div className="bld-stylegrid__col" style={{ maxWidth: 380 }}>
      <div className="bld-grouplabel" style={{ marginTop: 0 }}>{tr('mqr.builder.style.colors')}</div>
      <div className="bld-colors">
        <ColorCell label={tr('mqr.builder.barcode.bars')} value={s.fg} display={s.fg} onChange={(v) => set({ ...s, fg: v })} />
        <ColorCell label={tr('mqr.builder.style.background')} value={s.bg} display={s.bg} onChange={(v) => set({ ...s, bg: v })} />
      </div>
      <div className="bld-ecchint">
        <Icon name="infinity" size={14} sw={2} />
        <span>{tr('mqr.builder.barcode.note')}</span>
      </div>
    </div>
  );
}

function StylePanel({ s, set }) {
  const fileRef = React.useRef(null);
  const isBarcode = (s.renderMode || "qr") === "barcode";
  // Scannability guard: weakest contrast across every ink that paints modules —
  // body (solid or both gradient stops) plus any explicit eye/eyeball colour.
  const bgFileRef = React.useRef(null);
  // Transparent / image backgrounds are assumed placed on a light surface (the image
  // path draws a white scrim), so contrast is measured against white in those cases.
  const bgRef = (s.bg === "transparent" || s.bg === "none" || !s.bg || s.bgImage) ? "#ffffff" : s.bg;
  const eyeInks = [s.eyeColor, s.ballColor].filter((c) => c && c !== "match");
  const lowContrast = !isBarcode && Math.min(
    minModuleContrast({ fg: s.fg, bg: bgRef, gradient: s.gradient }),
    ...eyeInks.map((c) => contrastRatio(c, bgRef)),
  ) < MIN_SCAN_CONTRAST;
  const onFile = (e) => {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    const reader = new FileReader();
    // Adding a logo clears center modules — force ECC H so it still scans. Clear
    // logoPath so the old uploaded object is treated as replaced (cleaned up on save).
    reader.onload = () => set({ ...s, logo: reader.result, logoPath: null, ecc: "H" });
    reader.readAsDataURL(file);
  };
  const onBgFile = (e) => {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    const reader = new FileReader();
    // A background image bumps ECC to Q for robustness against the busier backdrop.
    reader.onload = () => set({ ...s, bgImage: reader.result, bgImagePath: null, ecc: s.ecc === "L" || s.ecc === "M" ? "Q" : s.ecc });
    reader.readAsDataURL(file);
  };
  return (
    <React.Fragment>
      {/* Barcode render-mode toggle (dynamic Code 128 of the same short link). The renderer
          (shared.jsx Barcode), kit, persistence + BarcodeStyle back it; the QR matrix is the
          default. Re-enabled under F003 (serialized/enterprise leans on 1-D symbology). */}
      <div className="bld-grouplabel" style={{ marginTop: 0 }}>{tr('mqr.builder.style.rendermode')}</div>
      <div className="bld-segs" style={{ marginBottom: 14 }}>
        {[["qr", tr('mqr.builder.rendermode.qr')], ["barcode", tr('mqr.builder.rendermode.barcode')]].map(([v, l]) => (
          <button key={v} className="bld-eccbtn" data-on={isBarcode ? v === "barcode" : v === "qr"}
            onClick={() => set({ ...s, renderMode: v })} style={{ flex: 1 }}>
            <span className="bld-eccbtn__k" style={{ fontSize: 12, fontWeight: 600 }}>{l}</span>
          </button>
        ))}
      </div>
      {isBarcode ? <BarcodeStyle s={s} set={set} /> : (
      <div className="bld-stylegrid">
      <div className="bld-stylegrid__col">
      <div className="bld-grouplabel" style={{ marginTop: 0 }}>{tr('mqr.builder.style.bodyshape')}</div>
      <div className="bld-shapebtns">
        {SHAPES_BODY.map((sh) => (
          <button key={sh} className="bld-shape" data-on={s.bodyShape === sh} onClick={() => set({ ...s, bodyShape: sh })} title={sh}><BodyGlyph shape={sh} /></button>
        ))}
      </div>

      <div className="bld-eyegrid">
        <div>
          <div className="bld-grouplabel">{tr('mqr.builder.style.eyeframe')}</div>
          <div className="bld-shapebtns bld-shapebtns--3">
            {SHAPES_EYE.map((sh) => (
              <button key={sh} className="bld-shape" data-on={s.frameShape === sh} onClick={() => set({ ...s, frameShape: sh })} title={sh}><EyeFrameGlyph shape={sh} /></button>
            ))}
          </div>
        </div>
        <div>
          <div className="bld-grouplabel">{tr('mqr.builder.style.eyeball')}</div>
          <div className="bld-shapebtns bld-shapebtns--3">
            {SHAPES_EYE.map((sh) => (
              <button key={sh} className="bld-shape" data-on={s.ballShape === sh} onClick={() => set({ ...s, ballShape: sh })} title={sh}><EyeBallGlyph shape={sh} /></button>
            ))}
          </div>
        </div>
      </div>
      </div>

      <div className="bld-stylegrid__col">
      <div className="bld-grouplabel" style={{ marginTop: 0 }}>{tr('mqr.builder.frame.label')}</div>
      <div className="bld-shapebtns bld-shapebtns--frame">
        {FRAMES.map((v) => (
          <button key={v} className="bld-shape" data-on={s.frame === v} onClick={() => set({ ...s, frame: v })}
            title={tr(FRAME_LABEL[v])} aria-label={tr(FRAME_LABEL[v])}><FrameGlyph type={v} /></button>
        ))}
      </div>
      {(s.frame === "scanme" || s.frame === "scanme-top" || s.frame === "circle") && (
        <div className="bld-frameopts">
          {(s.frame === "scanme" || s.frame === "scanme-top") && (
            <Input value={s.frameText || ""} placeholder="SCAN ME" maxLength={24}
              onChange={(e) => set({ ...s, frameText: (e.target ? e.target.value : e) })}
              aria-label={tr('mqr.builder.frame.text')} />
          )}
          <ColorCell label={tr('mqr.builder.frame.color')} value={s.frameColor || s.fg}
            display={s.frameColor || (s.eyeColor === "match" ? s.fg : s.eyeColor)}
            onChange={(v) => set({ ...s, frameColor: v })} />
        </div>
      )}
      <div className="bld-grouplabel">{tr('mqr.builder.style.colors')}</div>
      <div className="bld-colors">
        <ColorCell label={tr('mqr.builder.style.foreground')} value={s.fg} display={s.fg} onChange={(v) => set({ ...s, fg: v })} />
        <ColorCell label={tr('mqr.builder.style.background')} value={s.bg} display={s.bg} onChange={(v) => set({ ...s, bg: v })} />
        <ColorCell label={tr('mqr.builder.style.eyeframe')} value={s.eyeColor} display={s.eyeColor === "match" ? s.fg : s.eyeColor} matched={s.eyeColor === "match"}
          onChange={(v) => set({ ...s, eyeColor: v })} onMatch={() => set({ ...s, eyeColor: s.eyeColor === "match" ? s.fg : "match" })} />
        <ColorCell label={tr('mqr.builder.style.eyeball')} value={s.ballColor}
          display={(s.ballColor && s.ballColor !== "match") ? s.ballColor : s.fg} matched={!s.ballColor || s.ballColor === "match"}
          onChange={(v) => set({ ...s, ballColor: v })} onMatch={() => set({ ...s, ballColor: (!s.ballColor || s.ballColor === "match") ? s.fg : "match" })} />
      </div>
      <GradientControl g={s.gradient} onChange={(g) => set({ ...s, gradient: g })} />

      <div className="bld-grouplabel">{tr('mqr.builder.style.backgroundopts')}</div>
      <div className="bld-bgopts">
        <button className="bld-eccbtn" data-on={s.bg === "transparent"} style={{ flex: 1 }}
          onClick={() => set({ ...s, bg: s.bg === "transparent" ? "#ffffff" : "transparent" })}>
          <span className="bld-eccbtn__k" style={{ fontSize: 12, fontWeight: 600 }}>{tr('mqr.builder.style.transparent')}</span>
        </button>
        {s.bgImage ? (
          <button className="bld-eccbtn" style={{ flex: 1 }} onClick={() => set({ ...s, bgImage: null, bgImagePath: null })}>
            <span className="bld-eccbtn__k" style={{ fontSize: 12, fontWeight: 600 }}>{tr('mqr.builder.style.removebgimage')}</span>
          </button>
        ) : (
          <button className="bld-eccbtn" style={{ flex: 1 }} onClick={() => bgFileRef.current && bgFileRef.current.click()}>
            <span className="bld-eccbtn__k" style={{ fontSize: 12, fontWeight: 600 }}>{tr('mqr.builder.style.bgimage')}</span>
          </button>
        )}
        <input ref={bgFileRef} type="file" accept="image/*" onChange={onBgFile} style={{ display: "none" }} />
      </div>

      <div className="bld-grouplabel">{tr('mqr.builder.style.logo')}</div>
      <div className="bld-logo">
        <div className="bld-logo__preview">
          {s.logo ? <img src={s.logo} alt="logo" /> : <Icon name="image" size={18} style={{ color: "var(--fg-subtle)" }} />}
        </div>
        <button className="bld-logo__drop" onClick={() => fileRef.current && fileRef.current.click()}>
          <Icon name="download" size={15} /> {tr('mqr.builder.logo.upload')}
        </button>
        {s.logo && (
          <React.Fragment>
            <div className="bld-logo__fits">
              {["hug", "card", "circle"].map((v) => (
                <button key={v} className="bld-logofit" data-on={s.logoStyle === v}
                  onClick={() => set({ ...s, logoStyle: v })} title={tr('mqr.builder.logo.style.' + v)} aria-label={tr('mqr.builder.logo.style.' + v)}>
                  <LogoFitGlyph shape={v} />
                </button>
              ))}
            </div>
            <button className="bld-logo__rm" onClick={() => set({ ...s, logo: null, logoPath: null })} title={tr('mqr.builder.logo.remove')} aria-label={tr('mqr.builder.logo.remove')}>
              <Icon name="x" size={15} sw={2.2} />
            </button>
          </React.Fragment>
        )}
        <input ref={fileRef} type="file" accept="image/*" onChange={onFile} style={{ display: "none" }} />
      </div>
      {s.logo && (
        <div className="bld-logosize">
          <span className="bld-colorcell__lbl">{tr('mqr.builder.logo.size')}</span>
          <input type="range" min="0.16" max="0.30" step="0.01"
            value={Math.min(0.30, Math.max(0.16, s.logoSize || 0.24))}
            onChange={(e) => set({ ...s, logoSize: parseFloat(e.target.value) })} aria-label={tr('mqr.builder.logo.size')} />
        </div>
      )}

      <div className="bld-grouplabel">{tr('mqr.builder.ecc.label')}</div>
      <div className="bld-ecc">
        {ECC.map(([k, v]) => (
          <button key={k} className="bld-eccbtn" data-on={s.ecc === k} onClick={() => set({ ...s, ecc: k })}>
            <div className="bld-eccbtn__k">{k}</div>
            <div className="bld-eccbtn__v">{v}</div>
          </button>
        ))}
      </div>
      <div className="bld-ecchint">
        <Icon name={s.logo && (s.ecc === "L" || s.ecc === "M") ? "shield" : "infinity"} size={14} sw={2} />
        <span>
          {s.logo && (s.ecc === "L" || s.ecc === "M")
            ? tr('mqr.builder.ecc.logowarn')
            : ECC_HINT[s.ecc]}
        </span>
      </div>
      {lowContrast && (
        <div className="bld-ecchint bld-ecchint--warn">
          <Icon name="shield" size={14} sw={2} />
          <span>{tr('mqr.builder.style.contrastwarn')}</span>
        </div>
      )}
      </div>
    </div>
      )}
    </React.Fragment>
  );
}

/* ── preview ───────────────────────────────────────────── */
function Preview({ s, codeData, dest, name, scans, lock, onLockedDownload }) {
  const ref = React.useRef(null);
  const [busy, setBusy] = React.useState(null);
  const [barcodeErr, setBarcodeErr] = React.useState(null);
  const isBarcode = s.renderMode === "barcode";
  const fileName = (name || "mostlyqr").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "mostlyqr";
  const doExport = (fn, label) => async () => {
    // Download is locked until the code is saved (signed-in). Nudge through the
    // save/register flow instead of exporting a code whose short link doesn't exist yet.
    if (lock) { onLockedDownload && onLockedDownload(); return; }
    if (!ref.current || barcodeErr) return; // never export a broken/empty barcode (P7)
    setBusy(label);
    try { await fn(ref.current, fileName); } finally { setTimeout(() => setBusy(null), 500); }
  };
  return (
    <React.Fragment>
      <div className="bld-previewcard">
        <div className={"bld-qrwrap" + (s.bg === "transparent" ? " is-transparent" : "")}
          style={{ background: s.bg === "transparent" ? undefined : s.bg }}>
          {isBarcode ? (
            barcodeErr ? (
              <div className="bld-barcode-err" role="alert">
                <Icon name="warning" size={16} sw={2} /> {tr('mqr.builder.barcode.invalid')}
              </div>
            ) : (
              // Same mqr.sh short link as the QR — still dynamic (repointable + analytics).
              // Code 128 only (encodes the URL); the static "barcode" type may use ean13.
              <Barcode innerRef={ref} data={codeData} format={s.barcodeFormat || "code128"}
                fg={s.fg} bg={s.bg} showText width={520} onError={setBarcodeErr} />
            )
          ) : (
            <QR innerRef={ref} data={codeData} fg={s.fg} bg={s.bg} bgImage={s.bgImage || null}
              gradient={s.gradient && s.gradient.on ? s.gradient : null}
              eyeColor={s.eyeColor === "match" ? null : s.eyeColor}
              ballColor={!s.ballColor || s.ballColor === "match" ? null : s.ballColor}
              bodyShape={s.bodyShape} frameShape={s.frameShape} ballShape={s.ballShape}
              logo={s.logo} logoStyle={s.logoStyle} logoSize={s.logoSize || 0.24} ecc={s.ecc}
              frame={s.frame === "none" ? null : s.frame} frameText={s.frameText}
              frameColor={s.frameColor || (s.eyeColor === "match" ? s.fg : s.eyeColor)} size={250} quiet={4} />
          )}
        </div>
        <div className="bld-previewmeta">
          <div className="bld-destbar">
            <span className="bld-destbar__ic"><Icon name="infinity" size={15} sw={2} /></span>
            <div style={{ minWidth: 0, flex: 1 }}>
              <div className="bld-destbar__url">{/^https?:/i.test(codeData) ? bareUrl(codeData.toLowerCase()) : codeData}</div>
              <div className="bld-destbar__lbl">{tr('mqr.builder.preview.pointsto', { dest: dest })}</div>
            </div>
            <Badge tone="good" dot>{tr('mqr.builder.preview.live')}</Badge>
          </div>
          <div className="bld-neverexp">
            <Icon name="infinity" size={15} sw={2.2} /> {tr('mqr.builder.preview.neverexpires')}
          </div>
          <div className={"bld-exports" + (lock ? " is-locked" : "")}>
            <Button variant="primary" iconLeft={<Icon name={lock ? "lock" : "download"} size={15} />} onClick={doExport(exportPNG, "PNG")}>{busy === "PNG" ? tr('mqr.builder.saving') : "PNG"}</Button>
            <Button variant="secondary" iconLeft={<Icon name="download" size={15} />} onClick={doExport(exportSVG, "SVG")}>{busy === "SVG" ? tr('mqr.builder.saving') : "SVG"}</Button>
            <Button variant="secondary" iconLeft={<Icon name="download" size={15} />} onClick={doExport(exportJPG, "JPG")}>{busy === "JPG" ? tr('mqr.builder.saving') : "JPG"}</Button>
            <Button variant="secondary" iconLeft={<Icon name="download" size={15} />} onClick={doExport(exportPDF, "PDF")}>{busy === "PDF" ? tr('mqr.builder.saving') : "PDF"}</Button>
          </div>
          {lock && (
            <button className="bld-downloadhint" onClick={onLockedDownload}>
              <Icon name="lock" size={12} sw={2.2} /> {lock === "auth" ? tr('mqr.builder.download.locked.auth') : tr('mqr.builder.download.locked.unsaved')}
            </button>
          )}
        </div>
      </div>
      <div className="bld-scanline"><Icon name="chart" size={14} /> {tr('mqr.builder.preview.scanline', { count: scans })}</div>
    </React.Fragment>
  );
}

/* ── topbar ────────────────────────────────────────────── */
// One sticky place to name, save, or start a new code. The Save persists the
// whole code at once (name + destination + routing) via the flow editor's saver.
function Topbar({ name, setName, theme, setTheme, dirty, saving, canSave, saved, onSave, onNewCode }) {
  return (
    <header className="bld-top">
      <a className="bld-top__brand" href="index.html"><MQMark s={26} /><Wordmark size={17} /></a>
      <span className="bld-top__divider" />
      <nav className="bld-crumbs" aria-label="Breadcrumb">
        <a className="bld-crumb" href="/Dashboard">
          <Icon name="arrow" size={14} style={{ transform: "scaleX(-1)" }} /> {tr('mqr.builder.top.dashboard')}
        </a>
        <span className="bld-crumb__sep" aria-hidden="true">/</span>
        <span className="bld-top__name">
          <Icon name="qr" size={15} style={{ color: "var(--accent)" }} />
          <input value={name} onChange={(e) => setName(e.target.value)} aria-label={tr('mqr.builder.top.codename')} />
        </span>
      </nav>
      <span className="bld-top__spacer" />
      <div className="bld-top__actions">
        <div className="bld-themeseg">
          <button data-on={theme === "light"} onClick={() => setTheme("light")} aria-label={tr('mqr.builder.top.light')}><Icon name="sun" size={15} /></button>
          <button data-on={theme === "dark"} onClick={() => setTheme("dark")} aria-label={tr('mqr.builder.top.dark')}><Icon name="moon" size={15} /></button>
        </div>
        <Button variant="ghost" size="sm" iconLeft={<Icon name="plus" size={15} />} onClick={onNewCode}>{tr('mqr.builder.top.newcode')}</Button>
        {/* The Save button IS the save-state indicator. "✓ Saved" shows ONLY when a
            real code is persisted AND there are no pending changes — never for an
            unsaved draft (that must stay a clickable "Save QR"). */}
        {saving
          ? <Button variant="primary" size="sm" disabled>{tr('mqr.flow.saving')}</Button>
          : (saved && !dirty)
            ? <Button variant="secondary" size="sm" disabled iconLeft={<Icon name="check" size={14} sw={2.4} />}>{tr('mqr.builder.top.saved')}</Button>
            : <Button variant="primary" size="sm" disabled={!canSave} onClick={onSave}>{tr('mqr.builder.top.save')}</Button>}
      </div>
    </header>
  );
}

/* ── presets ───────────────────────────────────────────── */
const PRESETS = {
  classic: { bodyShape: "square", frameShape: "square", ballShape: "square" },
  rounded: { bodyShape: "rounded", frameShape: "rounded", ballShape: "rounded" },
  dots: { bodyShape: "dots", frameShape: "circle", ballShape: "circle" },
};

/* ── security (passcode protection) ────────────────────── */
// Pro control: lock a code's redirect behind a passcode. The server stores only
// a salted hash (see @mostly-tiny/redirect lib/passcode) — the entered value is
// never read back, so for an already-protected code the field stays empty and
// "keep current" is the default; typing replaces it. Lives in the Flow step, the
// Pro surface, so its gate matches the routing gate exactly.
function SecurityCard({ protect, passcode, protectedNow, locked, onToggle, onPasscode, onUpgrade }) {
  return (
    <section className={"bld-seccard" + (locked ? " is-locked" : "")}>
      <div className="bld-seccard__head">
        <span className="bld-seccard__ic"><Icon name="lock" size={16} sw={2.2} /></span>
        <div className="bld-seccard__txt">
          <div className="bld-seccard__t">
            {tr('mqr.builder.security.title')}
            {locked && <Badge tone="accent">{tr('mqr.builder.security.pro')}</Badge>}
            {!locked && protectedNow && <Badge tone="good" dot>{tr('mqr.builder.security.on')}</Badge>}
          </div>
          <div className="bld-seccard__d">{tr('mqr.builder.security.sub')}</div>
        </div>
        <Switch checked={!!protect} aria-label={tr('mqr.builder.security.title')}
          onChange={(e) => (locked ? onUpgrade() : onToggle(e.target.checked))} />
      </div>
      {protect && !locked && (
        <div className="bld-seccard__body">
          <Field label={tr('mqr.builder.security.passcode')}
            hint={protectedNow ? tr('mqr.builder.security.hint.set') : tr('mqr.builder.security.hint.new')}>
            <Input type="password" value={passcode} autoComplete="new-password"
              onChange={(e) => onPasscode(e.target.value)}
              placeholder={protectedNow ? tr('mqr.builder.security.placeholder.keep') : tr('mqr.builder.security.placeholder.new')}
              icon={<Icon name="lock" size={15} />} />
          </Field>
        </div>
      )}
    </section>
  );
}

/* ── flow section (embeds the routing editor below the builder) ── */
// Pro feature: for free users it renders grayed + non-interactive with an upgrade
// overlay (handled inside window.MQRFlow.FlowEditor via the `locked` prop).
function FlowSection({ code, link, baseDestination, defaultValue, onDefaultChange, name, content, style, passcode, builderDirty, draft, locked, onSaved, saveRef, onState, onUpgrade }) {
  const FlowEditor = window.MQRFlow && window.MQRFlow.FlowEditor;
  return (
    <section className="bld-flow" id="flow">
      <div className="bld-flow__head">
        <span className="bld-flow__eyebrow"><Icon name="zap" size={14} sw={2.2} style={{ color: "var(--accent)" }} /> {tr('mqr.builder.flow.eyebrow')}</span>
        <h2 className="bld-flow__title">{tr('mqr.builder.flow.title')}</h2>
        <p className="bld-flow__sub">{tr('mqr.builder.flow.sub')}</p>
      </div>
      {FlowEditor
        ? <FlowEditor code={code} link={link} baseDestination={baseDestination} defaultValue={defaultValue} onDefaultChange={onDefaultChange} name={name} content={content} style={style} passcode={passcode} builderDirty={builderDirty} draft={draft} locked={locked} onUpgrade={onUpgrade} onSaved={onSaved} saveRef={saveRef} onState={onState} />
        : <div className="bld-flow__missing">{tr('mqr.builder.flow.unavailable')}</div>}
    </section>
  );
}

/* ── stepper ───────────────────────────────────────────── */
// Three ordered steps under the persistent QR hero. Clickable (jump anywhere) —
// not a forced wizard — so editing an existing code can go straight to Flows.
function Stepper({ steps, step, setStep }) {
  return (
    <div className="bld-stepper" role="tablist" aria-label={tr('mqr.builder.steps.aria')}>
      {steps.map((s, i) => (
        <React.Fragment key={s.key}>
          {i > 0 && <div className="bld-stepper__line" data-done={i <= step} />}
          <button className="bld-stepper__step" data-on={i === step} data-done={i < step}
            role="tab" aria-selected={i === step} onClick={() => setStep(i)}>
            <span className="bld-stepper__num">
              {s.locked ? <Icon name="lock" size={13} sw={2.2} />
                : i < step ? <Icon name="check" size={14} sw={2.6} />
                : i + 1}
            </span>
            <span className="bld-stepper__txt">
              <span className="bld-stepper__t">{s.title}</span>
              <span className="bld-stepper__d">{s.sub}</span>
            </span>
          </button>
        </React.Fragment>
      ))}
    </div>
  );
}

/* ── registration gate ─────────────────────────────────── */
// Shown when a signed-out user tries to Save or Download. Designing stays free;
// this captures an email and sends a magic link (no password), then the builder
// resumes the save on return.
function AuthGate({ action, sent, busy, error, email, onEmail, onSubmit, onClose }) {
  return (
    <div className="bld-authwrap" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="bld-auth" role="dialog" aria-modal="true">
        <button className="bld-auth__x" onClick={onClose} aria-label={tr('mqr.builder.auth.close')}><Icon name="x" size={16} /></button>
        {sent ? (
          <div className="bld-auth__body">
            <span className="bld-auth__ic bld-auth__ic--ok"><Icon name="check" size={22} sw={2.6} /></span>
            <h3 className="bld-auth__t">{tr('mqr.builder.auth.sent.title')}</h3>
            <p className="bld-auth__d">{tr('mqr.builder.auth.sent.body', { email })}</p>
          </div>
        ) : (
          <div className="bld-auth__body">
            <span className="bld-auth__ic"><Icon name="infinity" size={20} sw={2.2} /></span>
            <h3 className="bld-auth__t">{action === "download" ? tr('mqr.builder.auth.title.download') : tr('mqr.builder.auth.title.save')}</h3>
            <p className="bld-auth__d">{tr('mqr.builder.auth.body')}</p>
            <input type="email" autoFocus required value={email} className="bld-auth__input"
              placeholder={tr('auth.signin.emailPlaceholder')} onChange={(e) => onEmail(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") onSubmit(); }} />
            {error ? <div className="bld-auth__err">{error}</div> : null}
            <Button variant="primary" disabled={busy} onClick={onSubmit} iconRight={busy ? null : <Icon name="arrow" size={15} />} style={{ width: "100%", justifyContent: "center" }}>
              {busy ? tr('auth.signin.submitting') : tr('mqr.builder.auth.cta')}
            </Button>
            <div className="bld-auth__foot">{tr('mqr.builder.auth.foot')}</div>
          </div>
        )}
      </div>
    </div>
  );
}

// Shown when a free user hits their dynamic-code limit on save (the backend throws
// resource-exhausted). Friendly upsell instead of a dead error.
function LimitGate({ limit, onUpgrade, onClose }) {
  return (
    <div className="bld-authwrap" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="bld-auth" role="dialog" aria-modal="true">
        <button className="bld-auth__x" onClick={onClose} aria-label={tr('mqr.builder.auth.close')}><Icon name="x" size={16} /></button>
        <div className="bld-auth__body">
          <span className="bld-auth__ic"><Icon name="zap" size={20} sw={2.2} /></span>
          <h3 className="bld-auth__t">{tr('mqr.builder.limit.title')}</h3>
          <p className="bld-auth__d">{tr('mqr.builder.limit.body', { limit: limit || 3 })}</p>
          <Button variant="primary" onClick={onUpgrade} iconRight={<Icon name="arrow" size={15} />} style={{ width: "100%", justifyContent: "center" }}>{tr('mqr.builder.limit.cta')}</Button>
          <div className="bld-auth__foot">{tr('mqr.builder.limit.foot')}</div>
        </div>
      </div>
    </div>
  );
}

/* ── app ───────────────────────────────────────────────── */
function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [type, setType] = React.useState("url");
  const [forms, setForms] = React.useState(DEFAULT_FORMS);
  const [name, setName] = React.useState("Spring menu");
  const [scans] = React.useState(4218);
  const [style, setStyle] = React.useState(DEFAULT_STYLE);
  // Password protection (Pro). `protect` = the user wants a passcode on; `passcode`
  // = the pending new code ("" while editing an already-protected code means "keep
  // the existing one" — the server never returns the value to re-show). Initialised
  // from the loaded link's passcode_protected flag.
  const [protect, setProtect] = React.useState(false);
  const [passcode, setPasscode] = React.useState("");
  // Saved brand styles (per-account; demo → localStorage). Loaded once we have an
  // account context (signed in, or demo mode).
  const [presets, setPresets] = React.useState([]);
  const [presetBusy, setPresetBusy] = React.useState(false);
  // Plan gating for the embedded Flow section. null = still loading (don't lock
  // yet, to avoid a gate flash); resolves to 'free'|'pro'|'team' (demo → 'pro').
  const [plan, setPlan] = React.useState(null);
  const [simFree, setSimFree] = React.useState(false); // design-tool: preview the free/locked state
  const [step, setStep] = React.useState(0); // 0 = type & destination · 1 = style · 2 = flows
  const codeParam = React.useMemo(() => new URLSearchParams(window.location.search).get("code"), []);
  // The CODE this builder is editing — its QR, its destination AND its flow all
  // refer to this one link. Loaded from ?code= (or your most recent code).
  const [link, setLink] = React.useState(null);
  const [isNew, setIsNew] = React.useState(false); // a fresh draft, not yet created
  // The flow editor owns routing; it registers its saver here and reports save
  // state up, so the sticky topbar can save the whole code from one place.
  const flowRef = React.useRef({});
  const [flow, setFlow] = React.useState({ dirty: false, saving: false, canSave: false });
  // Baseline snapshot of the saved design — anything that differs from it (name,
  // destination, content OR style, on any step) marks the code dirty → Save enabled.
  const savedSnap = React.useRef("");

  // ── Registration gate ──────────────────────────────────────────────────
  // Designing + previewing is open to everyone. Keeping a code (Save) — and
  // therefore Download, which needs a real saved code — requires an account.
  // signedIn: null = resolving, true/false once known. Demo (no backend) → no gate.
  const [signedIn, setSignedIn] = React.useState(Api.ready ? null : true);
  const [simSignedOut, setSimSignedOut] = React.useState(false); // design-tool: preview the gate
  const [auth, setAuth] = React.useState({ open: false, sent: false, busy: false, action: null, email: "", error: "" });
  const needsAuth = simSignedOut || (Api.ready && signedIn === false);
  const DRAFT_KEY = "mqr_builder_draft";
  // Free-tier code-limit upsell (backend createLink throws resource-exhausted).
  const [limitGate, setLimitGate] = React.useState({ open: false, count: 3 });
  const [simLimit, setSimLimit] = React.useState(false); // design-tool: preview the limit upsell
  const [upgradeOpen, setUpgradeOpen] = React.useState(false);
  // Why the upgrade modal was opened. 'flow' (from the Flow step's locked banner)
  // surfaces the missing-Flows caveat chip on the cheaper tiers; anything else
  // (the code-limit gate, a generic Upgrade) leaves it off.
  const [upgradeReason, setUpgradeReason] = React.useState(null);

  React.useEffect(() => {
    if (!Api.ready) return undefined;
    return Api.onAuth((u) => setSignedIn(!!u));
  }, []);

  // Magic-link return: complete sign-in, restore the in-progress design we stashed
  // before sending the link, then resume the pending Save.
  React.useEffect(() => {
    if (!Api.ready || !Api.isMagicLinkLanding()) return;
    (async () => {
      try {
        await Api.completeMagicLink();
        setSignedIn(true);
        let action = null;
        try {
          const d = JSON.parse(window.localStorage.getItem(DRAFT_KEY) || "null");
          if (d) {
            action = d.action;
            if (d.type) setType(d.type);
            if (d.forms) setForms(d.forms);
            if (d.name != null) setName(d.name);
            if (d.style) setStyle(d.style);
            setIsNew(true); setLink(null);
          }
        } catch (e) { /* no draft */ }
        window.localStorage.removeItem(DRAFT_KEY);
        try { window.history.replaceState(null, "", window.location.pathname); } catch (e) { /* no-op */ }
        // Resume the save now that they're authed (download then unlocks). Routed
        // through runSave so a free-limit hit surfaces the upsell, not a dead error.
        if (action) setTimeout(() => runSave(), 80);
      } catch (e) { /* sign-in failed → stay on the builder, ungated retry */ }
    })();
  }, []);

  const persistDraft = (action) => {
    try { window.localStorage.setItem(DRAFT_KEY, JSON.stringify({ type, forms, name, style, action })); } catch (e) { /* no-op */ }
  };
  const openAuth = (action) => { persistDraft(action); setAuth({ open: true, sent: false, busy: false, action, email: "", error: "" }); };
  const submitAuth = async () => {
    const email = (auth.email || "").trim();
    if (!email) return;
    setAuth((a) => ({ ...a, busy: true, error: "" }));
    try {
      await Api.requestMagicLink(email, window.location.origin + window.location.pathname);
      setAuth((a) => ({ ...a, busy: false, sent: true }));
    } catch (e) {
      const msg = String((e && (e.message || (e.details && e.details.reason))) || "");
      const closed = /registration is not open|registration_closed/i.test(msg);
      setAuth((a) => ({ ...a, busy: false, error: closed ? tr('mqr.builder.auth.closed') : tr('mqr.builder.auth.error') }));
    }
  };

  React.useEffect(() => {
    let alive = true;
    (async () => {
      if (Api.isMagicLinkLanding()) return; // the magic-link effect owns load on return
      try {
        const [ls, a] = await Promise.all([Api.listLinks(), Api.getAccountStats()]);
        if (!alive) return;
        setPlan((ls[0] && ls[0].account_plan) || (a && a.fullAnalytics ? "pro" : "free"));
        // listLinks() returns every code this account owns, and getLink is itself
        // ownership-scoped — so a code in ?code= that isn't in `ls` is not ours
        // (e.g. a demo/shared code). Don't probe the backend for it: that only
        // fires a guaranteed 404. Match within the account, else fall back.
        let l = (codeParam && ls.find((x) => x.code === codeParam)) || null;
        if (!l) l = ls[0] || null; // fall back to the most recent code
        if (alive && l) {
          setLink(l); setIsNew(false); // editing an existing code
          if (l.title) setName(l.title);
          // Restore the original authoring content (vCard/Wi-Fi/menu fields…) if we
          // stored it; otherwise fall back to showing the destination as a URL.
          if (l.content && l.content.type && l.content.form) {
            setType(l.content.type);
            setForms((fs) => ({ ...fs, [l.content.type]: { ...(fs[l.content.type] || {}), ...l.content.form } }));
          } else if (l.destination_url) {
            setType("url"); setForms((fs) => ({ ...fs, url: { ...fs.url, url: l.destination_url } }));
          }
          if (l.style) setStyle(l.style); // restore the saved visual style
          setProtect(!!l.passcode_protected); setPasscode(""); // reflect lock state; never re-show the code
        } else if (alive) {
          // Nothing to edit (new account, or no code matched) → a fresh, SAVEABLE
          // draft. Without this, save() early-returns and the QR encodes a local
          // preview id that doesn't exist → "link does not exist" on scan.
          setIsNew(true);
        }
      } catch (e) {
        if (alive) { setPlan("free"); setIsNew(true); } // signed out / offline → new draft
      }
    })();
    return () => { alive = false; };
  }, [codeParam]);

  // Deep-link: /Builder.html#flow opens straight on the Flows step.
  React.useEffect(() => {
    if (window.location.hash === "#flow") setStep(2);
  }, []);

  const flowLocked = simFree || plan === "free";

  // Saved brand styles need an account context: signed in, or demo (localStorage).
  // Signed-out live users see no bar — they get one once they sign in to save a code.
  const canUsePresets = !needsAuth;
  React.useEffect(() => {
    if (!canUsePresets) { setPresets([]); return undefined; }
    let alive = true;
    (async () => { try { const ps = await Api.listBrandPresets(); if (alive) setPresets(ps || []); } catch (e) { /* offline / none */ } })();
    return () => { alive = false; };
  }, [canUsePresets]);
  // Applying a saved style merges over DEFAULT_STYLE so any field the preset omits
  // resets cleanly rather than keeping a stale value from the current design.
  const applyPreset = (pstyle) => setStyle((s) => ({ ...DEFAULT_STYLE, ...(pstyle || {}) }));
  const saveCurrentPreset = async (nm) => {
    setPresetBusy(true);
    try {
      // Upload an inline logo to Storage first so the preset stores a URL, not base64.
      const up = Api.uploadStyle ? await Api.uploadStyle(style) : style;
      const id = "bp_" + Math.random().toString(36).slice(2, 9);
      const saved = await Api.saveBrandPresets([...presets, { id, name: nm, style: up }]);
      setPresets(saved);
    } finally { setPresetBusy(false); }
  };
  const deletePreset = async (id) => {
    const next = presets.filter((p) => p.id !== id);
    setPresets(next); // optimistic
    try { const saved = await Api.saveBrandPresets(next); setPresets(saved); } catch (e) { /* keep optimistic */ }
  };

  React.useEffect(() => { document.documentElement.dataset.theme = t.theme === "dark" ? "dark" : ""; }, [t.theme]);
  React.useEffect(() => { document.documentElement.dataset.accent = t.accent; }, [t.accent]);
  // apply preset from tweaks
  React.useEffect(() => {
    const p = PRESETS[t.preset]; if (p) setStyle((s) => ({ ...s, ...p }));
  }, [t.preset]);

  const f = forms[type];
  const setF = (nf) => setForms({ ...forms, [type]: nf });
  const id = React.useMemo(() => shortId(name + type), [name, type]);
  // The QR encodes the REAL code once a link is loaded (so the QR + the flow
  // refer to the same code); a fresh draft uses the local id until it's created.
  const liveCode = link ? link.code : id; // real code once saved; local preview while drafting
  // The QR must encode the FULL url (with https://) — without the scheme phones
  // treat it as a search term, not a link. UPPERCASE so the whole string encodes
  // in QR alphanumeric mode (smaller matrix); the resolver matches case-insensitively.
  // We strip the scheme and lowercase it only for the display chip.
  // Static inline types (F011) encode their payload DIRECTLY (tel:/mailto:/SMSTO:/text); dynamic
  // types encode the repointable mqr.sh short link (uppercased for QR alphanumeric mode).
  const isStatic = STATIC_INLINE.has(type);
  const codeData = isStatic ? inlineEncode(type, f) : codeUrl(liveCode).toUpperCase();
  const dest = destinationOf(type, f);
  // The real destination URL = this code's base destination = the flow's default.
  const baseDestination = destinationUrlOf(type, f) || (link && link.destination_url) || "";
  // The flow's "Otherwise" default is editable there too: it writes straight back
  // to this type's destination field, so the builder + flow stay one shared value.
  // vcard/wifi encode their data inline — no URL field — so they stay read-only.
  const destField = type === "url" ? "url" : (type === "menu" || type === "pdf" || type === "social") ? "dest" : null;
  const onDefaultChange = destField ? (v) => setF({ ...f, [destField]: v }) : null;
  const defaultValue = destField ? (f[destField] || "") : baseDestination;
  const accentHex = ACCENTS[t.accent] || ACCENTS.indigo;

  // Dirty tracking: serialize the full design and compare to the saved baseline.
  // Covers name + destination + content (all code types) + style, regardless of
  // which step you're on.
  const currentSnap = () => JSON.stringify({ n: name || "", d: baseDestination, c: { type, form: f }, s: style });
  // Re-baseline whenever the loaded/saved link changes (runs after the load
  // effect's batched setStates commit, so it captures the settled design).
  React.useEffect(() => { if (link) savedSnap.current = currentSnap(); }, [link]);
  // The passcode change to persist with the next save: undefined = leave protection
  // as-is, "" = clear it, "<value>" = set/replace. With protection on we only send a
  // value when the user typed a new one (empty keeps the existing hash).
  const wasProtected = !!(link && link.passcode_protected);
  const passcodePatch = protect
    ? (passcode ? passcode : undefined)
    : (wasProtected ? "" : undefined);
  const builderDirty = (!!link && currentSnap() !== savedSnap.current) || (!!link && passcodePatch !== undefined);

  // Save the whole code via the flow editor's saver, catching the free-tier limit
  // (backend throws resource-exhausted) → friendly upsell instead of a dead error.
  const isOverLimit = (e) => !!e && (String(e && e.code).indexOf("resource-exhausted") >= 0 || /resource-exhausted|plan limit reached/i.test((e && e.message) || ""));
  const runSave = async () => {
    if (simLimit) { setLimitGate({ open: true, count: 3 }); return; } // design-tool preview
    try {
      if (flowRef.current.save) await flowRef.current.save();
    } catch (e) {
      if (isOverLimit(e)) setLimitGate({ open: true, count: (e.details && e.details.limit) || 3 });
    }
  };
  // Save is gated: signed-out users get the sign-in/register prompt first.
  const onSave = () => { if (needsAuth) { openAuth("save"); return; } runSave(); };
  // Download needs a REAL saved code (the QR must encode a code that exists), so
  // it's locked until signed-in AND saved. Clicking a locked download nudges the
  // user through the same save/register flow.
  const downloadLock = needsAuth ? "auth" : (isNew ? "unsaved" : null);
  const onLockedDownload = () => { if (needsAuth) { openAuth("download"); return; } runSave(); };
  const onUpgrade = (reason) => { setUpgradeReason(typeof reason === "string" ? reason : null); setUpgradeOpen(true); };
  // Start a fresh draft: cleared name/destination, no link until the first save.
  const onNewCode = () => {
    setLink(null); setIsNew(true);
    setName(tr('mqr.builder.newcode.defaultname'));
    setType("url"); setForms((fs) => ({ ...fs, url: { ...fs.url, url: "" } }));
    setProtect(false); setPasscode("");
    setFlow({ dirty: false, saving: false, canSave: false });
    try { history.replaceState(null, "", location.pathname + location.hash); } catch (e) { /* no-op */ }
  };
  // The flow editor created/updated the link → become its editor + reflect in the URL.
  const onSaved = (updated) => {
    setLink(updated); setIsNew(false);
    // Adopt the persisted style (logo now a Storage URL, not base64) so the QR + the
    // dirty baseline match what's saved and the next save doesn't re-upload.
    if (updated && updated.style) setStyle(updated.style);
    // Re-sync protection state from the saved result; clear the pending input.
    setProtect(!!(updated && updated.passcode_protected)); setPasscode("");
    try { history.replaceState(null, "", location.pathname + "?code=" + encodeURIComponent(updated.code) + location.hash); } catch (e) { /* no-op */ }
  };

  const steps = [
    { key: "type", title: tr('mqr.builder.step.type.title'), sub: tr('mqr.builder.step.type.sub') },
    { key: "style", title: tr('mqr.builder.step.style.title'), sub: tr('mqr.builder.step.style.sub') },
    { key: "flow", title: tr('mqr.builder.step.flow.title'), sub: tr('mqr.builder.step.flow.sub'), locked: flowLocked },
  ];

  return (
    <div className="bld">
      <Topbar name={name} setName={setName} theme={t.theme} setTheme={(v) => setTweak("theme", v)}
        dirty={flow.dirty} saving={flow.saving} canSave={flow.canSave} saved={!!link && !isNew}
        onSave={onSave} onNewCode={onNewCode} />
      {/* Split: the stepped builder sits on the LEFT (type → style → flows, nav
          pinned to its bottom so Back/Continue stay put), and the QR sits LARGE
          on the RIGHT, always visible. */}
      <div className="bld-body">
        <div className="bld-right">
          <Stepper steps={steps} step={step} setStep={setStep} />

          <div className="bld-right__body">
            <div className={"bld-step bld-step--" + steps[step].key}>
              {step === 0 && (
                <div className="bld-typecols">
                  <div>
                    <div className="bld-grouplabel" style={{ marginTop: 0 }}>{tr('mqr.builder.codetype')}</div>
                    <div className="bld-types">
                      {TYPES.map((ty) => (
                        <button key={ty.key} className="bld-type" data-on={type === ty.key} onClick={() => setType(ty.key)}>
                          <span className="bld-type__ic"><Icon name={ty.icon} size={18} /></span>
                          <span className="bld-type__name">{ty.name}</span>
                          <span className="bld-type__sub">{ty.sub}</span>
                        </button>
                      ))}
                    </div>
                  </div>
                  <div>
                    <div className="bld-grouplabel" style={{ marginTop: 0 }}>{tr('mqr.builder.content')}</div>
                    <div className="bld-form">
                      <FormFields type={type} f={f} set={setF} />
                    </div>
                  </div>
                </div>
              )}

              {step === 1 && (
                <React.Fragment>
                  {canUsePresets && (
                    <StylePresetBar presets={presets} busy={presetBusy}
                      onApply={applyPreset} onSave={saveCurrentPreset} onDelete={deletePreset} />
                  )}
                  <StylePanel s={style} set={setStyle} />
                </React.Fragment>
              )}

              {/* The Flow section OWNS the save state + saver, so it must stay
                  mounted on every step — otherwise editing the destination/style on
                  steps 1-2 couldn't enable Save. Shown only on the Flows step. */}
              <div style={step === 2 ? undefined : { display: "none" }}>
                <SecurityCard protect={protect} passcode={passcode} protectedNow={wasProtected}
                  locked={flowLocked} onToggle={setProtect} onPasscode={setPasscode}
                  onUpgrade={() => onUpgrade('passcode')} />
                <FlowSection code={liveCode} link={link} baseDestination={baseDestination} defaultValue={defaultValue} onDefaultChange={onDefaultChange} name={name} content={{ type, form: f }} style={style} passcode={passcodePatch} builderDirty={builderDirty} draft={isNew}
                  locked={flowLocked} onSaved={onSaved} saveRef={flowRef} onState={setFlow} onUpgrade={() => onUpgrade('flow')} />
              </div>
            </div>
          </div>

          {/* Nav pinned to the bottom of the right column → constant height */}
          <div className="bld-footer">
            {step > 0
              ? <Button variant="ghost" iconLeft={<Icon name="arrow" size={15} style={{ transform: "scaleX(-1)" }} />} onClick={() => setStep(step - 1)}>{tr('mqr.builder.step.back')}</Button>
              : <span />}
            {step < steps.length - 1
              ? <Button variant="primary" iconRight={<Icon name="arrow" size={15} />} onClick={() => setStep(step + 1)}>{tr('mqr.builder.step.continue')}</Button>
              : <span />}
          </div>
        </div>

        <div className="bld-hero">
          <Preview s={style} codeData={codeData} dest={dest} name={name} scans={scans} lock={downloadLock} onLockedDownload={onLockedDownload} />
        </div>
      </div>

      {auth.open && (
        <AuthGate action={auth.action} sent={auth.sent} busy={auth.busy} error={auth.error}
          email={auth.email} onEmail={(v) => setAuth((a) => ({ ...a, email: v }))}
          onSubmit={submitAuth} onClose={() => setAuth((a) => ({ ...a, open: false }))} />
      )}
      {limitGate.open && (
        <LimitGate limit={limitGate.count} onUpgrade={() => { setLimitGate((g) => ({ ...g, open: false })); onUpgrade(); }} onClose={() => setLimitGate((g) => ({ ...g, open: false }))} />
      )}
      {upgradeOpen && UpgradeDialog && (
        <UpgradeDialog
          plans={window.MQR.plansAbove(tr, plan || "free", { flagNoFlows: upgradeReason === 'flow' })}
          currentPlan={plan || "free"}
          defaultAnnual={false}
          title={tr('mqr.upgrade.title')}
          labels={window.MQR.upgradeLabels(tr)}
          onSelect={(key, annual) => Api.checkout(key, annual)}
          onClose={() => { setUpgradeOpen(false); setUpgradeReason(null); }} />
      )}

      <TweaksPanel>
        <TweakSection label={tr('mqr.builder.tweaks.appearance')} />
        <TweakToggle label={tr('mqr.builder.tweaks.darkapp')} value={t.theme === "dark"} onChange={(v) => setTweak("theme", v ? "dark" : "light")} />
        <TweakColor label={tr('mqr.builder.tweaks.accent')} value={accentHex}
          options={[ACCENTS.indigo, ACCENTS.blue, ACCENTS.green, ACCENTS.pink, ACCENTS.amber]}
          onChange={(v) => setTweak("accent", Object.keys(ACCENTS).find((k) => ACCENTS[k] === v) || "indigo")} />
        <TweakSection label={tr('mqr.builder.tweaks.preset.section')} />
        <TweakRadio label={tr('mqr.builder.tweaks.preset')} value={t.preset}
          options={[{ value: "classic", label: tr('mqr.builder.tweaks.preset.classic') }, { value: "rounded", label: tr('mqr.builder.tweaks.preset.rounded') }, { value: "dots", label: tr('mqr.builder.tweaks.preset.dots') }]}
          onChange={(v) => setTweak("preset", v)} />
        <TweakSection label={tr('mqr.builder.tweaks.flow.section')} />
        <TweakToggle label={tr('mqr.builder.tweaks.simfree')} value={simFree} onChange={setSimFree} />
        <TweakToggle label={tr('mqr.builder.tweaks.simsignedout')} value={simSignedOut} onChange={setSimSignedOut} />
        <TweakToggle label={tr('mqr.builder.tweaks.simlimit')} value={simLimit} onChange={setSimLimit} />
      </TweaksPanel>
    </div>
  );
}

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