// Tools layer for the Museum of Forgotten Moments.
// A folder tab on the RIGHT edge, sitting directly below the INDEX tab, opens a
// "workshop" drawer of curator tools. The first tool is a ZINE GENERATOR:
// it imposes the live collection onto ONE A4 sheet that, printed single-sided,
// folds & cuts into an 8-page mini-zine — 1 front cover, 6 interior pages
// (each holding two horizontal photos or one vertical photo), and 1 back cover.
// The top row of the sheet is rotated 180° so every panel reads upright once
// the sheet is folded the classic single-cut way.

const { useState: useToolState, useMemo: useToolMemo, useEffect: useToolEffect, useRef: useToolRef } = React;

const ZINE_DRAWER_W = 460;

// ─── small helpers ───
function escZ(s) {
  return String(s == null ? '' : s)
    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}
function padZ(n) { return String(n).padStart(2, '0'); }
function absSrc(s) { try { return new URL(s, document.baseURI).href; } catch (e) { return s; } }

const ZINE_MONTHS = ['JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE',
  'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER'];

function zineRatio(aspect) {
  const parts = String(aspect || '3:2').split(':').map(Number);
  if (parts.length !== 2 || !parts[1]) return 1.5;
  return parts[0] / parts[1];
}
function isVerticalWork(w) { return zineRatio(w.aspect) < 0.98; }

// ─── zine counters (persist across sessions) ───
// · personal — how many zines THIS device has exported; drives the cover N°.
// · community total lives server-side (see MU.getZineTotal / bumpZineTotal).
const ZINE_MINE_KEY = 'mu_zine_mine_v1';
function readMine() {
  const n = parseInt(localStorage.getItem(ZINE_MINE_KEY) || '0', 10);
  return (isNaN(n) || n < 0) ? 0 : n;
}

// ─── Folder-tab on the right edge, below INDEX ───
function ToolsTab({ theme, open, onClick }) {
  const t = window.THEMES[theme];
  const mobile = window.useIsMobile ? window.useIsMobile() : false;
  return (
    <button
      onClick={onClick}
      aria-expanded={open}
      aria-label="Open tools"
      style={{
        position: 'fixed',
        // Both folder tabs live at the bottom-right edge, stacked (TOOLS above
        // INDEX), so they're easy to reach on every viewport.
        top: 'auto',
        bottom: mobile ? 158 : 191,
        right: 0,
        width: 34, height: mobile ? 112 : 116,
        background: open ? t.fg : t.bg,
        color: open ? t.bg : t.fg,
        border: `1px solid ${t.rule}`,
        borderRight: open ? `1px solid ${t.rule}` : 'none',
        cursor: 'pointer',
        fontFamily: '"JetBrains Mono", monospace',
        fontSize: 11,
        letterSpacing: '0.28em',
        zIndex: 42,
        display: 'flex',
        alignItems: 'center', justifyContent: 'center',
        transform: mobile ? 'translateX(0)' : (open ? `translateX(-${ZINE_DRAWER_W}px)` : 'translateX(0)'),
        opacity: mobile && open ? 0 : 1,
        pointerEvents: mobile && open ? 'none' : 'auto',
        transition: 'background 200ms, color 200ms, transform 360ms cubic-bezier(.2,.8,.2,1), border-color 200ms, opacity 200ms',
        boxShadow: open ? 'none' : (theme === 'dark'
          ? '-8px 8px 24px rgba(0,0,0,0.5)'
          : '-6px 6px 18px rgba(0,0,0,0.08)'),
      }}
      onMouseEnter={(e) => { if (!open && !mobile) e.currentTarget.style.transform = 'translateX(-2px)'; }}
      onMouseLeave={(e) => { if (!open && !mobile) e.currentTarget.style.transform = 'translateX(0)'; }}
    >
      <span style={{
        writingMode: 'vertical-rl',
        transform: 'rotate(180deg)',
        display: 'inline-flex', alignItems: 'center', gap: 10,
      }}>
        <span aria-hidden="true">{open ? '→' : '←'}</span>
        TOOLS
      </span>
    </button>
  );
}

// ─── Tools drawer — slides in from the right; lists available tools ───
function ToolsDrawer({ open, onClose, theme, onOpenZine }) {
  const t = window.THEMES[theme];

  useToolEffect(() => {
    function onKey(e) { if (e.key === 'Escape') onClose(); }
    if (open) document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [open, onClose]);

  const toolBtn = {
    display: 'flex', flexDirection: 'column', gap: 8,
    width: '100%', textAlign: 'left',
    padding: '20px 20px',
    background: 'transparent',
    color: t.fg,
    border: `1px solid ${t.rule}`,
    cursor: 'pointer',
    fontFamily: 'inherit',
    transition: 'background 160ms, color 160ms',
  };

  return (
    <>
      <div
        onClick={onClose}
        style={{
          position: 'fixed', top: 89, left: 0, right: 0, bottom: 0,
          background: theme === 'dark' ? 'rgba(0,0,0,0.45)' : 'rgba(10,10,10,0.18)',
          opacity: open ? 1 : 0,
          pointerEvents: open ? 'auto' : 'none',
          transition: 'opacity 240ms ease',
          zIndex: 40,
        }}
      />
      <aside
        aria-hidden={!open}
        style={{
          position: 'fixed',
          top: 89, right: 0, bottom: 0,
          width: ZINE_DRAWER_W, maxWidth: '92vw',
          background: t.bg,
          color: t.fg,
          border: `1px solid ${t.rule}`,
          borderRight: 'none',
          transform: open ? 'translateX(0)' : 'translateX(100%)',
          transition: 'transform 360ms cubic-bezier(.2,.8,.2,1)',
          pointerEvents: open ? 'auto' : 'none',
          boxShadow: open
            ? (theme === 'dark' ? '-30px 0 80px rgba(0,0,0,0.6)' : '-30px 0 80px rgba(0,0,0,0.18)')
            : 'none',
          zIndex: 41,
          display: 'flex', flexDirection: 'column',
          fontFamily: '"JetBrains Mono", monospace',
        }}
      >
        <div style={{
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          padding: '20px 24px',
          borderBottom: `1px solid ${t.rule}`,
        }}>
          <div>
            <div style={{ fontSize: 9, letterSpacing: '0.2em', color: t.veryMuted }}>WORKSHOP</div>
            <div style={{ fontSize: 13, letterSpacing: '0.08em', marginTop: 4 }}>TOOLS · 01</div>
          </div>
          <button
            onClick={onClose}
            style={{ background: 'transparent', border: 'none', color: t.muted, cursor: 'pointer', fontSize: 16, lineHeight: 1, padding: 0 }}
            aria-label="Close tools"
          >×</button>
        </div>

        <div style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 14, flex: 1, overflowY: 'auto' }}>
          <button
            style={toolBtn}
            onClick={onOpenZine}
            onMouseEnter={(e) => { e.currentTarget.style.background = t.fg; e.currentTarget.style.color = t.bg; }}
            onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = t.fg; }}
          >
            <span style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
              <span style={{ fontSize: 14, letterSpacing: '0.12em' }}>▤ ZINE GENERATOR</span>
              <span style={{ fontSize: 9, letterSpacing: '0.18em', opacity: 0.6 }}>01</span>
            </span>
            <span style={{ fontSize: 9.5, letterSpacing: '0.1em', opacity: 0.72, lineHeight: 1.6 }}>
              IMPOSE THE COLLECTION ONTO ONE A4 SHEET — PRINT SINGLE-SIDED, FOLD &amp; CUT INTO AN 8-PAGE MINI ZINE.
            </span>
          </button>
        </div>

        <div style={{
          padding: '14px 24px',
          borderTop: `1px solid ${t.rule}`,
          fontSize: 9, letterSpacing: '0.18em', color: t.veryMuted,
          display: 'flex', justifyContent: 'space-between',
        }}>
          <span>ESC TO CLOSE</span>
          <span>1 A4 · 8 PAGES</span>
        </div>
      </aside>
    </>
  );
}

// ─────────────────────────────────────────────────────────────────────
// ZINE IMPOSITION
// One A4 sheet, LANDSCAPE: 297 × 210 mm → 1123 × 794 px @ 96dpi.
// Divided into a 4-column × 2-row grid; each panel is 1/8 of the sheet
// (≈ 74 × 105 mm, portrait). Printed single-sided and folded the classic
// single-cut way, the panels assemble into an 8-page booklet. The TOP ROW is
// rotated 180° so it reads upright in the finished zine.
//
// Booklet order → sheet position (F & B adjacent first, then interior 1–6):
//   bottom row L→R : FRONT · BACK · page1 · page2
//   top row   L→R  : page3 · page4 · page5 · page6   (rotated 180°)
// ─────────────────────────────────────────────────────────────────────
const SHEET_W = 1123, SHEET_H = 794;
const CELL_W = SHEET_W / 4;   // 280.75
const CELL_H = SHEET_H / 2;   // 397

// Six interior pages, each an array of placed work ids (manual drag-drop).
const EMPTY_PLACEMENT = [[], [], [], [], [], []];
const EMPTY_OPTS = {};

const ZINE_PANELS = [
  // bottom row (upright) — F, B, then interior 1 & 2
  { r: 1, c: 0, rotate: false, kind: 'front' },             // F
  { r: 1, c: 1, rotate: false, kind: 'back' },              // B
  { r: 1, c: 2, rotate: false, kind: 'content', page: 0 },  // 1
  { r: 1, c: 3, rotate: false, kind: 'content', page: 1 },  // 2
  // top row (rotated 180°) — interior 3–6
  { r: 0, c: 0, rotate: true,  kind: 'content', page: 2 },  // 3
  { r: 0, c: 1, rotate: true,  kind: 'content', page: 3 },  // 4
  { r: 0, c: 2, rotate: true,  kind: 'content', page: 4 },  // 5
  { r: 0, c: 3, rotate: true,  kind: 'content', page: 5 },  // 6
];

