// Inner pages — localised. About · CV · Photos index · Series detail ·
// Essays index · Essay detail · Contact. photographs & essays show <AINote/>.

// ──────────────────────────────────────────────────────────────
// About
// ──────────────────────────────────────────────────────────────
function PageAbout() {
  const lang = useLang();
  const c = tr('about', lang);
  const levels = ['C2', 'C2', 'C1', 'C1', 'A2 ↗', 'A1', 'A1'];
  // Highlight the row matching the currently selected UI language.
  // langNames order: [Ukrainian, Russian, English, Slovak, Swedish, Spanish, German]
  const langRow = { uk: 0, ru: 1, en: 2, sk: 3, sv: 4 };
  const hi = langRow[lang] != null ? langRow[lang] : -1;
  return (
    <>
      <TopNav />
      <div className="pp-wrap">
        <div className="pp-rv" style={{ fontSize: 11, color:'var(--mute)', letterSpacing:'0.16em', textTransform:'uppercase', marginBottom: 16 }}>{c.kicker}</div>
        <h1 className="pp-rv pp-h1" style={{ fontSize: 'clamp(40px, 5.2vw, 68px)', lineHeight: 0.96, margin: '0 0 28px', maxWidth: 1040 }} dangerouslySetInnerHTML={{ __html: c.heading }} />

        <div className="pp-rv g-about" style={{ padding: '28px 0 0', borderTop:'1px solid var(--line)' }}>
          <div className="pp-prose" style={{ maxWidth: 640 }}>
            {c.prose.map((p, i) => (
              <p key={i} className={i === 0 ? 'dropcap' : ''} dangerouslySetInnerHTML={{ __html: p }} />
            ))}
          </div>

          <aside className="pp-side" style={{ display:'grid', gap: 28 }}>
            <div>
              <div style={{ fontSize: 10, textTransform:'uppercase', letterSpacing:'0.12em', color:'var(--mute)', marginBottom: 8 }}>{c.glanceLabel}</div>
              <div style={{ display:'grid', gridTemplateColumns:'auto 1fr', gap:'6px 18px', fontSize: 12 }}>
                {c.glance.map(([k, v], i) => (
                  <React.Fragment key={i}><span style={{ color:'var(--mute)' }}>{k}</span><span>{v}</span></React.Fragment>
                ))}
              </div>
            </div>
            <div>
              <div style={{ fontSize: 10, textTransform:'uppercase', letterSpacing:'0.12em', color:'var(--mute)', marginBottom: 8 }}>{c.citiesLabel}</div>
              <ol style={{ margin: 0, padding: '0 0 0 22px', fontSize: 12, lineHeight: 1.7, color:'var(--ink)' }}>
                {c.cities.map((city, i) => <li key={i}>{city}</li>)}
              </ol>
            </div>
            <div>
              <div style={{ fontSize: 10, textTransform:'uppercase', letterSpacing:'0.12em', color:'var(--mute)', marginBottom: 8 }}>{c.langLabel}</div>
              <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap: '4px 12px', fontSize: 12 }}>
                {c.langNames.map((nm, i) => (
                  <React.Fragment key={i}><span className={i === hi ? 'red' : ''}>{nm}</span><span className={i === hi ? 'red' : ''}>{levels[i]}</span></React.Fragment>
                ))}
              </div>
            </div>
            <div>
              <div style={{ fontSize: 10, textTransform:'uppercase', letterSpacing:'0.12em', color:'var(--mute)', marginBottom: 8 }}>{c.wrongLabel}</div>
              <ul style={{ margin: 0, padding: '0 0 0 16px', fontSize: 12, lineHeight: 1.7 }}>
                {c.wrong.map((w, i) => <li key={i}>{w}</li>)}
              </ul>
            </div>
          </aside>
        </div>
      </div>
      <Footer />
    </>
  );
}

