/* Pieces shared by the client report page, the admin view and the agency
   page.

   Both audiences render findings, sections, the action plan, the roadmap,
   the "beyond the website" cards and the proposal the same way — a finding
   looks identical whether a client is reading it or Adi is. Kept in one file
   so they cannot drift apart, and so neither page mounts a second React
   root.  */

function reportApiBase() {
  const tag = document.querySelector('meta[name="lojo-api-base"]');
  return ((tag && tag.content) || '').replace(/\/+$/, '');
}

/* fetch() that always yields something readable. A crashed function answers
   with HTML; res.json() on that throws and the message would have said
   nothing. Read as text first and keep the status. */
async function apiCall(path, options) {
  const res = await fetch(reportApiBase() + path, options);
  const raw = await res.text();
  let data = null;
  try { data = JSON.parse(raw); } catch (e) { /* not JSON */ }
  if (!data) {
    data = { ok: false, error: 'not-json', status: res.status,
      message: `The server answered HTTP ${res.status} without JSON` +
        (res.status === 404 ? ' — the API function is not deployed.' : ' — it may have crashed.') };
  }
  data.status = res.status;
  return data;
}

const SEV_TONE = { crit: 'crit', warn: 'warn', ok: 'good' };
const CATEGORY_LABEL = { technical: 'Technical', content: 'Content', entity: 'Identity', authority: 'Authority', measurement: 'Measurement' };
const BUCKET_LABEL = { P0: 'Fix now', P1: 'This quarter', P2: 'Later' };
const BUCKET_TONE = { P0: 'crit', P1: 'warn', P2: 'good' };
const WINDOW_LABEL = { '30': 'First 30 days', '60': 'Days 31–60', '90': 'Days 61–90' };
const OWNER_LABEL = { developer: 'Developer', marketing: 'Marketing', founder: 'Founder', agency: 'Agency' };
const AREA_LABEL = { 'ai-command-centre': 'AI command centre', 'e-commerce': 'E-commerce', crm: 'CRM',
  'customer-support': 'Customer support', sales: 'Sales', 'content-ops': 'Content ops', analytics: 'Analytics' };
const EFFORT_LABEL = { S: 'hours', M: 'days', L: 'weeks' };

function Finding({ f }) {
  const tone = SEV_TONE[f.severity] || 'warn';
  return (
    <div style={{ display: 'flex', gap: 12, alignItems: 'flex-start', padding: '14px 16px',
      borderRadius: 'var(--r-md)', background: `var(--${tone}-bg)`,
      border: '1px solid color-mix(in oklab, var(--' + tone + ') 26%, transparent)' }}>
      <span style={{ width: 9, height: 9, borderRadius: '50%', background: `var(--${tone})`,
        marginTop: 6, flex: '0 0 auto', boxShadow: `0 0 10px var(--${tone})` }}></span>
      <div style={{ minWidth: 0 }}>
        <div style={{ fontSize: 15, fontWeight: 600, lineHeight: 1.4 }}>{f.title}</div>
        {f.detail &&
        <div style={{ fontSize: 14, color: 'var(--fg-muted)', lineHeight: 1.6, marginTop: 6 }}>{f.detail}</div>}
        {f.fix &&
        <div style={{ fontSize: 13.5, color: 'var(--fg-muted)', lineHeight: 1.6, marginTop: 8 }}>
            <span className="mono" style={{ fontSize: 11, color: 'var(--accent)' }}>FIX · </span>{f.fix}
          </div>}
      </div>
    </div>);
}

function StatTile({ s }) {
  const tone = s.tone ? `var(--${s.tone})` : 'var(--fg)';
  return (
    <div style={{ padding: '16px 18px', borderRadius: 'var(--r-lg)', background: 'var(--navy-2)',
      border: '1px solid var(--line)' }}>
      <div className="mono" style={{ fontSize: 10.5, letterSpacing: '0.09em', textTransform: 'uppercase',
        color: 'var(--fg-subtle)' }}>{s.label}</div>
      <div style={{ fontFamily: 'var(--font-display)', fontSize: 30, fontWeight: 600, color: tone, marginTop: 6, lineHeight: 1 }}>{s.value}</div>
      {s.note && <div style={{ fontSize: 12.5, color: 'var(--fg-muted)', marginTop: 5 }}>{s.note}</div>}
    </div>);
}