function paperFor(paper) {
  // Reuse the museum's own light / dark tokens as paper stocks.
  if (paper === 'ink') return window.THEMES.dark;
  if (paper === 'blank') {
    // Pure-white stock: prints a solid white ground so coloured paper won't tint it.
    return { ...window.THEMES.light, bg: '#ffffff', matte: '#f1f0ea' };
  }
  return window.THEMES.light; // cream
}

// Distribute selected works across the 6 interior pages.
//  · a vertical photo fills a page on its own
//  · horizontal photos pair two-up
function paginateWorks(works) {
  const pages = [];
  let i = 0;
  while (i < works.length && pages.length < 6) {
    const w = works[i];
    if (isVerticalWork(w)) { pages.push([w]); i++; }
    else {
      const grp = [w]; i++;
      if (i < works.length && !isVerticalWork(works[i])) { grp.push(works[i]); i++; }
      pages.push(grp);
    }
  }
  const placed = pages.reduce((n, g) => n + g.length, 0);
  while (pages.length < 6) pages.push([]);
  return { pages, placed };
}

// ─── panel content (fills a 280 × 397 cell) ───
function frontPanel(p, info) {
  return `
    <div style="position:absolute;inset:0;padding:24px 20px;display:flex;flex-direction:column;justify-content:space-between;text-align:center;">
      <div style="display:flex;justify-content:space-between;font-size:7.5px;letter-spacing:0.24em;color:${p.veryMuted};text-align:left;">
        <span>EST. 2024</span><span>${escZ(info.issue)}</span>
      </div>
      <div>
        <div style="font-size:8px;letter-spacing:0.42em;color:${p.muted};margin-bottom:18px;">PHOTOGRAPH ZINE</div>
        <div style="font-size:26px;line-height:1.06;letter-spacing:-0.01em;text-wrap:balance;">${escZ(info.siteTitle)}</div>
        <div style="width:34px;height:1px;background:${p.rule};margin:18px auto;"></div>
        <div style="font-size:11px;letter-spacing:0.16em;color:${p.muted};">${escZ(info.author)}</div>
      </div>
      <div style="display:flex;justify-content:space-between;font-size:8.5px;letter-spacing:0.18em;color:${p.muted};">
        <span>${escZ(info.issue)}</span><span>${escZ(info.date)}</span>
      </div>
    </div>`;
}

// Fallback contact details for the zine back cover — used only until the
// site's ABOUT page has its own contact links. The back cover otherwise mirrors
// the ABOUT page (see aboutToZineContact + the `about` prop threaded into the tool).
const ZINE_CONTACT = {
  email: 'STUDIO@ACOZGUNES.COM',
  instagram: '@A.C.OZGUNES',
  web: 'ACOZGUNES.COM',
  location: 'İZMIR · TR',
};

// Derive the zine back-cover contact from the ABOUT page data
// ({ statement, links:[{label,value,kind}] }). Contact-type links (email/link)
// become the credit lines; the first free-text link becomes the location note.
// Returns null when ABOUT has no usable links, so callers fall back to ZINE_CONTACT.
function aboutToZineContact(about) {
  const links = (about && Array.isArray(about.links))
    ? about.links.filter(l => l && (l.value || '').trim()) : [];
  if (!links.length) return null;
  const contactLinks = links.filter(l => l.kind === 'email' || l.kind === 'link');
  const textLinks = links.filter(l => l.kind === 'text');
  let creditsLines = contactLinks.map(l => l.value.trim());
  let location = textLinks.length ? textLinks[0].value.trim() : '';
  if (!creditsLines.length) { creditsLines = links.map(l => l.value.trim()); location = ''; }
  return { creditsLines, creditsText: creditsLines.join('\n'), location };
}

function backPanel(p, info) {
  const c = info.contact || {};
  const lines = (c.creditsLines && c.creditsLines.length)
    ? c.creditsLines : [ZINE_CONTACT.email, ZINE_CONTACT.instagram, ZINE_CONTACT.web];
  const location = (c.location != null && c.location !== '') ? c.location : ZINE_CONTACT.location;
  return `
    <div style="position:absolute;inset:0;padding:24px 20px;display:flex;flex-direction:column;justify-content:space-between;text-align:center;">
      <div style="font-size:7.5px;letter-spacing:0.24em;color:${p.veryMuted};text-align:left;">CONTACT</div>
      <div>
        <div style="font-size:14px;line-height:1.2;letter-spacing:0.02em;">${escZ(info.author)}</div>
        <div style="width:28px;height:1px;background:${p.rule};margin:14px auto;"></div>
        <div style="font-size:9.5px;letter-spacing:0.12em;color:${p.muted};line-height:2.1;">
          ${lines.map(escZ).join('<br/>')}
        </div>
      </div>
      <div style="font-size:7px;letter-spacing:0.22em;color:${p.veryMuted};line-height:1.8;">
        ${location ? escZ(location) + '<br/>' : ''}PRINTS &amp; COMMISSIONS ON REQUEST
      </div>
    </div>`;
}

// One image + its caption as a group that rotates together (0 or 90).
function unitHTML(w, p, srcOf, single) {
  const turned = ((((w._rot || 0) % 360) + 360) % 360) === 90;
  const fit = w._fit === 'contain' ? 'contain' : 'cover';
  const groupStyle = turned
    ? `position:absolute;top:50%;left:50%;width:100cqh;height:100cqw;transform:translate(-50%,-50%) rotate(90deg);display:flex;flex-direction:column;`
    : `position:absolute;inset:0;display:flex;flex-direction:column;`;
  const tSize = single ? 18 : 14.5, sSize = single ? 9 : 8, tMargin = single ? 12 : 8, sMargin = single ? 6 : 4;
  return `
    <div style="position:relative;flex:1;min-height:0;container-type:size;overflow:hidden;">
      <div style="${groupStyle}">
        <div style="flex:1;min-height:0;position:relative;background:${p.bg};overflow:hidden;">
          <img src="${srcOf(w.src)}" alt="" style="position:absolute;inset:0;width:100%;height:100%;object-fit:${fit};display:block;"/>
        </div>
        <div style="flex:none;margin-top:${tMargin}px;">
          <div style="font-size:${tSize}px;line-height:1.12;letter-spacing:0.005em;text-wrap:balance;color:${p.fg};">${escZ(w.title)}</div>
          <div style="font-size:${sSize}px;letter-spacing:0.14em;color:${p.muted};margin-top:${sMargin}px;">${escZ(w.series.toUpperCase())} · ${escZ(w.year)}</div>
        </div>
      </div>
    </div>`;
}

// ─── Text blocks — can occupy a slot (or whole page) in place of a photo ───
const TEXT_VARIANTS = {
  title:   { base: 30,   lh: 1.06, ls: '-0.01em', weight: 500, tt: 'none' },
  subhead: { base: 18,   lh: 1.22, ls: '0.04em',  weight: 500, tt: 'none' },
  body:    { base: 12.5, lh: 1.5,  ls: '0.01em',  weight: 400, tt: 'none' },
  label:   { base: 9.5,  lh: 1.7,  ls: '0.22em',  weight: 500, tt: 'uppercase' },
};
const TEXT_SIZES = { s: 0.72, m: 1, l: 1.4, xl: 1.9 };
function textCss(variant, size) {
  const v = TEXT_VARIANTS[variant] || TEXT_VARIANTS.body;
  const m = TEXT_SIZES[size] || 1;
  return { fontSize: +(v.base * m).toFixed(1), lineHeight: v.lh, letterSpacing: v.ls, fontWeight: v.weight, textTransform: v.tt };
}
function alignParts(align) {
  align = align || 'mc';
  const v = align[0], h = align[1];
  return {
    justify: v === 't' ? 'flex-start' : v === 'b' ? 'flex-end' : 'center',
    items: h === 'l' ? 'flex-start' : h === 'r' ? 'flex-end' : 'center',
    ta: h === 'l' ? 'left' : h === 'r' ? 'right' : 'center',
  };
}
// ─── Designed page TEMPLATES ─────────────────────────────────────────
// Full-page compositions with editable fields. Each is "text-only" and
// renders on a clean WHITE ground (no paper tint) via TEXT_PAPER.
const TEXT_PAPER = { bg: '#ffffff', fg: '#0a0a0a', rule: '#0a0a0a', muted: 'rgba(10,10,10,0.55)', veryMuted: 'rgba(10,10,10,0.4)' };

const TEMPLATES = {
  front: {
    label: 'FRONT PAGE', blurb: 'COVER · KICKER · TITLE · BYLINE',
    fields: [
      { key: 'kicker', label: 'KICKER' },
      { key: 'title',  label: 'TITLE', area: true },
      { key: 'byline', label: 'BYLINE' },
      { key: 'left',   label: 'CORNER · LEFT' },
      { key: 'right',  label: 'CORNER · RIGHT' },
    ],
    seed: (m) => ({ kicker: 'PHOTOGRAPH ZINE', title: 'YOUR ZINE TITLE', byline: 'YOUR NAME', left: m.issue || 'N°01', right: m.date || '' }),
  },
  back: {
    label: 'BACK PAGE', blurb: 'CONTACT · LINKS · NOTE',
    fields: [
      { key: 'label',   label: 'LABEL' },
      { key: 'title',   label: 'NAME', area: true },
      { key: 'credits', label: 'CONTACT · ONE PER LINE', area: true },
      { key: 'note',    label: 'NOTE', area: true },
    ],
    seed: () => ({ label: 'CONTACT', title: 'YOUR NAME', credits: 'hello@email.com\ninstagram.com/you\nyoursite.com', note: 'CITY · COUNTRY\nPRINTS & COMMISSIONS ON REQUEST' }),
  },
  quote: {
    label: 'QUOTE', blurb: 'PULL-QUOTE · ATTRIBUTION',
    fields: [
      { key: 'quote', label: 'QUOTE', area: true },
      { key: 'by',    label: 'ATTRIBUTION' },
    ],
    seed: (m) => ({ quote: 'A short line about the work.', by: m.author || '' }),
  },
  divider: {
    label: 'DIVIDER', blurb: 'SECTION · NUMBER · LABEL',
    fields: [
      { key: 'num',   label: 'NUMBER' },
      { key: 'label', label: 'LABEL', area: true },
      { key: 'sub',   label: 'SUBTITLE' },
    ],
    seed: () => ({ num: '01', label: 'SERIES', sub: '' }),
  },
  contents: {
    label: 'CONTENTS', blurb: 'INDEX · ONE ENTRY PER LINE',
    fields: [
      { key: 'title', label: 'HEADING' },
      { key: 'lines', label: 'ENTRIES · ONE PER LINE', area: true },
    ],
    seed: () => ({ title: 'CONTENTS', lines: '01   First plate\n02   Second plate\n03   Third plate\n04   Fourth plate' }),
  },
};
const TEMPLATE_ORDER = ['front', 'back', 'quote', 'divider', 'contents'];