// ──────────────────────────────────────────────────────────────
// CV — tight resume
// ──────────────────────────────────────────────────────────────
function PageCV() {
  const lang = useLang();
  const c = I18N_CV[lang] || I18N_CV.en;
  const cv = cvFor(lang);
  const Section = ({ title, children, count }) => (
    <section className="pp-rv g-cvsec" style={{ padding: '36px 0', borderTop:'1px solid var(--line)' }}>
      <div className="pp-sticky">
        <div style={{ fontSize: 11, color:'var(--mute)', letterSpacing:'0.16em', textTransform:'uppercase' }}>{title}</div>
        {count != null && <div className="red" style={{ fontSize: 11, marginTop: 4 }}>{count}</div>}
      </div>
      <div className="cv-body">{children}</div>
    </section>
  );
  const Row = ({ from, to, title, org, kind, bullets, note, accent }) => (
    <div className="cv-entry" style={{ padding: '16px 0', borderTop: '1px solid var(--line)' }}>
      <div className="g-cvrow">
        <span className="pp-num" style={{ color: accent || 'var(--mute)', fontSize: 12, fontWeight: accent ? 600 : 400, borderLeft: `3px solid ${accent || 'transparent'}`, paddingLeft: 10 }}>{fmtDate(from)} → {fmtDate(to)}</span>
        <div>
          <div className="t-title" style={{ fontSize: 16, fontWeight: 500 }}>{title}</div>
          <div style={{ fontSize: 12, color:'var(--mute)', marginTop: 2 }}>{org}{note ? ` · ${note}` : ''}</div>
        </div>
        {kind && <span className="pp-chip">{kind}</span>}
      </div>
      {bullets && (
        <ul className="cv-bullets" style={{ color:'var(--ink)' }}>
          {bullets.map((b, i) => (
            <li key={i} style={{ position:'relative', paddingLeft: 18 }}>
              <span className="red" style={{ position:'absolute', left:0 }}>—</span>{b}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
  return (
    <>
      <TopNav />
      <div className="pp-wrap">
        <div className="pp-rv cv-head" style={{ display:'grid', gridTemplateColumns:'1fr auto', alignItems:'end', marginBottom: 28 }}>
          <h1 className="pp-h1 upper cv-h1" style={{ fontSize: 'clamp(44px, 6vw, 86px)', margin: 0 }} dangerouslySetInnerHTML={{ __html: c.title }} />
          <div style={{ fontSize: 11, color:'var(--mute)', textAlign:'right', lineHeight: 1.6 }}>
            <div>{c.updated}</div>
            <a className="pp-link cv-dl" href="#" style={{ color: THEME.red }}
               onClick={(ev) => { ev.preventDefault(); window.print(); }}>{c.download}</a>
          </div>
        </div>
        <div className="pp-rv cv-intro" style={{ fontSize: 13, color:'var(--mute)', marginBottom: 18, maxWidth: 700 }}>
          {c.intro}
        </div>

        <Section title={c.sWork} count={c.roles(cv.work.length)}>
          {cv.work.map((w, i) => <Row key={i} from={w.from} to={w.to} title={w.role} org={w.org} kind={w.kind} bullets={w.bullets} />)}
        </Section>

        <Section title={c.sEdu} count={c.degrees(cv.edu.length)}>
          {cv.edu.map((e, i) => <Row key={i} from={e.from} to={e.to} title={e.deg} org={e.org} note={e.note} />)}
        </Section>

        <Section title={c.sVol} count={c.roles(cv.vol.length)}>
          {cv.vol.map((v, i) => (
            <Row key={i} from={v.from} to={v.to} title={v.role} org={v.org} note={v.note} />
          ))}
        </Section>

        <Section title={c.sAwards}>
          {cv.awards.map((a, i) => (
            <div key={i} className="cv-award" style={{ display:'grid', gridTemplateColumns:'80px 1fr', gap: 20, padding: '12px 0', borderTop:'1px solid var(--line)', fontSize: 14 }}>
              <span className="pp-num red">{a.year}</span><span>{a.t}</span>
            </div>
          ))}
        </Section>

        <Section title={c.sSkills}>
          <div className="g-skills">
            {cv.skills.map((s, i) => (
              <div key={i}>
                <div style={{ fontSize: 11, color:'var(--mute)', textTransform:'uppercase', letterSpacing:'0.1em', marginBottom: 10 }}>{s.group}</div>
                <div style={{ display:'flex', flexWrap:'wrap', gap: 6 }}>
                  {s.items.map((it, j) => <span key={j} className="pp-chip" style={{ fontSize: 12 }}>{it}</span>)}
                </div>
              </div>
            ))}
          </div>
        </Section>
      </div>
      <Footer />
    </>
  );
}

// Localised text with user edits (edit tools) layered on top.
function seriesTextEx(store, s, lang) {
  if (s.added) return { title: s.title, sub: s.sub };
  const t = seriesText(s, lang), ov = store.seriesOver(s.id);
  return { title: ov.title || t.title, sub: ov.sub != null ? ov.sub : t.sub };
}
function essayTextEx(store, e, lang) {
  if (e.added) return { title: e.title, sub: '' };
  const t = essayText(e, lang), ov = store.essayOver(e.id);
  return { title: ov.title || t.title, sub: t.sub };
}
// User-written essay bodies are plain text: blank line = new paragraph,
// '## ' = section heading, '### ' = subheading. Inline HTML also works.
function essayHtmlFromText(text) {
  return (text || '').split(/\n{2,}/).map((b) => {
    const t = b.trim();
    if (!t) return '';
    if (t.startsWith('### ')) return '<h3>' + t.slice(4) + '</h3>';
    if (t.startsWith('## ')) return '<h2>' + t.slice(3) + '</h2>';
    if (/^<(h2|h3|p|ul|ol|blockquote|figure)[\s>]/i.test(t)) return t;
    return '<p>' + t.replace(/\n/g, '<br />') + '</p>';
  }).join('\n');
}

// ──────────────────────────────────────────────────────────────
// Photos index
// ──────────────────────────────────────────────────────────────
function PagePhotos() {
  const { go } = React.useContext(RouteCtx);
  const lang = useLang();
  const store = usePhotoStore();
  const editing = React.useContext(PhotoEditCtx);
  const LIST = store.seriesList(SERIES);
  const c = I18N_PHOTOS[lang] || I18N_PHOTOS.en;
  return (
    <>
      <TopNav />
      <div className="pp-wrap">
        <AINote />
        <div className="pp-rv" style={{ display:'flex', justifyContent:'space-between', alignItems:'end', marginBottom: 36 }}>
          <div>
            <div style={{ fontSize: 11, color:'var(--mute)', letterSpacing:'0.16em', textTransform:'uppercase', marginBottom: 12 }}>{c.kicker(LIST.length)}</div>
            <h1 className="pp-h1 upper" style={{ fontSize: 'clamp(44px, 6.2vw, 82px)', margin: 0 }} dangerouslySetInnerHTML={{ __html: c.title }} />
          </div>
          <div style={{ fontSize: 12, color:'var(--mute)', maxWidth: 300, flex:'0 0 auto', textAlign:'right', lineHeight: 1.55 }}>
            {c.intro}
          </div>
        </div>

        <div className="pp-rv" style={{ display:'grid', gridTemplateColumns:'80px 1fr', gap: 16, fontSize: 11, color:'var(--mute)', textTransform:'uppercase', letterSpacing:'0.12em', paddingBottom: 12, borderBottom:'1px solid var(--line)' }}>
          <span>{c.cNo}</span><span>{c.cSeries}</span>
        </div>
        {LIST.map((s) => {
          const st = seriesTextEx(store, s, lang);
          return (
          <div key={s.id} className="pp-rv g-photorow" onClick={() => go('series', s.id)}
               style={{ padding:'24px 0', borderTop: '1px solid var(--line)', cursor:'pointer' }}
               onMouseEnter={(e) => { e.currentTarget.style.background='var(--sub)'; e.currentTarget.querySelector('h3').style.color = THEME.red; }}
               onMouseLeave={(e) => { e.currentTarget.style.background=''; e.currentTarget.querySelector('h3').style.color = ''; }}>
            <span className="red pr-no" style={{ fontSize: 24, fontWeight: 500 }}>{s.no}</span>
            <div>
              <h3 className="t-title" style={{ margin: 0, fontSize: 22, fontWeight: 500, lineHeight: 1.15, transition: 'color .15s' }}>
                {st.title} <span style={{ color:'var(--mute)', fontWeight:400, fontSize: 16 }}>· {s.year}</span>
              </h3>
              <p style={{ margin: '4px 0 8px', fontSize: 13, color:'var(--mute)', maxWidth: 520 }}>{st.sub}</p>
              <div style={{ fontSize: 11, color:'var(--mute)', letterSpacing:'0.06em' }}>{c.framesView(s.frames)}</div>
              {editing && (
                <div style={{ display:'flex', gap: 4, marginTop: 10 }} onClick={(e) => e.stopPropagation()}>
                  <PhotoTool title={s.added ? 'delete this photo story' : 'hide this photo story'} onClick={() => store.removeSeries(s.id)}>✕ {s.added ? 'delete' : 'hide'}</PhotoTool>
                </div>
              )}
            </div>
            <div onClick={(e) => e.stopPropagation()}>
              <Img ratio="3 / 2" no={s.no} caption={st.title} src={s.cover} slot={'cover/' + s.id}
                   pos={store.coverPos(s.id)} onPos={(p) => store.patchCover(s.id, p)}
                   style={{ width: 280 }} onOpen={() => go('series', s.id)} />
            </div>
          </div>
        ); })}
        {editing && (
          <div className="pp-rv" style={{ display:'flex', gap: 8, padding: '20px 0', borderTop: '1px solid var(--line)' }}>
            <button className="pp-addframe" style={{ aspectRatio:'auto', minHeight: 64, flex: 1 }}
                    onClick={() => { const nid = store.addSeries(); go('series', nid); }}>+ add photo story</button>
            <button className="pp-toolbtn" title="bring back everything you hid" onClick={() => store.restoreHidden()}>restore hidden</button>
          </div>
        )}
      </div>
      <Footer />
    </>
  );
}

// ──────────────────────────────────────────────────────────────
// Series detail — hero + body shots + lightbox
// ──────────────────────────────────────────────────────────────
// Inline caption/description editor shown under a frame when edit tools are on.
function FrameEdit({ sid, base, f, store }) {
  return (
    <div style={{ display:'grid', gap: 4, marginTop: 6 }}>
      <input className="pp-editinput" value={f.caption || ''} placeholder="name / caption"
             onChange={(ev) => store.patch(sid, base, f.no, { caption: ev.target.value })} />
      <input className="pp-editinput" value={f.meta || ''} placeholder="description · e.g. 2024-06 · 50mm · Provia 100F"
             onChange={(ev) => store.patch(sid, base, f.no, { meta: ev.target.value })} />
    </div>
  );
}

function PageSeries({ id = 's6' }) {
  const lang = useLang();
  const c = I18N_PHOTOS[lang] || I18N_PHOTOS.en;
  const store = usePhotoStore();
  const editing = React.useContext(PhotoEditCtx);
  const LIST = store.seriesList(SERIES);
  const s = LIST.find((x) => x.id === id) || LIST[0];
  const st = seriesTextEx(store, s, lang);
  // Base frames come from SERIES_FRAMES[series id]; if a series has no frame
  // list yet, generate striped placeholders from its declared frame count.
  const base = (SERIES_FRAMES[s.id] && SERIES_FRAMES[s.id].length) ? SERIES_FRAMES[s.id]
    : Array.from({ length: Math.min(s.frames || 8, 9) }, (_, i) => ({
        no: String(i + 1).padStart(2, '0'),
        caption: st.title + ' · frame ' + (i + 1),
        meta: '', ratio: i % 4 === 0 ? '3/2' : '4/5', span: i % 4 === 0 ? 'wide' : undefined,
      }));
  // Layout edits (order / span / crop / added frames) overlay the base data,
  // and dropped images overlay the file-based `src`.
  const frames = store.frames(s.id, base).map((f) => ({
    ...f, src: f.type === 'text' ? undefined : (store.urls[store.slotKey(s.id, f.no)] || f.src),
  }));
  const imgFrames = frames.filter((f) => f.type !== 'text');
  const [lb, setLb] = React.useState(null);
  const { go } = React.useContext(RouteCtx);

  const frameTools = (f, i) => (
    <>
      {i > 0 && <PhotoTool title="move earlier" onClick={() => store.move(s.id, base, f.no, -1)}>←</PhotoTool>}
      {i > 0 && i < frames.length - 1 && <PhotoTool title="move later" onClick={() => store.move(s.id, base, f.no, +1)}>→</PhotoTool>}
      {i > 0 && <PhotoTool title="cycle width: 1× → 2× → 3×" onClick={() => store.cycleSpan(s.id, base, f.no)}>{f.span === 'wide' ? '2×' : f.span === 'full' ? '3×' : '1×'}</PhotoTool>}
      <PhotoTool title="orientation / aspect: 3/2 → 4/5 → 1/1 → 16/9" onClick={() => store.cycleRatio(s.id, base, f.no)}>{f.ratio || '3/2'}</PhotoTool>
      {f.src && <PhotoTool title="use as series cover" onClick={() => store.setCover(s.id, f.no)}>cover</PhotoTool>}
      <PhotoTool title="remove frame" onClick={() => store.remove(s.id, base, f.no)}>✕</PhotoTool>
    </>
  );
  const textTools = (f, i) => (
    <>
      {i > 1 && <PhotoTool title="move earlier" onClick={() => store.move(s.id, base, f.no, -1)}>←</PhotoTool>}
      {i < frames.length - 1 && <PhotoTool title="move later" onClick={() => store.move(s.id, base, f.no, +1)}>→</PhotoTool>}
      <PhotoTool title="cycle width: 1× → 2× → 3×" onClick={() => store.cycleSpan(s.id, base, f.no)}>{f.span === 'wide' ? '2×' : f.span === 'full' ? '3×' : '1×'}</PhotoTool>
      <PhotoTool title="remove text block" onClick={() => store.remove(s.id, base, f.no)}>✕</PhotoTool>
    </>
  );
  const framePos = (f) => (p) => store.patch(s.id, base, f.no, { pos: p });

  return (
    <>
      <TopNav />
      <div className="pp-wrap">
        <AINote />
        <div className="pp-rv" style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom: 20, fontSize: 12, color:'var(--mute)' }}>
          <div>
            <a className="pp-link" onClick={() => go('photos')}>{c.allSeries}</a>
            <span style={{ margin: '0 14px' }}>·</span>
            <span>~/photographs/{s.id}</span>
          </div>
          <div>{c.seriesNo} <span className="red">{s.no}</span> · {c.shown(frames.length, s.frames)}</div>
        </div>

        <h1 className="pp-rv pp-h1 upper" style={{ fontSize: 'clamp(52px, 7.5vw, 108px)', margin: '0 0 8px' }}>
          {editing
            ? <input className="pp-editinput" style={{ fontFamily:'inherit', fontSize:'clamp(30px,5vw,64px)', fontWeight:500, textTransform:'uppercase', padding:'8px 12px' }}
                     value={st.title} onChange={(ev) => store.patchSeries(s.id, { title: ev.target.value })} />
            : st.title}
        </h1>
        <div className="pp-rv g-serieshead" style={{ padding: '12px 0 28px', borderBottom: '1px solid var(--line)', marginBottom: 28 }}>
          {editing
            ? <textarea className="pp-editinput" rows={3} value={st.sub}
                        onChange={(ev) => store.patchSeries(s.id, { sub: ev.target.value })} />
            : <p style={{ margin: 0, fontFamily: FONT_SERIF, fontSize: 23, lineHeight: 1.4, maxWidth: 720 }}>{st.sub}</p>}
          <div style={{ fontSize: 11, color:'var(--mute)', lineHeight: 1.7 }}>
            <div style={{ display:'grid', gridTemplateColumns:'auto 1fr', gap:'4px 16px' }}>
              <span>{c.pShot}</span>
              {editing
                ? <input className="pp-editinput" style={{ width: 110, padding:'2px 6px' }} value={s.year}
                         onChange={(ev) => store.patchSeries(s.id, { year: ev.target.value })} />
                : <span>{s.year}</span>}
              <span>{c.pLoc}</span><span>{c.locVal}</span>
              <span>{c.pFormat}</span><span>35mm + 6×6, Provia 100F</span>
              <span>{c.pFrames}</span><span>{c.framesVal(s.frames)}</span>
              <span>{c.pScanner}</span><span>{c.scannerVal}</span>
            </div>
          </div>
        </div>

        {/* Edit-mode bar */}
        {editing && (
          <div className="pp-editbar pp-num">
            <span>edit tools on — title, description &amp; year are editable above · drop images onto frames · drag inside a photo to reframe · buttons: order / width / aspect / cover / remove</span>
            <button className="pp-toolbtn" onClick={() => store.reset(s.id)}>reset layout</button>
          </div>
        )}

        {/* Hero shot */}
        <div className="pp-rv" style={{ marginBottom: 8 }}>
          <Img ratio={frames[0].ratio === '3/2' || !frames[0].ratio ? '16 / 9' : frames[0].ratio} no={frames[0].no} caption={frames[0].caption}
               src={frames[0].src} pos={frames[0].pos} slot={store.slotKey(s.id, frames[0].no)}
               onPos={framePos(frames[0])} tools={frameTools(frames[0], 0)} onOpen={() => setLb(0)} />
          {editing ? <FrameEdit sid={s.id} base={base} f={frames[0]} store={store} /> : (
          <div style={{ fontSize: 11, color:'var(--mute)', marginTop: 8, display:'flex', justifyContent:'space-between' }}>
            <span>{c.fig} {frames[0].no} — {frames[0].caption}{frames[0].meta ? ' · ' + frames[0].meta : ''}</span>
            <span className="red">{c.lbHint}</span>
          </div>
          )}
        </div>

        {/* Body grid — photos and text blocks, interleaved */}
        <div className="pp-rv g-seriesgrid" style={{ marginTop: 36 }}>
          {frames.slice(1).map((f, i) => {
            if (f.type === 'text') {
              return (
                <div key={f.no} className={f.span ? 'span-' + f.span : ''}>
                  {editing ? (
                    <div style={{ display:'grid', gap: 6 }}>
                      <div style={{ display:'flex', gap: 4 }}>{textTools(f, i + 1)}</div>
                      <textarea className="pp-editinput" rows={5} value={f.text || ''}
                                onChange={(ev) => store.patch(s.id, base, f.no, { text: ev.target.value })} />
                    </div>
                  ) : (
                    <div className="pp-textblock">{f.text}</div>
                  )}
                </div>
              );
            }
            return (
              <div key={f.no} className={f.span ? 'span-' + f.span : ''}>
                <Img ratio={f.ratio} no={f.no} caption={f.caption} src={f.src} pos={f.pos}
                     slot={store.slotKey(s.id, f.no)} onPos={framePos(f)} tools={frameTools(f, i + 1)}
                     onOpen={() => setLb(imgFrames.indexOf(f))} />
                {editing ? <FrameEdit sid={s.id} base={base} f={f} store={store} /> : (
                  <div style={{ fontSize: 11, color:'var(--mute)', marginTop: 6 }}>{c.fig} {f.no} — {f.caption}{f.meta ? ' · ' + f.meta : ''}</div>
                )}
              </div>
            );
          })}
          {editing && (
            <button className="pp-addframe" onClick={() => store.add(s.id, base)}>+ add frame</button>
          )}
          {editing && (
            <button className="pp-addframe" style={{ aspectRatio: 'auto', minHeight: 120 }} onClick={() => store.addText(s.id, base)}>+ add text block</button>
          )}
        </div>

        {/* Colophon */}
        <div className="pp-rv g-colophon" style={{ marginTop: 56, padding: '24px 0', borderTop:'1px solid var(--line)', fontSize: 13, color:'var(--mute)' }}>
          <div>
            <div style={{ fontSize: 11, textTransform:'uppercase', letterSpacing:'0.12em', marginBottom: 8 }}>{c.colophon}</div>
            <p style={{ margin: 0 }} dangerouslySetInnerHTML={{ __html: c.colophonText }} />
          </div>
          <div>
            <div style={{ fontSize: 11, textTransform:'uppercase', letterSpacing:'0.12em', marginBottom: 8 }}>{c.next}</div>
            <a className="pp-link red" onClick={() => go('series', 's7')}>{c.nextLink}</a>
          </div>
        </div>
      </div>
      <Footer minimal />
      {lb != null && (
        <Lightbox items={imgFrames} index={lb} onClose={() => setLb(null)}
          onPrev={() => setLb((i) => (i - 1 + imgFrames.length) % imgFrames.length)}
          onNext={() => setLb((i) => (i + 1) % imgFrames.length)} />
      )}
    </>
  );
}

// ──────────────────────────────────────────────────────────────
// Essays index
// ──────────────────────────────────────────────────────────────
function PageEssays() {
  const { go } = React.useContext(RouteCtx);
  const lang = useLang();
  const store = usePhotoStore();
  const editing = React.useContext(PhotoEditCtx);
  const ALL = store.essaysList(ESSAYS);
  const c = I18N_ESSAYS[lang] || I18N_ESSAYS.en;
  const SECTIONS = [
    { key: 'mine', label: 'My essays', note: 'Written by me. Most are in Ukrainian.' },
    { key: 'sup', label: 'Published under my supervision', note: 'Edited and published by me; written by the ProSvita / Media Bober teams.' },
  ];
  return (
    <>
      <TopNav />
      <div className="pp-wrap">
        <div style={{ maxWidth: 1080, margin: '0 auto' }}>
        <AINote />
        <div className="pp-rv" style={{ display:'flex', justifyContent:'space-between', alignItems:'end', marginBottom: 36, flexWrap:'wrap', gap: 20 }}>
          <div>
            <div style={{ fontSize: 11, color:'var(--mute)', letterSpacing:'0.16em', textTransform:'uppercase', marginBottom: 12 }}>{c.kicker(ALL.length)}</div>
            <h1 className="pp-h1 upper" style={{ fontSize: 'clamp(48px, 7vw, 100px)', margin: 0 }} dangerouslySetInnerHTML={{ __html: c.title }} />
          </div>
          <div style={{ fontSize: 12, color:'var(--mute)', maxWidth: 340, flex:'0 0 auto', textAlign:'right', lineHeight: 1.55 }}>
            {c.intro}
          </div>
        </div>

        {SECTIONS.map((s) => {
          const list = ALL.filter((e) => e.sec === s.key).slice().reverse();
          if (!list.length && !editing) return null;
          return (
          <div key={s.key} className="pp-rv g-essaysec" style={{ padding: '32px 0', borderTop:'1px solid var(--line)' }}>
            <div className="pp-sticky">
              <h2 className="t-title" style={{ margin: 0, fontSize: 26, fontWeight: 500, lineHeight: 1.08, letterSpacing:'-0.01em' }}>{s.label}</h2>
              <div style={{ fontSize: 12, color:'var(--mute)', marginTop: 10, lineHeight: 1.5, maxWidth: 200 }}>{s.note}</div>
              <div className="red" style={{ fontSize: 11, marginTop: 10, letterSpacing:'0.04em' }}>{list.length} pieces</div>
            </div>
            <div>
              {list.map((e, i) => {
                const et = essayTextEx(store, e, lang);
                return (
                <div key={e.id} className="g-essayrow" onClick={() => go('essay', e.id)}
                     style={{ cursor:'pointer', padding:'13px 0', borderTop: i===0 ? 0 : '1px solid var(--line)' }}
                     onMouseEnter={(ev) => { ev.currentTarget.style.background='var(--sub)'; ev.currentTarget.querySelector('h3').style.color = THEME.red; ev.currentTarget.querySelector('.arr').style.color = THEME.red; }}
                     onMouseLeave={(ev) => { ev.currentTarget.style.background=''; ev.currentTarget.querySelector('h3').style.color = ''; ev.currentTarget.querySelector('.arr').style.color = ''; }}>
                  <span className="pp-num er-no" style={{ fontSize: 12, color:'var(--mute)' }}>{String(i+1).padStart(2,'0')}</span>
                  <h3 className="t-title" style={{ margin: 0, fontSize: 17, fontWeight: 500, lineHeight: 1.3, transition:'color .15s' }}>{et.title}</h3>
                  <span className="pp-num" style={{ fontSize: 12, color:'var(--mute)', whiteSpace:'nowrap' }}>{fmtDate(e.date)} · {readMins(e)} min</span>
                  {editing
                    ? <span onClick={(ev) => { ev.stopPropagation(); store.removeEssay(e.id); }}><PhotoTool title={e.added ? 'delete this essay' : 'hide this essay'}>✕</PhotoTool></span>
                    : <span className="arr" style={{ color:'var(--mute)', transition:'color .15s', textAlign:'right' }}>→</span>}
                  {editing && <span className="arr" style={{ display:'none' }}></span>}
                </div>
              ); })}
              {editing && (
                <button className="pp-addframe" style={{ aspectRatio:'auto', minHeight: 52, marginTop: 12 }}
                        onClick={() => { const nid = store.addEssay(s.key); go('essay', nid); }}>+ add essay here</button>
              )}
            </div>
          </div>
          );
        })}
        </div>
      </div>
      <Footer />
    </>
  );
}

// ── Essay photos ────────────────────────────────────────────────────
// An essay body may contain [photo:01] or [photo:01 A caption …] tokens —
// each becomes a droppable photo figure. Every figure can be moved up/down
// through the text (paragraph by paragraph) with edit tools on; “+ add photo”
// appends one you can then walk into place. Positions persist in the browser.
function essaySegments(html) {
  const re = /(?:<p[^>]*>\s*)?\[photo:([\w-]+)\s*([^\]]*)\](?:\s*<\/p>)?/g;
  const segs = []; let last = 0; let m;
  while ((m = re.exec(html))) {
    if (m.index > last) segs.push({ html: html.slice(last, m.index) });
    segs.push({ no: m[1], caption: m[2].trim() });
    last = m.index + m[0].length;
  }
  segs.push({ html: html.slice(last) });
  return segs;
}

// Split the body into paragraph-level blocks; token figures remember which
// block they sit after (`at`, -1 = before everything).
function essayBlocks(html) {
  const segs = essaySegments(html);
  const blocks = []; const tokens = [];
  for (const s of segs) {
    if (s.no) { tokens.push({ no: s.no, caption: s.caption, ratio: '3/2', at: blocks.length - 1 }); continue; }
    const parts = s.html.split(/(?=<(?:p|h2|h3|ul|ol|blockquote|figure|div)[\s>])/i).filter((t) => t.trim());
    blocks.push(...parts);
  }
  return { blocks, tokens };
}

function EssayFigure({ sid, base, f, store, editing, nBlocks }) {
  const slot = store.slotKey(sid, f.no);
  const src = store.urls[slot];
  return (
    <figure className="pp-essayfig">
      <Img ratio={(f.ratio || '3/2').replace('/', ' / ')} src={src} slot={slot} pos={f.pos}
           onPos={(p) => store.patch(sid, base, f.no, { pos: p })}
           tools={<>
             <PhotoTool title="move up (earlier in the text)" onClick={() => store.patch(sid, base, f.no, { at: Math.max(-1, f.at - 1) })}>↑</PhotoTool>
             <PhotoTool title="move down (later in the text)" onClick={() => store.patch(sid, base, f.no, { at: Math.min(nBlocks - 1, f.at + 1) })}>↓</PhotoTool>
             <PhotoTool title="orientation / aspect: 3/2 → 4/5 → 1/1 → 16/9" onClick={() => store.cycleRatio(sid, base, f.no)}>{f.ratio || '3/2'}</PhotoTool>
             {src && <PhotoTool title="remove the dropped image" onClick={() => store.clearImg(slot)}>clear</PhotoTool>}
             {f.added && <PhotoTool title="remove this figure" onClick={() => store.remove(sid, base, f.no)}>✕</PhotoTool>}
           </>} />
      {editing
        ? <FrameEdit sid={sid} base={base} f={f} store={store} />
        : (f.caption || f.meta) ? <figcaption>{f.caption}{f.meta ? ' · ' + f.meta : ''}</figcaption> : null}
    </figure>
  );
}

function EssayBody({ eid, html, style }) {
  const store = usePhotoStore();
  const editing = React.useContext(PhotoEditCtx);
  const sid = 'essay:' + eid;
  const { blocks, tokens } = React.useMemo(() => essayBlocks(html), [html]);
  const frames = store.frames(sid, tokens).map((f) => ({
    ...f,
    added: !tokens.some((t) => t.no === f.no),
    at: Math.max(-1, Math.min(blocks.length - 1, f.at == null ? blocks.length - 1 : f.at)),
  }));
  const renderFig = (f) => <EssayFigure key={f.no} sid={sid} base={tokens} f={f} store={store} editing={editing} nBlocks={blocks.length} />;
  return (
    <div className="pp-prose" lang="en" style={style}>
      {frames.filter((f) => f.at === -1).map(renderFig)}
      {blocks.map((b, i) => (
        <React.Fragment key={i}>
          <div dangerouslySetInnerHTML={{ __html: b }} />
          {frames.filter((f) => f.at === i).map(renderFig)}
        </React.Fragment>
      ))}
      {editing && (
        <button className="pp-addframe" style={{ aspectRatio: 'auto', minHeight: 100, margin: '28px 0' }}
                onClick={() => store.add(sid, tokens, { ratio: '3/2', at: blocks.length - 1 })}>+ add photo to this essay</button>
      )}
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// Essay detail — wide reading view with sidenotes
// ──────────────────────────────────────────────────────────────
// Essays come from more than one place now, so the "read the original" link
// names whatever actually published it rather than assuming Telegraph.
function pubName(url) {
  try {
    const h = new URL(url).hostname.replace(/^www\./, '');
    if (h.endsWith('telegra.ph')) return 'Telegraph';
    if (h.endsWith('substack.com')) return 'Substack';
    return h;
  } catch (err) {
    return 'the original site';
  }
}

function PageEssay({ id = 'e1' }) {
  const { go } = React.useContext(RouteCtx);
  const lang = useLang();
  const store = usePhotoStore();
  const editing = React.useContext(PhotoEditCtx);
  const c = I18N_ESSAYS[lang] || I18N_ESSAYS.en;
  const ALL = store.essaysList(ESSAYS);
  const e = ALL.find((x) => x.id === id) || ALL[0];
  const et = essayTextEx(store, e, lang);
  const further = ALL.filter((x) => x.sec === e.sec && x.id !== e.id).slice().reverse().slice(0, 5);
  const isSup = e.sec === 'sup';
  const ov = store.essayOver(e.id);
  const content = e.added
    ? { body: essayHtmlFromText(e.body), keywords: [] }
    : ov.bodyHtml != null
      ? { body: ov.bodyHtml, keywords: ((typeof ESSAY_BODY !== 'undefined' && ESSAY_BODY[e.id]) || {}).keywords || [] }
      : (typeof ESSAY_BODY !== 'undefined' && ESSAY_BODY[e.id]) || null;
  const mins = readMins(e);
  const toc = content ? essaySections(content.body) : { sections: [], body: null };
  const hasToc = toc.sections.length > 1;
  const keywords = (content && content.keywords) || [];
  const hasKw = keywords.length > 0;

  const [railOpen, setRailOpen] = React.useState(false);
  const [activeId, setActiveId] = React.useState(null);
  const [cMsg, setCMsg] = React.useState('');
  const [sentC, setSentC] = React.useState(false);
  const colRef = React.useRef(null);
  const railRef = React.useRef(null);

  // Manual "freeze" of the rail: real sticky won't engage because the document
  // scrolls while .pp-page is content-height. Toggle fixed/absolute on scroll.
  React.useEffect(() => {
    const col = colRef.current, rail = railRef.current;
    if (!col || !rail) return;
    const GAP = 90;
    const clear = () => { rail.style.position = ''; rail.style.top = ''; rail.style.bottom = ''; rail.style.left = ''; rail.style.width = ''; };
    const onScroll = () => {
      if (window.innerWidth <= 900) { clear(); return; }
      const cell = col.getBoundingClientRect();
      const railH = rail.offsetHeight;
      if (cell.top > GAP) {
        clear();
      } else if (cell.bottom - GAP >= railH) {
        rail.style.position = 'fixed';
        rail.style.top = GAP + 'px';
        rail.style.left = cell.left + 'px';
        rail.style.width = cell.width + 'px';
      } else {
        rail.style.position = 'absolute';
        rail.style.top = 'auto';
        rail.style.bottom = '0';
        rail.style.left = '0';
        rail.style.width = '100%';
      }
    };
    onScroll();
    window.addEventListener('scroll', onScroll, true);
    window.addEventListener('resize', onScroll);
    return () => { window.removeEventListener('scroll', onScroll, true); window.removeEventListener('resize', onScroll); clear(); };
  }, [id, hasToc, railOpen]);

  React.useEffect(() => {
    if (!hasToc) return;
    const ids = toc.sections.map((s) => s.id);
    const onScroll = () => {
      let cur = ids[0];
      for (const sid of ids) {
        const el = document.getElementById(sid);
        if (el && el.getBoundingClientRect().top <= 130) cur = sid; else break;
      }
      setActiveId(cur);
    };
    onScroll();
    window.addEventListener('scroll', onScroll, true);
    return () => window.removeEventListener('scroll', onScroll, true);
  }, [id, hasToc, toc.sections.length]);

  const sendComment = (ev) => {
    ev.preventDefault();
    if (!cMsg.trim()) return;
    const subject = encodeURIComponent('Comment on: ' + et.title);
    const body = encodeURIComponent(cMsg + '\n\n— sent from your site, re: ' + et.title);
    window.location.href = 'mailto:' + SITE.email + '?subject=' + subject + '&body=' + body;
    setSentC(true);
  };

  const jump = (sid) => {
    const el = document.getElementById(sid);
    if (!el) return;
    let cont = el.parentElement;
    while (cont && cont !== document.body) {
      const oy = getComputedStyle(cont).overflowY;
      if ((oy === 'auto' || oy === 'scroll') && cont.scrollHeight > cont.clientHeight + 4) break;
      cont = cont.parentElement;
    }
    const useWin = !(cont && cont !== document.body);
    const scroller = useWin ? (document.scrollingElement || document.documentElement) : cont;
    let target;
    if (useWin) {
      target = el.getBoundingClientRect().top + scroller.scrollTop - 80;
    } else {
      const r = el.getBoundingClientRect(), cr = scroller.getBoundingClientRect();
      target = scroller.scrollTop + (r.top - cr.top) - 80;
    }
    target = Math.max(0, Math.min(target, scroller.scrollHeight - scroller.clientHeight));
    const start = scroller.scrollTop, dist = target - start, dur = 420, t0 = performance.now();
    const step = (now) => {
      const p = Math.min(1, (now - t0) / dur);
      const e2 = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
      scroller.scrollTop = start + dist * e2;
      if (p < 1) requestAnimationFrame(step);
    };
    requestAnimationFrame(step);
    setTimeout(() => { scroller.scrollTop = target; }, dur + 80);
  };

  const railCss = `
    .essay-grid { display:grid; grid-template-columns: 250px minmax(0,1fr) 196px; gap: 46px; align-items: stretch; max-width: 1320px; margin: 0 auto; }
    .essay-rail-col { position: relative; }
    .essay-rail { align-self:start; max-height: calc(100vh - 106px); overflow:auto; padding-right: 6px; }
    .essay-rail-toggle { display:none; }
    .essay-rail-block { padding: 0 0 22px; border-bottom: 1px solid var(--line); margin-bottom: 22px; }
    .essay-rail-block:last-child { border-bottom: 0; margin-bottom: 0; }
    .rail-lbl { font-size: 10px; text-transform:uppercase; letter-spacing:0.14em; color:var(--mute); margin-bottom: 12px; }
    .toc-link { display:block; font-size:13px; line-height:1.32; padding:4px 0 4px 12px; color:var(--mute); cursor:pointer; border-left:2px solid var(--line); transition:color .15s, border-color .15s; }
    .toc-link:hover { color: var(--ink); border-color: var(--mute); }
    .toc-link.l3 { padding-left: 26px; font-size:12px; }
    .toc-link.active { color: ${THEME.red}; border-color: ${THEME.red}; }
    .essay-main { min-width: 0; }
    .essay-kw { position: sticky; top: 90px; align-self:start; }
    .kw-tag { display:inline-block; font-size:11px; line-height:1; padding:7px 10px; margin:0 6px 6px 0; border:1px solid var(--line); color:var(--mute); letter-spacing:0.02em; white-space:nowrap; }
    @media (max-width: 1100px) {
      .essay-grid { grid-template-columns: 250px minmax(0,1fr); }
      .essay-kw-col { display:none; }
    }
    @media (max-width: 900px) {
      .essay-grid { grid-template-columns: 1fr; gap: 0; }
      .essay-rail { position: static; max-height:none; overflow:visible; border:1px solid var(--line); margin-bottom: 30px; padding: 0; }
      .essay-rail-toggle { display:flex; align-items:center; justify-content:space-between; width:100%; padding: 14px 18px; background:var(--sub); border:0; font-family:inherit; font-size:12px; letter-spacing:0.1em; text-transform:uppercase; color:var(--ink); cursor:pointer; }
      .essay-rail-inner { padding: 18px 18px 4px; }
      .essay-rail.collapsed .essay-rail-inner { display:none; }
    }
  `;

  return (
    <>
      <style>{railCss}</style>
      <TopNav />
      <div className="pp-wrap tight">
        <div className="essay-grid">

          <div className="essay-rail-col" ref={colRef}>
          <aside className={'essay-rail' + (railOpen ? '' : ' collapsed')} ref={railRef}>
            <button className="essay-rail-toggle" onClick={() => setRailOpen((o) => !o)}>
              <span>Contents &amp; details</span><span>{railOpen ? '▴' : '▾'}</span>
            </button>
            <div className="essay-rail-inner">

              <div className="essay-rail-block">
                <div className="rail-lbl">Reading</div>
                <div style={{ fontSize: 22, fontWeight: 500, color: THEME.red, lineHeight: 1 }}>{mins} min</div>
                <div style={{ fontSize: 12, color:'var(--mute)', marginTop: 8, lineHeight: 1.5 }}>
                  {isSup ? 'Published under my supervision' : 'Essay'}{e.date ? ' · ' + fmtDate(e.date) : ''}
                </div>
                {e.url && (
                  <a className="pp-link" href={e.url} target="_blank" rel="noopener noreferrer" style={{ fontSize: 11, marginTop: 8, display:'inline-block', wordBreak:'break-all', lineHeight: 1.4 }}>original on {pubName(e.url)} →</a>
                )}
              </div>

              {hasToc && (
                <div className="essay-rail-block">
                  <div className="rail-lbl">On this page · {toc.sections.length}</div>
                  <nav aria-label="On this page">
                    {toc.sections.map((sct) => (
                      <a key={sct.id} onClick={() => jump(sct.id)}
                         className={'toc-link l' + sct.level + (activeId === sct.id ? ' active' : '')}>{sct.text}</a>
                    ))}
                  </nav>
                </div>
              )}

              <div className="essay-rail-block">
                <div className="rail-lbl">Send me a comment</div>
                <form onSubmit={sendComment} style={{ display:'grid', gap: 10 }}>
                  <textarea value={cMsg} onChange={(ev) => { setCMsg(ev.target.value); setSentC(false); }} required rows={4}
                            placeholder="A thought on this piece…"
                            style={{ fontFamily:'inherit', fontSize: 13, lineHeight: 1.5, padding:'10px 12px', background:'var(--sub)', border:'1px solid var(--line)', color:'var(--ink)', outline:'none', resize:'vertical' }} />
                  <button type="submit"
                          style={{ fontFamily:'inherit', fontSize: 11, fontWeight: 500, letterSpacing:'0.06em', textTransform:'uppercase', padding:'10px 14px', background: THEME.red, color:'#fff', border:'none', cursor:'pointer' }}>
                    Send →
                  </button>
                  <span style={{ fontSize: 11, color:'var(--mute)', lineHeight: 1.45 }}>
                    {sentC ? 'Mail app opened — just hit send.' : 'Opens your mail app, pre-filled to me.'}
                  </span>
                </form>
              </div>

            </div>
          </aside>
          </div>

          <div className="essay-main">
            <AINote />
            <div className="pp-rv" style={{ fontSize: 11, color:'var(--mute)', textTransform:'uppercase', letterSpacing:'0.16em', marginBottom: 14, textAlign:'center' }}>
              <a className="pp-link" onClick={() => go('essays')}>{c.back}</a>
              <span style={{ margin: '0 12px' }}>·</span>
              <span>{isSup ? 'published under my supervision' : 'essay'}{e.date ? ` · ${fmtDate(e.date)}` : ''}</span>
              <span style={{ margin: '0 12px' }}>·</span>
              <span className="red">{mins} min read</span>
            </div>

            {e.cover && !editing && (
              <img className="pp-rv" src={e.cover} alt="" loading="lazy"
                   style={{ display:'block', width:'100%', maxWidth: 860, margin:'0 auto 30px', border:'1px solid var(--line)' }} />
            )}
            {e.cover && editing && (
              <div className="pp-rv" style={{ maxWidth: 860, margin:'0 auto 20px' }}>
                <img src={e.cover} alt="" style={{ display:'block', width:'100%', border:'1px solid var(--line)' }} />
                <div style={{ fontSize: 11, color:'var(--mute)', marginTop: 6, textAlign:'center' }}>cover · {e.cover}</div>
              </div>
            )}
            <h1 className="pp-rv pp-h1 upper" style={{ fontSize: 'clamp(34px, 4.4vw, 64px)', lineHeight: 0.98, margin: '0 0 28px', textAlign:'center' }}>
              {editing
                ? <input className="pp-editinput" style={{ fontFamily:'inherit', fontSize:'clamp(24px,3vw,44px)', fontWeight:500, textTransform:'uppercase', textAlign:'center', padding:'10px 14px' }}
                         value={et.title} onChange={(ev) => store.patchEssay(e.id, { title: ev.target.value })} />
                : et.title}
            </h1>
            {editing && (
              <div className="pp-rv" style={{ display:'flex', justifyContent:'center', gap: 8, marginBottom: 20, alignItems:'center', fontSize: 11, color:'var(--mute)' }}>
                <span>date</span>
                <input className="pp-editinput" style={{ width: 90 }} value={e.date || ''}
                       onChange={(ev) => store.patchEssay(e.id, { date: ev.target.value })} />
              </div>
            )}

            <article className="pp-rv" style={{ maxWidth: 680, margin: '0 auto', paddingTop: 28, borderTop:'1px solid var(--line)' }}>
              {isSup && (
                <div style={{ fontSize: 13, color:'var(--mute)', marginBottom: 28, lineHeight: 1.6, textAlign:'center' }}>
                  Edited and published under the editorship of <b style={{ color:'var(--ink)', fontWeight: 500 }}>Vlady Ostroukhov</b> · written by the ProSvita&nbsp;/&nbsp;Media&nbsp;Bober editorial team.
                </div>
              )}

              {e.added && editing && (
                <div style={{ marginBottom: 28 }}>
                  <div style={{ fontSize: 11, color:'var(--mute)', textTransform:'uppercase', letterSpacing:'0.12em', marginBottom: 8 }}>Essay text — blank line = new paragraph · “## ” = heading</div>
                  <textarea className="pp-editinput" rows={16} value={e.body || ''}
                            placeholder={'First paragraph…\n\n## A section\n\nMore text…'}
                            onChange={(ev) => store.patchEssay(e.id, { body: ev.target.value })} />
                </div>
              )}
              {!e.added && editing && (
                <details style={{ marginBottom: 28, border:'1px dashed var(--line)', padding:'10px 14px' }}>
                  <summary style={{ fontSize: 11, color:'var(--mute)', textTransform:'uppercase', letterSpacing:'0.12em', cursor:'pointer' }}>
                    edit essay text {ov.bodyHtml != null ? '· edited' : ''}
                  </summary>
                  <div style={{ fontSize: 11, color:'var(--mute)', margin:'10px 0 8px', lineHeight: 1.5 }}>
                    Raw HTML — &lt;p&gt; paragraphs, &lt;h2&gt; sections, &lt;i&gt;/&lt;b&gt;/&lt;a&gt; inline. Changes preview live below.
                  </div>
                  <textarea className="pp-editinput" rows={18}
                            value={ov.bodyHtml != null ? ov.bodyHtml : ((content && (((typeof ESSAY_BODY !== 'undefined' && ESSAY_BODY[e.id]) || {}).body || '')) || '')}
                            placeholder={'<p>Essay text…</p>'}
                            onChange={(ev) => store.patchEssay(e.id, { bodyHtml: ev.target.value })} />
                  {ov.bodyHtml != null && (
                    <button className="pp-toolbtn" style={{ marginTop: 8 }} onClick={() => store.patchEssay(e.id, { bodyHtml: null })}>revert to original text</button>
                  )}
                </details>
              )}
              {content
                ? <EssayBody eid={e.id} html={toc.body} style={{ textAlign:'justify', WebkitHyphens:'auto', msHyphens:'auto', hyphens:'auto', hyphenateLimitChars: '6 3 3' }} />
                : (
                <div style={{ border:'1px solid var(--line)', padding: '30px 32px', background:'var(--sub)', textAlign:'left' }}>
                  <div style={{ fontSize: 11, color:'var(--mute)', textTransform:'uppercase', letterSpacing:'0.14em', marginBottom: 12 }}>Full text · pending import</div>
                  <p style={{ margin: '0 0 24px', fontSize: 16, lineHeight: 1.62 }}>
                    The full text and photographs of this piece live on the original publication. They are being migrated into the site — for now you can read it complete, with every image, on {pubName(e.url)}.
                  </p>
                  <a href={e.url} target="_blank" rel="noopener noreferrer"
                     style={{ display:'inline-block', fontSize: 14, fontWeight: 500, color: THEME.red, borderBottom:`1px solid ${THEME.red}`, paddingBottom: 2, textDecoration:'none' }}>
                    Read the original on {pubName(e.url)} →
                  </a>
                </div>
              )}
            </article>
          </div>

          {hasKw && (
          <div className="essay-kw-col">
            <div className="essay-kw">
              <div className="rail-lbl">Keywords</div>
              <div>
                {keywords.map((k) => (<span key={k} className="kw-tag">{k}</span>))}
              </div>
              <div style={{ marginTop: 26, paddingTop: 22, borderTop:'1px solid var(--line)' }}>
                <div className="rail-lbl">{c.further}</div>
                <div style={{ display:'grid', gap: 9 }}>
                  {further.map((f) => (
                    <a key={f.id} className="pp-link" onClick={() => go('essay', f.id)} style={{ fontSize: 12.5, lineHeight: 1.35, color:'var(--ink)' }}>{essayTextEx(store, f, lang).title} →</a>
                  ))}
                </div>
              </div>
            </div>
          </div>
          )}

        </div>
      </div>
      <Footer minimal />
    </>
  );
}

// ──────────────────────────────────────────────────────────────
// Projects — selected work, externally-linked
// ──────────────────────────────────────────────────────────────
function PageProjects() {
  const lang = useLang();
  const store = usePhotoStore();
  const editing = React.useContext(PhotoEditCtx);
  const c = I18N_PROJECTS[lang] || I18N_PROJECTS.en;
  const LIST = store.projectsList(PROJECTS_META);
  // localized base copy layered under user edits
  const itemFor = (p) => {
    const base = p._idx != null ? (c.items[p._idx] || {}) : {};
    return {
      desc: p.desc != null ? p.desc : base.desc,
      role: p.role != null ? p.role : base.role,
      context: p.context != null ? p.context : base.context,
      tags: p.tags != null ? p.tags : (base.tags || []),
    };
  };
  const EL = ({ label, children }) => (
    <label style={{ display:'grid', gridTemplateColumns:'70px 1fr', gap: 8, alignItems:'center', fontSize: 11, color:'var(--mute)' }}>
      <span style={{ textTransform:'uppercase', letterSpacing:'0.08em' }}>{label}</span>{children}
    </label>
  );
  return (
    <>
      <TopNav />
      <div className="pp-wrap">
        <div className="pp-rv" style={{ display:'flex', justifyContent:'space-between', alignItems:'end', marginBottom: 36 }}>
          <div>
            <div style={{ fontSize: 11, color:'var(--mute)', letterSpacing:'0.16em', textTransform:'uppercase', marginBottom: 12 }}>{c.kicker(LIST.length)}</div>
            <h1 className="pp-h1 upper" style={{ fontSize: 'clamp(44px, 6.2vw, 82px)', margin: 0 }} dangerouslySetInnerHTML={{ __html: c.title }} />
          </div>
          <div style={{ fontSize: 12, color:'var(--mute)', maxWidth: 340, flex:'0 0 auto', textAlign:'right', lineHeight: 1.55 }}>
            {c.intro}
          </div>
        </div>

        {LIST.map((p) => {
          const it = itemFor(p);
          const patch = (q) => store.patchProject(p, q);
          const host = p.host || (p.href ? p.href.replace(/^https?:\/\//, '').split('/')[0] : '');
          return (
            <div key={p.no} className="pp-rv g-proj" style={{ padding: '40px 0', borderTop:'1px solid var(--line)' }}>
              <div>
                <div style={{ display:'flex', gap: 14, alignItems: editing ? 'center' : 'baseline', marginBottom: 16 }}>
                  <span className="red" style={{ fontSize: 13 }}>{p.no}</span>
                  {editing
                    ? <input className="pp-editinput" style={{ fontFamily:'inherit', fontSize: 22, fontWeight: 500 }} value={p.name}
                             onChange={(ev) => patch({ name: ev.target.value })} />
                    : <h2 className="t-title" style={{ margin: 0, fontSize: 30, fontWeight: 500, letterSpacing:'-0.01em', lineHeight: 1.1 }}>{p.name}</h2>}
                  {editing && <PhotoTool title={p.added ? 'delete this project' : 'hide this project'} onClick={() => store.removeProject(p)}>✕</PhotoTool>}
                </div>
                {editing
                  ? <textarea className="pp-editinput" rows={3} style={{ marginBottom: 18 }} placeholder="project description"
                              value={it.desc || ''} onChange={(ev) => patch({ desc: ev.target.value })} />
                  : <p className="t-body" style={{ margin: '0 0 22px', fontSize: 15, lineHeight: 1.6, color:'var(--ink)', maxWidth: 560 }}>{it.desc}</p>}
                {editing ? (
                  <div style={{ display:'grid', gap: 8, marginBottom: 20, maxWidth: 560 }}>
                    <EL label={c.roleLabel}><input className="pp-editinput" value={it.role || ''} onChange={(ev) => patch({ role: ev.target.value })} /></EL>
                    <EL label={c.contextLabel}><input className="pp-editinput" value={it.context || ''} onChange={(ev) => patch({ context: ev.target.value })} /></EL>
                    <EL label={c.yearLabel}><input className="pp-editinput" style={{ width: 110 }} value={p.year || ''} onChange={(ev) => patch({ year: ev.target.value })} /></EL>
                    <EL label="link"><input className="pp-editinput" placeholder="https://…" value={p.href || ''} onChange={(ev) => patch({ href: ev.target.value, host })} /></EL>
                    <EL label="tags"><input className="pp-editinput" placeholder="comma, separated, tags" value={(it.tags || []).join(', ')}
                                            onChange={(ev) => patch({ tags: ev.target.value.split(',').map((t) => t.trim()).filter(Boolean) })} /></EL>
                  </div>
                ) : (
                  <div style={{ display:'grid', gridTemplateColumns:'auto 1fr', gap:'8px 20px', fontSize: 12, marginBottom: 20, maxWidth: 560 }}>
                    <span style={{ color:'var(--mute)', textTransform:'uppercase', letterSpacing:'0.1em' }}>{c.roleLabel}</span><span>{it.role}</span>
                    <span style={{ color:'var(--mute)', textTransform:'uppercase', letterSpacing:'0.1em' }}>{c.contextLabel}</span><span>{it.context}</span>
                    <span style={{ color:'var(--mute)', textTransform:'uppercase', letterSpacing:'0.1em' }}>{c.yearLabel}</span><span className="pp-num">{p.year}</span>
                  </div>
                )}
                {!editing && (
                  <div style={{ display:'flex', flexWrap:'wrap', gap: 6, marginBottom: 22 }}>
                    {(it.tags || []).map((t) => <span key={t} className="pp-chip" style={{ fontSize: 11 }}>{t}</span>)}
                  </div>
                )}
                {p.href && !editing && <a className="pp-link red" href={p.href} target="_blank" rel="noopener noreferrer" style={{ fontSize: 13 }}>{c.cta}</a>}
              </div>
              {editing ? (
                <div>
                  <Img ratio="4 / 3" no={p.no} caption={host} src={p.cover} slot={'proj/' + (p.id || p.no)} pos={p.pos}
                       onPos={(q) => patch({ pos: q })} tools={<></>} />
                </div>
              ) : (
                <a href={p.href || undefined} target="_blank" rel="noopener noreferrer" style={{ display:'block', textDecoration:'none' }}>
                  <Img ratio="4 / 3" no={'↗'} caption={host} src={p.cover} slot={'proj/' + (p.id || p.no)} pos={p.pos} />
                </a>
              )}
            </div>
          );
        })}
        {editing && (
          <div className="pp-rv" style={{ padding: '20px 0', borderTop: '1px solid var(--line)' }}>
            <button className="pp-addframe" style={{ aspectRatio:'auto', minHeight: 64, width: '100%' }}
                    onClick={() => store.addProject()}>+ add project</button>
          </div>
        )}
      </div>
      <Footer />
    </>
  );
}

// ──────────────────────────────────────────────────────────────
// Contact
// ──────────────────────────────────────────────────────────────
function PageContact() {
  const now = useNow();
  const lang = useLang();
  const c = tr('contact', lang);
  const [cName, setCName] = React.useState('');
  const [cFrom, setCFrom] = React.useState('');
  const [cMsg, setCMsg] = React.useState('');
  const [sent, setSent] = React.useState(false);
  const sendComment = (ev) => {
    ev.preventDefault();
    if (!cMsg.trim()) return;
    const subject = encodeURIComponent('Site comment' + (cName ? ' from ' + cName : ''));
    const body = encodeURIComponent([cMsg, '', '—', cName && ('Name: ' + cName), cFrom && ('Reply to: ' + cFrom)].filter(Boolean).join('\n'));
    window.location.href = 'mailto:' + SITE.email + '?subject=' + subject + '&body=' + body;
    setSent(true);
  };
  const L = ({ children }) => (
    <span style={{ fontSize: 11, color:'var(--mute)', textTransform:'uppercase', letterSpacing:'0.12em' }}>{children}</span>
  );
  return (
    <>
      <TopNav />
      <div className="pp-wrap">
        <div className="pp-rv" style={{ fontSize: 11, color:'var(--mute)', letterSpacing:'0.16em', textTransform:'uppercase', marginBottom: 14 }}>{c.kicker}</div>
        <h1 className="pp-rv pp-h1" style={{ fontSize: 'clamp(48px, 7vw, 100px)', lineHeight: 0.92, margin: '0 0 28px', maxWidth: 1100 }} dangerouslySetInnerHTML={{ __html: c.heading }} />

        <div className="pp-rv g-contact" style={{ paddingTop: 28, borderTop:'1px solid var(--line)' }}>
          <div>
            <div style={{ display:'grid', gridTemplateColumns:'120px 1fr', gap: '16px 24px', fontSize: 16, alignItems:'baseline' }}>
              <L>{c.lEmail}</L>
              <a className="pp-link" href={'mailto:' + SITE.email} style={{ color: THEME.red }}>{SITE.email}</a>
              <L>{c.lPhone}</L>
              <span className="pp-num">{SITE.phone}</span>
              <L>{c.lSignal}</L>
              <span className="pp-num">{SITE.signal} <span style={{ color:'var(--mute)', fontSize: 12 }}>· {c.phoneNote}</span></span>
              <L>{c.lPhysical}</L>
              <span>{c.physicalVal}</span>
              <L>{c.lLocal}</L>
              <span className="pp-num">{timeIn('Europe/Stockholm', now)} · CET</span>
              <L>{c.lElsewhere}</L>
              <span>
                <a className="pp-link" href={SITE.linkedin} target="_blank" rel="noopener noreferrer">linkedin/vlady-ost</a><br/>
                <a className="pp-link" href={SITE.substack} target="_blank" rel="noopener noreferrer">substack/@vladyost</a><br/>
                <a className="pp-link" href={SITE.instagram} target="_blank" rel="noopener noreferrer">instagram/vlady.ost</a><br/>
                <a className="pp-link" href={SITE.oldPortfolio} target="_blank" rel="noopener noreferrer">{c.lOldPortfolio}</a>
              </span>
            </div>
          </div>

          <aside className="pp-side" style={{ display:'grid', gap: 24 }}>
            <div>
              <div style={{ fontSize: 10, textTransform:'uppercase', letterSpacing:'0.12em', color:'var(--mute)', marginBottom: 8 }}>{c.goodLabel}</div>
              <ul style={{ margin: 0, padding: '0 0 0 18px', lineHeight: 1.7, fontSize: 12 }}>
                {c.good.map((g, i) => <li key={i}>{g}</li>)}
              </ul>
            </div>
            <div>
              <div style={{ fontSize: 10, textTransform:'uppercase', letterSpacing:'0.12em', color:'var(--mute)', marginBottom: 8 }}>{c.worseLabel}</div>
              <ul style={{ margin: 0, padding: '0 0 0 18px', lineHeight: 1.7, fontSize: 12, color:'var(--mute)' }}>
                {c.worse.map((w, i) => <li key={i}>{w}</li>)}
              </ul>
            </div>
            <div>
              <div style={{ fontSize: 10, textTransform:'uppercase', letterSpacing:'0.12em', color:'var(--mute)', marginBottom: 8 }}>{c.respLabel}</div>
              <div style={{ fontSize: 12 }}>{c.respVal}</div>
            </div>
          </aside>
        </div>

        <div className="pp-rv" style={{ marginTop: 64, paddingTop: 36, borderTop:'1px solid var(--line)' }}>
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', flexWrap:'wrap', gap: 12, marginBottom: 22 }}>
            <h2 className="t-title" style={{ margin: 0, fontSize: 'clamp(26px, 3vw, 40px)', fontWeight: 500, letterSpacing:'-0.01em' }}>Send me a comment</h2>
            <span style={{ fontSize: 11, color:'var(--mute)', textTransform:'uppercase', letterSpacing:'0.12em' }}>delivered straight to my inbox</span>
          </div>
          <form onSubmit={sendComment} className="g-form">
            <input value={cName} onChange={(ev) => setCName(ev.target.value)} placeholder="Your name (optional)"
                   style={{ fontFamily:'inherit', fontSize: 14, padding:'12px 14px', background:'var(--sub)', border:'1px solid var(--line)', color:'var(--ink)', outline:'none' }} />
            <input value={cFrom} onChange={(ev) => setCFrom(ev.target.value)} type="email" placeholder="Your email (optional)"
                   style={{ fontFamily:'inherit', fontSize: 14, padding:'12px 14px', background:'var(--sub)', border:'1px solid var(--line)', color:'var(--ink)', outline:'none' }} />
            <textarea value={cMsg} onChange={(ev) => { setCMsg(ev.target.value); setSent(false); }} required rows={5} placeholder="What's on your mind?"
                   style={{ gridColumn:'1 / -1', fontFamily:'inherit', fontSize: 15, lineHeight: 1.55, padding:'14px 16px', background:'var(--sub)', border:'1px solid var(--line)', color:'var(--ink)', outline:'none', resize:'vertical' }} />
            <div style={{ gridColumn:'1 / -1', display:'flex', alignItems:'center', gap: 18, flexWrap:'wrap' }}>
              <button type="submit"
                      style={{ fontFamily:'inherit', fontSize: 13, fontWeight: 500, letterSpacing:'0.04em', textTransform:'uppercase', padding:'12px 26px', background: THEME.red, color:'#fff', border:'none', cursor:'pointer' }}>
                Send comment →
              </button>
              <span style={{ fontSize: 12, color:'var(--mute)', lineHeight: 1.5, maxWidth: 380 }}>
                {sent ? 'Your mail app should have opened with the message ready — just hit send.' : 'Opens your email app with the message pre-filled to ' + SITE.email + '.'}
              </span>
            </div>
          </form>
        </div>
      </div>
      <Footer />
    </>
  );
}

Object.assign(window, { PageAbout, PageCV, PageProjects, PagePhotos, PageSeries, PageEssays, PageEssay, PageContact });
