// Surface 3 — Layout editor (the workhorse).
// Left column (~70%): per-layout edit form
// Right column (~30%): Section context, anecdotes, other suggestions

function SurfaceLayoutEditor({ course, selectedLayoutId, onBack,
  onChangeLayoutStatus, onSelectLayout, draft: externalDraft, onChangeDraft, allDrafts,
  gamingQuizEnabled = false, dftiFlow, onChangeDftiFlow }) {
  // Find the selected layout across modules
  let module = null, layout = null;
  course.modules.forEach(m => {
    m.layouts.forEach(l => { if (l.id === selectedLayoutId) { module = m; layout = l; } });
  });
  // Dangling / missing selection: fall back to the first layout that exists.
  // A from-zero course may have none at all — show a friendly empty state
  // instead of crashing (the old fallback hardcoded the demo course's M2-L3).
  const dangling = !layout;
  if (!layout) {
    module = course.modules.find(m => (m.layouts || []).length > 0) || null;
    layout = module ? module.layouts[0] : null;
  }
  // CRITICAL: sync the app-level selection to the fallback. app.jsx keys the
  // draft read/write by selectedLayoutId — without this, edits made while a
  // dangling id is selected would render THIS layout but save under the
  // dangling key (silently lost to export/preview).
  const fallbackId = dangling && layout ? layout.id : null;
  React.useEffect(() => {
    if (fallbackId && onSelectLayout) {
      // SAY SO. Silently opening a different layout is how Omar ended up on
      // M1.L1's text editor after pressing "Jump to" on a quiz threshold warning
      // (2026-07-30) — the target had been deleted, and the editor substituted the
      // first layout it could find without a word. Landing somewhere unrelated
      // with no explanation reads as the app being broken.
      //
      // Carried on a module-level one-shot rather than component state because
      // app.jsx renders this component with `key={selectedLayoutId}` — re-pointing
      // the selection UNMOUNTS us, so anything held in state here dies before it
      // can be shown. The remounted instance reads it once and clears it.
      window.__dynamoDanglingJump = { missing: selectedLayoutId, landedOn: fallbackId };
      onSelectLayout(fallbackId);
    }
  }, [fallbackId, onSelectLayout, selectedLayoutId]);

  // Read-and-clear the one-shot. Matched on `landedOn` so the notice appears only
  // on the instance that was actually substituted in — never on an unrelated later
  // navigation that happens to mount while a stale one-shot is lying around.
  const [danglingNotice, setDanglingNotice] = React.useState(() => {
    const n = window.__dynamoDanglingJump;
    if (n && n.landedOn === selectedLayoutId) {
      window.__dynamoDanglingJump = null;
      return n;
    }
    return null;
  });
  if (!layout) {
    return (
      <div style={{ height: '100%', display: 'flex', flexDirection: 'column',
        alignItems: 'center', justifyContent: 'center', gap: 12, background: 'var(--bg)' }}
        data-screen-label="Surface 3 · Layout editor (empty)">
        <I.PenLine size={28} style={{ color: 'var(--text-faint)' }} />
        <div style={{ fontSize: 14.5, fontWeight: 600, color: 'var(--text)' }}>No layouts yet</div>
        <p style={{ margin: 0, fontSize: 12.5, color: 'var(--text-muted)', maxWidth: 360, textAlign: 'center' }}>
          Add a module and its first layout from Course architecture, then open it here to edit its content.
        </p>
        <button className="btn sm primary" onClick={onBack}>
          <I.ArrowLeft size={12} />Go to Course architecture
        </button>
      </div>
    );
  }

  // `layoutType` is a DISPLAY id. For hidden_items it is
  // a flat/360° display id; the value written to the data is the CANONICAL type
  // (canonicalLayoutType) — hidden_items is UI sugar (Fix 4). Initialise from
  // the existing draft if present (so a 360° layout re-opens on the 360° id),
  // else from the architecture entry.
  const initialDisplayType = window.displayLayoutType(externalDraft && externalDraft.type
    ? externalDraft : layout) || layout.type;
  // Fixed for the life of the editor session: the type-switch dropdown was
  // removed (see LayoutEditorHeader), so nothing changes this after mount.
  const [layoutType] = React.useState(initialDisplayType);
  const canon = window.canonicalLayoutType(layoutType);
  const status = layout.status;
  const setStatus = React.useCallback((next) => {
    if (onChangeLayoutStatus) onChangeLayoutStatus(layout.id, next);
  }, [onChangeLayoutStatus, layout.id]);

  // Draft: the editable layout data. Seeded from sample-content + inline layout
  // fields; persists at the app level so the Module preview and PreviewPane
  // mirror the same edits. `type` is canonicalised so the stored data never
  // carries a hidden_items display id.
  const seed = React.useMemo(() => ({
    ...window.layoutContentBase(layoutType, course.contentMode),
    ...(layout || {}),
    type: window.canonicalLayoutType(layoutType),
  }), [layout, layoutType, course.contentMode]);
  // IMPORTANT: render against a draft whose `.type` matches `layoutType`
  // (canonically). On first open the app-level draft may still be absent or
  // hold a different shape; falling back to `seed` for that render ensures
  // uncontrolled children (e.g. BackgroundPicker's mode/color state) never
  // mount against a stale shape.
  const draft = (externalDraft && externalDraft.type === canon)
    ? externalDraft
    : seed;
  // Track the display id we last seeded for. `layoutType` is now frozen at
  // mount (the type-switch dropdown was removed), so `displayChanged` can no
  // longer become true — kept only so the seeding condition below reads the
  // same as the draft-hydration path it mirrors.
  const seededFor = React.useRef(null);
  React.useEffect(() => {
    // Persist the seed up to the app whenever it diverges from the stored
    // draft — in practice, on entering the editor.
    const displayChanged = seededFor.current != null && seededFor.current !== layoutType;
    if (!externalDraft || externalDraft.type !== canon || displayChanged) {
      onChangeDraft?.(seed);
    }
    seededFor.current = layoutType;
  }, [layoutType, externalDraft, seed, canon, onChangeDraft]);

  // Keep a ref to the latest draft so multiple setPath / setField calls
  // inside the same event handler ACCUMULATE instead of clobbering each
  // other. Without this, e.g. switching a row from Image → Video (which
  // writes videoUrl + videoThumbUrl + clears imageUrl in sequence) would
  // silently drop all but the last write, since each closure read the
  // pre-update draft and the parent re-render hadn't happened yet.
  const draftRef = React.useRef(draft);
  draftRef.current = draft;
  const commit = React.useCallback((next) => {
    draftRef.current = next;
    onChangeDraft?.(next);
  }, [onChangeDraft]);
  const setField = React.useCallback((k, v) => {
    commit({ ...draftRef.current, [k]: v });
  }, [commit]);
  const setPath = React.useCallback((path, v) => {
    commit(setAtPath(draftRef.current, path, v));
  }, [commit]);
  const ctx = window.SAMPLE_SECTION_CONTEXT;

  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: 'minmax(0, 1fr) 440px',
      gridTemplateRows: 'minmax(0, 1fr)',
      height: '100%',
      background: 'var(--bg)',
      overflow: 'hidden',
    }} data-screen-label="Surface 3 · Layout editor">

      {/* ── Left column · Editor ─────────────────────────────────── */}
      <section style={{ display: 'flex', flexDirection: 'column', minHeight: 0 }}>
        <LayoutEditorHeader
          module={module}
          layout={layout}
          layoutType={layoutType}
          status={status}
          onChangeStatus={setStatus}
          course={course}
          onBack={onBack} />

        <div style={{ flex: 1, overflowY: 'auto', padding: '20px 28px 32px',
          display: 'flex', flexDirection: 'column', gap: 20 }}>

          {/* Gaming quiz disabled at course level — see Course settings.
              The layout still exists in the architecture; this ribbon warns the
              author it can't be edited/exported until the master switch is on. */}
          {danglingNotice && (
            <DanglingJumpRibbon missing={danglingNotice.missing}
              onDismiss={() => setDanglingNotice(null)} />
          )}

          {layoutType === 'quiz_gaming' && !gamingQuizEnabled && (
            <GamingQuizDisabledRibbon />
          )}

          {/* The per-type editor handles its own controls (Title/Subtitle
              presence varies per the catalogue). `courseValue` / `onCourseChange`
              expose the shared course-level slice — only quiz_gaming's Profile
              form tab consumes them; every other editor ignores them. */}
          <PerTypeEditor type={layoutType} draft={draft}
            setField={setField} setPath={setPath}
            courseValue={dftiFlow} onCourseChange={onChangeDftiFlow} />
        </div>
      </section>

      {/* ── Right column · Module preview ────────────────────────── */}
      <LayoutEditorRightPanel context={ctx} layout={draft} layoutType={layoutType}
        module={module} onSelectLayout={onSelectLayout} allDrafts={allDrafts} />
    </div>
  );
}