// Render a template's inner HTML from its editable fields (it.f), on the chosen paper.
function templateInnerHTML(it, paper) {
  const p = paper || TEXT_PAPER;
  const f = it.f || {};
  const base = `position:absolute;inset:0;color:${p.fg};font-family:'JetBrains Mono',monospace;`;
  if (it.tpl === 'front') {
    return `<div style="${base}padding:26px 22px;display:flex;flex-direction:column;justify-content:space-between;text-align:center;">
      <div style="display:flex;justify-content:space-between;font-size:7.5px;letter-spacing:0.24em;color:${p.veryMuted};text-align:left;"><span>${escZ(f.left)}</span><span>${escZ(f.right)}</span></div>
      <div>
        <div style="font-size:8px;letter-spacing:0.42em;color:${p.muted};margin-bottom:18px;">${escZ(f.kicker)}</div>
        <div style="font-size:26px;line-height:1.06;letter-spacing:-0.01em;text-wrap:balance;white-space:pre-wrap;">${escZ(f.title)}</div>
        <div style="width:34px;height:1px;background:${p.rule};margin:18px auto;"></div>
        <div style="font-size:11px;letter-spacing:0.16em;color:${p.muted};">${escZ(f.byline)}</div>
      </div>
      <div style="display:flex;justify-content:space-between;font-size:8.5px;letter-spacing:0.18em;color:${p.muted};"><span>${escZ(f.left)}</span><span>${escZ(f.right)}</span></div>
    </div>`;
  }
  if (it.tpl === 'back') {
    return `<div style="${base}padding:26px 22px;display:flex;flex-direction:column;justify-content:space-between;text-align:center;">
      <div style="font-size:7.5px;letter-spacing:0.24em;color:${p.veryMuted};text-align:left;">${escZ(f.label)}</div>
      <div>
        <div style="font-size:14px;line-height:1.2;letter-spacing:0.02em;white-space:pre-wrap;">${escZ(f.title)}</div>
        <div style="width:28px;height:1px;background:${p.rule};margin:14px auto;"></div>
        <div style="font-size:9.5px;letter-spacing:0.14em;color:${p.muted};line-height:1.9;white-space:pre-wrap;">${escZ(f.credits)}</div>
      </div>
      <div style="font-size:7px;letter-spacing:0.22em;color:${p.veryMuted};line-height:1.8;white-space:pre-wrap;">${escZ(f.note)}</div>
    </div>`;
  }
  if (it.tpl === 'quote') {
    return `<div style="${base}padding:30px 26px;display:flex;flex-direction:column;justify-content:center;text-align:center;">
      <div style="font-size:34px;line-height:0.7;color:${p.muted};">“</div>
      <div style="font-size:17px;line-height:1.36;letter-spacing:0.01em;text-wrap:balance;margin-top:6px;white-space:pre-wrap;">${escZ(f.quote)}</div>
      <div style="width:24px;height:1px;background:${p.rule};margin:16px auto;"></div>
      <div style="font-size:9px;letter-spacing:0.2em;color:${p.muted};">${escZ(f.by)}</div>
    </div>`;
  }
  if (it.tpl === 'divider') {
    return `<div style="${base}padding:28px 24px;display:flex;flex-direction:column;justify-content:center;text-align:center;">
      <div style="font-size:64px;line-height:0.9;letter-spacing:-0.02em;">${escZ(f.num)}</div>
      <div style="width:28px;height:1px;background:${p.rule};margin:18px auto;"></div>
      <div style="font-size:15px;letter-spacing:0.18em;text-transform:uppercase;line-height:1.3;white-space:pre-wrap;">${escZ(f.label)}</div>
      ${f.sub ? `<div style="font-size:9px;letter-spacing:0.16em;color:${p.muted};margin-top:10px;">${escZ(f.sub)}</div>` : ''}
    </div>`;
  }
  if (it.tpl === 'contents') {
    const rows = String(f.lines || '').split('\n').filter((s) => s.trim().length).map((line) =>
      `<div style="font-size:10px;letter-spacing:0.04em;padding:6px 0;border-bottom:1px solid ${p.rule};white-space:pre-wrap;">${escZ(line)}</div>`).join('');
    return `<div style="${base}padding:26px 22px;display:flex;flex-direction:column;">
      <div style="font-size:9px;letter-spacing:0.28em;color:${p.muted};text-transform:uppercase;margin-bottom:14px;">${escZ(f.title)}</div>
      <div>${rows}</div>
    </div>`;
  }
  return '';
}

function textUnitHTML(it, p, meta) {
  const turned = it.rot === 90;
  const groupStyle = turned
    ? `position:absolute;top:50%;left:50%;width:100cqh;height:100cqw;transform:translate(-50%,-50%) rotate(90deg);`
    : `position:absolute;inset:0;`;
  let inner;
  if (it.tpl) {
    inner = templateInnerHTML(it, p);
  } else {
    const c = textCss(it.variant, it.size);
    const a = alignParts(it.align);
    const ts = `font-size:${c.fontSize}px;line-height:${c.lineHeight};letter-spacing:${c.letterSpacing};font-weight:${c.fontWeight};text-transform:${c.textTransform};color:${p.fg};text-align:${a.ta};white-space:pre-wrap;max-width:100%;`;
    inner = `<div style="position:absolute;inset:0;padding:20px;display:flex;flex-direction:column;justify-content:${a.justify};align-items:${a.items};"><div style="${ts}">${escZ(it.text || '')}</div></div>`;
  }
  return `<div style="position:relative;flex:1;min-height:0;container-type:size;overflow:hidden;"><div style="${groupStyle}">${inner}</div></div>`;
}

function contentPanel(p, items, pageNo, srcOf, cornerInfo, meta) {
  // All pages (text or photo) render on the selected paper stock.
  const ep = p;
  const footer = cornerInfo === false ? '' :
    `<div style="position:absolute;bottom:11px;left:18px;right:18px;display:flex;justify-content:space-between;` +
    `font-size:7.5px;letter-spacing:0.22em;color:${ep.veryMuted};pointer-events:none;">` +
    `<span>${escZ(meta && meta.mark != null ? meta.mark : 'M·F·M')}</span><span>${padZ(pageNo)}</span></div>`;

  if (!items || items.length === 0) {
    return `<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:${p.veryMuted};` +
      `font-size:9px;letter-spacing:0.22em;">— ${padZ(pageNo)} —</div>`;
  }

  const single = items.length === 1;
  const render = (it) => (it.k === 'txt' ? textUnitHTML(it, ep, meta) : unitHTML(it, p, srcOf, single));
  if (single) {
    return `
      <div style="position:absolute;inset:0;padding:18px 18px 30px;display:flex;flex-direction:column;">
        ${render(items[0])}
      </div>${footer}`;
  }
  const blocks = items.map(render).join(`<div style="flex:none;height:16px;"></div>`);
  return `
    <div style="position:absolute;inset:0;padding:18px 18px 30px;display:flex;flex-direction:column;">
      ${blocks}
    </div>${footer}`;
}

// Render a single panel's inner HTML by kind. In "my own" mode all eight panels
// are plain content pages (front=page 1, interior=pages 2–7, back=page 8) so the
// maker designs every page; otherwise the covers auto-generate.
function panelHTML(panel, p, info, pages, srcOf, cornerInfo, meta) {
  if (info && info.myOwn) {
    const ri = panel.kind === 'front' ? 0 : panel.kind === 'back' ? 7 : panel.page + 1;
    return contentPanel(p, pages[ri] || [], ri + 1, srcOf, cornerInfo, meta);
  }
  if (panel.kind === 'front') return frontPanel(p, info);
  if (panel.kind === 'back') return backPanel(p, info);
  return contentPanel(p, pages[panel.page] || [], panel.page + 1, srcOf, cornerInfo, meta);
}

// Faint fold (dashed) + cut (solid) guides for the single-cut fold.
function sheetGuides(p) {
  const fold = p.veryMuted, cut = p.muted;
  let g = '';
  const vfold = (x) => `<div style="position:absolute;left:${x}px;top:0;height:${SHEET_H}px;width:0;border-left:1px dashed ${fold};opacity:0.4;pointer-events:none;"></div>`;
  const hfold = (x, w) => `<div style="position:absolute;left:${x}px;top:${CELL_H}px;width:${w}px;height:0;border-top:1px dashed ${fold};opacity:0.4;pointer-events:none;"></div>`;
  // vertical folds at the three interior columns
  g += vfold(CELL_W) + vfold(2 * CELL_W) + vfold(3 * CELL_W);
  // horizontal fold along the two outer quarters
  g += hfold(0, CELL_W) + hfold(3 * CELL_W, CELL_W);
  // the central cut (solid) spanning the middle two columns
  g += `<div style="position:absolute;left:${CELL_W}px;top:${CELL_H}px;width:${2 * CELL_W}px;height:0;border-top:1px solid ${cut};opacity:0.6;pointer-events:none;"></div>`;
  g += `<div style="position:absolute;left:${CELL_W + 5}px;top:${CELL_H - 9}px;font-size:11px;line-height:1;color:${cut};opacity:0.75;pointer-events:none;">✂</div>`;
  g += `<div style="position:absolute;left:${2 * CELL_W}px;top:${CELL_H + 5}px;transform:translateX(-50%);font-size:7px;letter-spacing:0.3em;color:${cut};opacity:0.75;pointer-events:none;">C U T</div>`;
  return g;
}

