/* tenroin-top/article-page.jsx — READING LIFE 記事リーダー（/magazine/<slug> → article.html）
   ・slug は location.pathname（/magazine/<slug>）→ フォールバックで ?slug= から解決
   ・データは window.MAG.ARTICLES / ISSUE（articles.jsx が正本）
   ・記事専属AIドック＝Phase 1 は「顔と名前」まで（対話は9月βで解禁）
*/

function readingMinutes(body){
  const chars = body.filter(b=>b.t==='p'||b.t==='q').reduce((n,b)=>n+(b.v||'').length,0);
  return Math.max(1, Math.round(chars/600));
}

function ArtBlock({b}){
  if(b.t==='h')  return <h2 className="artb-h">{b.v}</h2>;
  if(b.t==='q')  return <blockquote className="artb-q">{b.v}</blockquote>;
  if(b.t==='hr') return <div className="artb-hr" aria-hidden="true"><span>❖</span></div>;
  if(b.t==='video'){
    /* 本文の動画（2026/9/23 代表「記事の途中に動画や写真を入れ込めるように」）。
       サーバが Vimeo の住所だけを `{t:'video'}` に組む（自社の置き場に動画は置かない）。
       ⚠️住所が読めない回は**器ごと出さない**（空の枠を紙面に出さない）。
       ★埋め込みの形は既存の週刊の面と同じ（player.vimeo.com）＝新しい作法を作らない。 */
    const m = String(b.src || b.v || '').match(/vimeo\.com\/(\d{6,12})(?:\/([0-9a-f]{6,16}))?/i);
    if(!m) return null;
    const vsrc = 'https://player.vimeo.com/video/' + m[1] + (m[2] ? ('?h=' + m[2]) : '');
    return (
      <figure className="artb-fig">
        <div style={{position:'relative', paddingTop:'56.25%'}}>
          <iframe src={vsrc} title={b.cap || '映像'} loading="lazy"
            allow="autoplay; fullscreen; picture-in-picture" allowFullScreen
            style={{position:'absolute', inset:0, width:'100%', height:'100%', border:0}}></iframe>
        </div>
        {b.cap && <figcaption>{b.cap}</figcaption>}
      </figure>
    );
  }
  if(b.t==='img') return (
    /* 🔴鍵が二つある＝静的な記事は `v`、台帳の作は `src`（2026/9/23 に入った口）。
       どちらも受ける。片方だけ見ると、会員が本文に入れた写真が**出ない**（実測で見つけた）。 */
    <figure className="artb-fig">
      <img src={b.src || b.v} alt={b.alt||b.cap||''} loading="lazy"/>
      {b.cap && <figcaption>{b.cap}</figcaption>}
    </figure>
  );
  return <p className="artb-p">{b.v}</p>;
}

/* ── 言問の袋とじ ── 金では開かない。答えるか、ナイフを持つか。 ── */
function FukurotojiBlock({slug, f}){
  const { useState, useEffect } = React;
  const KEY = 'fkt_' + slug;
  const [data, setData] = useState(null);
  const [phase, setPhase] = useState('closed');
  const [ans, setAns] = useState('');
  const [err, setErr] = useState('');
  useEffect(()=>{
    try{ const s = JSON.parse(localStorage.getItem(KEY) || 'null'); if(s && Array.isArray(s.sealed)){ setData(s); setPhase('open'); } }catch(e){}
  },[]);
  const [buying, setBuying] = useState(false);
  useEffect(()=>{
    if(f.mode !== 'paid' || data) return;
    const sid = new URLSearchParams(location.search).get('fkt_session');
    if(!sid) return;
    setPhase('cutting');
    fetch('/api/magazine/fukurotoji/verify', {
      method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ slug, sessionId: sid }),
    })
      .then(r=>r.json())
      .then(j=>{
        try{ history.replaceState(null, '', location.pathname); }catch(e){}
        if(!j.ok){ setErr(j.error || '解錠を確認できませんでした。'); setPhase('closed'); return; }
        try{ localStorage.setItem(KEY, JSON.stringify(j)); }catch(e){}
        setPhase('tearing');
        setTimeout(()=>{ setData(j); setPhase('open'); }, 950);
      })
      .catch(()=>{ setErr('解錠を確認できませんでした。ページを再読み込みしてください。'); setPhase('closed'); });
  },[]);
  const buy = () => {
    if(buying || phase !== 'closed') return;
    setBuying(true); setErr('');
    fetch('/api/magazine/fukurotoji/checkout', {
      method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ slug }),
    })
      .then(r=>r.json())
      .then(j=>{
        if(!j.ok || !j.url){ setErr(j.error || '決済ページを開けませんでした。'); setBuying(false); return; }
        location.href = j.url;
      })
      .catch(()=>{ setErr('決済ページを開けませんでした。少し待ってお試しください。'); setBuying(false); });
  };
  const cut = () => {
    if(phase !== 'closed') return;
    setErr(''); setPhase('cutting');
    fetch('/api/magazine/kototoi', {
      method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ slug, answer: ans }),
    })
      .then(r=>r.json())
      .then(j=>{
        if(!j.ok){ setErr(j.error || '紙がうまく切れませんでした。'); setPhase('closed'); return; }
        try{ localStorage.setItem(KEY, JSON.stringify(j)); }catch(e){}
        setPhase('tearing');
        setTimeout(()=>{ setData(j); setPhase('open'); }, 950);
      })
      .catch(()=>{ setErr('紙がうまく切れませんでした。少し待って、もう一度お試しください。'); setPhase('closed'); });
  };
  if(phase === 'open' && data){
    return (
      <section className="fkt is-open">
        <div className="fkt-kick"><span>{f.kick}</span><span className="fkt-no">切り口 ── あなたの書庫に残ります</span></div>
        <div className="fkt-edge" aria-hidden="true"></div>
        <div className="fkt-inner">
          {data.sealed.map((b,i)=><ArtBlock key={i} b={b}/>)}
          {data.henrei && (
            <aside className="fkt-henrei">
              <div className="fh-kick">返礼 ── あなたへ</div>
              <p>{data.henrei}</p>
              <div className="fh-sign">スピカ 拝</div>
            </aside>
          )}
          {!data.henrei && (
            <div className="fkt-knife-mark">金のペーパーナイフ ── この封は、あなたの鍵で切られました</div>
          )}
        </div>
      </section>
    );
  }
  if(f.mode === 'paid'){
    return (
      <section className={'fkt fkt-paid'+(phase==='tearing'?' is-tearing':'')}>
        <div className="fkt-kick"><span>{f.kick}</span></div>
        <div className="fkt-pouch">
          <div className="fkt-ghost" aria-hidden="true">{f.teaser}</div>
          <div className="fkt-seal">
            <p className="fkt-q"><b>この袋とじは、金のペーパーナイフで開きます</b>{f.note}</p>
            {err && <p className="fkt-err">{err}</p>}
            <div className="fkt-doors">
              <button className="fkt-cut fkt-buy" onClick={buy} disabled={buying || phase !== 'closed'} data-cta="fukurotoji-buy">
                {buying ? '決済ページへ…' : (phase === 'cutting' ? '解錠を確認しています…' : '金のペーパーナイフを買う（¥550）── この袋とじを開ける')}
              </button>
            </div>
            <p className="fkt-soon">会員・非会員に関わらず、この袋とじはナイフでのみ開きます ── お支払い後、この場で切れます</p>
          </div>
        </div>
      </section>
    );
  }
  return (
    <section className={'fkt'+(phase==='tearing'?' is-tearing':'')}>
      <div className="fkt-kick"><span>{f.kick}</span><span className="fkt-no">綴 No.001</span></div>
      <div className="fkt-pouch">
        <div className="fkt-ghost" aria-hidden="true">{f.teaser}</div>
        <div className="fkt-seal">
          <p className="fkt-q"><b>スピカの問い</b>{f.question}</p>
          <textarea className="fkt-ans" rows={3} maxLength={600} placeholder="ひとことで、構いません。"
            value={ans} onChange={e=>setAns(e.target.value)} disabled={phase!=='closed'}/>
          {err && <p className="fkt-err">{err}</p>}
          <div className="fkt-doors">
            <button className="fkt-cut" onClick={cut} disabled={phase!=='closed' || ans.trim().length<4}>
              {phase==='cutting' ? '切りひらいています…' : '答えて、切りひらく'}
            </button>
          </div>
        </div>
      </div>
    </section>
  );
}

