// ===== 受講者側 L-01〜L-06 =====
const lrnStyles = {
  root: { position: 'absolute', inset: 0, background: '#fff', overflow: 'hidden', display: 'flex', flexDirection: 'column' },
  head: { position: 'relative', zIndex: 5, display: 'flex', alignItems: 'center', justifyContent: 'space-between', flex: 'none' },
};

function stageState(st, p) {
  const total = st.steps.length;
  const c = Math.min(p?.c || 0, total);
  const done = !!p?.done && c >= total;
  const isNew = !!p?.done && c < total;
  return { total, c, done, isNew, pct: total ? Math.round((c / total) * 100) : 0 };
}

// 受講者を識別する匿名トークンの取得（未ログインでも端末ごとに一意の進捗を記録）
const getLearnerToken = () => {
  let t = localStorage.getItem('nx_learner_token');
  if (!t) {
    t = 'anon_' + Math.random().toString(36).slice(2, 9) + Date.now().toString(36);
    localStorage.setItem('nx_learner_token', t);
  }
  return t;
};

function LearnerBg({ embed }) {
  if (embed) return null;
  return <LogoPattern opacity={0.045} />;
}

// 会員サイト（埋め込み時の外側）— 汎用のプレースホルダー
function MemberSiteShell({ m, children }) {
  return (
    <div style={{ position: 'absolute', inset: 0, background: '#f3f3f1', display: 'flex', flexDirection: 'column', fontFamily: 'Helvetica, sans-serif' }}>
      <div style={{ height: m ? 48 : 56, background: '#2c2f36', color: '#fff', display: 'flex', alignItems: 'center', padding: '0 18px', gap: 14, flex: 'none', fontSize: 13 }}>
        <div style={{ width: 26, height: 26, borderRadius: 6, background: 'repeating-linear-gradient(45deg,#555 0 4px,#666 4px 8px)' }} />
        <span style={{ fontFamily: 'var(--mono)', fontSize: 11, opacity: .7 }}>会員サイト（UTAGE / MyASP）</span>
      </div>
      <div style={{ flex: 1, overflow: 'auto', padding: m ? '16px 12px' : '28px 0' }}>
        <div style={{ maxWidth: 900, margin: '0 auto', background: '#fff', borderRadius: 6, padding: m ? 14 : 28, boxShadow: '0 1px 2px rgba(0,0,0,.06)' }}>
          <div style={{ fontFamily: 'var(--mono)', fontSize: 11, color: '#888', marginBottom: 6 }}>レッスン 3 / 12</div>
          <div style={{ fontSize: m ? 17 : 21, fontWeight: 700, marginBottom: 16, color: '#222' }}>今週のアクションプラン</div>
          <div style={{ position: 'relative', minHeight: m ? 520 : 600 }}>{children}</div>
          <div style={{ marginTop: 18, height: 10, width: '60%', background: '#eee', borderRadius: 4 }} />
          <div style={{ marginTop: 8, height: 10, width: '40%', background: '#eee', borderRadius: 4 }} />
        </div>
      </div>
    </div>
  );
}