// ── Dangling-selection ribbon — the layout we were asked to open is gone, and
// ── this is a DIFFERENT one. Names the id so a stale link is recognisable.
function DanglingJumpRibbon({ missing, onDismiss }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'flex-start', gap: 10,
      padding: '10px 14px', background: 'var(--warning-bg)',
      border: '1px solid var(--warning)', borderRadius: 'var(--radius-md)',
      color: 'var(--warning-text)',
    }}>
      <I.AlertTriangle size={15} style={{ flexShrink: 0, marginTop: 1 }} />
      <div style={{ fontSize: 12.5, lineHeight: 1.45, flex: 1 }}>
        <strong style={{ fontWeight: 600 }}>That layout no longer exists.</strong>{' '}
        {missing ? <>The link pointed at <code>{missing}</code>, which has been deleted
          from this course.</> : <>The layout that was asked for has been deleted from
          this course.</>}{' '}
        You are looking at a different layout — anything you edit here changes
        <strong> this</strong> one.
      </div>
      <button className="btn sm ghost" onClick={onDismiss}
        title="Dismiss">Dismiss</button>
    </div>
  );
}

// ── Gaming-quiz-disabled ribbon — shown on an existing quiz_gaming layout when
// ── the course-level master switch (Course settings) is off.
function GamingQuizDisabledRibbon() {
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 10,
      padding: '10px 14px', background: 'var(--warning-bg)',
      border: '1px solid var(--warning)', borderRadius: 'var(--radius-md)',
      color: 'var(--warning-text)',
    }}>
      <I.AlertTriangle size={15} style={{ flexShrink: 0 }} />
      <div style={{ fontSize: 12.5, lineHeight: 1.45 }}>
        <strong style={{ fontWeight: 600 }}>Gaming quiz disabled at course level.</strong>{' '}
        Re-enable the gaming quiz flow in Course settings to edit this layout.
      </div>
    </div>
  );
}