/* ── 記事担当AIコンシェルジュ（右下フロート・体験版） ── */
function ConciergeFloat({art}){
  const { useState, useEffect, useRef } = React;
  const ai = art.ai;
  const KEY = 'rl_ccg_' + art.slug;
  const [open, setOpen] = useState(false);
  const [msgs, setMsgs] = useState(()=>{ try{ const m = JSON.parse(localStorage.getItem(KEY) || '[]'); return Array.isArray(m) ? m : []; }catch(e){ return []; } });
  const [inp, setInp] = useState('');
  const [busy, setBusy] = useState(false);
  const [note, setNote] = useState('');
  const boxRef = useRef(null);
  useEffect(()=>{ window.__openConcierge = ()=>setOpen(true); return ()=>{ delete window.__openConcierge; }; },[]);
  useEffect(()=>{
    try{ localStorage.setItem(KEY, JSON.stringify(msgs.slice(-20))); }catch(e){}
    if(boxRef.current) boxRef.current.scrollTop = boxRef.current.scrollHeight;
  },[msgs, open, busy]);
  const quota = ()=>{ const day = new Date().toISOString().slice(0,10); let q = null; try{ q = JSON.parse(localStorage.getItem('rl_ccg_quota') || 'null'); }catch(e){} return (q && q.day === day) ? q : { day, n: 0 }; };
  const left = ()=> Math.max(0, 5 - quota().n);
  const send = ()=>{
    const t = inp.trim();
    if(!t || busy) return;
    if(left() <= 0){ setNote('体験版の本日分はここまで。続きは読書クラブで、無制限に。'); return; }
    setMsgs(m=>[...m, {r:'u', t}]); setInp(''); setBusy(true); setNote('');
    fetch('/api/magazine/concierge', {
      method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ slug: art.slug, message: t, history: msgs.slice(-6).map(m=>({ role: m.r==='u'?'user':'assistant', content: m.t })) }),
    })
      .then(r=>r.json())
      .then(j=>{
        setBusy(false);
        if(!j.ok){ setNote(j.error || 'うまくつながりませんでした。'); return; }
        const q = quota(); q.n += 1;
        try{ localStorage.setItem('rl_ccg_quota', JSON.stringify(q)); }catch(e){}
        setMsgs(m=>[...m, {r:'a', t:j.reply}]);
      })
      .catch(()=>{ setBusy(false); setNote('つながりませんでした。少し待ってもう一度どうぞ。'); });
  };
  return (
    <div className="ccg">
      {open && (
        <div className="ccg-panel">
          <div className="ccg-head">
            <video className="ccg-face" src={busy && ai.talking ? ai.talking : ai.idle} poster={ai.avatar} autoPlay muted loop playsInline></video>
            <div className="ccg-who"><b>{ai.name}</b><span>この記事の担当 ── 体験版・本日あと{left()}問</span></div>
            <button className="ccg-close" onClick={()=>setOpen(false)} aria-label="閉じる">✕</button>
          </div>
          <div className="ccg-body" ref={boxRef}>
            {msgs.length === 0 && <p className="ccg-hint">「{ai.quote}」──この記事のこと、READING LIFEのこと、聞いてください。</p>}
            {msgs.map((m,i)=><p key={i} className={'ccg-m '+(m.r==='u'?'is-u':'is-a')}>{m.t}</p>)}
            {busy && <p className="ccg-m is-a ccg-typing">…</p>}
          </div>
          {note && <p className="ccg-note">{note}</p>}
          <div className="ccg-in">
            <input value={inp} maxLength={300} placeholder={ai.name+'に話しかける…'}
              onChange={e=>setInp(e.target.value)}
              onKeyDown={e=>{ if(e.key==='Enter' && !e.nativeEvent.isComposing) send(); }}/>
            <button onClick={send} disabled={busy || !inp.trim()}>送る</button>
          </div>
        </div>
      )}
      <button className={'ccg-fab'+(open?' is-open':'')} onClick={()=>setOpen(o=>!o)} aria-label={ai.name+'と話す'}>
        <img src={ai.avatar} alt={ai.name}/>
        <span className="ccg-fab-ring" aria-hidden="true"></span>
      </button>
    </div>
  );
}