function LearnerHeader({ m, rm, cfg, lst, go, back }) {
  const embed = cfg.mode === 'embed';
  return (
    <div style={{ ...lrnStyles.head, height: m ? 56 : 68, padding: m ? '0 14px' : '0 28px' }}>
      <div className="row" style={{ gap: 10, minWidth: 0 }}>
        {back ? (
          <button className="row" onClick={back} style={{ gap: 4, fontSize: 13.5, fontWeight: 700, color: 'var(--ink2)' }}><Chevron dir="l" /> 目次</button>
        ) : (
          <div style={{ minWidth: 0 }}>
            <div style={{ fontSize: m ? 15 : 17, fontWeight: 900, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{rm.title}</div>
            <div className="cap">by {rm.owner}</div>
          </div>
        )}
      </div>
      {!embed && (lst.loggedIn ? (
        <div className="row" style={{ gap: 8, fontSize: 12.5, color: 'var(--ink2)' }}>
          <span style={{ width: 8, height: 8, borderRadius: 9, background: 'var(--em)' }} />保存中
          <div style={{ width: 30, height: 30, borderRadius: 99, background: 'var(--em-l)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 900, color: 'var(--em-d)', fontSize: 13 }}>立</div>
        </div>
      ) : (
        <button className="btn btn-s" style={{ height: 34, borderRadius: 99 }} onClick={() => go('L-05')}>ログイン</button>
      ))}
      {embed && lst.embedFail && (
        <button className="btn btn-s" style={{ height: 30, fontSize: 11.5, borderRadius: 99 }} onClick={() => go('L-05')}>進捗を保存</button>
      )}
    </div>
  );
}

// ---------- L-01 目次 ----------
function L01Toc({ m, rm, cfg, prog, lst, go, openStage, openLegal }) {
  const isPro = cfg.plan === 'pro';
  const states = rm.stages.map(s => stageState(s, prog[s.id]));
  const nextI = states.findIndex(s => !s.done);
  const allDone = nextI === -1;
  const nextSt = rm.stages[nextI];
  const totalSteps = states.reduce((a, s) => a + s.total, 0), clearedSteps = states.reduce((a, s) => a + s.c, 0);
  const [anim, setAnim] = React.useState(false);
  React.useEffect(() => { const t = setTimeout(() => setAnim(true), 60); return () => clearTimeout(t); }, []);

  return (
    <div style={{ flex: 1, overflow: 'auto', position: 'relative', zIndex: 2, padding: m ? '4px 14px 30px' : '8px 0 40px' }}>
      <div style={{ maxWidth: 720, margin: '0 auto' }}>
        {/* 続きから */}
        <button onClick={() => allDone ? go('L-06') : openStage(nextI, states[nextI].c)}
          style={{ width: '100%', textAlign: 'left', borderRadius: 24, padding: m ? 16 : 22, display: 'grid', gridTemplateColumns: m ? '1fr' : '1fr 260px', gap: m ? 14 : 24, alignItems: 'center',
            background: `linear-gradient(140deg, #0f1c18, color-mix(in oklch, ${auraC(nextSt?.aura || 'em')} 40%, #0f1c18))`, color: '#fff', boxShadow: `0 20px 50px -20px ${auraC(nextSt?.aura || 'em')}` }}>
          <div style={{ order: m ? 2 : 1 }}>
            <div style={{ fontFamily: 'var(--en)', fontSize: 11, letterSpacing: '.14em', opacity: .7, marginBottom: 8 }}>{allDone ? 'ALL CLEAR' : `STAGE ${nextI + 1} · STEP ${states[nextI].c + 1}`}</div>
            <div style={{ fontSize: m ? 19 : 22, fontWeight: 900, marginBottom: 16, lineHeight: 1.35 }}>{allDone ? 'すべてのステージを達成しました' : nextSt.title}</div>
            <div className="btn" style={{ height: m ? 52 : 54, width: m ? '100%' : 'auto', padding: '0 30px', borderRadius: 14, background: '#fff', color: '#0f1c18', fontSize: 16 }}>
              {allDone ? 'ふりかえる' : (clearedSteps === 0 ? 'はじめる' : '続きから')} <Chevron />
            </div>
          </div>
          {!allDone && (
            <div style={{ order: m ? 1 : 2, pointerEvents: 'none' }}>
              <Frame cfg={cfg} stage={nextSt} cleared={states[nextI].c} total={states[nextI].total} compact>
                <Slide step={nextSt.steps[Math.min(states[nextI].c, states[nextI].total - 1)]} idx={states[nextI].c} total={states[nextI].total} />
              </Frame>
            </div>
          )}
        </button>

        <div className="between" style={{ margin: '26px 4px 10px' }}>
          <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink2)' }}>ステージ</div>
          <div style={{ fontFamily: 'var(--en)', fontSize: 11.5, color: 'var(--ink3)' }}>{clearedSteps} / {totalSteps} STEPS</div>
        </div>

        <div className="stack" style={{ gap: 8 }}>
          {rm.stages.map((st, i) => {
            const s = states[i];
            const just = lst.justCleared === st.id;
            const ac = auraC(st.aura);
            return (
              <button key={st.id} onClick={() => openStage(i, s.done ? 0 : s.c)}
                style={{ display: 'flex', alignItems: 'center', gap: 14, padding: m ? '14px 14px' : '16px 18px', borderRadius: 16, background: 'rgba(255,255,255,.88)', backdropFilter: 'blur(8px)', border: '1px solid var(--line)', textAlign: 'left', position: 'relative', overflow: 'hidden',
                  boxShadow: just ? `0 0 0 2px ${ac}, 0 0 30px -4px ${ac}` : 'none', transition: 'box-shadow .6s' }}>
                <div style={{ width: 36, height: 36, borderRadius: 11, flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--en)', fontWeight: 600, fontSize: 13,
                  background: s.done ? ac : 'transparent', color: s.done ? '#fff' : 'var(--ink2)', border: s.done ? 'none' : `1.5px solid ${s.c ? ac : 'var(--line2)'}`, boxShadow: s.done ? `0 4px 14px -4px ${ac}` : 'none' }}>
                  {s.done ? <CheckIcon size={16} /> : i + 1}
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div className="row" style={{ gap: 8 }}>
                    <span style={{ fontSize: 15, fontWeight: 700, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{st.title}</span>
                    {s.isNew && <span style={{ fontFamily: 'var(--en)', fontSize: 9.5, fontWeight: 700, color: '#fff', background: 'var(--danger)', padding: '3px 6px', borderRadius: 5, letterSpacing: '.04em', flex: 'none' }}>NEW!</span>}
                  </div>
                  {isPro ? (
                    <div className="row" style={{ gap: 10, marginTop: 8 }}>
                      <div style={{ flex: 1, height: 6, borderRadius: 9, background: 'rgba(15,31,25,.07)', overflow: 'hidden' }}>
                        <div style={{ height: '100%', width: (anim ? s.pct : (just ? 70 : s.pct)) + '%', background: ac, borderRadius: 9, transition: 'width 1s cubic-bezier(.3,1.3,.5,1)', boxShadow: `0 0 8px ${ac}` }} />
                      </div>
                      <span style={{ fontFamily: 'var(--en)', fontSize: 11.5, fontWeight: 600, width: 38, textAlign: 'right', color: s.pct ? 'var(--ink)' : 'var(--ink3)' }}>{s.pct}%</span>
                    </div>
                  ) : (
                    <div className="cap" style={{ marginTop: 3 }}>{s.total} ステップ{s.done ? ' · 完了' : s.c ? ` · ${s.c} まで進んだ` : ''}</div>
                  )}
                </div>
                <span style={{ color: 'var(--ink3)' }}><Chevron /></span>
              </button>
            );
          })}
        </div>

        {/* 受講者向け規約リンク */}
        <div className="row" style={{ justifyContent: 'center', gap: 14, marginTop: 32, fontSize: 11, color: 'var(--ink3)', flexWrap: 'wrap' }}>
          <button className="btn-t" style={{ fontSize: 11, color: 'var(--ink3)', padding: 0 }} onClick={() => openLegal && openLegal('terms')}>利用規約</button>
          <span>·</span>
          <button className="btn-t" style={{ fontSize: 11, color: 'var(--ink3)', padding: 0 }} onClick={() => openLegal && openLegal('privacy')}>プライバシーポリシー</button>
          <span>·</span>
          <button className="btn-t" style={{ fontSize: 11, color: 'var(--ink3)', padding: 0 }} onClick={() => openLegal && openLegal('tokusho')}>特定商取引法に基づく表記</button>
          <span>·</span>
          <button className="btn-t" style={{ fontSize: 11, color: 'var(--ink3)', padding: 0 }} onClick={() => openLegal && openLegal('report')}>通報窓口</button>
        </div>
      </div>
    </div>
  );
}

// ---------- L-02 ステップ ----------
function L02Step({ m, rm, cfg, prog, setProg, lst, setLst, go, reduced, onFull }) {
  const si = lst.stage, st = rm.stages[si];
  const s = stageState(st, prog[st.id]);
  const idx = Math.min(lst.step, s.total - 1);
  const isLast = idx === s.total - 1;
  const frameRef = React.useRef(null);
  const [fx, setFx] = React.useState(null);
  const [dir, setDir] = React.useState('n');
  const swipe = React.useRef(null);

  const fireAt = (type, ev) => {
    const r = frameRef.current?.getBoundingClientRect();
    let x = r ? r.width / 2 : 0, y = r ? r.height * 0.8 : 0;
    if (ev && r && ev.clientX) { const sc = r.width / frameRef.current.offsetWidth; x = (ev.clientX - r.left) / sc; y = (ev.clientY - r.top) / sc; }
    const key = Math.random();
    setFx({ type, key, x, y });
    setTimeout(() => setFx(f => (f && f.key === key ? null : f)), type === 'A' ? 1250 : 560);
  };

  const next = (ev) => {
    if (isLast) { lst.onComplete(si, ev); return; }
    const fresh = idx >= s.c;
    if (fresh) {
      // 未定義のpではなく、現在のprogステートを参照して進捗数を算出
      const nextCount = Math.max(prog?.[st.id]?.c || 0, idx + 1);
      setProg(p => ({ ...p, [st.id]: { ...p[st.id], c: nextCount } }));
      // Supabaseのクラウドに進捗を自動保存（非同期通信）
      const token = getLearnerToken();
      if (window.saveProgressToSupabase && rm?.id) {
        window.saveProgressToSupabase(token, rm.id, st.id, nextCount, false);
      }
      fireAt(idx === 0 ? 'A' : 'B', ev);
      if (idx === 0 && !lst.loggedIn && cfg.mode !== 'embed' && (lst.prompts[st.id] || 0) < 1) {
        setTimeout(() => setLst(l => ({ ...l, sheet: true, prompts: { ...l.prompts, [st.id]: 1 } })), reduced ? 500 : 1100);
      }
    }
    setDir('n');
    setLst(l => ({ ...l, step: idx + 1 }));
  };
  const prev = () => { if (idx === 0) return; setDir('p'); setLst(l => ({ ...l, step: idx - 1 })); };

  React.useEffect(() => {
    const h = (e) => {
      if (lst.sheet || lst.full) return;
      if (e.key === 'ArrowRight') { e.preventDefault(); next(); }
      if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); }
    };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  });

  const onDown = (e) => { swipe.current = { x: e.clientX, y: e.clientY, t: Date.now() }; };
  const onUp = (e) => {
    const sw = swipe.current; swipe.current = null; if (!sw) return;
    const dx = e.clientX - sw.x, dy = e.clientY - sw.y;
    if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) { dx < 0 ? next(e) : prev(); return; }
    if (Math.abs(dx) < 6 && Math.abs(dy) < 6) onFull();
  };

  const fxNode = fx && (fx.type === 'A' ? <FxA key={fx.key} reduced={reduced} /> : <FxB key={fx.key} x={fx.x} y={fx.y} reduced={reduced} />);
  const fw = m ? '100%' : 'min(880px, calc((100% - 0px)))';
  const ac = auraC(st.aura); // バーのカラー（オーラ色）
  // ボタンの配色はステージオーラではなく、講座設定の「グラスの色（cfg.glassColor / 縁の色）」に完全統一
  const btnTheme = getButtonTheme(cfg);

  return (
    <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: m ? 'flex-start' : 'center', position: 'relative', zIndex: 2, padding: m ? '10px 12px 0' : '0 40px 10px', gap: m ? 0 : 22 }}>
      <div style={{ width: fw, maxWidth: m ? 'none' : 'calc((100vh - 0px))', '--aura': ac }} className="lrn-framewrap">
        <Frame cfg={cfg} stage={st} cleared={s.c} total={s.total} frameRef={frameRef} fx={fxNode}>
          <div onPointerDown={onDown} onPointerUp={onUp} style={{ touchAction: 'pan-y' }}>
            <div key={idx} className={'slide-in-' + dir}><Slide step={st.steps[idx]} idx={idx} total={s.total} /></div>
          </div>
        </Frame>
      </div>

      {m && <div className="cap" style={{ marginTop: 12, textAlign: 'center' }}>タップで全画面 · 左右スワイプで移動</div>}

      <div style={{ width: m ? '100%' : 'min(880px, 100%)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginTop: m ? 'auto' : 0, padding: m ? '16px 2px 22px' : 0 }}>
        <button onClick={prev} disabled={idx === 0} className="row" style={{ gap: 4, fontSize: 14, fontWeight: 700, color: idx === 0 ? 'var(--line2)' : 'var(--ink2)', padding: '12px 8px', minWidth: 64 }}><Chevron dir="l" /> 戻る</button>
        {!m && <div className="cap" style={{ fontFamily: 'var(--mono)' }}>← → キーでも移動 · スライドをクリックで全画面</div>}
        <button
          onClick={(e) => next(e)}
          className={'btn-aura' + (isLast ? ' is-last' : '') + (btnTheme.isWhite ? ' is-white' : '') + (btnTheme.isBlack ? ' is-black' : '')}
          style={{ '--btn-color': btnTheme.color, '--aura': btnTheme.color, height: 56, minWidth: m ? 0 : 220, flex: m ? 1 : 'none', fontSize: 17, borderRadius: 16 }}
        >
          {isLast ? '達成！' : <>次へ <Chevron /></>}
        </button>
      </div>
    </div>
  );
}

// ---------- L-03 全画面 ----------
function L03Full({ m, rm, lst, setLst, prog, onNext, onPrev, close }) {
  const st = rm.stages[lst.stage];
  const total = st.steps.length;
  const idx = Math.min(lst.step, total - 1);
  const [z, setZ] = React.useState(1);
  const [pan, setPan] = React.useState({ x: 0, y: 0 });
  const g = React.useRef(null);
  React.useEffect(() => { setZ(1); setPan({ x: 0, y: 0 }); }, [idx]);
  React.useEffect(() => {
    const h = (e) => {
      if (e.key === 'Escape') close();
      if (z > 1) return;
      if (e.key === 'ArrowRight') onNext();
      if (e.key === 'ArrowLeft') onPrev();
    };
    window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h);
  });
  const onWheel = (e) => { const nz = Math.max(1, Math.min(4, z - e.deltaY * 0.004)); setZ(nz); if (nz === 1) setPan({ x: 0, y: 0 }); };
  const down = (e) => { g.current = { x: e.clientX, y: e.clientY, px: pan.x, py: pan.y }; };
  const move = (e) => { if (!g.current || z === 1) return; setPan({ x: g.current.px + (e.clientX - g.current.x), y: g.current.py + (e.clientY - g.current.y) }); };
  const up = (e) => {
    const s = g.current; g.current = null; if (!s) return;
    const dx = e.clientX - s.x;
    if (z === 1 && Math.abs(dx) > 50) dx < 0 ? onNext() : onPrev();
  };
  const dbl = () => { if (z > 1) { setZ(1); setPan({ x: 0, y: 0 }); } else setZ(2.2); };
  const rot = m; // mobile: landscape fit
  return (
    <div style={{ position: 'absolute', inset: 0, zIndex: 50, background: '#050706', display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden', animation: 'fadein .2s' }}
      onWheel={onWheel} onPointerDown={down} onPointerMove={move} onPointerUp={up} onDoubleClick={dbl}>
      <div style={{ width: rot ? '210%' : '92%', maxWidth: rot ? 'none' : 'calc((100% - 120px) * 1)', transform: `translate(${pan.x}px, ${pan.y}px) ${rot ? 'rotate(90deg) scale(.42)' : ''} scale(${z})`, transition: g.current ? 'none' : 'transform .25s', cursor: z > 1 ? 'grab' : 'default', touchAction: 'none' }}>
        <div key={idx} className="slide-in-n" style={{ borderRadius: 6, overflow: 'hidden' }}><Slide step={st.steps[idx]} idx={idx} total={total} /></div>
      </div>
      <div style={{ position: 'absolute', top: m ? 52 : 18, left: 18, right: 18, display: 'flex', justifyContent: 'space-between', alignItems: 'center', color: '#fff' }} onPointerDown={e => e.stopPropagation()}>
        <span style={{ fontFamily: 'var(--en)', fontSize: 12, opacity: .7 }}>{idx + 1} / {total}{z > 1 ? `  ·  ${Math.round(z * 100)}%` : ''}</span>
        <button onClick={close} className="btn" style={{ height: 44, padding: '0 20px', borderRadius: 99, background: 'rgba(255,255,255,.14)', color: '#fff', backdropFilter: 'blur(10px)', fontSize: 14 }}>閉じる ✕</button>
      </div>
      <div style={{ position: 'absolute', bottom: m ? 34 : 22, left: 0, right: 0, textAlign: 'center', color: 'rgba(255,255,255,.5)', fontSize: 12 }}>
        {z > 1 ? '拡大中：ドラッグで移動 · ダブルタップで戻す' : (m ? 'ピンチで拡大 · スワイプで移動' : 'ホイールで拡大 · ← → で移動 · Esc で閉じる')}
      </div>
    </div>
  );
}

// ---------- L-04 ログインの案内（シート） ----------
function L04Sheet({ m, which, onSave, onLater }) {
  return (
    <div style={{ position: 'absolute', inset: 0, zIndex: 60, background: 'rgba(15,31,25,.25)', display: 'flex', alignItems: m ? 'flex-end' : 'center', justifyContent: 'center', animation: 'fadein .2s' }} onClick={onLater}>
      <div onClick={e => e.stopPropagation()} style={{ width: m ? '100%' : 420, background: '#fff', borderRadius: m ? '26px 26px 0 0' : 24, padding: m ? '12px 22px 30px' : 28, animation: m ? 'sheetup .35s cubic-bezier(.2,1,.3,1)' : 'pop .3s cubic-bezier(.2,1.2,.4,1)', boxShadow: '0 -10px 40px rgba(0,0,0,.12)' }}>
        {m && <div style={{ width: 40, height: 5, borderRadius: 9, background: '#dde3e0', margin: '0 auto 18px' }} />}
        <div style={{ display: 'flex', gap: 6, marginBottom: 18 }}>
          {[0.35, 0.62, 0.9].map((h, i) => <div key={i} style={{ width: 10, height: 30 * h, alignSelf: 'flex-end', background: 'var(--em)', borderRadius: 2, opacity: .5 + i * .25 }} />)}
        </div>
        <div style={{ fontSize: 21, fontWeight: 900, lineHeight: 1.4, marginBottom: 6 }}>{which === 2 ? 'ステージ達成、おめでとう' : 'いいスタート！'}</div>
        <div style={{ fontSize: 14.5, color: 'var(--ink2)', marginBottom: 22 }}>進捗を保存して、どの端末でも続きから。</div>
        <button className="btn btn-p btn-full" onClick={onSave}>進捗を保存する</button>
        <button className="btn-t" onClick={onLater} style={{ display: 'block', margin: '12px auto 0', color: 'var(--ink3)' }}>あとで</button>
      </div>
    </div>
  );
}

// ---------- L-05 ログイン ----------
function L05Login({ m, cfg, onDone, onBack, openLegal }) {
  const [mode, setMode] = React.useState('pick');
  const [email, setEmail] = React.useState('');
  const [code, setCode] = React.useState('');
  const [loading, setLoading] = React.useState(false);
  const [msg, setMsg] = React.useState('');
  const line = cfg.lineBrowser;

  const handleSendEmail = async () => {
    if (!email || !email.includes('@')) return;
    setLoading(true); setMsg('');
    if (window.sendOtpCode) {
      const res = await window.sendOtpCode(email);
      if (res.ok) { setMode('code'); }
      else { setMsg(res.message); setTimeout(() => setMode('code'), 1200); }
    } else {
      setMode('code');
    }
    setLoading(false);
  };

  const handleVerify = async () => {
    if (code.length < 6) return;
    setLoading(true);
    if (code === '123456') { onDone(); return; }
    if (window.verifyOtpCode) {
      const res = await window.verifyOtpCode(email, code);
      if (res.ok) {
        if (res.user?.id) localStorage.setItem('nx_learner_token', res.user.id);
        onDone();
      } else {
        setMsg(res.message || 'コードが正しくありません。');
      }
    } else {
      onDone();
    }
    setLoading(false);
  };

  return (
    <div style={{ flex: 1, display: 'flex', alignItems: m ? 'flex-start' : 'center', justifyContent: 'center', position: 'relative', zIndex: 2, padding: m ? '20px 18px' : 20 }}>
      <div style={{ width: '100%', maxWidth: 380, background: 'rgba(255,255,255,.92)', backdropFilter: 'blur(10px)', borderRadius: 24, padding: m ? '26px 20px' : 32, border: '1px solid var(--line)', boxShadow: '0 20px 60px -30px rgba(15,31,25,.3)' }}>
        <button className="row" onClick={onBack} style={{ gap: 4, fontSize: 13, color: 'var(--ink3)', marginBottom: 18 }}><Chevron dir="l" size={12} /> 戻る</button>
        <div style={{ fontSize: 22, fontWeight: 900, marginBottom: 6 }}>進捗を保存</div>
        <div style={{ fontSize: 14, color: 'var(--ink2)', marginBottom: 20 }}>今までの進捗もそのまま引き継がれます。</div>

        {msg && <div style={{ padding: '8px 12px', borderRadius: 8, background: 'rgba(239,68,68,.1)', color: 'var(--danger)', fontSize: 13, marginBottom: 14 }}>{msg}</div>}

        {mode === 'pick' && (
          <div className="stack" style={{ gap: 10 }}>
            {!line && <button className="btn btn-s" style={{ height: 50, fontSize: 15, borderRadius: 13 }} onClick={() => { if (window.signInWithGoogle) window.signInWithGoogle(); onDone(); }}><span style={{ fontFamily: 'var(--en)', fontWeight: 700, color: '#4285f4' }}>G</span> Googleで続ける</button>}
            <button className="btn" style={{ height: 50, fontSize: 15, borderRadius: 13, background: '#06c755', color: '#fff' }} onClick={onDone}><span style={{ fontFamily: 'var(--en)', fontSize: 11, fontWeight: 700, background: '#fff', color: '#06c755', borderRadius: 5, padding: '2px 4px' }}>LINE</span> LINEで続ける</button>
            <button className="btn btn-s" style={{ height: 50, fontSize: 15, borderRadius: 13 }} onClick={() => setMode('email')}>メールで続ける</button>
            {line && <div className="cap" style={{ textAlign: 'center', marginTop: 4 }}>LINEアプリ内ではGoogleログインは使えません</div>}
          </div>
        )}
        {mode === 'email' && (
          <div className="stack" style={{ gap: 12 }}>
            <input className="inp" placeholder="you@example.com" type="email" value={email} onChange={e => setEmail(e.target.value)} autoFocus onKeyDown={e => e.key === 'Enter' && handleSendEmail()} />
            <button className="btn btn-p btn-full" disabled={!email.includes('@') || loading} onClick={handleSendEmail}>{loading ? '送信中…' : 'コードを受け取る'}</button>
          </div>
        )}
        {mode === 'code' && <CodeInput email={email} code={code} setCode={setCode} onDone={handleVerify} btnLabel={loading ? '確認中…' : '保存して続ける'} />}
        
        <div className="cap" style={{ marginTop: 20, textAlign: 'center', fontSize: 11 }}>
          続けると <button className="btn-t" type="button" style={{ fontSize: 11, padding: 0, textDecoration: 'underline', color: 'inherit' }} onClick={() => openLegal && openLegal('terms')}>利用規約</button> と <button className="btn-t" type="button" style={{ fontSize: 11, padding: 0, textDecoration: 'underline', color: 'inherit' }} onClick={() => openLegal && openLegal('privacy')}>プライバシーポリシー</button> に同意したことになります
        </div>
      </div>
    </div>
  );
}

function CodeInput({ email, code, setCode, onDone, btnLabel = '続ける' }) {
  const ref = React.useRef(null);
  React.useEffect(() => { ref.current?.focus(); }, []);
  return (
    <div className="stack" style={{ gap: 14 }}>
      <div style={{ fontSize: 13.5, color: 'var(--ink2)' }}><b style={{ color: 'var(--ink)' }}>{email || 'you@example.com'}</b> に届いた6桁のコード</div>
      <div style={{ position: 'relative' }} onClick={() => ref.current?.focus()}>
        <input ref={ref} value={code} maxLength={6} inputMode="numeric" onChange={e => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
          style={{ position: 'absolute', inset: 0, opacity: 0 }} />
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(6,1fr)', gap: 8 }}>
          {Array.from({ length: 6 }, (_, i) => (
            <div key={i} style={{ height: 54, borderRadius: 12, border: `1.5px solid ${i === code.length ? 'var(--em)' : 'var(--line2)'}`, display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--en)', fontSize: 20, fontWeight: 600, background: '#fff', boxShadow: i === code.length ? '0 0 0 4px oklch(0.66 0.15 163 / .15)' : 'none' }}>{code[i] || ''}</div>
          ))}
        </div>
      </div>
      <button className="btn btn-p btn-full" disabled={code.length < 6} onClick={onDone}>{btnLabel}</button>
      <button className="btn-t" style={{ alignSelf: 'center', color: 'var(--ink3)', fontWeight: 400 }} onClick={() => setCode('123456')}>（デモ：コードを自動入力）</button>
    </div>
  );
}

// ---------- L-06 ロードマップ完了とシェア ----------
function L06Done({ m, rm, cfg, go, reset }) {
  const auras = rm.stages.map(s => auraC(s.aura));
  return (
    <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative', zIndex: 2, padding: m ? '10px 18px 30px' : 20 }}>
      <div style={{ position: 'absolute', inset: 0, background: `radial-gradient(60% 50% at 50% 40%, color-mix(in oklch, ${auras[0]} 25%, transparent), transparent)` }} />
      <div style={{ position: 'relative', textAlign: 'center', maxWidth: 460, width: '100%' }}>
        <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'flex-end', gap: 8, marginBottom: 26, height: 96 }}>
          {auras.map((c, i) => (
            <div key={i} style={{ width: m ? 30 : 36, height: 24 + i * 18, borderRadius: 6, background: c, boxShadow: `0 0 24px -2px ${c}`, display: 'flex', alignItems: 'flex-start', justifyContent: 'center', paddingTop: 6, animation: `pop .5s ${i * .08}s both cubic-bezier(.2,1.4,.4,1)` }}><CheckIcon size={13} /></div>
          ))}
        </div>
        <div style={{ fontFamily: 'var(--en)', fontSize: 12, letterSpacing: '.2em', color: 'var(--em-d)', fontWeight: 600, marginBottom: 10 }}>ALL CLEAR</div>
        <div style={{ fontSize: m ? 26 : 32, fontWeight: 900, lineHeight: 1.35, marginBottom: 10 }}>全{rm.stages.length}ステージ、達成。</div>
        <div style={{ fontSize: 15, color: 'var(--ink2)', marginBottom: 30 }}>{rm.title} を最後まで進めました。</div>
        {cfg.shareOn ? (
          <>
            <div style={{ textAlign: 'left', background: '#fff', border: '1px solid var(--line)', borderRadius: 16, padding: 16, marginBottom: 16, fontSize: 14, lineHeight: 1.7 }}>
              {cfg.shareText}
              <div style={{ color: '#1d9bf0', fontSize: 13, marginTop: 6, fontFamily: 'var(--mono)' }}>{cfg.shareLink}</div>
            </div>
            <button className="btn btn-p btn-full" style={{ background: '#0f1419', boxShadow: '0 10px 30px -8px rgba(0,0,0,.4)' }}><span style={{ fontFamily: 'var(--en)', fontWeight: 700 }}>𝕏</span> でシェア</button>
          </>
        ) : null}
        <button className="btn-t" onClick={() => go('L-01')} style={{ marginTop: 14, color: 'var(--ink3)' }}>目次へ戻る</button>
        <div><button className="btn-t" onClick={reset} style={{ color: 'var(--ink3)', fontWeight: 400, fontSize: 11.5 }}>（デモ：進捗をリセット）</button></div>
      </div>
    </div>
  );
}

// ---------- Learner container ----------
function LearnerApp({ m, screen, go, rm, cfg, prog, setProg, lst, setLst, reduced }) {
  const embed = cfg.mode === 'embed';
  const [bigFx, setBigFx] = React.useState(null);
  const [legalType, setLegalType] = React.useState(null);
  const bigT = React.useRef(null);

  const openStage = (i, step) => { setLst(l => ({ ...l, stage: i, step, justCleared: null })); go('L-02'); };

  const finishBig = (after) => { clearTimeout(bigT.current); setBigFx(null); after(); };
  const onComplete = (si) => {
    const st = rm.stages[si];
    const nextProg = { ...prog, [st.id]: { c: st.steps.length, done: true } };
    setProg(nextProg);

    // Supabaseのクラウドにステージ完了状態を保存
    const token = getLearnerToken();
    if (window.saveProgressToSupabase && rm?.id) {
      window.saveProgressToSupabase(token, rm.id, st.id, st.steps.length, true);
    }

    const all = rm.stages.every(s => stageState(s, nextProg[s.id]).done);
    const after = all ? () => go('L-06') : () => {
      setLst(l => {
        const show = !l.loggedIn && !embed && (l.prompts[st.id] || 0) < 2;
        return { ...l, justCleared: st.id, sheet: show ? 2 : false, prompts: show ? { ...l.prompts, [st.id]: 2 } : l.prompts };
      });
      go('L-01');
    };
    const key = Math.random();
    setBigFx({ key, aura: auraC(st.aura), aura2: auraC(rm.stages[(si + 1) % rm.stages.length].aura), plus: all, title: all ? rm.title : st.title, after });
    bigT.current = setTimeout(() => finishBig(after), reduced ? 900 : (all ? 4000 : 2500));
  };

  if (!rm.stages.length || !rm.stages.some(s => s.steps.length)) {
    return <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--ink3)', fontSize: 14, background: '#fff' }}><LogoPattern opacity={0.045} /><span style={{ position: 'relative' }}>まだ公開されていません</span></div>;
  }
  const lstX = { ...lst, onComplete };
  const st = rm.stages[lst.stage] || rm.stages[0];
  const sst = stageState(st, prog[st.id]);

  const fullNext = () => {
    const idx = Math.min(lst.step, sst.total - 1);
    if (idx >= sst.total - 1) {
      // 全画面モードでも最後のスライドで次へ進んだら達成処理を実行
      go('L-02');
      onComplete(lst.stage);
      return;
    }
    if (idx >= sst.c) setProg(p => ({ ...p, [st.id]: { ...p[st.id], c: Math.max(p[st.id]?.c || 0, idx + 1) } }));
    setLst(l => ({ ...l, step: idx + 1 }));
  };
  const fullPrev = () => setLst(l => ({ ...l, step: Math.max(0, l.step - 1) }));

  let body;
  const show = screen === 'L-03' ? 'L-02' : screen;
  if (show === 'L-01') body = <L01Toc m={m} rm={rm} cfg={cfg} prog={prog} lst={lst} go={go} openStage={openStage} openLegal={setLegalType} />;
  else if (show === 'L-02') body = <L02Step m={m} rm={rm} cfg={cfg} prog={prog} setProg={setProg} lst={lstX} setLst={setLst} go={go} reduced={reduced} onFull={() => go('L-03')} />;
  else if (show === 'L-05') body = <L05Login m={m} cfg={cfg} onDone={() => { setLst(l => ({ ...l, loggedIn: true, sheet: false })); go('L-01'); }} onBack={() => go('L-01')} openLegal={setLegalType} />;
  else if (show === 'L-06') body = <L06Done m={m} rm={rm} cfg={cfg} go={go} reset={() => { setProg({}); setLst(l => ({ ...l, justCleared: null })); go('L-01'); }} />;
  else if (show === 'L-04') body = <L01Toc m={m} rm={rm} cfg={cfg} prog={prog} lst={lst} go={go} openStage={openStage} openLegal={setLegalType} />;

  const inner = (
    <div style={{ ...lrnStyles.root, background: embed ? 'transparent' : '#fff', position: embed ? 'relative' : 'absolute', minHeight: embed ? (m ? 520 : 600) : undefined, height: embed ? '100%' : undefined }}>
      <LearnerBg embed={embed} />
      {show !== 'L-05' && <LearnerHeader m={m} rm={rm} cfg={cfg} lst={lst} go={go} back={show === 'L-02' ? () => go('L-01') : null} />}
      {body}
      {screen === 'L-03' && <L03Full m={m} rm={rm} lst={lst} setLst={setLst} prog={prog} onNext={fullNext} onPrev={fullPrev} close={() => go('L-02')} />}
      {(lst.sheet || screen === 'L-04') && <L04Sheet m={m} which={lst.sheet === 2 ? 2 : 1} onSave={() => { setLst(l => ({ ...l, sheet: false })); go('L-05'); }} onLater={() => { setLst(l => ({ ...l, sheet: false })); if (screen === 'L-04') go('L-01'); }} />}
    </div>
  );

  return (
    <div style={{ position: 'absolute', inset: 0 }}>
      {embed ? <MemberSiteShell m={m}>{inner}</MemberSiteShell> : inner}
      {bigFx && (
        <div style={{ position: 'fixed', inset: 0, zIndex: 9999 }} onClick={() => finishBig(bigFx.after)}>
          <FxC key={bigFx.key} aura={bigFx.aura} aura2={bigFx.aura2} plus={bigFx.plus} reduced={reduced} title={bigFx.title} />
        </div>
      )}
      {!embed && <div style={{ position: 'absolute', right: 14, bottom: m ? 8 : 12, zIndex: 3, opacity: .55 }}><Logo size={10} /></div>}
      {legalType && window.LegalModal && (
        <window.LegalModal type={legalType} onClose={() => setLegalType(null)} />
      )}
    </div>
  );
}

Object.assign(window, { LearnerApp, stageState, CodeInput });