// Build the full imposed A4 sheet (one <section>).
function buildZineSheet(opts, pages, srcOf, withGuides) {
  const p = paperFor(opts.paper);
  const info = { siteTitle: opts.siteName, author: opts.byline, issue: opts.issue, date: opts.date, count: opts.count, contact: opts.contact || ZINE_CONTACT, myOwn: opts.myOwn || false, mark: opts.mark != null ? opts.mark : 'M·F·M' };
  const cells = ZINE_PANELS.map((panel) => {
    const inner = panelHTML(panel, p, info, pages, srcOf, opts.cornerInfo, info);
    const left = panel.c * CELL_W, top = panel.r * CELL_H;
    const rot = panel.rotate ? 'transform:rotate(180deg);' : '';
    return `<div style="position:absolute;left:${left}px;top:${top}px;width:${CELL_W}px;height:${CELL_H}px;overflow:hidden;${rot}">${inner}</div>`;
  }).join('');
  const guides = withGuides ? sheetGuides(p) : '';
  return (
    `<section class="zsheet" style="position:relative;width:${SHEET_W}px;height:${SHEET_H}px;background:${p.bg};color:${p.fg};` +
    `font-family:'JetBrains Mono',monospace;overflow:hidden;">${cells}${guides}</section>`
  );
}

// Print the single imposed A4 sheet (landscape); the user saves to PDF.
function printZine(opts, pages) {
  const sheet = buildZineSheet(opts, pages, absSrc, true);
  const frame = document.createElement('iframe');
  frame.setAttribute('aria-hidden', 'true');
  frame.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:0;';
  document.body.appendChild(frame);
  const doc = frame.contentWindow.document;
  doc.open();
  doc.write(
    `<!doctype html><html><head><meta charset="utf-8"><title>${escZ(opts.siteName)} — zine ${escZ(opts.issue)}</title>` +
    `<link rel="preconnect" href="https://fonts.googleapis.com">` +
    `<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">` +
    `<style>` +
    `@page{size:A4 landscape;margin:0}` +
    `*{box-sizing:border-box}` +
    `html,body{margin:0;padding:0;background:#fff;-webkit-print-color-adjust:exact;print-color-adjust:exact}` +
    `.zsheet{break-after:auto;page-break-after:auto}` +
    `</style></head><body>${sheet}</body></html>`
  );
  doc.close();
  const imgs = Array.from(doc.images || []);
  const waitImg = (im) => new Promise((res) => {
    if (im.complete) return res();
    im.onload = res; im.onerror = res;
  });
  const go = () => {
    try { frame.contentWindow.focus(); frame.contentWindow.print(); }
    finally { setTimeout(() => frame.remove(), 1500); }
  };
  Promise.all(imgs.map(waitImg)).then(() => setTimeout(go, 150));
}

// ─── Editable page (React) — each image gets hover controls: switch
// orientation (0/90) and fill/fit. Mirrors contentPanel's layout. ───
function Unit({ w, p, single, onRotate, onFit, onRemove }) {
  const [hover, setHover] = useToolState(false);
  const mobile = window.useIsMobile ? window.useIsMobile() : false;
  const turned = w._rot === 90;
  const fit = w._fit === 'contain' ? 'contain' : 'cover';
  // The image AND its caption rotate together as one group; controls stay upright.
  const groupStyle = turned
    ? { position: 'absolute', top: '50%', left: '50%', width: '100cqh', height: '100cqw', transform: 'translate(-50%,-50%) rotate(90deg)', display: 'flex', flexDirection: 'column' }
    : { position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column' };
  const ctrl = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', height: 22, minWidth: 22, padding: '0 6px', fontSize: 9.5, lineHeight: 1, letterSpacing: '0.06em', cursor: 'pointer', background: 'rgba(12,12,10,0.78)', color: '#fff', border: '1px solid rgba(255,255,255,0.32)', fontFamily: 'inherit' };
  return (
    <div
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      style={{ position: 'relative', flex: 1, minHeight: 0, containerType: 'size', overflow: 'hidden' }}
    >
      <div style={groupStyle}>
        <div style={{ flex: 1, minHeight: 0, position: 'relative', background: p.bg, overflow: 'hidden' }}>
          <img src={w.src} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: fit, display: 'block' }} />
        </div>
        <div style={{ flex: 'none', marginTop: single ? 12 : 8 }}>
          <div style={{ fontSize: single ? 18 : 14.5, lineHeight: 1.12, letterSpacing: '0.005em', color: p.fg }}>{w.title}</div>
          <div style={{ fontSize: single ? 9 : 8, letterSpacing: '0.14em', color: p.muted, marginTop: single ? 6 : 4 }}>{w.series.toUpperCase()} · {w.year}</div>
        </div>
      </div>
      {(hover || mobile) && (
        <div style={{ position: 'absolute', top: 6, right: 6, display: 'flex', gap: 5, zIndex: 2 }}>
          <button onClick={() => onRotate(w.id)} title="Switch orientation" style={ctrl}>↻</button>
          <button onClick={() => onFit(w.id)} title="Toggle fill / fit" style={ctrl}>{fit === 'cover' ? 'FILL' : 'FIT'}</button>
          <button onClick={() => onRemove(w.id)} title="Remove from page" style={ctrl}>×</button>
        </div>
      )}
    </div>
  );
}

// ─── Editable — a contentEditable text node bound to a field. Initialises its
// text once on mount and never overwrites the DOM while typing, so the caret is
// preserved; the parent keys it per item+field to re-init when selection changes.
function Editable({ value, multiline, style, onInput, placeholder }) {
  const ref = useToolRef(null);
  useToolEffect(() => {
    if (ref.current && ref.current.innerText !== (value || '')) ref.current.innerText = value || '';
    // eslint-disable-next-line
  }, []);
  return (
    <div
      ref={ref}
      contentEditable
      suppressContentEditableWarning
      spellCheck={false}
      data-ph={placeholder || ''}
      onMouseDown={(e) => e.stopPropagation()}
      onInput={(e) => onInput(e.currentTarget.innerText)}
      style={{ outline: 'none', cursor: 'text', whiteSpace: multiline ? 'pre-wrap' : 'pre', wordBreak: 'break-word', ...style }}
    />
  );
}