// ── Header for the layout editor ────────────────────────────────────────────
// The layout-type dropdown that used to sit between the M#.L# label and the
// status pill is gone (Omar, 2026-07-27): switching a layout's type in place
// "creates a lot of complexity" — it re-seeds the whole draft shape and can
// drop fields that don't migrate. A layout's type is now fixed when it is
// added in Course architecture. Header keeps the M#.L# label + status pill.
function LayoutEditorHeader({ module, layout, status, onChangeStatus, course, onBack }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 12,
      padding: '12px 24px',
      borderBottom: '1px solid var(--border)',
      background: 'var(--surface)',
      minHeight: 56,
    }}>
      <button className="btn sm ghost" onClick={onBack}><I.ArrowLeft size={13} />Back to architecture</button>
      <div style={{ width: 1, height: 20, background: 'var(--border)' }} />

      <span style={{ fontSize: 12, color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>
        {module.id}.L{(module.layouts.findIndex(l => l.id === layout.id) + 1) || layout.n || '?'}
      </span>

      <StatusPill status={status} />

      <div style={{ flex: 1 }} />

      <LayoutHeaderActions status={status} onChangeStatus={onChangeStatus}
        layout={layout} module={module} course={course} />
    </div>
  );
}

// ── LayoutHeaderActions — status-aware primary + secondary CTAs
function LayoutHeaderActions({ status, onChangeStatus, layout, module, course }) {
  const [confirm, setConfirm] = React.useState(null); // 'delete' | 'duplicate' | null

  // SAVE is the only primary action. The old Generate / Re-roll / Re-open-for-
  // edits trio modelled an AI accept-or-regenerate workflow that isn't built and
  // made the editor read as locked (an "Accepted" layout had to be re-opened
  // before it could be typed in). Per Omar 2026-07-26 the editor now has exactly
  // two actions — Save, plus Duplicate/Delete in the kebab — and fields are
  // always editable.
  const [saving, setSaving] = React.useState(false);
  const [saveError, setSaveError] = React.useState(null);

  const menuItems = [
    { label: 'Duplicate layout', icon: 'Copy',
      hint: 'Copy this layout into another module',
      onSelect: () => setConfirm('duplicate') },
    { divider: true },
    { label: 'Delete layout', icon: 'Trash', danger: true,
      onSelect: () => setConfirm('delete') },
  ];
  const overflow = <KebabMenu items={menuItems} />;

  // Writes the whole course draft to the server, then marks this layout saved.
  // Status only flips on a CONFIRMED server write — a failed save must never
  // read as "Saved".
  // A read-only role is not offered this button (see `primary` below), and the
  // handler refuses too — the same two-layer guard as the shared `DraftSaveButton`,
  // for the same reason: `disabled` is presentation, this is the control.
  const readOnly = !!window.dynamoReadOnly;

  const handleSave = async () => {
    if (readOnly) return;
    setSaving(true);
    setSaveError(null);
    try {
      const save = window.dynamoSaveDraftToServer;
      const result = save ? await save() : { ok: false, message: 'save unavailable' };
      if (result && result.ok) {
        onChangeStatus('accepted'); // rendered as "Saved"
      } else {
        setSaveError((result && result.message) || 'Save failed');
      }
    } finally {
      setSaving(false);
    }
  };

  const secondary = saveError
    ? (
      <span title={saveError}
        style={{ display: 'inline-flex', alignItems: 'flex-start', gap: 5,
          // `--danger-text` is not defined anywhere (index.html declares
          // `--error-text`), and this was the ONE use of it with no fallback — so a
          // failed layout save rendered "Not saved: …" in the inherited body colour
          // instead of red. An error that does not look like an error is the same
          // defect as a warning that looks like a success
          // (`feedback_honest_gates_over_standins`). Found by sweeping every
          // `var(--token)` in apps/fe/src against the tokens index.html actually
          // declares, after I introduced an identical slip in DraftSaveButton.
          // WRAPS since 2026-08-19, the same fix as `DraftSaveButton`: clipped to
          // one 240px line, a 79-character refusal reached Omar as "your role is
          // reviewer — you can". A reason cut off before it gives the reason is
          // worse than no message, because it looks like a fault in the page.
          fontSize: 11.5, color: 'var(--error-text)', maxWidth: 420,
          lineHeight: 1.35, textAlign: 'left' }}>
        <I.AlertTriangle size={12} style={{ flex: '0 0 auto', marginTop: 1 }} />
        <span>Not saved: {saveError}</span>
      </span>
    )
    : null;

  const primary = (
    <button className="btn sm primary" onClick={handleSave} disabled={saving || readOnly}
      title={readOnly
        ? 'Your role is reviewer — you can open and read this course, but not save changes'
        : 'Save this course to the server'}>
      <I.Check size={13} />{saving ? 'Saving…' : 'Save'}
    </button>
  );

  return (
    <>
      {overflow}
      {secondary}
      {primary}
      {confirm === 'delete' && (
        <ConfirmDialog
          icon="Trash"
          title={`Delete layout ${layout.id}?`}
          body="This removes the layout from the module. The source paragraphs it covered will be marked unmapped and surface in Validation."
          confirmLabel="Delete layout"
          tone="danger"
          onCancel={() => setConfirm(null)}
          onConfirm={() => {
            setConfirm(null);
            // Actually delete. This used to just close the dialog, so the layout
            // stayed in the module and in the export (fixed 2026-07-27) — the
            // same no-op Duplicate had, ten lines below.
            if (window.dynamoDeleteLayout) window.dynamoDeleteLayout(layout.id);
          }} />
      )}
      {confirm === 'duplicate' && (
        <DuplicateLayoutDialog
          layout={layout}
          sourceModule={module}
          modules={course?.modules || []}
          onCancel={() => setConfirm(null)}
          onConfirm={(targetModuleId) => {
            setConfirm(null);
            // Actually duplicate. This used to just close the dialog, so the
            // action silently did nothing (fixed 2026-07-26).
            if (window.dynamoDuplicateLayout) {
              window.dynamoDuplicateLayout(layout.id, targetModuleId);
            }
          }} />
      )}
    </>
  );
}