/* Section bodies come back as paragraphs separated by blank lines. */
function Paragraphs({ text, style }) {
  const parts = String(text || '').split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
  return (
    <React.Fragment>
      {parts.map((p, i) =>
      <p key={i} style={{ fontSize: 16, color: 'var(--fg-muted)', lineHeight: 1.65, margin: '0 0 16px', maxWidth: 720, whiteSpace: 'pre-line', ...style }}>{p}</p>)}
    </React.Fragment>);
}

function Section({ s }) {
  return (
    <div>
      <h2 className="display" style={{ fontSize: 'clamp(22px,3.4vw,30px)', margin: '0 0 12px' }}>{s.heading}</h2>
      {s.body && <div style={{ marginBottom: 8 }}><Paragraphs text={s.body} /></div>}
      {s.findings && s.findings.length > 0 &&
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {s.findings.map((f, i) => <Finding key={i} f={f} />)}
        </div>}
    </div>);
}

function TonePill({ tone, children }) {
  return (
    <span className="mono" style={{ fontSize: 10.5, padding: '3px 8px', borderRadius: 'var(--r-pill)',
      border: '1px solid color-mix(in oklab, var(--' + tone + ') 40%, transparent)',
      color: `var(--${tone})`, whiteSpace: 'nowrap' }}>{children}</span>);
}

function textButton(tone) {
  return { background: 'transparent', border: '1px solid ' + (tone ? `var(--${tone})` : 'var(--line-strong)'),
    borderRadius: 'var(--r-pill)', color: tone ? `var(--${tone})` : 'var(--fg-muted)',
    fontFamily: 'var(--font-sans)', fontSize: 13, padding: '7px 14px', cursor: 'pointer' };
}

function CopyText({ text, label, tone }) {
  const [done, setDone] = React.useState(false);
  const copy = async () => {
    try { await navigator.clipboard.writeText(text); setDone(true); setTimeout(() => setDone(false), 1500); }
    catch (e) { window.prompt('Copy this:', text); }
  };
  return <button onClick={copy} style={textButton(tone)}>{done ? 'Copied' : (label || 'Copy')}</button>;
}