// React render of a designed template with each field directly editable on the
// page. Mirrors templateInnerHTML's layout (which still drives the A4 / print).
function TemplateEditView({ it, p, onField }) {
  p = p || TEXT_PAPER;
  const f = it.f || {};
  const E = (k, style, multiline, ph) => (
    <Editable value={f[k]} multiline={multiline} placeholder={ph} onInput={(v) => onField(k, v)} style={style} />
  );
  const base = { position: 'absolute', inset: 0, color: p.fg, fontFamily: "'JetBrains Mono', monospace" };
  if (it.tpl === 'front') {
    return (
      <div style={{ ...base, padding: '26px 22px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', textAlign: 'center' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 7.5, letterSpacing: '0.24em', color: p.veryMuted, textAlign: 'left' }}>
          {E('left', { fontSize: 7.5 }, false, 'LEFT')}
          {E('right', { fontSize: 7.5 }, false, 'RIGHT')}
        </div>
        <div>
          {E('kicker', { fontSize: 8, letterSpacing: '0.42em', color: p.muted, marginBottom: 18 }, false, 'KICKER')}
          {E('title', { fontSize: 26, lineHeight: 1.06, letterSpacing: '-0.01em' }, true, 'Title')}
          <div style={{ width: 34, height: 1, background: p.rule, margin: '18px auto' }} />
          {E('byline', { fontSize: 11, letterSpacing: '0.16em', color: p.muted }, false, 'Byline')}
        </div>
        <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 8.5, letterSpacing: '0.18em', color: p.muted }}>
          <span>{f.left}</span><span>{f.right}</span>
        </div>
      </div>
    );
  }
  if (it.tpl === 'back') {
    return (
      <div style={{ ...base, padding: '26px 22px', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', textAlign: 'center' }}>
        <div style={{ textAlign: 'left' }}>{E('label', { fontSize: 7.5, letterSpacing: '0.24em', color: p.veryMuted }, false, 'LABEL')}</div>
        <div>
          {E('title', { fontSize: 14, lineHeight: 1.2, letterSpacing: '0.02em' }, true, 'Title')}
          <div style={{ width: 28, height: 1, background: p.rule, margin: '14px auto' }} />
          {E('credits', { fontSize: 9.5, letterSpacing: '0.14em', color: p.muted, lineHeight: 1.9 }, true, 'Credits')}
        </div>
        {E('note', { fontSize: 7, letterSpacing: '0.22em', color: p.veryMuted, lineHeight: 1.8 }, true, 'Note')}
      </div>
    );
  }
  if (it.tpl === 'quote') {
    return (
      <div style={{ ...base, padding: '30px 26px', display: 'flex', flexDirection: 'column', justifyContent: 'center', textAlign: 'center' }}>
        <div style={{ fontSize: 34, lineHeight: 0.7, color: p.muted }}>“</div>
        {E('quote', { fontSize: 17, lineHeight: 1.36, letterSpacing: '0.01em', marginTop: 6 }, true, 'Quote')}
        <div style={{ width: 24, height: 1, background: p.rule, margin: '16px auto' }} />
        {E('by', { fontSize: 9, letterSpacing: '0.2em', color: p.muted }, false, 'Attribution')}
      </div>
    );
  }
  if (it.tpl === 'divider') {
    return (
      <div style={{ ...base, padding: '28px 24px', display: 'flex', flexDirection: 'column', justifyContent: 'center', textAlign: 'center' }}>
        {E('num', { fontSize: 64, lineHeight: 0.9, letterSpacing: '-0.02em' }, false, '01')}
        <div style={{ width: 28, height: 1, background: p.rule, margin: '18px auto' }} />
        {E('label', { fontSize: 15, letterSpacing: '0.18em', textTransform: 'uppercase', lineHeight: 1.3 }, true, 'Label')}
        {E('sub', { fontSize: 9, letterSpacing: '0.16em', color: p.muted, marginTop: 10 }, false, 'Subtitle')}
      </div>
    );
  }
  if (it.tpl === 'contents') {
    return (
      <div style={{ ...base, padding: '26px 22px', display: 'flex', flexDirection: 'column' }}>
        {E('title', { fontSize: 9, letterSpacing: '0.28em', color: p.muted, textTransform: 'uppercase', marginBottom: 14 }, false, 'HEADING')}
        {E('lines', { fontSize: 10, letterSpacing: '0.04em', lineHeight: 2.1 }, true, 'One entry per line')}
      </div>
    );
  }
  return null;
}

function TextUnit({ it, p, meta, selected, onRotate, onRemove, onSelect, onFieldEdit, onTextEdit }) {
  const [hover, setHover] = useToolState(false);
  const mobile = window.useIsMobile ? window.useIsMobile() : false;
  const turned = it.rot === 90;
  const groupStyle = turned
    ? { position: 'absolute', top: '50%', left: '50%', width: '100cqh', height: '100cqw', transform: 'translate(-50%,-50%) rotate(90deg)' }
    : { position: 'absolute', inset: 0 };
  const ctrl = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', height: 22, minWidth: 22, padding: '0 6px', fontSize: 9.5, lineHeight: 1, letterSpacing: '0.06em', cursor: 'pointer', background: 'rgba(12,12,10,0.78)', color: '#fff', border: '1px solid rgba(255,255,255,0.32)', fontFamily: 'inherit' };
  let body;
  if (it.tpl) {
    body = <TemplateEditView it={it} p={p} onField={(k, v) => onFieldEdit && onFieldEdit(it.id, k, v)} />;
  } else {
    const c = textCss(it.variant, it.size);
    const a = alignParts(it.align);
    body = (
      <div style={{ position: 'absolute', inset: 0, padding: 20, display: 'flex', flexDirection: 'column', justifyContent: a.justify, alignItems: a.items }}>
        <Editable multiline value={it.text} placeholder="Text" onInput={(v) => onTextEdit && onTextEdit(it.id, v)} style={{ ...c, color: p.fg, textAlign: a.ta, maxWidth: '100%', width: '100%' }} />
      </div>
    );
  }
  return (
    <div
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      onClick={() => onSelect && onSelect(it.id)}
      style={{ position: 'relative', flex: 1, minHeight: 0, containerType: 'size', overflow: 'hidden', cursor: 'text', outline: selected ? `2px solid ${p.fg}` : '2px solid transparent', outlineOffset: -2 }}
    >
      <div style={groupStyle}>{body}</div>
      {(hover || mobile || selected) && (
        <div style={{ position: 'absolute', top: 6, right: 6, display: 'flex', gap: 5, zIndex: 2 }}>
          <button onClick={(e) => { e.stopPropagation(); onRotate(it.id); }} title="Switch orientation" style={ctrl}>↻</button>
          <button onClick={(e) => { e.stopPropagation(); onRemove(it.id); }} title="Remove from page" style={ctrl}>×</button>
        </div>
      )}
    </div>
  );
}

function EditPage({ p, works, pageNo, cornerInfo, meta, editId, onRotate, onFit, onRemove, onTextRotate, onTextSelect, onFieldEdit, onTextEdit }) {
  // Every page renders on the selected paper stock.
  const ep = p;
  const footer = cornerInfo === false ? null : (
    <div style={{ position: 'absolute', bottom: 11, left: 18, right: 18, display: 'flex', justifyContent: 'space-between', fontSize: 7.5, letterSpacing: '0.22em', color: ep.veryMuted, pointerEvents: 'none' }}>
      <span>{meta && meta.mark != null ? meta.mark : 'M·F·M'}</span><span>{padZ(pageNo)}</span>
    </div>
  );
  if (!works || works.length === 0) {
    return (
      <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', color: p.veryMuted, fontSize: 9, letterSpacing: '0.22em' }}>— {padZ(pageNo)} —</div>
    );
  }
  const single = works.length === 1;
  return (
    <div style={{ position: 'absolute', inset: 0, background: ep.bg, padding: '18px 18px 30px', display: 'flex', flexDirection: 'column' }}>
      {works.map((it, i) => (
        <React.Fragment key={it.id}>
          {i > 0 && <div style={{ flex: 'none', height: 16 }} />}
          {it.k === 'txt'
            ? <TextUnit it={it} p={ep} meta={meta} selected={it.id === editId} onRotate={onTextRotate} onRemove={onRemove} onSelect={onTextSelect} onFieldEdit={onFieldEdit} onTextEdit={onTextEdit} />
            : <Unit w={it} p={p} single={single} onRotate={onRotate} onFit={onFit} onRemove={onRemove} />}
        </React.Fragment>
      ))}
      {footer}
    </div>
  );
}

// ─── Text & template editor (left of the page rail) ───
// Controlled: every field edits the active item in real time (no re-add needed).
function TextEditor({ t, active, canAdd, onCreate, onUpdate, onRemove, cornerInfo, setCornerInfo }) {
  const lbl = { fontSize: 8, letterSpacing: '0.2em', color: t.veryMuted, marginBottom: 6, display: 'block' };
  const opt = (a) => ({ flex: 1, padding: '6px 0', fontFamily: 'inherit', fontSize: 9, letterSpacing: '0.08em', background: a ? t.fg : 'transparent', color: a ? t.bg : t.fg, border: 'none', cursor: 'pointer', transition: 'background 120ms, color 120ms' });
  const wrap = { display: 'flex', border: `1px solid ${t.rule}` };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <div>
        <label style={lbl}>DESIGNED TEMPLATES</label>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
          {TEMPLATE_ORDER.map((key) => {
            const d = TEMPLATES[key];
            return (
              <button key={key} disabled={!canAdd} onClick={() => onCreate({ tpl: key })}
                style={{ textAlign: 'left', padding: '8px 9px', fontFamily: 'inherit', background: 'transparent', color: canAdd ? t.fg : t.veryMuted, border: `1px solid ${t.rule}`, cursor: canAdd ? 'pointer' : 'not-allowed', display: 'flex', flexDirection: 'column', gap: 3 }}
                onMouseEnter={(e) => { if (canAdd) { e.currentTarget.style.background = t.fg; e.currentTarget.style.color = t.bg; } }}
                onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = canAdd ? t.fg : t.veryMuted; }}>
                <span style={{ fontSize: 10, letterSpacing: '0.14em' }}>+ {d.label}</span>
                <span style={{ fontSize: 7.5, letterSpacing: '0.16em', opacity: 0.6 }}>{d.blurb}</span>
              </button>
            );
          })}
          <button disabled={!canAdd} onClick={() => onCreate({ plain: true })}
            style={{ textAlign: 'left', padding: '8px 9px', fontFamily: 'inherit', fontSize: 9, letterSpacing: '0.14em', background: 'transparent', color: canAdd ? t.fg : t.veryMuted, border: `1px dashed ${t.rule}`, cursor: canAdd ? 'pointer' : 'not-allowed' }}
            onMouseEnter={(e) => { if (canAdd) { e.currentTarget.style.background = t.fg; e.currentTarget.style.color = t.bg; } }}
            onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = canAdd ? t.fg : t.veryMuted; }}>+ PLAIN TEXT</button>
        </div>
        {!canAdd && <div style={{ fontSize: 8, letterSpacing: '0.14em', color: t.veryMuted, lineHeight: 1.6, marginTop: 8 }}>SELECT A PAGE (1–6) FIRST.</div>}
      </div>

      {active && (
        <div style={{ fontSize: 8, letterSpacing: '0.16em', color: t.muted, lineHeight: 1.7, display: 'flex', alignItems: 'flex-start', gap: 6, borderTop: `1px solid ${t.rule}`, paddingTop: 12 }}>
          <span style={{ fontSize: 11, lineHeight: 1, marginTop: -1 }}>✎</span>
          <span>CONTROLS ARE BESIDE THE PAGE →</span>
        </div>
      )}

      <div style={{ borderTop: `1px solid ${t.rule}`, paddingTop: 12 }}>
        <label style={lbl}>CORNER INFO</label>
        <div style={wrap}>
          <button style={opt(cornerInfo !== false)} onClick={() => setCornerInfo(true)}>ON</button>
          <button style={opt(cornerInfo === false)} onClick={() => setCornerInfo(false)}>OFF</button>
        </div>
      </div>
    </div>
  );
}