// ── DuplicateLayoutDialog ─ pick which module the duplicate lands in
function DuplicateLayoutDialog({ layout, sourceModule, modules, onCancel, onConfirm }) {
  const [targetId, setTargetId] = React.useState(sourceModule?.id || modules[0]?.id);
  const target = modules.find(m => m.id === targetId) || modules[0];
  const meta = (window.LAYOUT_TYPES || []).find(t => t.id === layout.type) || {};
  const Icon = I[meta.icon] || I.Hash;

  return (
    <>
      <div onClick={onCancel} style={{
        position: 'fixed', inset: 0, background: 'rgba(15,23,42,0.4)',
        backdropFilter: 'blur(2px)', zIndex: 60,
      }} />
      <div style={{
        position: 'fixed', top: '22%', left: '50%', transform: 'translateX(-50%)',
        width: 'min(520px, 92vw)', zIndex: 61,
        background: 'var(--surface)', border: '1px solid var(--border)',
        borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-xl)',
        padding: 20,
      }}>
        <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, marginBottom: 16 }}>
          <div style={{
            width: 32, height: 32, borderRadius: '50%', background: 'var(--accent-bg)',
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            color: 'var(--accent-text)', flexShrink: 0,
          }}>
            <I.Copy size={16} />
          </div>
          <div style={{ flex: 1 }}>
            <h3 style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>
              Duplicate layout {layout.id}
            </h3>
          </div>
        </div>

        {/* Layout summary */}
        <div style={{
          display: 'flex', alignItems: 'center', gap: 10,
          padding: '10px 12px', background: 'var(--surface-inset)',
          borderRadius: 'var(--radius-md)', marginBottom: 14,
        }}>
          <Icon size={14} style={{ color: 'var(--text-muted)' }} />
          <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11.5, color: 'var(--text-muted)' }}>
            {layout.type}
          </span>
          <span style={{ width: 1, height: 12, background: 'var(--border)' }} />
          <span className="truncate" style={{ fontSize: 12.5, color: 'var(--text)', flex: 1 }}>
            {locText(layout.summary)}
          </span>
        </div>

        {/* Destination module */}
        <div style={{ marginBottom: 18 }}>
          <label style={{ display: 'block', fontSize: 11, color: 'var(--text-muted)',
            fontWeight: 600, letterSpacing: '.04em', textTransform: 'uppercase', marginBottom: 6 }}>
            Destination module
          </label>
          <div style={{ display: 'grid', gap: 4, maxHeight: 200, overflowY: 'auto',
            border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', padding: 4 }}>
            {modules.map(m => {
              const sel = m.id === targetId;
              const isSource = m.id === sourceModule?.id;
              return (
                <button key={m.id} onClick={() => setTargetId(m.id)}
                  style={{
                    display: 'grid', gridTemplateColumns: 'auto auto 1fr auto', gap: 10,
                    alignItems: 'center', padding: '8px 10px', textAlign: 'left',
                    background: sel ? 'var(--accent-bg)' : 'transparent',
                    border: '1px solid', borderColor: sel ? 'var(--accent-border)' : 'transparent',
                    borderRadius: 'var(--radius)', cursor: 'default', fontFamily: 'inherit',
                    color: 'var(--text)',
                  }}
                  onMouseOver={e => { if (!sel) e.currentTarget.style.background = 'var(--surface-inset)'; }}
                  onMouseOut={e => { if (!sel) e.currentTarget.style.background = 'transparent'; }}>
                  <span style={{
                    width: 14, height: 14, borderRadius: '50%',
                    border: '1.5px solid', borderColor: sel ? 'var(--accent)' : 'var(--border-strong)',
                    background: sel ? 'var(--accent)' : 'transparent',
                    display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                  }}>{sel && <I.Check size={8} style={{ color: '#fff' }} />}</span>
                  <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
                    color: 'var(--text-muted)', letterSpacing: '.04em' }}>{m.id}</span>
                  <span className="truncate" style={{ fontSize: 12.5 }}>{locText(m.title)}</span>
                  <span style={{ fontSize: 11, color: 'var(--text-faint)',
                    fontFamily: 'var(--font-mono)' }}>
                    {(m.layouts?.length || 0)} layouts
                    {isSource && <span style={{ marginLeft: 6,
                      padding: '1px 5px', background: 'var(--surface-inset)',
                      color: 'var(--text-muted)', borderRadius: 3,
                      fontFamily: 'inherit', textTransform: 'uppercase', letterSpacing: '.04em',
                      fontSize: 9.5, fontWeight: 600 }}>Current</span>}
                  </span>
                </button>
              );
            })}
          </div>
        </div>

        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
          <button className="btn sm ghost" onClick={onCancel}>Cancel</button>
          <button className="btn sm primary" onClick={() => onConfirm(targetId)}>
            <I.Copy size={13} />Duplicate into {target?.id}
          </button>
        </div>
      </div>
    </>
  );
}