function downloadText(filename, text, type) {
  const blob = new Blob([text], { type: type || 'text/plain;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = filename; document.body.appendChild(a); a.click();
  setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0);
}

/* ---------------------------------------------------------- the action plan */

function actionsToMarkdown(actions, title) {
  const lines = [`# ${title || 'Action plan'}`, ''];
  for (const w of ['30', '60', '90']) {
    const mine = actions.filter((a) => a.window === w);
    if (!mine.length) continue;
    lines.push(`## ${WINDOW_LABEL[w]}`, '');
    for (const a of mine) {
      lines.push(`- [${a.manual_done || a.status === 'verified' ? 'x' : ' '}] **${a.id} · ${a.title}** — ${BUCKET_LABEL[a.bucket]} · ${CATEGORY_LABEL[a.category]} · ${OWNER_LABEL[a.owner]} · ${a.effort} effort, ${a.impact} impact`);
      if (a.why) lines.push(`  - Why: ${a.why}`);
      if (a.how) lines.push(`  - How: ${a.how}`);
      if (a.pages && a.pages.length) lines.push(`  - Pages: ${a.pages.join(', ')}`);
      if (a.done_when) lines.push(`  - Done when: ${a.done_when}`);
      if (a.evidence && a.evidence.detail) lines.push(`  - Last check (${String(a.evidence.checked_at || '').slice(0, 10)}): ${a.status} — ${a.evidence.detail}`);
    }
    lines.push('');
  }
  return lines.join('\n');
}

function csvCell(v) {
  const s = String(v == null ? '' : v);
  return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
}

/* Columns Asana, Trello, Jira, Notion and Linear all take on import. */
function actionsToCsv(actions) {
  const header = ['Name', 'Description', 'Priority', 'Due window', 'Category', 'Assignee role', 'Effort', 'Impact', 'Pages', 'Done when'];
  const rows = actions.map((a) => [
    `${a.id} ${a.title}`, [a.why, a.how].filter(Boolean).join(' — '), BUCKET_LABEL[a.bucket],
    WINDOW_LABEL[a.window], CATEGORY_LABEL[a.category], OWNER_LABEL[a.owner],
    EFFORT_LABEL[a.effort] || a.effort, a.impact, (a.pages || []).join(' '), a.done_when]);
  return [header, ...rows].map((r) => r.map(csvCell).join(',')).join('\n');
}

function readDone(slug) {
  try { return JSON.parse(localStorage.getItem('lojo.done.' + slug) || '{}'); } catch (e) { return {}; }
}
function writeDone(slug, done) {
  try { localStorage.setItem('lojo.done.' + slug, JSON.stringify(done)); } catch (e) { /* private mode */ }
}

function ActionCard({ a, done, onToggle, deliverables }) {
  const [open, setOpen] = React.useState(false);
  const hasVerify = typeof VerifyLine === 'function' && (a.verify || a.status);
  const files = deliverables || [];
  return (
    <div style={{ borderRadius: 'var(--r-md)', background: 'var(--navy-2)', border: '1px solid var(--line)',
      padding: '14px 16px', opacity: done ? 0.55 : 1 }}>
      <div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
        <input type="checkbox" checked={!!done} onChange={onToggle} aria-label={`Done: ${a.title}`}
          style={{ marginTop: 4, width: 16, height: 16, accentColor: 'var(--accent)', flex: '0 0 auto' }} />
        <div style={{ minWidth: 0, flex: 1 }}>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', marginBottom: 4 }}>
            <span className="mono" style={{ fontSize: 11, color: 'var(--fg-subtle)' }}>{a.id}</span>
            <TonePill tone={BUCKET_TONE[a.bucket]}>{BUCKET_LABEL[a.bucket]}</TonePill>
            <TonePill tone="fg-subtle">{CATEGORY_LABEL[a.category]}</TonePill>
            <TonePill tone="fg-subtle">{OWNER_LABEL[a.owner]}</TonePill>
            <span className="mono" style={{ fontSize: 11, color: 'var(--fg-subtle)' }}>{EFFORT_LABEL[a.effort]} · {a.impact} impact</span>
          </div>
          <div style={{ fontSize: 15.5, fontWeight: 600, lineHeight: 1.4, textDecoration: done ? 'line-through' : 'none' }}>{a.title}</div>
          {a.why && <div style={{ fontSize: 14, color: 'var(--fg-muted)', lineHeight: 1.6, marginTop: 6 }}>{a.why}</div>}
          {hasVerify && <VerifyLine a={a} />}
          <button onClick={() => setOpen(!open)} style={{ background: 'transparent', border: 'none', color: 'var(--accent)',
            fontFamily: 'var(--font-mono)', fontSize: 12, cursor: 'pointer', padding: '6px 0 0' }}>
            {open ? '− Hide the steps' : '+ How to do it'}
          </button>
          {open &&
          <div style={{ marginTop: 8, fontSize: 14, color: 'var(--fg-muted)', lineHeight: 1.7 }}>
              {a.how && <div><span className="mono" style={{ fontSize: 11, color: 'var(--accent)' }}>HOW · </span>{a.how}</div>}
              {a.pages && a.pages.length > 0 && <div><span className="mono" style={{ fontSize: 11, color: 'var(--accent)' }}>PAGES · </span><span className="mono" style={{ fontSize: 12.5 }}>{a.pages.join('  ')}</span></div>}
              {a.done_when && <div><span className="mono" style={{ fontSize: 11, color: 'var(--accent)' }}>DONE WHEN · </span>{a.done_when}</div>}
              {files.length > 0 &&
              <div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 8 }}>
                <span className="mono" style={{ fontSize: 11, color: 'var(--accent)' }}>READY TO USE · {files.length === 1 ? 'one file' : files.length + ' files'}, placeholders marked</span>
                {files.map((d) => <DeliverableCard key={d.kind} d={d} />)}
              </div>}
            </div>}
        </div>
      </div>
    </div>);
}

function FilterRow({ label, options, value, onChange }) {
  return (
    <div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
      <span className="mono" style={{ fontSize: 10.5, color: 'var(--fg-subtle)', letterSpacing: '0.08em', textTransform: 'uppercase', marginRight: 4 }}>{label}</span>
      {[['', 'All'], ...options].map(([v, l]) =>
      <button key={v} onClick={() => onChange(v)} style={{ ...textButton(value === v ? 'accent' : undefined), padding: '5px 11px', fontSize: 12.5 }}>{l}</button>)}
    </div>);
}

