// The remaining 10 layout editors — replaces StubEditor for the types not
// already built. Each editor follows the existing component vocabulary:
// EditorBlock, RichTextEditor, MediaSlot, LocalizedStringEditor,
// FourAnswerEditor, FreeFormQuestionEditor, FieldRow, Switch, ColorSwatchRow.
//
// New tools used: ImageHotspotPlacer, PanoramaHotspotPlacer, ThreeDHotspotPlacer,
// InVideoInteractionTimeline (from src/editor-tools.jsx).

// ─────────────────────────────────────────────────────────────────────────────
// 3.1  fullscreen_text_and_image
// ─────────────────────────────────────────────────────────────────────────────
function FullscreenTextAndImageEditor() {
  const c = LAYOUT_CONTENT_SAMPLES.fullscreen_text_and_image;
  return (
    <>
      <EditorBlock label="Background"
        action={<span style={{ fontSize: 11, color: 'var(--text-faint)' }}>
          Upload, generate, or pick a solid color
        </span>}>
        <BackgroundPicker
          defaultPrompt="A tense conversation between two managers in a glass-walled office, soft afternoon light"
          defaultColor="#0f172a"
          defaultFilename="office-tense-conversation.jpg" />
      </EditorBlock>
      <EditorBlock label="Body text">
        <RichTextEditor defaultValue={c.text}
          placeholder="One line of body copy that overlays the background…" />
      </EditorBlock>
      <EditorBlock label="Colors">
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
          <FieldRow label="Title text color"><ColorSwatchRow value={c.titleTextColor} /></FieldRow>
          <FieldRow label="Body text color"><ColorSwatchRow value={c.textColor} /></FieldRow>
        </div>
      </EditorBlock>
    </>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// 3.6  horizontal_tabs
// ─────────────────────────────────────────────────────────────────────────────
function HorizontalTabsEditor() {
  const c = LAYOUT_CONTENT_SAMPLES.horizontal_tabs;
  const [active, setActive] = React.useState(0);
  const [tabs, setTabs] = React.useState(c.tabs);
  const sel = tabs[active] || tabs[0];

  return (
    <>
      <EditorBlock label="Tabs" count={tabs.length}
        action={<button className="btn sm" onClick={() => {
          setTabs([...tabs, { tabTitle: `Tab ${tabs.length + 1}`, tabText: '' }]);
        }}><I.Plus size={12} />Add tab</button>}>
        <div style={{ background: 'var(--surface)', border: '1px solid var(--border)',
          borderRadius: 'var(--radius-md)', padding: 6 }}>
          <div style={{ display: 'flex', gap: 4, marginBottom: 6 }}>
            {tabs.map((t, i) => (
              <button key={i} onClick={() => setActive(i)}
                style={{
                  flex: '1 1 0', padding: '8px 10px', fontSize: 12, textAlign: 'left',
                  background: i === active ? 'var(--accent-bg)' : 'var(--surface-inset)',
                  border: '1px solid', borderColor: i === active ? 'var(--accent-border)' : 'transparent',
                  borderRadius: 'var(--radius)', cursor: 'default', fontFamily: 'inherit',
                  color: i === active ? 'var(--accent-text)' : 'var(--text)',
                  whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                }}>
                <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-muted)' }}>
                  T{i+1}
                </span>&nbsp;{t.tabTitle}
              </button>
            ))}
          </div>
          {tabs.length < 3 && (
            <div style={{ padding: '6px 8px', fontSize: 11.5, color: 'var(--warning-text)',
              background: 'var(--warning-bg)', borderRadius: 'var(--radius-sm)',
              display: 'flex', alignItems: 'center', gap: 6 }}>
              <I.AlertTriangle size={11} />Needs at least 3 tabs (catalogue §3.6).
            </div>
          )}
        </div>
      </EditorBlock>
      <EditorBlock label={`Editing tab ${active+1}: ${sel.tabTitle}`}>
        <div style={{ display: 'grid', gap: 12 }}>
          <input className="field" defaultValue={sel.tabTitle} placeholder="tabTitle (clickable label)"
            style={{ fontWeight: 600 }} />
          <RichTextEditor value={sel.tabText} placeholder="tabText — body content for this tab…" />
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
            <MediaSlot kind="image" label="imageUrl (optional)"
              bound={sel.imageUrl
                ? { alt: 'Tab image', previewBg: 'linear-gradient(135deg, #475569, #1e293b)' }
                : null} />
            <MediaSlot kind="video" label="videoUrl (optional)" />
          </div>
        </div>
      </EditorBlock>
      <EditorBlock label="Tab colors">
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8 }}>
          <FieldRow label="Selected tab background"><ColorSwatchRow value={c.selectedBgColor} /></FieldRow>
          <FieldRow label="Selected tab text"><ColorSwatchRow value={c.selectedTextColor} /></FieldRow>
          <FieldRow label="Tab text color"><ColorSwatchRow value={c.tabTextColor} /></FieldRow>
        </div>
      </EditorBlock>
      {sel.videoUrl && (
        <EditorBlock label={`In-video interactions on tab ${active+1}`}>
          <InVideoInteractionTimeline
            videoThumbUrl={sel.videoThumbUrl}
            duration={272}
            interactions={[]}
            onChange={() => {}} />
        </EditorBlock>
      )}
    </>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// 3.7  vertical_tabs
// ─────────────────────────────────────────────────────────────────────────────
function VerticalTabsEditor() {
  const c = LAYOUT_CONTENT_SAMPLES.vertical_tabs;
  const [active, setActive] = React.useState(0);
  const [tabs, setTabs] = React.useState(c.tabs);
  return (
    <>
      <EditorBlock label="Tabs" count={tabs.length}
        action={<button className="btn sm" onClick={() => {
          setTabs([...tabs, { tabTitle: 'New tab', tabText: '' }]);
        }}><I.Plus size={12} />Add tab</button>}>
        <div style={{ display: 'grid', gridTemplateColumns: '220px 1fr', gap: 12,
          background: 'var(--surface)', border: '1px solid var(--border)',
          borderRadius: 'var(--radius-md)', padding: 8 }}>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
            {tabs.map((t, i) => (
              <button key={i} onClick={() => setActive(i)}
                style={{
                  padding: '10px 12px', textAlign: 'left', fontSize: 12.5,
                  background: i === active ? 'var(--accent-bg)' : 'var(--surface-inset)',
                  color: i === active ? 'var(--accent-text)' : 'var(--text)',
                  border: '1px solid', borderColor: i === active ? 'var(--accent-border)' : 'transparent',
                  borderRadius: 'var(--radius)', cursor: 'default', fontFamily: 'inherit',
                  display: 'flex', alignItems: 'center', gap: 6,
                }}>
                <I.GripVertical size={11} style={{ color: 'var(--text-faint)' }} />
                <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                  {t.tabTitle}
                </span>
              </button>
            ))}
          </div>
          <div style={{ display: 'grid', gap: 10 }}>
            <input className="field" defaultValue={tabs[active]?.tabTitle} placeholder="tabTitle"
              style={{ fontWeight: 600 }} />
            <RichTextEditor value={tabs[active]?.tabText}
              placeholder="tabText — vertical tabs hold rich text only, no media." minHeight={140} />
          </div>
        </div>
      </EditorBlock>
      <EditorBlock label="Colors">
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
          <FieldRow label="Content background color"><ColorSwatchRow value={c.contentBackgroundColor} /></FieldRow>
          <FieldRow label="Title text color"><ColorSwatchRow value={c.titleTextColor} /></FieldRow>
        </div>
      </EditorBlock>
    </>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// 3.9  hidden_items (flat + 360°/virtual-tour)
// ─────────────────────────────────────────────────────────────────────────────
function HiddenItemsEditor() {
  const c = LAYOUT_CONTENT_SAMPLES.hidden_items;
  const [variant, setVariant] = React.useState('flat');     // 'flat' | '360'
  const [pins, setPins] = React.useState(c.items.map((it, i) => {
    const [x, y] = it.itemPosition.split(',').map(Number);
    return { id: 'h' + i, x, y, title: it.itemTitle, text: it.itemText };
  }));
  const [sel, setSel] = React.useState(0);
  const active = pins[sel];

  return (
    <>
      {/* Variant chooser */}
      <EditorBlock label="Image source">
        <div style={{ display: 'flex', gap: 4, padding: 3, background: 'var(--surface-inset)',
          borderRadius: 'var(--radius)', border: '1px solid var(--border)', width: 'fit-content' }}>
          <VarBtn active={variant === 'flat'} onClick={() => setVariant('flat')}>2D image · <code>imageUrl</code></VarBtn>
          <VarBtn active={variant === '360'} onClick={() => setVariant('360')}>360° panorama · <code>image360Url</code></VarBtn>
        </div>
        <div style={{ marginTop: 10 }}>
          {variant === 'flat'
            ? <MediaSlot kind="image" label="Background image"
                bound={{ alt: 'Office scene with hidden hazards',
                  previewBg: 'linear-gradient(135deg, #94a3b8, #64748b)' }} />
            : <>
                <MediaSlot kind="image" label="image360Url (equirectangular panorama)"
                  bound={{ alt: 'Office 360° panorama',
                    previewBg: 'linear-gradient(135deg, #5514B4, #2D0466)' }} />
                <div style={{ marginTop: 8 }}>
                  <label style={{ fontSize: 11.5, color: 'var(--text-muted)' }}>image360CoverText</label>
                  <input className="field" defaultValue="Drag to look around"
                    style={{ width: '100%', marginTop: 4 }} />
                </div>
              </>}
        </div>
      </EditorBlock>

      {/* The placer */}
      <EditorBlock label={`${variant === 'flat' ? 'Image' : 'Panorama'}-hotspot placer`} count={pins.length}>
        {variant === 'flat'
          ? <ImageHotspotPlacer
              imageUrl="placeholder:scene-office-fire" imageLabel="Office background"
              pins={pins} onChange={setPins} selectedIndex={sel} onSelect={setSel}
              colors={{ fill: c.itemColor, stroke: c.itemColor }} />
          : <PanoramaHotspotPlacer
              imageUrl="placeholder:scene-office-fire" image360CoverText="Drag to look around"
              pins={pins} onChange={setPins} selectedIndex={sel} onSelect={setSel} />}
      </EditorBlock>

      {/* Selected hotspot panel editor */}
      {active && (
        <EditorBlock label={`Editing hotspot ${sel+1}`}>
          <div style={{ display: 'grid', gap: 10 }}>
            <input className="field" value={active.title}
              onChange={e => setPins(pins.map((p, i) => i === sel ? { ...p, title: e.target.value } : p))}
              placeholder="Hotspot title" style={{ fontWeight: 600 }} />
            <RichTextEditor value={active.text} placeholder="Hotspot description shown in the panel…" />
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
              <MediaSlot kind="image" label="Hotspot image (optional)" />
              <MediaSlot kind="video" label="Hotspot video (optional)" />
            </div>
            <div style={{ background: 'var(--surface-inset)', padding: '10px 12px',
              borderRadius: 'var(--radius)', fontSize: 11.5, color: 'var(--text-muted)' }}>
              <I.Info size={11} /> Add a <code>questions</code> block instead of media to make this hotspot an in-panel quiz.
            </div>
            <div style={{ display: 'flex', gap: 8 }}>
              <button className="btn sm">
                <I.AlertCircle size={12} />Convert to mini-quiz panel
              </button>
              <button className="btn sm ghost danger"
                onClick={() => { setPins(pins.filter((_, i) => i !== sel)); setSel(Math.max(0, sel - 1)); }}>
                <I.Trash size={12} />Delete hotspot
              </button>
            </div>
          </div>
        </EditorBlock>
      )}

      <EditorBlock label="Panel colors">
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
          <FieldRow label="Hotspot color"><ColorSwatchRow value={c.itemColor} /></FieldRow>
          <FieldRow label="Panel background"><ColorSwatchRow value={c.bgColor} /></FieldRow>
          <FieldRow label="Border color"><ColorSwatchRow value={c.borderColor} /></FieldRow>
          <FieldRow label="Text color"><ColorSwatchRow value={c.textColor} /></FieldRow>
        </div>
      </EditorBlock>
    </>
  );
}

function VarBtn({ active, onClick, children }) {
  return (
    <button onClick={onClick}
      style={{
        padding: '5px 10px', borderRadius: 4, border: 0,
        background: active ? 'var(--surface)' : 'transparent',
        color: active ? 'var(--text)' : 'var(--text-muted)',
        fontSize: 12, fontFamily: 'inherit', cursor: 'default',
        fontWeight: active ? 600 : 500,
        boxShadow: active ? 'var(--shadow-sm)' : 'none',
      }}>{children}</button>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// HTML template picker (Fix C) — the html media kind for a quiz question.
// Replaces the old file-upload stub. Authors choose a template from
// window.HTML_TEMPLATES; the question's content is written as
//   { html: { htmlUrl, htmlAlt }, _templateId }
// where _templateId is an authoring-only sidecar (stripped at emit time).
// ─────────────────────────────────────────────────────────────────────────────
function HtmlTemplateChip({ children, tone = 'neutral' }) {
  const c = tone === 'accent'
    ? { bg: 'var(--accent-bg)', fg: 'var(--accent-text)' }
    : { bg: 'var(--surface-inset)', fg: 'var(--text-muted)' };
  return (
    <span style={{ fontSize: 10.5, padding: '2px 7px', borderRadius: 999, fontWeight: 600,
      background: c.bg, color: c.fg, fontFamily: 'var(--font-mono)', whiteSpace: 'nowrap' }}>
      {children}
    </span>
  );
}

function HtmlTemplateSlot() {
  const templates = window.HTML_TEMPLATES || [];
  const [templateId, setTemplateId] = React.useState(null);
  const [alt, setAlt] = React.useState('');
  const [tooltipValues, setTooltipValues] = React.useState({});
  const [picking, setPicking] = React.useState(false);
  const selected = templates.find(t => t.id === templateId) || null;
  const currentLang = React.useContext(LocDefaultLangContext) || 'en';

  // The content object the editor would persist. The _templateId sidecar lets
  // the editor re-find the template when re-opened; it's stripped at emit time
  // (QuizQuestionMediaSchema has no _templateId). `tooltipValues` IS part of the
  // persisted draft (per-slot LocalizedString filled by the Author). Stamped on
  // the DOM as JSON so the prototype's in-memory selection is inspectable.
  const content = selected
    ? { html: { htmlUrl: selected.renderedHtmlUrl, htmlAlt: alt }, _templateId: selected.id,
        ...(Object.keys(tooltipValues).length ? { tooltipValues } : {}) }
    : null;

  return (
    <div data-html-template-slot data-template-id={templateId || ''}
      data-content={content ? JSON.stringify(content) : ''}
      style={{ padding: 14, background: 'var(--surface)',
        border: '1px dashed var(--border-strong)', borderRadius: 'var(--radius-md)',
        display: 'grid', gap: 10 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <I.FileText size={14} style={{ color: 'var(--text-muted)' }} />
        <span style={{ fontSize: 12.5, fontWeight: 600 }}>HTML document</span>
        <span style={{ fontSize: 11, color: 'var(--text-faint)' }}>
          from the template library
        </span>
      </div>

      {!selected ? (
        <button className="btn sm primary" style={{ alignSelf: 'flex-start' }}
          onClick={() => setPicking(true)}>
          <I.Grid size={12} />Choose template
        </button>
      ) : (
        <div style={{ display: 'grid', gap: 10 }}>
          <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
            {/* Live preview of the chosen template */}
            <div style={{ width: 200, height: 140, flexShrink: 0, background: '#fff',
              border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden',
              position: 'relative' }}>
              <iframe src={selected.previewUrl} title={`Preview: ${selected.name}`}
                tabIndex={-1} scrolling="no"
                style={{ width: '200%', height: '200%', border: 0, transform: 'scale(0.5)',
                  transformOrigin: 'top left', background: '#fff', pointerEvents: 'none' }} />
            </div>
            <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 7 }}>
              <div style={{ fontSize: 13, fontWeight: 600 }}>{selected.name}</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
                <HtmlTemplateChip>{selected.category}</HtmlTemplateChip>
                <HtmlTemplateChip tone="accent">{selected.complexity}</HtmlTemplateChip>
              </div>
              {selected.description && (
                <p style={{ margin: 0, fontSize: 11.5, color: 'var(--text-muted)', lineHeight: 1.5 }}>
                  {selected.description}
                </p>
              )}
              <button className="btn sm" style={{ alignSelf: 'flex-start' }}
                onClick={() => setPicking(true)}>
                <I.RefreshCw size={12} />Change template
              </button>
            </div>
          </div>

          {selected.parameters && selected.parameters.length > 0 && (
            <div style={{ padding: '8px 10px', background: 'var(--surface-inset)',
              borderRadius: 'var(--radius)', fontSize: 11.5, color: 'var(--text-muted)',
              display: 'flex', gap: 7, alignItems: 'flex-start' }}>
              <I.Info size={12} style={{ marginTop: 1, flexShrink: 0 }} />
              <span>This template uses tokens that get filled by the Player from the Profile form
                at runtime — no author input needed here.</span>
            </div>
          )}

          <div>
            <label style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--text-muted)',
              display: 'block', marginBottom: 5 }}>
              HTML description (htmlAlt) · required
            </label>
            <textarea className="field" value={alt} onChange={e => setAlt(e.target.value)}
              placeholder="What the embedded document shows — read by screen readers and used when the document can't render…"
              style={{ minHeight: 56, fontSize: 12 }} />
          </div>

          {/* Tooltip-text form (Phase 2) — only when the template exposes slots.
              Reuses the shared TooltipParamsForm / ControlledLocalized widget
              (§26). Filled values persist into content.tooltipValues. */}
          {selected.tooltipParams && selected.tooltipParams.length > 0 && (
            window.TooltipParamsForm
              ? <window.TooltipParamsForm params={selected.tooltipParams} values={tooltipValues}
                  onChange={(name, v) => setTooltipValues(prev => ({ ...prev, [name]: v }))} />
              : null
          )}
        </div>
      )}

      {picking && (
        <HtmlTemplatePickerModal templates={templates} selectedId={templateId}
          onPick={(id) => { setTemplateId(id); setPicking(false); }}
          onClose={() => setPicking(false)} />
      )}
    </div>
  );
}

function HtmlTemplatePickerModal({ templates, selectedId, onPick, onClose }) {
  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, background: 'color-mix(in oklab, var(--text) 35%, transparent)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 50, padding: 24,
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        width: 720, maxWidth: '100%', maxHeight: '88vh', display: 'flex', flexDirection: 'column',
        background: 'var(--surface)', border: '1px solid var(--border)',
        borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-lg)', overflow: 'hidden',
      }}>
        <div style={{ padding: '14px 18px', borderBottom: '1px solid var(--border)',
          display: 'flex', alignItems: 'center', gap: 10 }}>
          <div style={{ width: 26, height: 26, borderRadius: 6, background: 'var(--accent-bg)',
            color: 'var(--accent-text)', display: 'inline-flex', alignItems: 'center',
            justifyContent: 'center' }}>
            <I.Grid size={13} />
          </div>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 13, fontWeight: 600 }}>Choose an HTML template</div>
            <div style={{ fontSize: 11.5, color: 'var(--text-muted)' }}>
              {templates.length} templates in the library
            </div>
          </div>
          <button className="btn sm ghost" onClick={onClose}><I.X size={13} /></button>
        </div>

        <div style={{ padding: 16, overflow: 'auto', display: 'grid',
          gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 12 }}>
          {templates.map(t => {
            const active = t.id === selectedId;
            return (
              <button key={t.id} onClick={() => onPick(t.id)} style={{
                textAlign: 'left', padding: 0, cursor: 'default', fontFamily: 'inherit',
                background: 'var(--surface)', borderRadius: 'var(--radius-md)', overflow: 'hidden',
                border: '1px solid', borderColor: active ? 'var(--accent)' : 'var(--border)',
                boxShadow: active ? '0 0 0 2px var(--accent-bg)' : 'none',
                display: 'flex', flexDirection: 'column',
              }}>
                <div style={{ height: 100, background: '#fff', borderBottom: '1px solid var(--border)',
                  position: 'relative', overflow: 'hidden' }}>
                  <iframe src={t.previewUrl} title={`Preview: ${t.name}`} tabIndex={-1} scrolling="no"
                    style={{ width: '300%', height: '300%', border: 0, transform: 'scale(0.333)',
                      transformOrigin: 'top left', background: '#fff', pointerEvents: 'none' }} />
                  {active && (
                    <span style={{ position: 'absolute', top: 6, right: 6, width: 18, height: 18,
                      borderRadius: '50%', background: 'var(--accent)', color: '#fff',
                      display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
                      <I.Check size={11} />
                    </span>
                  )}
                </div>
                <div style={{ padding: '9px 11px', display: 'flex', flexDirection: 'column', gap: 6 }}>
                  <div style={{ fontSize: 12.5, fontWeight: 600, lineHeight: 1.3 }}>{t.name}</div>
                  <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
                    <HtmlTemplateChip>{t.category}</HtmlTemplateChip>
                    <HtmlTemplateChip tone="accent">{t.complexity}</HtmlTemplateChip>
                  </div>
                </div>
              </button>
            );
          })}
        </div>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// 3.10  quiz_images
// ─────────────────────────────────────────────────────────────────────────────
function QuizImagesEditor() {
  const c = LAYOUT_CONTENT_SAMPLES.quiz_images;
  const [active, setActive] = React.useState(0);
  const [mediaKind, setMediaKind] = React.useState('image');  // image | image+hotspots | video | html
  const [dfti, setDfti] = React.useState(false);
  const q = c.questions[active] || c.questions[0];

  return (
    <>
      <EditorBlock label="Questions" count={c.questions.length}
        action={<button className="btn sm"><I.Plus size={12} />Add question</button>}>
        <div style={{ display: 'flex', gap: 4, padding: 4, background: 'var(--surface-inset)',
          borderRadius: 'var(--radius)', overflowX: 'auto' }}>
          {c.questions.map((qx, i) => (
            <button key={i} onClick={() => setActive(i)}
              style={{
                padding: '8px 12px', fontSize: 12, fontFamily: 'inherit', cursor: 'default',
                background: i === active ? 'var(--surface)' : 'transparent',
                color: i === active ? 'var(--text)' : 'var(--text-muted)',
                border: '1px solid', borderColor: i === active ? 'var(--accent-border)' : 'transparent',
                borderRadius: 4, whiteSpace: 'nowrap', minWidth: 120,
                textAlign: 'left',
              }}>
              <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text-faint)' }}>Q{i+1}</span>
              &nbsp;{(qx.text || '').slice(0, 32)}…
            </button>
          ))}
        </div>
      </EditorBlock>

      <EditorBlock label={`Question ${active+1}`}>
        <div style={{ display: 'grid', gap: 12 }}>
          <RichTextEditor value={q.text} placeholder="Question prompt…" minHeight={56} />

          <div>
            <div style={{ fontSize: 11.5, color: 'var(--text-muted)', marginBottom: 4 }}>Question media</div>
            <div style={{ display: 'flex', gap: 4, padding: 3, background: 'var(--surface-inset)',
              borderRadius: 'var(--radius)', border: '1px solid var(--border)', width: 'fit-content' }}>
              {[
                ['image', 'Image'],
                ['image+hotspots', 'Image w/ hotspots'],
                ['video', 'Video'],
                ['html', 'HTML document'],
              ].map(([id, lbl]) => (
                <VarBtn key={id} active={mediaKind === id} onClick={() => setMediaKind(id)}>{lbl}</VarBtn>
              ))}
            </div>
          </div>

          {mediaKind === 'image' && <MediaSlot kind="image" label="Question image"
            bound={{ alt: 'Question image', previewBg: 'linear-gradient(135deg, #52525b, #27272a)' }} />}
          {mediaKind === 'image+hotspots' && (
            <div>
              <MediaSlot kind="image" label="Question image"
                bound={{ alt: 'Question image', previewBg: 'linear-gradient(135deg, #52525b, #27272a)' }} />
              <div style={{ marginTop: 10 }}>
                <ImageHotspotPlacer
                  imageUrl="placeholder:scene-doors-split" imageLabel="Question image"
                  pins={[{ id: 'h1', x: 30, y: 50, title: 'Sliding doors' }, { id: 'h2', x: 70, y: 50, title: 'Revolving doors' }]}
                  onChange={() => {}} selectedIndex={0} onSelect={() => {}}
                  colors={{ fill: '#F26430', stroke: '#F26430' }} height={220} />
              </div>
            </div>
          )}
          {mediaKind === 'video' && <MediaSlot kind="video" label="Question video" />}
          {mediaKind === 'html' && <HtmlTemplateSlot />}

          <FourAnswerEditor answers={Object.fromEntries(
            q.answers.slice(0, 4).map((a, i) => [
              a.isCorrect ? 'correct' : (i === 1 ? 'wrong-1' : i === 2 ? 'wrong-2' : 'playful'),
              a.text || '',
            ])
          )} />
          <RichTextEditor value={q.feedbackText} placeholder="Feedback shown after the learner answers…" minHeight={56} />
        </div>
      </EditorBlock>

      <EditorBlock label="Gaming quiz wrapper"
        action={<Switch on={dfti} />}>
        <div style={{
          padding: 12, background: dfti ? 'var(--surface)' : 'var(--surface-inset)',
          border: '1px solid var(--border)', borderRadius: 'var(--radius-md)',
          opacity: dfti ? 1 : 0.6,
        }}>
          <p style={{ margin: '0 0 10px', fontSize: 12, color: 'var(--text-muted)' }}>
            Wraps the quiz with start/end screens and a running counter; can complete the course.
          </p>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
            <FieldRow label="Correct answers needed">
              <input className="field" type="number" defaultValue="6" style={{ width: 60 }} />
            </FieldRow>
            <FieldRow label="Count as course completion"><Switch on={true} /></FieldRow>
            <FieldRow label="Show correct / wrong feedback"><Switch on={true} /></FieldRow>
            <FieldRow label="Show progress counter"><Switch on={true} /></FieldRow>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginTop: 8 }}>
            <MediaSlot kind="image" label="Start screen background" />
            <MediaSlot kind="image" label="End screen background (win)" />
          </div>
        </div>
      </EditorBlock>
    </>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// 3.11  object_viewer
// ─────────────────────────────────────────────────────────────────────────────
function ObjectViewerEditor() {
  const c = LAYOUT_CONTENT_SAMPLES.object_viewer;
  const [pins, setPins] = React.useState(c.items.map((it, i) => {
    const [x, y, z] = it.itemPosition.split(',').map(Number);
    return { id: 'o' + i, x, y, z, title: it.itemTitle, text: it.itemText };
  }));
  const [sel, setSel] = React.useState(0);
  const active = pins[sel];

  return (
    <>
      <EditorBlock label="3D model">
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
          <MediaSlot kind="object3d" label="3D model file (.glb / .gltf)"
            bound={{ alt: 'centrifuge.glb', generatedBy: 'asset library',
              previewBg: 'linear-gradient(135deg, #94a3b8, #475569)' }} />
          <div style={{ display: 'grid', gap: 8 }}>
            <MediaSlot kind="image" label="Loading poster image" />
            <MediaSlot kind="image" label="Environment map (.hdr)" />
          </div>
        </div>
      </EditorBlock>

      <EditorBlock label="3D-hotspot placer" count={pins.length}>
        <ThreeDHotspotPlacer
          objectUrl={c.objectUrl} objectPosterImgUrl="placeholder:object-centrifuge"
          pins={pins} onChange={setPins} selectedIndex={sel} onSelect={setSel} />
      </EditorBlock>

      {active && (
        <EditorBlock label={`Editing hotspot ${sel+1}`}>
          <div style={{ display: 'grid', gap: 10 }}>
            <input className="field" value={active.title}
              onChange={e => setPins(pins.map((p, i) => i === sel ? { ...p, title: e.target.value } : p))}
              placeholder="Hotspot title" style={{ fontWeight: 600 }} />
            <RichTextEditor value={active.text} placeholder="Hotspot description shown in the panel…" />
            <MediaSlot kind="video" label="Hotspot video (optional)" />
            <div style={{ fontSize: 11.5, color: 'var(--text-faint)', fontFamily: 'var(--font-mono)' }}>
              Position = {active.x},{active.y},{active.z}
            </div>
          </div>
        </EditorBlock>
      )}

      <EditorBlock label="Viewer behaviour">
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
          <FieldRow label="Auto-rotate"><Switch on={c.autoRotate} /></FieldRow>
          <FieldRow label="Disable zoom"><Switch on={c.disableZoom} /></FieldRow>
          <FieldRow label="Disable pan"><Switch on={c.disablePan} /></FieldRow>
          <FieldRow label="Mobile AR enabled"><Switch on={c.mobileArEnabled} /></FieldRow>
          <FieldRow label={'Hide “drag to look” hint'}><Switch on={c.disableInteractionPrompt} /></FieldRow>
        </div>
      </EditorBlock>

      <EditorBlock label="Cover text + accessibility">
        <div style={{ display: 'grid', gap: 10 }}>
          <input className="field" defaultValue={c.initialCoverText}
            placeholder="Initial cover text (shown over the model before interaction)" />
          <textarea className="field" defaultValue={c.objectAltText}
            placeholder="Model description (for screen readers)" style={{ minHeight: 56 }} />
        </div>
      </EditorBlock>

      <EditorBlock label="Background + panel colors">
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
          <FieldRow label="Scene background"><ColorSwatchRow value={c.backgroundColor} /></FieldRow>
          <FieldRow label="Hotspot color"><ColorSwatchRow value={c.itemColor} /></FieldRow>
          <FieldRow label="Panel background"><ColorSwatchRow value={c.bgColor} /></FieldRow>
          <FieldRow label="Border color"><ColorSwatchRow value={c.borderColor} /></FieldRow>
        </div>
      </EditorBlock>
    </>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// 3.12  two_columns_text
// ─────────────────────────────────────────────────────────────────────────────
function TwoColumnsTextEditor() {
  const c = LAYOUT_CONTENT_SAMPLES.two_columns_text;
  return (
    <>
      <EditorBlock label="Columns">
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
          <div style={{ background: 'var(--surface)', border: '1px solid var(--border)',
            borderRadius: 'var(--radius-md)', padding: 12 }}>
            <div style={{ fontSize: 11, color: 'var(--text-muted)', fontWeight: 600,
              letterSpacing: '.04em', textTransform: 'uppercase', marginBottom: 8 }}>
              Left column
            </div>
            <RichTextEditor value={c.leftColumnContent} placeholder="Left column content — supports headings, lists, bold, italic, links" />
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, marginTop: 8 }}>
              <FieldRow label="Text color"><ColorSwatchRow value={c.leftColumnTextColor} /></FieldRow>
              <FieldRow label="Panel background"><ColorSwatchRow value={c.leftColumnBackgroundColor} /></FieldRow>
            </div>
          </div>
          <div style={{ background: 'var(--surface)', border: '1px solid var(--border)',
            borderRadius: 'var(--radius-md)', padding: 12 }}>
            <div style={{ fontSize: 11, color: 'var(--text-muted)', fontWeight: 600,
              letterSpacing: '.04em', textTransform: 'uppercase', marginBottom: 8 }}>
              Right column
            </div>
            <RichTextEditor value={c.rightColumnContent} placeholder="Right column content" />
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, marginTop: 8 }}>
              <FieldRow label="Text color"><ColorSwatchRow value={c.rightColumnTextColor} /></FieldRow>
              <FieldRow label="Panel background"><ColorSwatchRow value={c.rightColumnBackgroundColor} /></FieldRow>
            </div>
          </div>
        </div>
      </EditorBlock>
    </>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// 3.14  discover — REMOVED as a standalone layout. See
//                 layout-editor-bodies.jsx for the rationale. The
//                 in-video `discover` interaction is the only surviving form.
// ─────────────────────────────────────────────────────────────────────────────

// ─────────────────────────────────────────────────────────────────────────────
// 3.15  question (standalone, non-blocking)
// ─────────────────────────────────────────────────────────────────────────────
function QuestionEditor() {
  const c = LAYOUT_CONTENT_SAMPLES.question;
  const [editor, setEditor] = React.useState('four');
  return (
    <>
      <EditorBlock label="Question prompt">
        <RichTextEditor value={c.text} placeholder="Question prompt…" minHeight={60} />
      </EditorBlock>
      <EditorBlock label="Answers"
        action={<div style={{ display: 'flex', gap: 4 }}>
          <button className={`btn sm ${editor === 'four' ? '' : 'ghost'}`}
            onClick={() => setEditor('four')}>4-answer</button>
          <button className={`btn sm ${editor === 'free' ? '' : 'ghost'}`}
            onClick={() => setEditor('free')}>Free-form</button>
        </div>}>
        {editor === 'four'
          ? <FourAnswerEditor answers={{
              correct: c.answers[0].text,
              'wrong-1': c.answers[1].text,
              'wrong-2': c.answers[2].text,
              playful: 'Hand them a flowchart and look smug.',
            }} />
          : <FreeFormQuestionEditor answers={c.answers.map(a => a.text)} correctIdx={0} />}
      </EditorBlock>
      <EditorBlock label="Feedback">
        <RichTextEditor value={c.feedbackText} placeholder="Feedback shown after the learner answers" minHeight={60} />
      </EditorBlock>
      <div style={{ padding: '10px 12px', background: 'var(--surface-inset)',
        borderRadius: 'var(--radius)', fontSize: 11.5, color: 'var(--text-muted)' }}>
        <I.Info size={11} /> Non-blocking — the learner can move on without answering correctly.
      </div>
    </>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// 3.16  title (section divider)
// ─────────────────────────────────────────────────────────────────────────────
function TitleEditor() {
  const c = LAYOUT_CONTENT_SAMPLES.title;
  return (
    <>
      <EditorBlock label="Background">
        <MediaSlot kind="image" label="Background image (optional)"
          bound={{ alt: 'Section divider', previewBg: 'linear-gradient(135deg, #0f172a, #020617)' }} />
      </EditorBlock>
      <EditorBlock label="Colors">
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
          <FieldRow label="Title text color"><ColorSwatchRow value={c.titleTextColor} /></FieldRow>
          <FieldRow label="Title background color"><ColorSwatchRow value={c.titleBackgroundColor} /></FieldRow>
        </div>
      </EditorBlock>
      <div style={{ padding: '10px 12px', background: 'var(--surface-inset)',
        borderRadius: 'var(--radius)', fontSize: 11.5, color: 'var(--text-muted)' }}>
        <I.Info size={11} /> The smallest editor in the kit — a title-only section divider.
        No raw <code>title</code> XML fragment exists yet; flag any standalone-only fields if you find them.
      </div>
    </>
  );
}

Object.assign(window, {
  FullscreenTextAndImageEditor, HorizontalTabsEditor, VerticalTabsEditor,
  HiddenItemsEditor, QuizImagesEditor, ObjectViewerEditor,
  TwoColumnsTextEditor, QuestionEditor, TitleEditor,
});