// ── KebabMenu — generic overflow menu (used by the header)
function KebabMenu({ items = [] }) {
  const [open, setOpen] = React.useState(false);
  return (
    <div style={{ position: 'relative' }}>
      <button className="btn sm ghost" onClick={() => setOpen(o => !o)}
        title="More"><I.MoreHorizontal size={13} /></button>
      {open && (
        <>
          <div onClick={() => setOpen(false)}
            style={{ position: 'fixed', inset: 0, zIndex: 30 }} />
          <div style={{
            position: 'absolute', top: 'calc(100% + 4px)', right: 0, zIndex: 31,
            minWidth: 240, padding: 6,
            background: 'var(--surface)', border: '1px solid var(--border)',
            borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-lg)',
          }}>
            {items.map((it, i) => it.divider ? (
              <div key={i} style={{ height: 1, background: 'var(--border)', margin: '4px -2px' }} />
            ) : (
              <button key={i} onClick={() => { it.onSelect?.(); setOpen(false); }}
                style={{
                  display: 'flex', alignItems: 'flex-start', gap: 8,
                  width: '100%', padding: '7px 9px', textAlign: 'left',
                  background: 'transparent', border: 0, borderRadius: 'var(--radius)',
                  cursor: 'default', fontFamily: 'inherit',
                  color: it.danger ? 'var(--error-text)' : 'var(--text)',
                  fontSize: 12.5,
                }}
                onMouseOver={e => e.currentTarget.style.background = 'var(--surface-inset)'}
                onMouseOut={e => e.currentTarget.style.background = 'transparent'}>
                {it.icon && React.createElement(I[it.icon] || I.Hash, {
                  size: 13, style: { marginTop: 2,
                    color: it.danger ? 'var(--error-text)' : 'var(--text-muted)' } })}
                <div style={{ flex: 1 }}>
                  <div>{it.label}</div>
                  {it.hint && <div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: 1 }}>
                    {it.hint}</div>}
                </div>
              </button>
            ))}
          </div>
        </>
      )}
    </div>
  );
}