function ActionPlan({ slug, actions, client, full, ctx }) {
  ctx = ctx || {};
  // Ticks: the server's record when verification is on (shared by everyone
  // who opens the report), this browser's otherwise.
  const serverTicks = actions.some((a) => a.manual_done !== undefined);
  const [done, setDone] = React.useState(() => {
    const local = readDone(slug);
    if (!serverTicks) return local;
    const out = {};
    actions.forEach((a) => { out[a.id] = !!a.manual_done; });
    return out;
  });
  const [category, setCategory] = React.useState('');
  const [owner, setOwner] = React.useState('');
  const [bucket, setBucket] = React.useState('');
  const toggle = async (id) => {
    const next = { ...done, [id]: !done[id] }; setDone(next); writeDone(slug, next);
    if (serverTicks && (ctx.token || ctx.code)) {
      const headers = { 'Content-Type': 'application/json' };
      if (ctx.token) headers.Authorization = 'Bearer ' + ctx.token;
      try {
        const d = await apiCall('/api/verify', { method: 'POST', headers,
          body: JSON.stringify({ action: 'tick-manual', slug, id, done: next[id], ...(ctx.code ? { code: ctx.code } : {}) }) });
        if (d.ok && ctx.onTick) ctx.onTick(d.action);
      } catch (e) { /* the local tick stands */ }
    }
  };
  const shown = actions.filter((a) => (!category || a.category === category) && (!owner || a.owner === owner) && (!bucket || a.bucket === bucket));
  const finished = actions.filter((a) => done[a.id]).length;
  const title = `${client || slug} — AI visibility action plan`;
  return (
    <div>
      <h2 className="display" style={{ fontSize: 'clamp(22px,3.4vw,30px)', margin: '0 0 10px' }}>The action plan</h2>
      <p style={{ fontSize: 15.5, color: 'var(--fg-muted)', lineHeight: 1.6, margin: '0 0 18px', maxWidth: 720 }}>
        Every fix, in priority order, with who does it and how you will know it is done.
        {serverTicks ? ' Tick items off here (saved to this report), or send the list to whoever does the work. Items a machine can check are re-checked against the live site on every re-scan.'
          : ' Tick items off here (saved in this browser), or send the list to whoever does the work.'}
      </p>
      {typeof VerifyTimeline === 'function' && ctx.verify && <VerifyTimeline verify={ctx.verify} />}
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center', marginBottom: 14 }}>
        <span className="mono" style={{ fontSize: 12, color: 'var(--fg-subtle)' }}>{finished} of {actions.length} done</span>
        <span style={{ flex: 1 }}></span>
        <CopyText text={actionsToMarkdown(actions, title)} label="Copy as checklist" />
        <button onClick={() => downloadText(`${slug}-action-plan.csv`, actionsToCsv(actions), 'text/csv;charset=utf-8')} style={textButton()}>CSV for Asana / Trello / Jira</button>
        <button onClick={() => downloadText(`${slug}-action-plan.md`, actionsToMarkdown(actions, title), 'text/markdown;charset=utf-8')} style={textButton()}>Download Markdown</button>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 18 }}>
        <FilterRow label="Priority" value={bucket} onChange={setBucket} options={[['P0', 'Fix now'], ['P1', 'This quarter'], ['P2', 'Later']]} />
        <FilterRow label="Type" value={category} onChange={setCategory} options={Object.entries(CATEGORY_LABEL)} />
        <FilterRow label="Owner" value={owner} onChange={setOwner} options={Object.entries(OWNER_LABEL)} />
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        {shown.map((a) => <ActionCard key={a.id} a={a} done={done[a.id]} onToggle={() => toggle(a.id)}
          deliverables={typeof deliverablesFor === 'function' ? deliverablesFor(full, a.id) : []} />)}
        {shown.length === 0 && <div style={{ color: 'var(--fg-subtle)', fontSize: 14 }}>Nothing matches those filters.</div>}
      </div>
      {typeof GeneralDeliverables === 'function' && <GeneralDeliverables full={full} />}
    </div>);
}

/* ---------------------------------------------------------- the roadmap */