// ─── Per-item controls — floats in the empty space to the RIGHT of the page
// preview so the option stack never pushes the page down. ───
function TextControls({ t, active, onUpdate, onRemove }) {
  const cfg = window.ZINE_EDIT_PANEL || {};
  const panelW = cfg.width != null ? cfg.width : 216;
  const panelBorderW = cfg.border != null ? cfg.border : 1;
  const panelBorderColor = cfg.borderColor || t.rule;
  const panelPad = cfg.pad != null ? cfg.pad : 12;
  const lbl = { fontSize: 8, letterSpacing: '0.2em', color: t.veryMuted, marginBottom: 6, display: 'block' };
  const opt = (a) => ({ flex: 1, padding: '6px 0', fontFamily: 'inherit', fontSize: 9, letterSpacing: '0.08em', background: a ? t.fg : 'transparent', color: a ? t.bg : t.fg, border: 'none', cursor: 'pointer', transition: 'background 120ms, color 120ms' });
  const wrap = { display: 'flex', border: `1px solid ${t.rule}` };
  const variants = [['title', 'TITLE'], ['subhead', 'SUB'], ['body', 'BODY'], ['label', 'LABEL']];
  const sizes = [['s', 'S'], ['m', 'M'], ['l', 'L'], ['xl', 'XL']];
  const aligns = ['tl', 'tc', 'tr', 'ml', 'mc', 'mr', 'bl', 'bc', 'br'];
  const tpl = !!active.tpl;
  return (
    <div style={{ flex: 'none', alignSelf: 'stretch', width: panelW, display: 'flex', flexDirection: 'column', gap: 12, padding: panelPad, border: `${panelBorderW}px solid ${panelBorderColor}`, background: t.bg }}>
      <div style={{ fontSize: 8, letterSpacing: '0.16em', color: t.muted, lineHeight: 1.7, display: 'flex', alignItems: 'flex-start', gap: 6 }}>
        <span style={{ fontSize: 11, lineHeight: 1, marginTop: -1 }}>✎</span>
        <span>{tpl ? `EDITING · ${(TEMPLATES[active.tpl] || {}).label || ''}` : 'EDITING · PLAIN TEXT'}<br />CLICK ANY LINE ON THE PAGE TO TYPE.</span>
      </div>
      <div style={{ fontSize: 8.5, letterSpacing: '0.2em', color: t.muted, borderTop: `1px solid ${t.rule}`, paddingTop: 12 }}>{tpl ? 'TEMPLATE' : 'PLAIN TEXT'}</div>
      {!tpl && (
        <>
          <div><label style={lbl}>STYLE</label><div style={wrap}>{variants.map(([v, l]) => <button key={v} style={opt(active.variant === v)} onClick={() => onUpdate({ variant: v })}>{l}</button>)}</div></div>
          <div><label style={lbl}>SIZE</label><div style={wrap}>{sizes.map(([v, l]) => <button key={v} style={opt(active.size === v)} onClick={() => onUpdate({ size: v })}>{l}</button>)}</div></div>
          <div>
            <label style={lbl}>POSITION</label>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 3, width: 78 }}>
              {aligns.map((aa) => (
                <button key={aa} onClick={() => onUpdate({ align: aa })} title={aa}
                  style={{ height: 22, border: `1px solid ${t.rule}`, background: active.align === aa ? t.fg : 'transparent', cursor: 'pointer', padding: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                  <span style={{ width: 4, height: 4, borderRadius: '50%', background: active.align === aa ? t.bg : t.muted, display: 'block' }} />
                </button>
              ))}
            </div>
          </div>
        </>
      )}
      <div>
        <label style={lbl}>ORIENTATION</label>
        <div style={wrap}>
          <button style={opt((active.rot || 0) === 0)} onClick={() => onUpdate({ rot: 0 })}>0°</button>
          <button style={opt(active.rot === 90)} onClick={() => onUpdate({ rot: 90 })}>90°</button>
        </div>
      </div>
      <button onClick={onRemove}
        style={{ padding: '9px 0', fontFamily: 'inherit', fontSize: 9.5, letterSpacing: '0.16em', cursor: 'pointer', background: 'transparent', color: t.fg, border: `1px solid ${t.rule}` }}>× REMOVE</button>
    </div>
  );
}