// ── ConfirmDialog — small modal for destructive actions
function ConfirmDialog({ icon = 'AlertCircle', title, body, confirmLabel = 'Confirm',
  cancelLabel = 'Cancel', tone = 'danger', onConfirm, onCancel }) {
  const Icon = I[icon] || I.AlertCircle;
  const tint = tone === 'danger' ? 'var(--error-text)'
             : tone === 'warning' ? 'var(--warning-text)'
             : 'var(--accent-text)';
  const bg = tone === 'danger' ? 'var(--error-bg)'
           : tone === 'warning' ? 'var(--warning-bg)'
           : 'var(--accent-bg)';
  return (
    <>
      <div onClick={onCancel} style={{
        position: 'fixed', inset: 0, background: 'rgba(15,23,42,0.4)',
        backdropFilter: 'blur(2px)', zIndex: 60,
      }} />
      <div style={{
        position: 'fixed', top: '30%', left: '50%', transform: 'translateX(-50%)',
        width: 'min(440px, 92vw)', zIndex: 61,
        background: 'var(--surface)', border: '1px solid var(--border)',
        borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-xl)',
        padding: 18,
      }}>
        <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, marginBottom: 12 }}>
          <div style={{
            width: 32, height: 32, borderRadius: '50%', background: bg,
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            color: tint, flexShrink: 0,
          }}>
            <Icon size={16} />
          </div>
          <div style={{ flex: 1 }}>
            <h3 style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>{title}</h3>
            <p style={{ margin: '6px 0 0', fontSize: 12.5, color: 'var(--text-muted)',
              lineHeight: 1.5 }}>{body}</p>
          </div>
        </div>
        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
          <button className="btn sm ghost" onClick={onCancel}>{cancelLabel}</button>
          <button className={`btn sm ${tone === 'danger' ? 'danger' : 'primary'}`}
            style={tone === 'warning' ? { background: 'var(--warning)', color: '#fff',
              borderColor: 'var(--warning)' } : undefined}
            onClick={onConfirm}>{confirmLabel}</button>
        </div>
      </div>
    </>
  );
}