function Roadmap({ actions }) {
  return (
    <div>
      <h2 className="display" style={{ fontSize: 'clamp(22px,3.4vw,30px)', margin: '0 0 10px' }}>The next 90 days</h2>
      <p style={{ fontSize: 15.5, color: 'var(--fg-muted)', lineHeight: 1.6, margin: '0 0 22px', maxWidth: 720 }}>
        The same work laid out in time. The first 30 days are the items that stop engines reading you at all; the rest compounds on top.
      </p>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 14 }}>
        {['30', '60', '90'].map((w) => {
          const mine = actions.filter((a) => a.window === w);
          return (
            <div key={w} style={{ borderRadius: 'var(--r-lg)', background: 'var(--navy-2)', border: '1px solid var(--line)', padding: '16px 16px' }}>
              <div className="mono" style={{ fontSize: 10.5, letterSpacing: '0.09em', textTransform: 'uppercase', color: 'var(--accent)', marginBottom: 12 }}>{WINDOW_LABEL[w]}</div>
              {mine.length === 0 && <div style={{ fontSize: 13.5, color: 'var(--fg-subtle)' }}>Nothing scheduled.</div>}
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                {mine.map((a) =>
                <div key={a.id} style={{ padding: '10px 12px', borderRadius: 'var(--r-md)', background: 'var(--navy)', border: '1px solid var(--line)' }}>
                    <div style={{ fontSize: 14, fontWeight: 600, lineHeight: 1.4 }}>{a.title}</div>
                    <div className="mono" style={{ fontSize: 11, color: 'var(--fg-subtle)', marginTop: 4 }}>
                      {BUCKET_LABEL[a.bucket]} · {OWNER_LABEL[a.owner]} · {EFFORT_LABEL[a.effort]}
                    </div>
                  </div>)}
              </div>
            </div>);
        })}
      </div>
    </div>);
}

/* ------------------------------------------------ beyond the website */

function Beyond({ items }) {
  return (
    <div>
      <h2 className="display" style={{ fontSize: 'clamp(22px,3.4vw,30px)', margin: '0 0 10px' }}>Beyond the website</h2>
      <p style={{ fontSize: 15.5, color: 'var(--fg-muted)', lineHeight: 1.6, margin: '0 0 22px', maxWidth: 720 }}>
        Where the same thinking applies to how the business runs: the repetitive work an agent could do, the questions that should be a page and a prompt library, the data nothing reads.
      </p>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 14 }}>
        {items.map((b, i) =>
        <div key={i} style={{ borderRadius: 'var(--r-lg)', background: 'var(--navy-2)', border: '1px solid var(--line)', padding: '18px 18px' }}>
            <TonePill tone="accent">{AREA_LABEL[b.area] || b.area}</TonePill>
            <div style={{ fontFamily: 'var(--font-display)', fontSize: 19, fontWeight: 600, margin: '10px 0 8px', lineHeight: 1.3 }}>{b.title}</div>
            <div style={{ fontSize: 14, color: 'var(--fg-muted)', lineHeight: 1.65 }}>{b.what}</div>
            {b.replaces && <div style={{ fontSize: 13.5, color: 'var(--fg-muted)', lineHeight: 1.6, marginTop: 10 }}><span className="mono" style={{ fontSize: 11, color: 'var(--accent)' }}>REPLACES · </span>{b.replaces}</div>}
            {b.stand_up && <div style={{ fontSize: 13.5, color: 'var(--fg-muted)', lineHeight: 1.6, marginTop: 6 }}><span className="mono" style={{ fontSize: 11, color: 'var(--accent)' }}>TO STAND UP · </span>{b.stand_up}</div>}
            {b.tools && <div style={{ fontSize: 13.5, color: 'var(--fg-muted)', lineHeight: 1.6, marginTop: 6 }}><span className="mono" style={{ fontSize: 11, color: 'var(--accent)' }}>TOOLS · </span>{b.tools}</div>}
          </div>)}
      </div>
    </div>);
}

/* ------------------------------------------------------------ the proposal */

function money(n) {
  return '$' + Math.round(Number(n) || 0).toLocaleString('en-US');
}

function proposalToMarkdown(p, client, agency) {
  const lines = [`# ${p.headline || 'Proposal'}`, '', `Prepared by ${agency || 'the agency'} for ${client || 'the client'}`, '', p.summary, '', '## What we propose', ''];
  for (const o of p.opportunities || []) {
    lines.push(`### ${o.title}`, '', `**The problem.** ${o.problem}`, '', `**What we recommend.** ${o.recommendation}`, '');
    if (o.deliverables && o.deliverables.length) lines.push('Deliverables:', ...o.deliverables.map((d) => `- ${d}`), '');
    lines.push(`Estimate: ${o.weeks} weeks · ${money(o.price_low)}–${money(o.price_high)} · ${o.owner === 'client' ? 'client-side' : 'agency'}`, '');
  }
  if (p.phases && p.phases.length) {
    lines.push('## Phases', '');
    for (const ph of p.phases) lines.push(`- **${ph.name}** — ${ph.weeks} weeks · ${money(ph.price_low)}–${money(ph.price_high)} · includes: ${(ph.includes || []).join(', ')}`);
    lines.push('');
  }
  if (p.assumptions && p.assumptions.length) lines.push('## Assumptions', '', ...p.assumptions.map((a) => `- ${a}`), '');
  if (p.pitch) lines.push('## The email', '', p.pitch, '');
  return lines.join('\n');
}