/* ── ひろめる（SHARE）── MY READING LIFE=拡散の武器。X/LINE/コピー/端末共有 ── */
function ShareBar({title, label, img}){
  const { useState } = React;
  const [copied, setCopied] = useState(false);
  const [storyBusy, setStoryBusy] = useState(false);
  const [storyDone, setStoryDone] = useState(false);
  const url = () => location.origin + location.pathname;
  const text = () => title + '｜READING LIFE ── 体験できるマガジン';
  const canNative = typeof navigator !== 'undefined' && !!navigator.share;
  const copy = () => {
    if(navigator.clipboard && navigator.clipboard.writeText){
      navigator.clipboard.writeText(url()).then(()=>{ setCopied(true); setTimeout(()=>setCopied(false), 2200); }).catch(()=>{});
    }
  };
  const story = () => {
    if(storyBusy) return;
    setStoryBusy(true);
    if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(url()).catch(()=>{}); }
    const load = (src) => new Promise((ok, ng)=>{ const im = new Image(); im.onload = ()=>ok(im); im.onerror = ng; im.src = src; });
    Promise.all([
      load(img || 'assets/rl-hero-taiken.jpg').catch(()=>load('assets/rl-hero-taiken.jpg')),
      load('assets/rl-logo-line-cream.png'),
      (document.fonts && document.fonts.load) ? document.fonts.load('600 64px "Klee One"').catch(()=>{}) : Promise.resolve(),
    ]).then(([hero, logo])=>{
      const W = 1080, H = 1920;
      const c = document.createElement('canvas'); c.width = W; c.height = H;
      const x = c.getContext('2d');
      const s = Math.max(W/hero.width, H/hero.height);
      x.drawImage(hero, (W-hero.width*s)/2, (H-hero.height*s)/2, hero.width*s, hero.height*s);
      let g = x.createLinearGradient(0,0,0,H*0.3);
      g.addColorStop(0,'rgba(10,16,20,.74)'); g.addColorStop(1,'rgba(10,16,20,0)');
      x.fillStyle = g; x.fillRect(0,0,W,H*0.3);
      g = x.createLinearGradient(0,H*0.42,0,H);
      g.addColorStop(0,'rgba(10,16,20,0)'); g.addColorStop(1,'rgba(10,16,20,.95)');
      x.fillStyle = g; x.fillRect(0,H*0.42,W,H*0.58);
      const lw = 560, lh = lw*logo.height/logo.width;
      x.drawImage(logo, (W-lw)/2, 120, lw, lh);
      x.textAlign = 'center'; x.fillStyle = '#FCFBF6'; x.font = '600 64px "Klee One", serif';
      const lines = []; let line = '';
      (title || '').split('').forEach(ch=>{ line += ch; if(x.measureText(line).width > W-200){ lines.push(line); line=''; } });
      if(line) lines.push(line);
      lines.slice(0,4).forEach((l,i)=>x.fillText(l, W/2, H-540+i*100));
      x.font = '500 34px "Jost", sans-serif'; x.fillStyle = 'rgba(214,188,126,.95)';
      x.fillText('READING LIFE ── 体験できるマガジン', W/2, H-160);
      x.font = '400 30px "Jost", sans-serif'; x.fillStyle = 'rgba(252,251,246,.75)';
      x.fillText(url().replace(/^https?:\/\//,''), W/2, H-104);
      c.toBlob(b=>{
        setStoryBusy(false);
        if(!b) return;
        const a = document.createElement('a');
        a.href = URL.createObjectURL(b); a.download = 'readinglife-story.png';
        document.body.appendChild(a); a.click(); a.remove();
        setStoryDone(true); setTimeout(()=>setStoryDone(false), 4200);
      }, 'image/png');
    }).catch(()=>setStoryBusy(false));
  };
  return (
    <div className="shr">
      <div className="shr-kick">{label || 'この記事を、ひろめる'} ── SHARE</div>
      <div className="shr-row">
        <a className="shr-btn" target="_blank" rel="noopener" data-cta="share-x"
           href={'https://x.com/intent/post?text='+encodeURIComponent(text())+'&url='+encodeURIComponent(url())}>
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M4 4l16 16M20 4L4 20"/></svg>
          Xに投稿
        </a>
        <a className="shr-btn" target="_blank" rel="noopener" data-cta="share-threads"
           href={'https://www.threads.net/intent/post?text='+encodeURIComponent(text()+' '+url())}>
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/></svg>
          Threadsに投稿
        </a>
        <a className="shr-btn" target="_blank" rel="noopener" data-cta="share-facebook"
           href={'https://www.facebook.com/sharer/sharer.php?u='+encodeURIComponent(url())}>
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/></svg>
          Facebookでシェア
        </a>
        <a className="shr-btn" target="_blank" rel="noopener" data-cta="share-line"
           href={'https://social-plugins.line.me/lineit/share?url='+encodeURIComponent(url())+'&text='+encodeURIComponent(text())}>
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>
          LINEで送る
        </a>
        <button className="shr-btn" onClick={story} data-cta="share-story">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="20" height="20" rx="5"/><circle cx="12" cy="12" r="4"/><circle cx="17.5" cy="6.5" r="0.5" fill="currentColor"/></svg>
          {storyDone ? 'カード保存・リンクもコピー済み' : (storyBusy ? 'カードを作っています…' : 'ストーリーズ用カードを保存')}
        </button>
        <button className="shr-btn" onClick={copy} data-cta="share-copy">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
          {copied ? 'コピーしました' : 'リンクをコピー'}
        </button>
        {canNative && (
          <button className="shr-btn" onClick={()=>{ navigator.share({ title: text(), url: url() }).catch(()=>{}); }} data-cta="share-native">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="M8.6 13.5l6.8 4M15.4 6.5l-6.8 4"/></svg>
            共有
          </button>
        )}
      </div>
    </div>
  );
}

/* ── UPXカード（この記事から、次の一歩）── AIの下に置く商品への橋 ── */
function UpxCard({u}){
  return (
    <aside className="upx" data-reveal>
      <a className="upx-card" href={u.href} target="_blank" rel="noopener" data-cta="article-upx">
        <span className="upx-th"><img src={u.img} alt="" loading="lazy"/></span>
        <span className="upx-main">
          <span className="upx-kick">{u.kick}</span>
          <span className="upx-title">{u.title}</span>
          <span className="upx-desc">{u.desc}</span>
          <span className="upx-foot"><b>{u.price}</b><i>{u.cta} →</i></span>
        </span>
      </a>
    </aside>
  );
}

function ContributorBlock({c}){
  return (
    <aside className="contrib" data-reveal>
      <div className="ct-photo"><img src={c.avatar} alt={c.name} loading="lazy" style={c.focus?{objectPosition:c.focus}:null}/></div>
      <div className="ct-main">
        {/* 🔴2026/9/23＝値が無い行は器ごと出さない（会員の作にもこの枠を使うため。
            書庫の98頁は四つとも値を持つので姿は変わらない＝前後で実測した）。 */}
        <span className="ct-stamp" aria-hidden="true">{c.stamp || '寄稿'}</span>
        {c.file ? <div className="ct-file">{c.file}</div> : null}
        <div className="ct-name">{c.name}</div>
        {c.en ? <div className="ct-en">{c.en}</div> : null}
        {c.title ? <div className="ct-title">{c.title}</div> : null}
        {c.bio ? <p className="ct-bio">{c.bio}</p> : null}
        <div className="ct-links">
          {c.links.map((l,i)=> l.soon
            ? <span key={i} className="ct-soon">{l.label}</span>
            : <a key={i} className="melt-cta" href={l.href} data-cta="contributor-link">{l.label}<span className="ar">▸</span></a>
          )}
        </div>
      </div>
    </aside>
  );
}

function AiDock({ai}){
  return (
    <aside className="ai-dock" data-reveal>
      <div className={'aid-face'+(ai && ai.avatar ? ' has-video' : '')} aria-hidden="true">
        {ai && ai.avatar
          ? <video src={ai.idle} poster={ai.avatar} autoPlay muted loop playsInline></video>
          : (ai ? ai.name.slice(0,1) : '？')}
      </div>
      <div className="aid-main">
        <div className="aid-kick">この記事の担当</div>
        {ai ? (
          <React.Fragment>
            <div className="aid-name">{ai.name}<small>〈{ai.title}〉</small></div>
            <p className="aid-quote">「{ai.quote}」</p>
            <div className="aid-actions">
              {ai.avatar
                ? <button className="aid-talk" onClick={()=>window.__openConcierge && window.__openConcierge()} data-cta="ai-dock-talk">{ai.name}と、この記事の話をする ── 体験版</button>
                : <span className="aid-soon">記事と話す ── 体験版・まもなく</span>}
              <a className="melt-cta" href="https://tenro-in.com/book-club" data-cta="ai-dock-club">読書クラブで先行体験<span className="ar">▸</span></a>
            </div>
          </React.Fragment>
        ) : (
          <React.Fragment>
            <div className="aid-name dim">近日就任</div>
            <p className="aid-quote">この記事の専属AIは、ただいま編集部で研修中です。就任をお楽しみに。</p>
          </React.Fragment>
        )}
      </div>
    </aside>
  );
}

function BridgeBand(){
  return (
    <section className="bridge" data-reveal>
      <div className="br-kick">READING LIFE は、無料で読める「町の雑誌」です</div>
      <h3 className="br-h">その先へ——読んだ物語を、あなたの現実に。</h3>
      <p className="br-p">天狼院読書クラブ with AI（月額 ¥11,000）には、この雑誌のAI体験のすべてと、水曜夜の編集会議、そして5つのクラブが待っています。</p>
      <a className="melt-cta" href="https://tenro-in.com/book-club" data-cta="bridge-club">天狼院読書クラブ with AI を見る<span className="ar">▸</span></a>
    </section>
  );
}

function ArtColophon({issue}){
  return (
    <section className="art-colo" data-reveal>
      <div className="artc-strip"><img className="artc-logo" src="assets/rl-logo-line-ink.png" alt="READING LIFE"/><span>── No.{issue.no} ／ {issue.ja}</span></div>
      <dl className="artc-list">
        <div><dt>発行</dt><dd><a href={issue.publisherUrl}>{issue.publisher}</a></dd></div>
        <div><dt>編集</dt><dd>{issue.editorial}</dd></div>
        <div><dt>制作</dt><dd>三浦崇典＋AIエージェント</dd></div>
        <div><dt>LIVEの場</dt><dd>天狼院書店（渋谷・名古屋・京都・福岡）</dd></div>
      </dl>
    </section>
  );
}

function miuraContrib(){
  const P = window.LP_PROFILE_DATA && window.LP_PROFILE_DATA.handle === 'miura'
    ? window.LP_PROFILE_DATA : (window.LP_PROFILES || {}).miura;
  if(!P) return null;
  return {
    stamp: '店主',
    file: 'TENRO-IN CREATORS ── FILE 001',
    name: P.hero.name.replace(/\s+/g, ''),
    en: P.hero.reading,
    title: P.hero.role,
    avatar: P.hero.photo,
    focus: P.hero.focus,
    bio: (P.intro && P.intro.bio && P.intro.bio[0]) || P.hero.tagline,
    links: [
      { label: 'プロフィールをひらく ── クリエイターズ・ファイル', href: '/creators/miura' },
    ],
  };
}

function ArchivePage({slug}){
  const { useState, useEffect } = React;
  const { ISSUE } = window.MAG;
  const [art, setArt] = useState(null);
  const [err, setErr] = useState(false);
  useEffect(()=>{
    fetch('/tenroin-top/archive/miura/'+slug+'.json')
      .then(r=>r.ok?r.json():Promise.reject())
      .then(setArt).catch(()=>setErr(true));
  },[slug]);
  useEffect(()=>{ if(art) document.title = `${art.t}｜天狼院通信 ── READING LIFE 書庫｜天狼院書店`; },[art]);
  useEffect(()=>{
    document.querySelectorAll('[data-reveal]:not(.is-in)').forEach(e=>e.classList.add('is-in'));
  },[art]);
  return (
    <div className="app-root ti">
      <header className="evp-head">
        <div className="evp-head-in">
          <a className="evp-logo" href="/tenroin-top/" aria-label="天狼院書店 トップへ">
            <img src={R('logoInk','/tenroin-top/assets/logo-wordmark-ink.png')} alt="天狼院書店 TENRO-IN"/>
          </a>
          <nav className="evp-nav">
            <a className="evp-back" href="/tenroin-top/?toc=1#articles">目次をひらく</a>
            <a className="evp-back" href="/tenroin-top/#articles">‹ マガジンに戻る</a>
          </nav>
        </div>
      </header>
      <article className="art-body-wrap">
        <div className="wrap-narrow">
          {err && <p className="art-lede" style={{marginTop:'60px'}}>記事を読み込めませんでした。時間をおいて、もう一度お試しください。</p>}
          {art && (
            <React.Fragment>
              <header className="art-head" style={{'--acc':'#93702F'}}>
                {/* 天狼院通信の共通の扉（代表 2026/9/12「絵が無い場合は天狼院通信のビジュアルを
                    かっこよく作成して、共通でそれを入れる。あるものはそのビジュアルを使っていい」）。
                    ★98本のうち 13本が旧サイトから絵を持って来られず、扉ごと出ずに素の題だけだった。
                      共通の一枚を敷くことで、書庫の 98本が同じ扉の組みになる。
                    ★個別の絵を持つ本は、そのまま自分の絵を使う（上書きしない）。 */}
                {(art.hero || window.ARCHIVE_HERO) ? (
                <div className="art-photo-hero">
                  <img className="aph-img" src={art.hero || window.ARCHIVE_HERO} alt=""/>
                  <div className="aph-scrim" aria-hidden="true"></div>
                  <div className="photo-veil" aria-hidden="true"></div>
                  <div className="aph-top">
                    <span className="aph-mag"><img src="assets/rl-logo-line-cream.png" alt="READING LIFE"/></span>
                    <span className="aph-no">書庫 ── 天狼院通信</span>
                    <span className="aph-cat">ARCHIVE<em>{art.d}</em></span>
                  </div>
                  <div className="aph-in">
                    <div className="aph-in-narrow">
                      <h1 className="art-title light">{art.t}</h1>
                      {/* 欄＝owner（店主・天狼院通信の98本／CTO 2026/9/7 17:10）。
                          ★札は重ねない ── 署名が既に「天狼院書店店主」と名乗っている。
                          ★98件それぞれに欄を書かない＝この通し道が丸ごと owner なので、ここ一箇所で足りる。 */}
                      <div className="aph-author"><span className="aa-line"></span>文 ── 三浦崇典（天狼院書店店主）
                        {/* 落款（第一層）と形の札（第二層）＝代表 2026/9/9。
                            ★新しい段は作らない ── 既にある署名の行に並べる。
                            ★どちらも札を持たない記事では描かれない（書庫はいま全件そう）。 */}
                        {window.RlSeal ? <window.RlSeal art={art} /> : null}
                        {art && art.genre ? <span className="aa-col">{art.genre}</span> : null}</div>
                    </div>
                  </div>
                </div>
                ) : (
                <div>
                  <div className="art-kick" data-reveal><span>書庫 ── 天狼院通信</span><span className="ak-pg">{art.d}</span></div>
                  <h1 className="art-title" data-reveal>{art.t}</h1>
                </div>
                )}
                <div className="art-meta" data-reveal>
                  <span>三浦崇典（天狼院書店店主）</span>
                  <span className="am-dot">・</span>
                  <span>{art.d}</span>
                  <span className="am-dot">・</span>
                  <span>天狼院通信 ── READING LIFE 書庫</span>
                </div>
              </header>
              <div className="art-html" dangerouslySetInnerHTML={{__html: art.html}}/>
              {miuraContrib() && <ContributorBlock c={miuraContrib()}/>}
              <ShareBar title={art.t}/>
              <BridgeBand/>
              <ArtColophon issue={ISSUE}/>
            </React.Fragment>
          )}
        </div>
      </article>
      <footer className="evp-foot">
        <div className="wrap evp-foot-in">
          <img src={R('footLogo','/tenroin-top/assets/logo-cream-sm.png')} alt="天狼院書店"/>
          <img className="evpf-logo" src="assets/rl-logo-copy-cream.png" alt="READING LIFE — THE TENRO-IN EXPERIENCE"/>
          <a className="melt-cta light" href="/tenroin-top/">マガジン トップへ戻る<span className="ar">→</span></a>
        </div>
      </footer>
    </div>
  );
}

function WeeklyPage({slug}){
  const { useEffect } = React;
  const { ISSUE, WEEKLY } = window.MAG;
  const ep = (WEEKLY.episodes||[]).find(e=>e.slug===slug);
  const others = (WEEKLY.episodes||[]).filter(e=>e.slug!==slug);
  useEffect(()=>{ document.title = (ep ? `第${ep.no}回 ${ep.title}` : '週刊READING LIFE') + '｜週刊READING LIFE｜天狼院書店'; },[ep]);
  useEffect(()=>{ document.querySelectorAll('[data-reveal]:not(.is-in)').forEach(e=>e.classList.add('is-in')); },[ep]);
  return (
    <div className="app-root ti">
      <header className="evp-head">
        <div className="evp-head-in">
          <a className="evp-logo" href="/tenroin-top/" aria-label="天狼院書店 トップへ">
            <img src={R('logoInk','/tenroin-top/assets/logo-wordmark-ink.png')} alt="天狼院書店 TENRO-IN"/>
          </a>
          <nav className="evp-nav">
            <a className="evp-back" href="/tenroin-top/?toc=1#articles">目次をひらく</a>
            <a className="evp-back" href="/tenroin-top/#articles">‹ マガジンに戻る</a>
          </nav>
        </div>
      </header>
      <article className="art-body-wrap">
        <div className="wrap-narrow">
          {!ep && <p className="art-lede" style={{marginTop:'60px'}}>この回はまだ書庫にありません。<a href="/tenroin-top/#articles">マガジンに戻る</a></p>}
          {ep && (
            <React.Fragment>
              <header className="art-head" style={{'--acc':'#C2A052'}}>
                {ep.img ? (
                <div className="art-photo-hero">
                  <img className="aph-img" src={ep.img} alt=""/>
                  <div className="aph-scrim" aria-hidden="true"></div>
                  <div className="photo-veil" aria-hidden="true"></div>
                  <div className="aph-top">
                    <span className="aph-mag">{WEEKLY.ser}</span>
                    <span className="aph-no">{WEEKLY.en}</span>
                    <span className="aph-cat">第{ep.no}回<em>{ep.date}</em>{ep.preview && <i>準備号</i>}</span>
                  </div>
                  <div className="aph-in">
                    <div className="aph-in-narrow">
                      <h1 className="art-title light">{ep.title}</h1>
                      <p className="aph-lede">{ep.lede}</p>
                      <div className="aph-author"><span className="aa-line"></span>番組 ── 公開編集会議より</div>
                    </div>
                  </div>
                </div>
                ) : (
                <div>
                  <div className="art-kick" data-reveal><span>{WEEKLY.ser}</span><span className="ak-pg">第{ep.no}回</span>{ep.preview && <span className="ak-proof">準備号</span>}</div>
                  <h1 className="art-title" data-reveal>{ep.title}</h1>
                  <p className="art-lede" data-reveal>{ep.lede}</p>
                </div>
                )}
                <div className="art-meta" data-reveal>
                  <span>{ep.author || 'READING LIFE編集部'}</span>
                  <span className="am-dot">・</span>
                  <span>{ep.date}</span>
                  <span className="am-dot">・</span>
                  <span>{WEEKLY.ser} ── 公開編集会議</span>
                </div>
              </header>

              <div className="wk-player" data-reveal>
                {ep.video ? (
                  <div className="wk-frame">
                    <iframe
                      src={ep.video.type==='vimeo'
                        ? 'https://player.vimeo.com/video/'+ep.video.id
                        : 'https://www.youtube-nocookie.com/embed/'+ep.video.id}
                      allow="autoplay; fullscreen; picture-in-picture" allowFullScreen title={ep.title}></iframe>
                  </div>
                ) : (
                  <div className="wk-empty"><span className="twl-dot" aria-hidden="true"></span>この回の映像は、放送後にここに掛かります。</div>
                )}
              </div>

              {ep.notes && ep.notes.length > 0 && (
                <div className="art-body" data-reveal>
                  {ep.notes.map((b,i)=><ArtBlock key={i} b={b}/>)}
                </div>
              )}

              <ShareBar title={'第'+ep.no+'回 '+ep.title} label="この放送を、ひろめる" img={ep.img}/>

              {others.length > 0 && (
                <nav className="art-rel" data-reveal>
                  <div className="ar-kick">ほかの放送</div>
                  {others.map(e=>(
                    <a key={e.slug} className="ar-link" href={'/magazine/'+e.slug} data-cta="weekly-ep">
                      <span className="ta-date">{e.date}　第{e.no}回</span><span className="ta-t">{e.title}</span><span className="ar-arrow">→</span>
                    </a>
                  ))}
                </nav>
              )}

              <BridgeBand/>
              <ArtColophon issue={ISSUE}/>
            </React.Fragment>
          )}
        </div>
      </article>
      <footer className="evp-foot">
        <div className="wrap evp-foot-in">
          <img src={R('footLogo','/tenroin-top/assets/logo-cream-sm.png')} alt="天狼院書店"/>
          <img className="evpf-logo" src="assets/rl-logo-copy-cream.png" alt="READING LIFE — THE TENRO-IN EXPERIENCE"/>
          <a className="melt-cta light" href="/tenroin-top/">マガジン トップへ戻る<span className="ar">→</span></a>
        </div>
      </footer>
    </div>
  );
}

/* ══════════ 会員の記事（あなたの欄）── 日刊READING LIFE ══════════
   GET /api/magazine/works/<slug> だけを見て描く。静的な配列には焼き込まない
   （焼き込むと、失効した方の作を下げる門が素通りになる＝設計書30 3-5 の経路表）。 */
/* 会員の作の書き手の欄＝**既存の寄稿者の枠（ContributorBlock）に入れ込む**。
   🔴2026/9/23 代表「プロフィールの枠も元々あった。前に作ったもので作り直して」。
     当席は一度ここに新しい器を作ったが、書庫の98頁が使っている枠が既に在った（miuraContrib と同じ形）。
   ★写真が無ければ枠ごと出さない（この枠は写真が主役＝空の全幅枠になるため）。
     ラウンジの「作家ページを整えると、記事に肖像と店が載ります」と同じ約束になる。
   ★店と棚の本は、枠の中の戸口に寄せる（器を増やさない）。 */
function authorContrib(w){
  const a = (w && w.author) || {};
  if(!a.photoUrl) return null;
  const kind = window.columnLabel ? window.columnLabel({ column: w.authorKind, author: a.penName }) : '';
  const links = [];
  if(a.profileUrl) links.push({ label: '作家ページをひらく ── クリエイターズ・ファイル', href: a.profileUrl });
  if(a.shop && a.shop.url) links.push({ label: '天狼院楽市の店をひらく ── ' + (a.shop.shopName || '店'), href: a.shop.url });
  if(Array.isArray(a.books) && a.books.length) links.push({ label: '棚の本を見る（' + a.books.length + '冊）', href: '/creators/market' });
  return {
    stamp: kind || '寄稿',
    file: a.columnTitle ? 'TENRO-IN CREATORS ── ' + a.columnTitle : 'TENRO-IN CREATORS',
    name: a.penName || '',
    en: null,                          /* 読み（英字）は台帳の口が返さない＝出さない（作らない） */
    title: a.role || null,
    avatar: a.photoUrl,
    bio: a.tagline || a.columnLine || null,
    links: links,
  };
}

function MemberArticlePage({slug}){
  const { useState, useEffect } = React;
  const [d, setD] = useState(null);
  const [failed, setFailed] = useState(false);
  /* 写真扉の絵（設計書30 §386＝記事リーダーを流用・§341＝Phase 1 は既定の扉画）。
     出どころ＝①作家ページの肖像（代表 2026/9/23「プロフィールページから引っ張ってきて」）
             ②読めなければ既定の扉画 ③それも読めなければ扉ごと畳む（空の器を出さない）。
     ⚠️作そのものの絵は Phase 2（写真部）＝いまは台帳に無い。 */
  const [doorSrc, setDoorSrc] = useState(undefined);   /* undefined=まだ／null=畳む */
  useEffect(()=>{
    fetch('/api/magazine/works/' + encodeURIComponent(slug), { credentials:'include', headers:{accept:'application/json'} })
      .then(r=>r.json())
      .then(j=>{ if(j && j.ok) setD(j); else setFailed(true); })
      .catch(()=>setFailed(true));
  },[slug]);
  useEffect(()=>{
    const t = d && d.work && d.work.title;
    document.title = (t ? t + '｜' : '') + '日刊READING LIFE｜天狼院書店';
  },[d]);
  useEffect(()=>{
    if(!d || !d.work) return;
    const a = d.work.author || {};
    /* 扉の順＝①本人が納めた扉の写真（doorUrl・2026/9/23）②作家ページの肖像 ③既定の扉画。
       ⚠️ギャラリーは**挟まない**＝代表 2026/9/23「肖像のやつでやったほうがいい。
         しおりだけじゃなく、他の方も投稿するときに統一できるから」。 */
    setDoorSrc(d.work.doorUrl || a.photoUrl || 'assets/rl-hero-taiken.jpg');
  },[d]);
  /* 現れる仕掛け（data-reveal）＝この面は演出を持たないので、描けたらそのまま出す
     （書庫・週刊の面と同じ一手）。
     🔴これが無いと、既存の寄稿者の枠は `opacity:0` のままで**空白に見える**。
     ⚠️数では「枠 1 件」と出るので、**絵でしか分からない**（2026/9/23 実測）。 */
  /* ⚠️依存を置かない＝**毎回の描き直しのあとに**当てる。
     一度きりだと、器が作り直された回に取り残されて `opacity:0` のまま残る。
     ⚠️裏のタブでは動き（transition）が進まないので、当席の目では空白に見える＝道具の話。 */
  useEffect(()=>{
    document.querySelectorAll('[data-reveal]:not(.is-in)').forEach(e=>e.classList.add('is-in'));
  });

  const Frame = ({children}) => (
    <div className="app-root ti">
      <header className="evp-head">
        <div className="evp-head-in">
          <a className="evp-logo" href="/tenroin-top/" aria-label="天狼院書店 トップへ">
            <img src={R('logoInk','/tenroin-top/assets/logo-wordmark-ink.png')} alt="天狼院書店 TENRO-IN"/>
          </a>
          <nav className="evp-nav">
            <a className="evp-back" href="/magazine/daily">‹ 今日の紙面へ</a>
          </nav>
        </div>
      </header>
      <article className="art-body-wrap"><div className="wrap-narrow">{children}</div></article>
      <footer className="evp-foot">
        <div className="wrap evp-foot-in">
          <img src={R('footLogo','/tenroin-top/assets/logo-cream-sm.png')} alt="天狼院書店"/>
          <a className="melt-cta light" href="/magazine/daily">今日の紙面をひらく<span className="ar">→</span></a>
        </div>
      </footer>
    </div>
  );

  if(failed || (d && d.unavailable)) return <Frame><p className="rlm-note">いま紙面をお持ちできません。少し置いて、もう一度お開きください。</p></Frame>;
  if(!d) return <Frame><p className="rlm-note">ひらいています…</p></Frame>;
  if(d.gate) return (
    <Frame>
      <p className="rlm-gate">{d.gate.note}</p>
      <p className="rlm-note">クラブハウスはβ版で動いています。Web READING LIFE への投稿は、いま支度をしています。</p>
      <p className="rlm-cta"><a href={d.gate.href || '/reading-club'}>天狼院読書クラブ with AI</a></p>
    </Frame>
  );
  if(d.gone) return <Frame><p className="rlm-note">{d.note || 'この頁は、いまご覧いただけません。'}</p></Frame>;

  const w = d.work || {};
  /* 写真扉＝既存の記事リーダーと同じ器（art-photo-hero）。新しい意匠は作らない。 */
  const door = doorSrc ? (
    <div className="art-photo-hero">
      <img className="aph-img" src={doorSrc} alt=""
           onError={()=>setDoorSrc(prev => {
             const a = (w && w.author) || {};
             if(prev && a.photoUrl && prev !== a.photoUrl && prev !== 'assets/rl-hero-taiken.jpg') return a.photoUrl;
             return prev === 'assets/rl-hero-taiken.jpg' ? null : 'assets/rl-hero-taiken.jpg';
           })}/>
      <div className="aph-scrim" aria-hidden="true"></div>
      <div className="aph-top">
        <span className="aph-mag"><img src="assets/rl-logo-line-cream.png" alt="READING LIFE"/></span>
        <span className="aph-no">日刊READING LIFE{w.issueNo ? ' ── 第' + w.issueNo + '号' : ''}</span>
        <span className="aph-cat">DAILY{w.issueDate ? <em>{Number(w.issueDate.slice(5,7))}月{Number(w.issueDate.slice(8,10))}日</em> : null}</span>
      </div>
      <div className="aph-in">
        <div className="aph-in-narrow">
          {/* 🔴白地の上の字は既存の `art-kick light`（金＋白）を使う。
              `rlm-by` には白地用の規則が無く、実測で rgb(111,106,94)＝**暗い写真に暗い字**だった
              （2026/9/23・絵では読めても数で出た）。新しい意匠は作らず、既存の型に寄せる。 */}
          <div className="art-kick light">
            {w.columnTitle && w.columnTitle !== w.penName ? <span>{w.columnTitle}</span> : null}
            <span className="ak-pg">{w.penName}</span>
            {w.columnNo ? <span className="ak-pg">第{w.columnNo}回</span> : null}
          </div>
          <h1 className="art-title light">{w.title}</h1>
        </div>
      </div>
    </div>
  ) : null;
  const head = (
    <header className="art-head rlm-head">
      <p className="rlm-kick">
        日刊READING LIFE
        {w.issueNo ? <span className="rlm-no">　第{w.issueNo}号</span> : null}
        {/* ★日付はゼロ詰めしない（既存の誌面と同じ「9月4日」型・2026/9/4 CTO） */}
        {w.issueDate ? <span className="rlm-date">　{Number(w.issueDate.slice(5,7))}月{Number(w.issueDate.slice(8,10))}日</span> : null}
      </p>
      <h1 className="art-title">{w.title}</h1>
      <p className="rlm-by">
        {/* 🔴連載の題を持たない人は、口の既定で columnTitle に筆名が入る＝そのまま描くと
            「筆名／筆名」と同じ名が二度出る（模擬で実測 2026/9/23）。同じなら欄の側を出さない。 */}
        {w.columnTitle && w.columnTitle !== w.penName ? <span className="rlm-col">{w.columnTitle}</span> : null}
        {w.columnTitle && w.columnTitle !== w.penName ? <span className="rlm-sep">／</span> : null}
        <span className="rlm-pen">{w.penName}</span>
        {/* 書き手の欄の札（CTO 2026/9/7 17:10 の 3）。台帳の作は authorKind を持つ。
            ⚠️`columnTitle`（左の rlm-col）とは別物 ── あちらは**誌面の欄の名**（連載の題）、
              こちらは**書き手の欄**（会員・受講生…）。字が似ているので混ぜない。
            ★列が入る前の行は null が返るので、札はそのとき出ない。 */}
        {window.columnLabel && window.columnLabel({ column: w.authorKind, author: w.penName })
          ? <span className="aa-col rlm-kind">{window.columnLabel({ column: w.authorKind, author: w.penName })}</span> : null}
        {w.columnNo ? <span className="rlm-cno">第{w.columnNo}回</span> : null}
      </p>
    </header>
  );

  /* 失効された方の作＝冒頭だけ（投稿条件で同意を得た範囲）。本文は出さない。 */
  if(d.teaser) return (
    <Frame>
      {head}
      <p className="rlm-teaser">{w.lede}…</p>
      <p className="rlm-note">この欄の書き手は、いま天狼院読書クラブを離れています。本文の公開は止まっています。</p>
      <p className="rlm-cta"><a href="/reading-club">天狼院読書クラブ with AI</a></p>
    </Frame>
  );

  return (
    <Frame>
      {/* 扉が出た回は、扉が題と署名を持つ＝頭は出さない（同じ題が二度出ない） */}
      {door || head}
      <div className="art-body">
        {(w.body||[]).map((b,i)=><ArtBlock key={i} b={b}/>)}
      </div>

      {/* 書き手の欄＝既存の寄稿者の枠に、プロフィールから入れ込む */}
      {authorContrib(w) && <ContributorBlock c={authorContrib(w)}/>}

      {/* ── 奥付 ── 校閲AIの一言・AI利用の開示・通報・分かち合い ── */}
      <section className="rlm-colophon">
        {w.praise ? (
          <p className="rlm-praise">
            <span className="rlm-praise-by">校閲AIの一言</span>
            <span className="rlm-praise-t">{w.praise}</span>
          </p>
        ) : null}
        <p className="rlm-meta">
          <span>AI利用：{w.aiUsed ? 'あり' : 'なし'}</span>
          <span className="rlm-sep">／</span>
          <a href="/magazine/terms">投稿条件と編集部のルール</a>
        </p>
        <ShareBar title={w.title} label="この作を分かち合う" img={null}/>
        <ReportBlock slug={slug}/>
        <p className="rlm-note">掲載は、編集部のルールと三審を通った作品です。週刊・本誌への登用は編集部の選考によります。</p>
      </section>
    </Frame>
  );
}

/* 通報の窓口。**ログインしていなくても出せる**（第三者の削除の申出の入口）。 */
function ReportBlock({slug}){
  const { useState } = React;
  const [open, setOpen] = useState(false);
  const [reason, setReason] = useState('');
  const [detail, setDetail] = useState('');
  const [contact, setContact] = useState('');
  const [done, setDone] = useState('');
  const [err, setErr] = useState('');
  const REASONS = [['rights','権利の侵害（著作物・写真・商標）'],['defamation','名誉・プライバシー'],
                   ['adult','わいせつ・有害'],['plagiarism','盗作・なりすまし'],['other','その他']];
  const send = () => {
    if(!reason){ setErr('お知らせの種類をお選びください。'); return; }
    setErr('');
    fetch('/api/magazine/works/' + encodeURIComponent(slug) + '/report', {
      method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ reason, detail, contact }),
    }).then(r=>r.json()).then(j=>{
      if(j && j.ok) setDone(j.note || 'お知らせをお預かりしました。');
      else setErr((j && j.error) || 'お預かりできませんでした。');
    }).catch(()=>setErr('お預かりできませんでした。'));
  };
  if(done) return <p className="rlm-report-done">{done}</p>;
  return (
    <div className="rlm-report">
      {!open ? (
        <button type="button" className="rlm-report-open" onClick={()=>setOpen(true)}>この頁について編集部に知らせる</button>
      ) : (
        <div className="rlm-report-form">
          <p className="rlm-report-lead">どなたでもお出しになれます。お名前や連絡先は、書かなくても構いません。</p>
          <select value={reason} onChange={e=>setReason(e.target.value)} className="rlm-report-sel">
            <option value="">種類をお選びください</option>
            {REASONS.map(([k,l])=><option key={k} value={k}>{l}</option>)}
          </select>
          <textarea value={detail} onChange={e=>setDetail(e.target.value)} rows={3} maxLength={1000}
                    placeholder="差し支えなければ、お気づきの点をお書きください。" className="rlm-report-ta"/>
          <input value={contact} onChange={e=>setContact(e.target.value)} maxLength={200}
                 placeholder="ご連絡先（任意）" className="rlm-report-in"/>
          {err ? <p className="rlm-report-err">{err}</p> : null}
          <div className="rlm-report-act">
            <button type="button" onClick={()=>setOpen(false)}>やめる</button>
            <button type="button" onClick={send}>編集部に知らせる</button>
          </div>
        </div>
      )}
    </div>
  );
}