// ── EditorBlock — section header inside the editor ─────────────────────────
function EditorBlock({ label, count, action, children }) {
  return (
    <section>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
        <h3 style={{ margin: 0, fontSize: 13, fontWeight: 600, color: 'var(--text)',
          letterSpacing: '-.005em', whiteSpace: 'nowrap', flexShrink: 0 }}>{label}</h3>
        {count != null && <span style={{
          fontSize: 11, color: 'var(--text-faint)',
          fontFamily: 'var(--font-mono)',
          background: 'var(--surface-inset)', padding: '1px 5px', borderRadius: 3,
        }}>{count}</span>}
        <div style={{ flex: 1, height: 1, background: 'var(--border)', marginLeft: 4 }} />
        {action}
      </div>
      {children}
    </section>
  );
}

// ── SequenceEditor — the populated sequence (M2-L3) ────────────────────────
function SequenceEditor() {
  const tabs = window.SAMPLE_SEQUENCE_TABS;
  const [activeTab, setActiveTab] = React.useState(tabs[0].id);
  const active = tabs.find(t => t.id === activeTab);

  return (
    <>
      {/* Tab preview strip */}
      <EditorBlock label="Tab progression preview"
        action={<button className="btn sm ghost"><I.Eye size={12} />Preview in player</button>}>
        <div style={{
          padding: '14px 16px', background: 'var(--surface)',
          border: '1px solid var(--border)', borderRadius: 'var(--radius-md)',
          display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'center',
        }}>
          <div style={{ display: 'flex', gap: 10 }}>
            {tabs.map((t, i) => (
              <button key={t.id} onClick={() => setActiveTab(t.id)}
                style={{
                  width: 22, height: 22, borderRadius: '50%',
                  border: '1.5px solid', borderColor: t.id === activeTab ? 'var(--accent)' : 'var(--border-strong)',
                  background: t.id === activeTab ? 'var(--accent)' : 'transparent',
                  color: t.id === activeTab ? '#fff' : 'var(--text-muted)',
                  fontSize: 10.5, fontWeight: 600, cursor: 'default', fontFamily: 'inherit',
                  display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                  position: 'relative',
                }}>
                {i + 1}
                {t.branch === 'correct' && (
                  <span style={{ position: 'absolute', top: -4, right: -4,
                    width: 10, height: 10, background: 'var(--success)', color: '#fff',
                    borderRadius: '50%', fontSize: 7, lineHeight: '10px',
                    display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontWeight: 700 }}>
                    ✓
                  </span>
                )}
              </button>
            ))}
          </div>
          <span style={{ fontSize: 11.5, color: 'var(--text-faint)' }}>
            Click a dot to edit that tab · 6 tabs · learner sees this strip in the Player footer
          </span>
        </div>
      </EditorBlock>

      {/* Tab list */}
      <EditorBlock label="Tabs" count={tabs.length}
        action={<button className="btn sm"><I.Plus size={12} />Add tab</button>}>
        <div style={{ display: 'grid', gap: 6 }}>
          {tabs.map((t, i) => (
            <SequenceTabRow key={t.id} tab={t} index={i}
              active={t.id === activeTab}
              onClick={() => setActiveTab(t.id)} />
          ))}
        </div>
      </EditorBlock>

      {/* Active tab editor */}
      <EditorBlock label={`Editing tab ${tabs.findIndex(t => t.id === activeTab) + 1}: ${active.title}`}>
        <SequenceTabEditor tab={active} />
      </EditorBlock>
    </>
  );
}

function SequenceTabRow({ tab, index, active, onClick }) {
  const kindMeta = {
    text:     { label: 'Text',     icon: I.FileText, tint: 'var(--text-muted)' },
    video:    { label: 'Video',    icon: I.Film,     tint: 'var(--accent)' },
    questionMultiple: { label: 'Question', icon: I.AlertCircle, tint: 'var(--warning)' },
    questionShort: { label: 'Short Q', icon: I.AlertCircle, tint: 'var(--warning)' },
    question: { label: 'Question', icon: I.AlertCircle, tint: 'var(--warning)' },
  }[tab.kind];
  const KI = kindMeta?.icon || I.Hash;
  return (
    <div onClick={onClick}
      style={{
        display: 'grid', gridTemplateColumns: 'auto 28px 100px 1fr auto auto',
        gap: 10, alignItems: 'center',
        padding: '8px 10px',
        background: active ? 'var(--accent-bg)' : 'var(--surface)',
        border: '1px solid', borderColor: active ? 'var(--accent-border)' : 'var(--border)',
        borderRadius: 'var(--radius)', cursor: 'default',
      }}
      onMouseOver={e => { if (!active) e.currentTarget.style.borderColor = 'var(--border-strong)'; }}
      onMouseOut={e => { if (!active) e.currentTarget.style.borderColor = 'var(--border)'; }}>
      <I.GripVertical size={13} style={{ color: 'var(--text-faint)' }} />
      <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-muted)' }}>
        T{index + 1}
      </span>
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5,
        fontSize: 11.5, color: kindMeta?.tint || 'var(--text-muted)' }}>
        <KI size={12} />{kindMeta?.label || tab.kind}
      </span>
      <span className="truncate" style={{ fontSize: 12.5, color: 'var(--text)' }}>
        {tab.title}
      </span>
      <span style={{ fontSize: 11, color: 'var(--text-faint)' }}>
        {tab.mobileText}
      </span>
      <button className="btn sm ghost"><I.MoreHorizontal size={12} /></button>
    </div>
  );
}