function Proposal({ proposal, client, agency, slug }) {
  const p = proposal;
  const md = proposalToMarkdown(p, client, agency);
  const total = (p.phases || []).reduce((acc, ph) => [acc[0] + (ph.price_low || 0), acc[1] + (ph.price_high || 0)], [0, 0]);
  return (
    <div>
      <div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap', marginBottom: 10 }}>
        <TonePill tone="accent">Proposal draft</TonePill>
        <span className="mono" style={{ fontSize: 11, color: 'var(--fg-subtle)' }}>in {agency || 'the agency'}’s voice · edit before sending</span>
      </div>
      <h2 className="display" style={{ fontSize: 'clamp(22px,3.4vw,30px)', margin: '0 0 12px' }}>{p.headline}</h2>
      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 18 }}>
        <CopyText text={md} label="Copy proposal as Markdown" tone="accent" />
        <button onClick={() => downloadText(`${slug}-proposal.md`, md, 'text/markdown;charset=utf-8')} style={textButton()}>Download</button>
        {p.pitch && <CopyText text={p.pitch} label="Copy the email" />}
      </div>
      <Paragraphs text={p.summary} />
      <h3 className="display" style={{ fontSize: 22, margin: '26px 0 12px' }}>What the agency proposes</h3>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        {(p.opportunities || []).map((o, i) =>
        <div key={i} style={{ borderRadius: 'var(--r-lg)', background: 'var(--navy-2)', border: '1px solid var(--line)', padding: '16px 18px' }}>
            <div style={{ display: 'flex', gap: 10, alignItems: 'baseline', flexWrap: 'wrap', justifyContent: 'space-between' }}>
              <div style={{ fontFamily: 'var(--font-display)', fontSize: 19, fontWeight: 600 }}>{o.title}</div>
              <div className="mono" style={{ fontSize: 12.5, color: 'var(--accent)' }}>{o.weeks} wk · {money(o.price_low)}–{money(o.price_high)} · {o.owner === 'client' ? 'client-side' : 'agency'}</div>
            </div>
            <div style={{ fontSize: 14, color: 'var(--fg-muted)', lineHeight: 1.65, marginTop: 8 }}><span className="mono" style={{ fontSize: 11, color: 'var(--crit)' }}>PROBLEM · </span>{o.problem}</div>
            <div style={{ fontSize: 14, color: 'var(--fg-muted)', lineHeight: 1.65, marginTop: 6 }}><span className="mono" style={{ fontSize: 11, color: 'var(--good)' }}>RECOMMENDATION · </span>{o.recommendation}</div>
            {o.deliverables && o.deliverables.length > 0 &&
            <ul style={{ margin: '8px 0 0', paddingLeft: 18, fontSize: 13.5, color: 'var(--fg-muted)', lineHeight: 1.6 }}>
                {o.deliverables.map((d, j) => <li key={j}>{d}</li>)}
              </ul>}
          </div>)}
      </div>
      {p.phases && p.phases.length > 0 &&
      <React.Fragment>
          <h3 className="display" style={{ fontSize: 22, margin: '26px 0 12px' }}>Phases</h3>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 12 }}>
            {p.phases.map((ph, i) =>
            <div key={i} style={{ borderRadius: 'var(--r-lg)', background: 'var(--navy-2)', border: '1px solid var(--line)', padding: '16px' }}>
                <div style={{ fontWeight: 600, fontSize: 15 }}>{ph.name}</div>
                <div className="mono" style={{ fontSize: 12, color: 'var(--accent)', margin: '6px 0 8px' }}>{ph.weeks} wk · {money(ph.price_low)}–{money(ph.price_high)}</div>
                <div style={{ fontSize: 13, color: 'var(--fg-muted)', lineHeight: 1.6 }}>{(ph.includes || []).join(' · ')}</div>
              </div>)}
          </div>
          <div className="mono" style={{ fontSize: 12.5, color: 'var(--fg-muted)', marginTop: 10 }}>Whole engagement: {money(total[0])}–{money(total[1])}</div>
        </React.Fragment>}
      {p.pitch &&
      <React.Fragment>
          <h3 className="display" style={{ fontSize: 22, margin: '26px 0 12px' }}>The email</h3>
          <div style={{ borderRadius: 'var(--r-lg)', background: 'var(--navy-2)', border: '1px solid var(--line)', padding: '18px 20px' }}>
            <Paragraphs text={p.pitch} style={{ fontSize: 15 }} />
          </div>
        </React.Fragment>}
      {p.assumptions && p.assumptions.length > 0 &&
      <div style={{ marginTop: 22, padding: '14px 16px', borderRadius: 'var(--r-md)', background: 'var(--navy-2)', border: '1px solid var(--line)', fontSize: 13.5, color: 'var(--fg-muted)', lineHeight: 1.65 }}>
          <span className="mono" style={{ fontSize: 10.5, color: 'var(--fg-subtle)', letterSpacing: '0.08em' }}>ASSUMPTIONS · </span>
          {p.assumptions.join(' · ')}
        </div>}
    </div>);
}