function ArticleReaderPage(){
  const { useEffect } = React;
  const { ARTICLES, ISSUE } = window.MAG;
  const slug = (location.pathname.match(/\/magazine\/([a-z0-9_-]+)/i)||[])[1]
            || new URLSearchParams(location.search).get('slug');
  if(slug && /^tsushin-\d+$/.test(slug)) return <ArchivePage slug={slug}/>;
  if(slug && /^weekly-\d+$/.test(slug)) return <WeeklyPage slug={slug}/>;
  /* 会員の記事（あなたの欄）＝静的 ARTICLES には無いので、台帳APIから描く一段。
     ★これが無いと、下の `|| ARTICLES[0]` で**別人の記事が出る**（先頭記事へ落ちる）。 */
  if(slug && /^my-[a-z0-9]+-\d+$/i.test(slug)) return <MemberArticlePage slug={slug}/>;
  const art = ARTICLES.find(a=>a.slug===slug) || ARTICLES[0];
  const others = ARTICLES.filter(a=>a.slug!==art.slug);

  useEffect(()=>{ document.title = `${art.title}｜READING LIFE — ${ISSUE.ja}｜天狼院書店`; },[]);

  // reveal（events.html と同じ流儀・簡略版）
  useEffect(()=>{
    const reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
    const all = ()=>document.querySelectorAll('[data-reveal]:not(.is-in)').forEach(e=>e.classList.add('is-in'));
    if(!('IntersectionObserver' in window) || reduce){ all(); return; }
    const io = new IntersectionObserver((es,obs)=>es.forEach(e=>{ if(e.isIntersecting){ e.target.classList.add('is-in'); obs.unobserve(e.target); } }),{threshold:.08, rootMargin:'0px 0px -5% 0px'});
    document.querySelectorAll('[data-reveal]:not(.is-in)').forEach(el=>{
      if(el.getBoundingClientRect().top < innerHeight*1.05){ el.classList.add('is-in'); return; }
      io.observe(el);
    });
    return ()=>io.disconnect();
  },[]);

  return (
    <div className="app-root ti">
      <header className="evp-head">
        <div className="evp-head-in">
          <a className="evp-logo" href="/tenroin-top/" aria-label="天狼院書店 トップへ">
            <img src={R('logoInk','/tenroin-top/assets/logo-wordmark-ink.png')} alt="天狼院書店 TENRO-IN"/>
          </a>
          <nav className="evp-nav">
            <a className="evp-back" href="/tenroin-top/?toc=1#articles">目次をひらく</a>
            <a className="evp-back" href="/tenroin-top/#articles">‹ マガジンに戻る</a>
          </nav>
        </div>
      </header>

      <article className="art-body-wrap">
        <div className="wrap-narrow">
          {!art.hero && (
          <div className="evp-strip" data-reveal>
            <span>READING LIFE</span>
            <span className="evp-strip-mid">No.{ISSUE.no} ／ {ISSUE.ja}</span>
            <span>ARTICLE</span>
          </div>
          )}

          {art.hero ? null : (
          <header className="art-head" style={{'--acc':art.accent}}>
            {/* 🔴裁定#9＝架名の作は、扉で「編集部による創作（架名）」と名乗る */}
            <div className="art-kick" data-reveal><span>{art.cat}</span><span className="ak-pg">{art.pg}</span>{art.fiction ? <span className="ak-proof">{art.fiction}</span> : (art.proof && <span className="ak-proof">試し刷り</span>)}</div>
            <h1 className="art-title" data-reveal>{art.title}</h1>
          </header>
          )}
          <header className="art-head" style={{'--acc':art.accent}}>
            {art.hero && (
            <div className="art-photo-hero">
              <img className="aph-img" src={art.hero.img} alt={art.hero.alt||''} style={art.hero.focus?{objectPosition:art.hero.focus}:null}/>
              <div className="aph-scrim" aria-hidden="true"></div>
              <div className="photo-veil" aria-hidden="true"></div>
              <div className="aph-top">
                <span className="aph-mag"><img src="assets/rl-logo-line-cream.png" alt="READING LIFE"/></span>
                <span className="aph-no">No.{ISSUE.no} ／ {ISSUE.ja}</span>
                <span className="aph-cat">{art.cat}<em>{art.pg}</em>{art.fiction ? <i>{art.fiction}</i> : (art.proof && <i>試し刷り</i>)}</span>
              </div>
              <div className="aph-in">
                <div className="aph-in-narrow">
                  <h1 className="art-title light">{art.title}</h1>
                  <p className="aph-lede">{art.lede}</p>
                  {/* 欄の札＝署名の脇に小さく（CTO 2026/9/7 17:10）。判じは articles.jsx の
                      columnLabel（署名と食い違うときは空文字が返る＝そのとき札は出ない）。 */}
                  <div className="aph-author"><span className="aa-line"></span>文 ── {art.author}
                    {window.columnLabel && window.columnLabel(art) ? <span className="aa-col">{window.columnLabel(art)}</span> : null}
                    {/* 落款（第一層）＋形の札（第二層）＝代表 2026/9/9。欄の札とは別物。
                        欄＝誰が書いたか／落款＝何のスキルの話か。並ぶが重ならない。 */}
                    {window.RlSeal ? <window.RlSeal art={art} /> : null}
                    {art.genre ? <span className="aa-col">{art.genre}</span> : null}</div>
                </div>
              </div>
              {art.hero.credit && <div className="aph-credit">{art.hero.credit}</div>}
            </div>
            )}
            {!art.hero && <p className="art-lede" data-reveal>{art.lede}</p>}
            <div className="art-meta" data-reveal>
              <span>{art.author}</span>
              {window.columnLabel && window.columnLabel(art) ? <><span className="am-dot">・</span><span>{window.columnLabel(art)}</span></> : null}
              <span className="am-dot">・</span>
              <span>読了 約{readingMinutes(art.body)}分</span>
              <span className="am-dot">・</span>
              <span>{ISSUE.ja}</span>
            </div>
          </header>

          <div className="art-body" data-reveal>
            {art.body.map((b,i)=><ArtBlock key={i} b={b}/>)}
          </div>

          {art.fukurotoji && <FukurotojiBlock slug={art.slug} f={art.fukurotoji}/>}

          {art.contributor && <ContributorBlock c={art.contributor}/>}

          <ShareBar title={art.title} img={art.hero ? art.hero.img : null}/>

          <AiDock ai={art.ai}/>

          {art.upx && <UpxCard u={art.upx}/>}

          {art.related && art.related.length>0 && (
            <nav className="art-rel" data-reveal>
              <div className="ar-kick">この記事から、ひらく</div>
              {art.related.map((r,i)=>(
                <a key={i} className="ar-link" href={r.href} data-cta="article-related">{r.label}<span className="ar-arrow">→</span></a>
              ))}
            </nav>
          )}

          <BridgeBand/>

          {others.length>0 && (
            <nav className="art-others" data-reveal>
              <div className="ao-kick">この号の、ほかの記事</div>
              <div className="arts-grid">
                {others.map((a,i)=>(
                  <a key={a.slug} className="art-card" href={'/magazine/'+a.slug} style={{'--acc':a.accent}} data-cta="article">
                    {a.hero && (
                      <div className="ac-ph"><img src={a.hero.img} alt="" loading="lazy" style={a.hero.focus?{objectPosition:a.hero.focus}:null}/></div>
                    )}
                    <div className="ac-kick"><span>{a.cat}</span><span className="ac-pg">{a.pg}</span></div>
                    <h3 className="ac-title">{a.title}</h3>
                    <div className="ac-foot"><span className="ac-by">{a.author}</span><span className="ac-arrow">→</span></div>
                  </a>
                ))}
              </div>
            </nav>
          )}

          <ArtColophon issue={ISSUE}/>
        </div>
      </article>

      {art.ai && art.ai.avatar && <ConciergeFloat art={art}/>}

      <footer className="evp-foot">
        <div className="wrap evp-foot-in">
          <img src={R('footLogo','/tenroin-top/assets/logo-cream-sm.png')} alt="天狼院書店"/>
          <img className="evpf-logo" src="assets/rl-logo-copy-cream.png" alt="READING LIFE — THE TENRO-IN EXPERIENCE"/>
          <a className="melt-cta light" href="/tenroin-top/">マガジン トップへ戻る<span className="ar">→</span></a>
        </div>
      </footer>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('artroot')).render(<ArticleReaderPage/>);