// ─── Zine Generator — renders INLINE in place of the photo viewing area ───
function ZineOverlay({ theme, collection, photographerName, siteName, onClose, inline, indexOpen, onRequestIndex, placement, setPlacement, imgOpts, setImgOpts, cornerInfo, setCornerInfo, about, myOwn, tapPlace }) {
  const t = window.THEMES[theme];
  const mobile = window.useIsMobile ? window.useIsMobile() : false;
  const today = useToolMemo(() => {
    const d = new Date();
    return `${ZINE_MONTHS[d.getMonth()]} ${d.getFullYear()}`;
  }, []);

  // Back-cover contact mirrors the site's ABOUT page (falls back to ZINE_CONTACT).
  const aboutContact = useToolMemo(() => aboutToZineContact(about), [about]);

  const [mine, setMine] = useToolState(readMine);
  const issueNo = mine + 1;
  const [zineTotal, setZineTotal] = useToolState(null);
  const [archiveTotal, setArchiveTotal] = useToolState(null);
  useToolEffect(() => {
    let alive = true;
    if (window.MU && window.MU.getZineTotals) {
      window.MU.getZineTotals().then((v) => {
        if (!alive) return;
        setZineTotal(v.total); setArchiveTotal(v.archive);
      }).catch(() => {});
    }
    return () => { alive = false; };
  }, []);
  const [paper, setPaper] = useToolState('blank');
  const [view, setView] = useToolState('pages'); // 'pages' (edit) | 'sheet' (preview)
  const [dragOverPage, setDragOverPage] = useToolState(null);
  const [sel, setSel] = useToolState(0); // editor selection: 'F' | 0..5 | 'B'
  const [textMode, setTextMode] = useToolState(false);
  const [editId, setEditId] = useToolState(null); // text item being edited

  // In "my own" mode pages 1–8 are all plain content pages (the maker builds
  // their own covers with the text editor + templates). Reset selection on toggle.
  useToolEffect(() => { setSel(0); setEditId(null); }, [myOwn]);

  // Auto-fill: every time the archive zine opens, seed the six interior pages
  // with one randomly-chosen series from the collection (grouped by the primary
  // series key — text before the first "·"). Runs once per open; the maker can
  // then rearrange, swap, or clear from there. Skipped in "my own" mode, which
  // is built from the maker's own uploaded images.
  const didAutoFill = useToolRef(false);
  useToolEffect(() => {
    if (myOwn || didAutoFill.current) return;
    didAutoFill.current = true;
    if (!collection || !collection.length) return;
    const groups = {};
    collection.forEach(w => {
      const key = String(w.series || 'Untitled').split('·')[0].trim();
      (groups[key] = groups[key] || []).push(w);
    });
    const keys = Object.keys(groups);
    if (!keys.length) return;
    const pick = keys[Math.floor(Math.random() * keys.length)];
    const { pages: grouped } = paginateWorks(groups[pick]);
    setPlacement(grouped.map(g => g.map(w => ({ k: 'img', id: w.id }))));
  }, []);

  const issueLabel = 'N°' + padZ(issueNo);

  // Resolve placed ids → live work objects.
  const byId = useToolMemo(() => {
    const m = {}; collection.forEach(w => { m[w.id] = w; }); return m;
  }, [collection]);

  const placeArr = placement || EMPTY_PLACEMENT;
  const optMap = imgOpts || EMPTY_OPTS;
  const pages = useToolMemo(
    () => placeArr.map(items => items.map(it => {
      if (it.k === 'txt') return it;
      const w = byId[it.id];
      if (!w) return null;
      return { k: 'img', ...w, _rot: (optMap[it.id] && optMap[it.id].rot) || 0, _fit: (optMap[it.id] && optMap[it.id].fit) || 'cover' };
    }).filter(Boolean)),
    [placeArr, byId, optMap]
  );
  const placed = useToolMemo(() => pages.reduce((n, g) => n + g.length, 0), [pages]);
  const activeItem = (typeof sel === 'number') ? ((pages[sel] || []).find(it => it.k === 'txt' && it.id === editId) || null) : null;

  // Drop a work onto a page. A page holds up to two items (images or text).
  const placeWork = (pageIdx, workId) => {
    if (!byId[workId]) return;
    setPlacement(prev => {
      const base = (prev || EMPTY_PLACEMENT).map(a => a.filter(it => it.id !== workId)); // de-dupe images globally
      const cur = base[pageIdx];
      const item = { k: 'img', id: workId };
      if (cur.length === 0) base[pageIdx] = [item];
      else if (cur.length === 1) base[pageIdx] = [cur[0], item];   // pair two-up
      else base[pageIdx] = [item];                                 // already full → reset
      return base;
    });
  };
  const removeWork = (pageIdx, itemId) => {
    setPlacement(prev => (prev || EMPTY_PLACEMENT).map((a, i) => i === pageIdx ? a.filter(it => it.id !== itemId) : a));
  };
  const createText = (pageIdx, preset) => {
    const id = 't' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
    const meta = { siteTitle: siteName, author: photographerName, issue: issueLabel, date: today, contact: aboutContact };
    let draft, fullPage = false;
    if (preset.tpl) {
      draft = { tpl: preset.tpl, f: TEMPLATES[preset.tpl].seed(meta), rot: 0 };
      fullPage = true; // designed templates occupy the whole page
    } else {
      draft = { text: 'Text', variant: 'title', size: 'm', align: 'mc', rot: 0 };
    }
    setPlacement(prev => {
      const base = (prev || EMPTY_PLACEMENT).map(a => a.slice());
      const item = { k: 'txt', id, ...draft };
      const cur = base[pageIdx];
      base[pageIdx] = fullPage ? [item] : (cur.length >= 2 ? [item] : [...cur, item]);
      return base;
    });
    setEditId(id);
  };
  const updateText = (pageIdx, itemId, patch) => {
    setPlacement(prev => (prev || EMPTY_PLACEMENT).map((a, i) => i === pageIdx ? a.map(it => it.id === itemId ? { ...it, ...patch } : it) : a));
  };
  const clearAll = () => setPlacement(prev => (prev || EMPTY_PLACEMENT).map(() => []));
  const rotateWork = (workId) => {
    if (!setImgOpts) return;
    setImgOpts(prev => { const m = prev || {}; const o = m[workId] || {}; return { ...m, [workId]: { ...o, rot: o.rot === 90 ? 0 : 90 } }; });
  };
  const toggleFit = (workId) => {
    if (!setImgOpts) return;
    setImgOpts(prev => { const m = prev || {}; const o = m[workId] || {}; return { ...m, [workId]: { ...o, fit: o.fit === 'contain' ? 'cover' : 'contain' } }; });
  };

  // Touch tap-to-place: when the host bumps tapPlace.n, drop that work onto the
  // currently-selected page (covers fall back to the first interior page).
  const lastTapRef = useToolRef(0);
  useToolEffect(() => {
    if (!tapPlace || !tapPlace.id || tapPlace.n === lastTapRef.current) return;
    lastTapRef.current = tapPlace.n;
    const target = (typeof sel === 'number') ? sel : 0;
    if (typeof sel !== 'number') setSel(0);
    placeWork(target, tapPlace.id);
  }, [tapPlace && tapPlace.n]);

  const zineMark = myOwn ? '' : 'M·F·M';
  const opts = { siteName, byline: photographerName, issue: issueLabel, date: today, paper, count: placed, cornerInfo, contact: aboutContact, myOwn, mark: zineMark };
  const info = { siteTitle: siteName, author: photographerName, issue: issueLabel, date: today, count: placed, contact: aboutContact, myOwn, mark: zineMark };

  const sheetHTML = useToolMemo(
    () => buildZineSheet(opts, pages, (s) => s, true),
    [pages, paper, issueLabel, today, placed, siteName, photographerName, cornerInfo, aboutContact, myOwn]
  );

  useToolEffect(() => {
    function onKey(e) { if (e.key === 'Escape') onClose(); }
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [onClose]);

  const handlePrint = () => {
    if (placed === 0) return;
    printZine(opts, pages);
    const nextMine = mine + 1;
    localStorage.setItem(ZINE_MINE_KEY, String(nextMine));
    setMine(nextMine);
    const isArchive = !myOwn; // built from the owner's archive photos
    if (window.MU && window.MU.bumpZineTotals) {
      window.MU.bumpZineTotals(isArchive).then((v) => {
        setZineTotal(v.total); setArchiveTotal(v.archive);
      }).catch(() => {});
    } else {
      setZineTotal((n) => (n == null ? null : n + 1));
      if (isArchive) setArchiveTotal((n) => (n == null ? null : n + 1));
    }
  };

  const labelStyle = { fontSize: 9, letterSpacing: '0.2em', color: t.veryMuted, marginBottom: 7, display: 'block' };
  const seg = (active) => ({
    flex: 1, padding: '8px 6px', fontFamily: 'inherit', fontSize: 10, letterSpacing: '0.12em',
    background: active ? t.fg : 'transparent', color: active ? t.bg : t.fg,
    border: 'none', cursor: 'pointer', transition: 'background 140ms, color 140ms',
  });
  const segWrap = { display: 'flex', border: `1px solid ${t.rule}` };
  const segBtn = { flex: 'none', width: mobile ? 78 : 104, padding: '7px 0', textAlign: 'center' };

  const canPrint = placed > 0;
  const vw = window.useViewportWidth ? window.useViewportWidth() : 1280;
  // On mobile the A4 sheet is landscape, so we rotate the preview 90° to fill the
  // narrow screen: scale to fit SHEET_H (the post-rotation visual width).
  const SHEET_S = mobile ? Math.min(0.62, (vw - 36) / SHEET_H) : 0.6; // imposed-sheet preview scale
  const EDIT_S = mobile ? Math.min(1.05, (vw - 40) / CELL_W) : 1.05; // single editable page scale
  const sheetBoxW = mobile ? SHEET_H * SHEET_S : SHEET_W * SHEET_S;
  const sheetBoxH = mobile ? SHEET_W * SHEET_S : SHEET_H * SHEET_S;

  const outerStyle = inline
    ? {
        flex: '1 0 auto',
        background: t.bg,
        display: 'flex', flexDirection: 'column',
        overflowY: 'auto',
        paddingRight: (indexOpen && !mobile) ? 460 : 0,
        fontFamily: '"JetBrains Mono", monospace',
        transition: 'padding 360ms cubic-bezier(.2,.8,.2,1)',
      }
    : {
        position: 'fixed', inset: 0, zIndex: 60,
        background: theme === 'dark' ? 'rgba(8,8,6,0.92)' : 'rgba(232,230,223,0.94)',
        backdropFilter: 'blur(6px)', WebkitBackdropFilter: 'blur(6px)',
        display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
        overflowY: 'auto', padding: '40px 24px',
        fontFamily: '"JetBrains Mono", monospace',
      };

  return (
    <div style={outerStyle}>
      <div style={{
        width: '100%',
        background: t.bg, color: t.fg,
        display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0,
      }}>
        {/* header — actions live here so the panel has no bottom bar (keeps height compact, no page resize) */}
        <div style={{
          display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: mobile ? 10 : 16,
          padding: mobile ? '12px 14px' : '16px 56px 16px 28px', borderBottom: `1px solid ${t.rule}`,
        }}>
          <div>
            <div style={{ fontSize: 9, letterSpacing: '0.2em', color: t.veryMuted }}>WORKSHOP · TOOL 01</div>
            <div style={{ fontSize: mobile ? 13 : 15, letterSpacing: '0.06em', marginTop: 4 }}>ZINE GENERATOR</div>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: mobile ? 8 : 16 }}>
            {!mobile && (
            <span style={{ fontSize: 9.5, letterSpacing: '0.08em', color: t.veryMuted, textAlign: 'right', maxWidth: 300, lineHeight: 1.5 }}>
              {canPrint ? `1 A4 SHEET · ${issueLabel} · OPENS PRINT DIALOG — “SAVE AS PDF”` : (myOwn ? 'YOUR IMAGES · UPLOAD VIA INDEX → MY OWN, THEN DRAG ONTO A PAGE' : 'DRAG WORKS ONTO A PAGE TO BEGIN')}
            </span>
            )}
            <button
              onClick={handlePrint}
              disabled={!canPrint}
              style={{
                background: canPrint ? t.fg : 'transparent',
                color: canPrint ? t.bg : t.veryMuted,
                border: `1px solid ${t.rule}`,
                padding: mobile ? '8px 12px' : '9px 18px', fontFamily: 'inherit', fontSize: mobile ? 10 : 11, letterSpacing: mobile ? '0.1em' : '0.16em',
                cursor: canPrint ? 'pointer' : 'not-allowed', whiteSpace: 'nowrap',
                transition: 'background 160ms, color 160ms',
              }}
            >{mobile ? '↧ PDF' : '↧ PRINT / EXPORT PDF'}</button>
            <button
              onClick={onClose}
              style={{ background: 'transparent', border: `1px solid ${t.rule}`, color: t.fg, width: 30, height: 30, flex: 'none', cursor: 'pointer', fontSize: 15, lineHeight: 1 }}
              aria-label="Close"
            >×</button>
          </div>
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
          {/* single column — preview fills the width; controls live in the toolbar + bottom bar */}
          <div style={{
            display: 'flex', flexDirection: 'column', alignItems: 'center',
            padding: mobile ? 14 : 24, background: theme === 'dark' ? 'rgba(255,255,255,0.015)' : 'rgba(0,0,0,0.02)',
            flex: 1, minHeight: 0, overflowY: 'auto',
          }}>
            <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 14, flexWrap: 'wrap', marginBottom: 18 }}>
              <div style={segWrap}>
                <button style={{ ...seg(view === 'pages'), ...segBtn }} onClick={() => setView('pages')}>PAGES · EDIT</button>
                <button style={{ ...seg(view === 'sheet'), ...segBtn }} onClick={() => setView('sheet')}>A4 SHEET</button>
              </div>
              <div style={segWrap}>
                <button style={{ ...seg(paper === 'cream'), ...segBtn }} onClick={() => setPaper('cream')}>CREAM</button>
                <button style={{ ...seg(paper === 'ink'), ...segBtn }} onClick={() => setPaper('ink')}>INK</button>
                <button style={{ ...seg(paper === 'blank'), ...segBtn }} onClick={() => setPaper('blank')}>BLANK</button>
              </div>
            </div>

            {view === 'sheet' ? (
              <>
                <div style={{ width: sheetBoxW, height: sheetBoxH, position: 'relative', flex: 'none', boxShadow: theme === 'dark' ? '0 30px 70px rgba(0,0,0,0.6)' : '0 30px 70px rgba(0,0,0,0.18)' }}>
                  <div
                    style={mobile
                      ? { position: 'absolute', top: '50%', left: '50%', width: SHEET_W, height: SHEET_H, transform: `translate(-50%,-50%) rotate(90deg) scale(${SHEET_S})`, transformOrigin: 'center' }
                      : { position: 'absolute', top: 0, left: 0, width: SHEET_W, height: SHEET_H, transform: `scale(${SHEET_S})`, transformOrigin: 'top left' }}
                    dangerouslySetInnerHTML={{ __html: sheetHTML }}
                  />
                </div>
                <div style={{ marginTop: 16, fontSize: 8.5, letterSpacing: '0.14em', color: t.veryMuted, textAlign: 'center', lineHeight: 1.7, maxWidth: 460 }}>
                  ONE A4, SINGLE-SIDED · TOP ROW PRINTS INVERTED BY DESIGN<br />
                  FOLD INTO EIGHTHS · CUT THE CENTRE SLIT · FOLD INTO A BOOKLET
                </div>
              </>
            ) : (
              <div style={{ display: 'flex', flexDirection: mobile ? 'column' : 'row', gap: 16, alignItems: 'center', justifyContent: 'center', width: '100%' }}>
                {/* text editor column */}
                <div style={{ flex: 'none', display: 'flex', flexDirection: 'column', gap: 10, width: (textMode && !mobile) ? 172 : 'auto', order: mobile ? 3 : 0 }}>
                  <button onClick={() => setTextMode(v => !v)}
                    style={{ alignSelf: 'flex-start', padding: '7px 12px', fontFamily: 'inherit', fontSize: 10, letterSpacing: '0.14em', background: textMode ? t.fg : 'transparent', color: textMode ? t.bg : t.fg, border: `1px solid ${t.rule}`, cursor: 'pointer' }}>
                    {textMode ? 'TEXT ✕' : '+ TEXT'}
                  </button>
                  {textMode && (
                    <TextEditor t={t}
                      canAdd={typeof sel === 'number'}
                      active={activeItem}
                      onCreate={(pr) => { if (typeof sel === 'number') createText(sel, pr); }}
                      onUpdate={(patch) => { if (typeof sel === 'number' && editId) updateText(sel, editId, patch); }}
                      onRemove={() => { if (typeof sel === 'number' && editId) { removeWork(sel, editId); setEditId(null); } }}
                      cornerInfo={cornerInfo} setCornerInfo={setCornerInfo} />
                  )}
                </div>

                {/* page rail: F · 1–6 · B */}
                <div style={{ display: 'flex', flexDirection: mobile ? 'row' : 'column', flexWrap: mobile ? 'wrap' : 'nowrap', justifyContent: 'center', gap: 6, flex: 'none', order: mobile ? 1 : 0 }}>
                  {(myOwn ? [0, 1, 2, 3, 4, 5, 6, 7] : ['F', 0, 1, 2, 3, 4, 5, 'B']).map((k) => {
                    const active = sel === k;
                    const cover = k === 'F' || k === 'B';
                    const label = cover ? k : String(k + 1);
                    return (
                      <button key={String(k)} onClick={() => setSel(k)}
                        title={cover ? (k === 'F' ? 'Front cover' : 'Back cover') : `Page ${k + 1}`}
                        style={{ width: 30, height: 30, flex: 'none', fontFamily: 'inherit', fontSize: 11, letterSpacing: '0.04em',
                          background: active ? t.fg : 'transparent', color: active ? t.bg : (cover ? t.muted : t.fg),
                          border: `1px solid ${t.rule}`, cursor: 'pointer', transition: 'background 140ms, color 140ms' }}>{label}</button>
                    );
                  })}
                </div>

                {/* selected page */}
                <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flex: 'none', order: mobile ? 2 : 0 }}>
                  {typeof sel === 'number' ? (
                    <>
                      <div
                        onClick={(e) => {
                          // Mobile: tapping the page opens the INDEX so the maker can
                          // pick an image. Skip in text mode (taps are for editing text)
                          // and ignore taps on an existing image's controls.
                          if (!mobile || indexOpen || textMode) return;
                          if (e.target.closest('button')) return;
                          onRequestIndex && onRequestIndex();
                        }}
                        onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; if (dragOverPage !== sel) setDragOverPage(sel); }}
                        onDragLeave={() => setDragOverPage(v => (v === sel ? null : v))}
                        onDrop={(e) => { e.preventDefault(); const id = e.dataTransfer.getData('text/zine-work'); setDragOverPage(null); if (id) placeWork(sel, id); }}
                        style={{ width: CELL_W * EDIT_S, height: CELL_H * EDIT_S, position: 'relative', flex: 'none',
                          border: `1px solid ${dragOverPage === sel ? t.fg : t.rule}`,
                          outline: dragOverPage === sel ? `2px solid ${t.fg}` : '2px solid transparent', outlineOffset: -1,
                          boxShadow: theme === 'dark' ? '0 20px 50px rgba(0,0,0,0.5)' : '0 20px 50px rgba(0,0,0,0.14)',
                          transition: 'border-color 120ms, outline-color 120ms' }}>
                        <div style={{ position: 'absolute', top: 0, left: 0, width: CELL_W, height: CELL_H, transform: `scale(${EDIT_S})`, transformOrigin: 'top left' }}>
                          <EditPage p={paperFor(paper)} works={pages[sel]} pageNo={sel + 1} cornerInfo={cornerInfo} meta={info} editId={editId} onRotate={rotateWork} onFit={toggleFit} onRemove={(id) => removeWork(sel, id)} onTextRotate={(id) => { const it = pages[sel].find(x => x.id === id); updateText(sel, id, { rot: it && it.rot === 90 ? 0 : 90 }); }} onTextSelect={setEditId} onFieldEdit={(id, key, val) => { const it = pages[sel].find(x => x.id === id); updateText(sel, id, { f: { ...(it && it.f || {}), [key]: val } }); }} onTextEdit={(id, val) => updateText(sel, id, { text: val })} />
                        </div>
                        {dragOverPage === sel && (
                          <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: theme === 'dark' ? 'rgba(235,231,220,0.12)' : 'rgba(10,10,10,0.06)', fontSize: 11, letterSpacing: '0.24em', color: t.fg, pointerEvents: 'none' }}>DROP</div>
                        )}
                      </div>
                      <div style={{ minHeight: 14, marginTop: 12, textAlign: 'center', fontSize: 8.5, letterSpacing: '0.14em', color: t.veryMuted }}>
                        {pages[sel].length === 0 ? (mobile ? 'TAP THE PAGE TO PICK AN IMAGE' : 'DROP A WORK HERE') : (mobile ? 'TAP AN IMAGE’S ↻ / FILL / × TO EDIT' : 'HOVER AN IMAGE TO ROTATE · FILL / FIT · REMOVE')}
                      </div>
                    </>
                  ) : (
                    <>
                      <div style={{ width: CELL_W * EDIT_S, height: CELL_H * EDIT_S, position: 'relative', flex: 'none', boxShadow: theme === 'dark' ? '0 20px 50px rgba(0,0,0,0.5)' : '0 20px 50px rgba(0,0,0,0.14)' }}>
                        <div style={{ position: 'absolute', top: 0, left: 0, width: CELL_W, height: CELL_H, transform: `scale(${EDIT_S})`, transformOrigin: 'top left', background: paperFor(paper).bg, color: paperFor(paper).fg, border: `1px solid ${paperFor(paper).rule}` }}
                          dangerouslySetInnerHTML={{ __html: sel === 'F' ? frontPanel(paperFor(paper), info) : backPanel(paperFor(paper), info) }} />
                      </div>
                      <div style={{ minHeight: 14, marginTop: 12, textAlign: 'center', fontSize: 8.5, letterSpacing: '0.14em', color: t.veryMuted }}>
                        {sel === 'F' ? 'FRONT COVER · GENERATED AUTOMATICALLY' : 'BACK COVER · GENERATED AUTOMATICALLY'}
                      </div>
                    </>
                  )}
                </div>

                {/* per-item controls — float in the empty space right of the page */}
                {textMode && typeof sel === 'number' && activeItem && (
                  <div style={{ order: mobile ? 4 : 0, flex: 'none' }}>
                  <TextControls t={t} active={activeItem}
                    onUpdate={(patch) => { if (editId) updateText(sel, editId, patch); }}
                    onRemove={() => { if (editId) { removeWork(sel, editId); setEditId(null); } }} />
                  </div>
                )}
              </div>
            )}
          </div>

          {/* info bar — relocated from the removed left column */}
          <div style={{ borderTop: `1px solid ${t.rule}`, padding: '16px 24px 18px', display: 'flex', flexWrap: 'wrap', gap: '14px 32px', alignItems: 'flex-start', justifyContent: 'space-between' }}>
            <div style={{ minWidth: 0 }}>
              <div style={{ fontSize: 8.5, letterSpacing: '0.22em', color: t.veryMuted, marginBottom: 7 }}>{myOwn ? 'ZINE' : 'COVER'}</div>
              <div style={{ fontSize: 12.5, letterSpacing: '0.02em', lineHeight: 1.3 }}>{myOwn ? 'YOUR ZINE' : siteName}</div>
              <div style={{ fontSize: 9.5, letterSpacing: '0.1em', color: t.muted, marginTop: 5 }}>{(myOwn ? 'PAGES 1–8' : photographerName)} · {issueLabel} · {today}</div>
              <div style={{ fontSize: 7.5, letterSpacing: '0.14em', color: t.veryMuted, marginTop: 8, lineHeight: 1.6, maxWidth: 360 }}>
                {myOwn ? 'EVERY PAGE IS YOURS · ADD TITLES & CONTACT WITH + TEXT, DRAG YOUR IMAGES FROM THE INDEX.' : 'COVERS ARE AUTOMATIC · ISSUE NUMBER ADVANCES ON EACH PRINT / EXPORT.'}
              </div>
            </div>
            <div style={{ textAlign: 'right' }}>
              <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, justifyContent: 'flex-end' }}>
                <span style={{ fontSize: 9, letterSpacing: '0.2em', color: t.veryMuted }}>PAGES · {padZ(placed)} PLACED</span>
                {placed > 0 && (
                  <button onClick={clearAll} style={{ background: 'none', border: 'none', color: t.muted, cursor: 'pointer', fontFamily: 'inherit', fontSize: 8.5, letterSpacing: '0.14em', textDecoration: 'underline', padding: 0 }}>CLEAR ALL</button>
                )}
              </div>
              <div style={{ fontSize: 9, letterSpacing: '0.06em', color: t.muted, lineHeight: 1.7, marginTop: 8, maxWidth: 320, marginLeft: 'auto' }}>
                DRAG WORKS FROM THE <strong style={{ color: t.fg, fontWeight: 'normal' }}>INDEX</strong> ONTO A PAGE. UP TO TWO IMAGES PER PAGE.
              </div>
              <div style={{ display: 'flex', gap: 20, justifyContent: 'flex-end', marginTop: 12, paddingTop: 10, borderTop: `1px solid ${t.rule}`, fontSize: 9, letterSpacing: '0.16em', color: t.muted, whiteSpace: 'nowrap' }}>
                <span>YOUR ZINES · <strong style={{ color: t.fg, fontWeight: 'normal' }}>{archiveTotal == null ? '—' : archiveTotal.toLocaleString()}</strong></span>
                <span style={{ color: t.veryMuted }}>ALL MAKERS · <strong style={{ color: t.fg, fontWeight: 'normal' }}>{zineTotal == null ? '—' : zineTotal.toLocaleString()}</strong></span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Tools layer — tab + drawer; the zine now renders inline in the page,
// so opening it is delegated to the host (MuseumLanding) via onOpenZine. ───
function ToolsLayer({ theme, open, onToggle, onClose, onOpenZine }) {
  return (
    <>
      <ToolsTab theme={theme} open={open} onClick={onToggle} />
      <ToolsDrawer
        open={open}
        onClose={onClose}
        theme={theme}
        onOpenZine={onOpenZine}
      />
    </>
  );
}

window.ToolsLayer = ToolsLayer;
window.ZineOverlay = ZineOverlay;