function SequenceTabEditor({ tab }) {
  return (
    <div style={{ display: 'grid', gap: 14 }}>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 200px', gap: 12 }}>
        <input key={tab.id + ':title'} className="field" defaultValue={tab.title}
          style={{ height: 36, fontSize: 14, fontWeight: 600 }} />
        <input key={tab.id + ':mob'} className="field" defaultValue={tab.mobileText} placeholder="Mobile label"
          style={{ height: 36 }} />
      </div>

      {tab.kind === 'text' && (
        <>
          <RichTextEditor key={tab.id + ':body'} value={tab.body}
            placeholder="Tab body text — bold, italic, lists, inline links supported." />
          <MediaSlot kind="image" label="Background image (optional)"
            bound={{ alt: 'Scenario opener · office', previewBg: 'linear-gradient(135deg, #64748b, #1e293b)', generatedBy: 'Gemini' }} />
        </>
      )}

      {tab.kind === 'video' && (
        <>
          <MediaSlot kind="video"
            bound={{ alt: 'Sarah prep · how to use SBI (00:00 - 01:24)', duration: '01:24',
              previewBg: 'linear-gradient(135deg, #475569, #0f172a)', generatedBy: 'HeyGen' }} />
          <RichTextEditor key={tab.id + ':cap'} value={tab.body} placeholder="Caption text under video…" />
        </>
      )}

      {(tab.kind === 'questionMultiple' || tab.kind === 'question' || tab.kind === 'questionShort') && (
        <>
          <RichTextEditor key={tab.id + ':prompt'} value={tab.body} placeholder="Question prompt…" minHeight={64} />
          <AnswersEditor answers={[
            { text: 'When you missed the API spec review on Tuesday and the design sign-off on Friday, two engineers were blocked.',
              isCorrect: true, feedback: 'Right — specific, observable, and tied to impact.' },
            { text: 'I\'ve noticed you\'ve been disengaged from the team lately.',
              isCorrect: false, feedback: 'Too vague and reads as a character judgement.' },
            { text: 'Your work has been below expectations for several weeks now.',
              isCorrect: false, feedback: 'No specifics — hard to act on.' },
          ]} />
        </>
      )}
    </div>
  );
}

function CorrectWrongFeedbackEditor() {
  return (
    <div style={{
      border: '1px solid var(--border)', borderRadius: 'var(--radius-md)',
      background: 'var(--surface-2)', overflow: 'hidden',
    }}>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 0 }}>
        <div style={{ padding: 12, borderRight: '1px solid var(--border)' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
            <I.CheckCircle size={13} style={{ color: 'var(--success)' }} />
            <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--success-text)' }}>Correct feedback (T4)</span>
          </div>
          <p style={{ margin: 0, fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.5 }}>
            Naming the specific behaviour without judgement is the SBI core move.
          </p>
        </div>
        <div style={{ padding: 12 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
            <I.X size={13} style={{ color: 'var(--error)' }} />
            <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--error-text)' }}>Wrong feedback (branch tab)</span>
          </div>
          <p style={{ margin: 0, fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.5 }}>
            That phrasing speaks to character rather than behaviour. Try naming a concrete action…
          </p>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { SurfaceLayoutEditor, LayoutEditorHeader,
  EditorBlock, SequenceEditor, LayoutHeaderActions,
  KebabMenu, ConfirmDialog, DuplicateLayoutDialog });