/* The full tier as tabs, for whichever page is showing it. */
/* Prose tabs that only make sense once an engine benchmark has run. Older
   reports carry them with "no benchmark has been run" text; they are
   hidden until there are figures, and the Engine benchmark tab says what
   it takes. */
const BENCHMARK_PROSE = /^(benchmark|competitors|what engines say)/i;

function benchmarkReady(report) {
  const b = report.benchmark || null;
  return !!(b && (b.overall || (b.summary && b.summary.overall) || b.last_completed_run_id));
}

function fullTabs(report, ctx) {
  ctx = ctx || {};
  const full = report.full || {};
  const ready = benchmarkReady(report);
  const tabs = (full.sections || [])
    .filter((s) => ready || !BENCHMARK_PROSE.test(String(s.tab || '')))
    .map((s) => ({ tab: s.tab, render: () => <Section s={s} /> }));
  if (full.actions && full.actions.length) {
    tabs.push({ tab: 'Action plan', render: () => <ActionPlan slug={report.slug} actions={full.actions} client={report.client} full={full}
      ctx={{ token: ctx.token, code: ctx.code, verify: report.verify, onTick: ctx.onTick }} /> });
    tabs.push({ tab: '90-day roadmap', render: () => <Roadmap actions={full.actions} /> });
  }
  if (full.beyond && full.beyond.length) {
    tabs.push({ tab: 'Beyond the website', render: () => <Beyond items={full.beyond} /> });
  }
  if (full.proposal) {
    tabs.push({ tab: 'Proposal', render: () => <Proposal proposal={full.proposal} client={report.client} agency={(report.agency || {}).name} slug={report.slug} /> });
  }
  // The benchmark is an add-on to the full tier, listed after it. With
  // payments on it shows what it is and the price until the add-on is
  // bought (or comped); then "not run yet" until a run has finished.
  const pay = report.pay || {};
  const bstate = (pay.benchmark || {}).status || (ready ? 'ready' : 'none');
  const ownsBenchmark = !pay.enabled || !!(pay.owned || {}).benchmark;
  if (typeof BenchmarkTab === 'function' && (tabs.length > 0 || report.benchmark)) {
    const pending = pay.enabled && ownsBenchmark && !ready && typeof BenchmarkPending === 'function';
    tabs.push({ tab: 'Engine benchmark', secondary: true, addon: !ownsBenchmark, pending,
      render: () => (!ownsBenchmark && typeof BenchmarkUpsell === 'function')
        ? <BenchmarkUpsell report={report} admin={!!report.lead} />
        : pending
          ? <BenchmarkPending report={report} state={pay.benchmark} code={ctx.code} admin={!!report.lead} onUpdated={ctx.onReport} />
          : <BenchmarkTab report={report} admin={!!report.lead} code={ctx.code} /> });
  }
  // AI traffic (phase 4): present when the feature is on; "not measured
  // yet" until a connection exists and the numbers were pulled.
  if (typeof AttributionTab === 'function' && report.attribution && tabs.length > 0) {
    tabs.push({ tab: 'AI traffic', secondary: true, render: () => <AttributionTab report={report} admin={!!report.lead} /> });
  }
  return tabs;
}

/* The gate, as the visitor experiences it. */
function Unlock({ slug, status, onUnlocked, pay }) {
  const [code, setCode] = React.useState('');
  const [state, setState] = React.useState('idle');
  const [message, setMessage] = React.useState('');

  const submit = async (e) => {
    if (e) e.preventDefault();
    if (!code.trim() || state === 'sending') return;
    setState('sending'); setMessage('');
    try {
      const data = await apiCall('/api/report', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ slug, code: code.trim() })
      });
      if (data.ok) { onUnlocked(data.report, code.trim()); return; }
      setState('idle');
      setMessage(data.message || 'That did not work. Check the code and try again.');
    } catch (err) {
      setState('idle');
      setMessage('Could not reach the server. Try again in a moment.');
    }
  };

  const preparing = status === 'generating' || status === 'requested' || status === 'queued';
  // With payments on, a report nobody has bought is bought here; one that
  // is bought (or was given out before payments existed) just needs its code.
  const buyable = !preparing && pay && pay.enabled && !(pay.owned || {}).full_report;
  const price = typeof payPrice === 'function' ? payPrice(pay, 'full_report') : '';
  return (
    <div style={{ maxWidth: 460, margin: '0 auto', textAlign: 'center', padding: 'clamp(28px,5vw,48px) 0' }}>
      <div style={{ color: 'var(--accent)', marginBottom: 16 }}><Icon name={preparing ? 'spinner' : 'lock'} size={30} stroke={1.6} /></div>
      <h2 className="display" style={{ fontSize: 'clamp(22px,3.4vw,28px)', margin: '0 0 10px' }}>
        {preparing ? 'Your detailed report is being prepared' : buyable ? 'Get the full report' : 'The detailed report'}
      </h2>
      <p style={{ fontSize: 15, color: 'var(--fg-muted)', lineHeight: 1.6, margin: '0 0 24px' }}>
        {preparing
          ? 'Every finding, the prioritised action plan, the 90-day roadmap and where to take it next. It is prepared within 24 hours, usually much sooner. This page updates itself when it is ready.'
          : buyable
            ? `Every finding on the site, the prioritised action plan with owners and ready-to-paste files, the 90-day roadmap, re-scans that verify each fix, and where to take it next. One-time${price ? ', ' + price : ''}, for this website. Ready within 24 hours; this page unlocks itself.`
            : 'Every finding, the prioritised action plan with owners, the 90-day roadmap, what engines say about you today, and where to take it next. It opens with the access code the team gives you.'}
      </p>
      {buyable && typeof BuyButton === 'function' &&
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 26 }}>
        <BuyButton slug={slug} product="full_report" pay={pay} size="lg" label="Get the full report" />
        <SecurePayNote pay={pay} />
        <a href={'/pricing/?c=' + encodeURIComponent(slug)} style={{ color: 'var(--accent)', fontSize: 13.5 }}>What is in it, and what else is available</a>
        <div className="mono" style={{ fontSize: 11, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--fg-subtle)', marginTop: 8 }}>Or, already have a code?</div>
      </div>}
      {!preparing &&
      <form onSubmit={submit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        <input type="text" value={code} onChange={(e) => setCode(e.target.value)}
          placeholder="Enter your access code" autoComplete="off" spellCheck={false}
          style={{ width: '100%', padding: '13px 14px', borderRadius: 'var(--r-md)', fontSize: 16,
            fontFamily: 'var(--font-mono)', color: 'var(--fg)', background: 'var(--navy)',
            border: '1px solid ' + (message ? 'var(--crit)' : 'var(--line-strong)'),
            outline: 'none', boxSizing: 'border-box', textAlign: 'center', letterSpacing: '0.06em' }} />
        <button type="submit" className="btn btn-primary btn-lg" disabled={state === 'sending'}
          style={{ width: '100%', justifyContent: 'center' }}>
          {state === 'sending' ? 'Checking…' : 'Unlock the report'}
        </button>
      </form>}
      {message && <div style={{ marginTop: 12, fontSize: 13, color: 'var(--crit)' }}>{message}</div>}
      {!preparing && !buyable &&
      <p style={{ marginTop: 22, fontSize: 12.5, color: 'var(--fg-subtle)', lineHeight: 1.6 }}>
        {pay && pay.enabled
          ? 'This report is paid for. The code was shown at checkout and emailed; if the Lojo team or your agency arranged it, they have it. Can’t find it? Email the team and it is resent.'
          : 'Don’t have a code? Adi or Colin give it out after a short conversation about what the findings mean for your business.'}
      </p>}
    </div>);
}
