// ============================================================================
// NOVARE CLIENT REVIEW PORTAL — real-data SPA
//
// Public front door:  clientreview.NovareDigital.com   (clients only)
// Adapted from the prototype clientreview-portal.jsx — SEED data removed,
// MockSite replaced with uploaded design images, all state wired to the API.
// ============================================================================
const { useState, useEffect, useRef, useMemo, useCallback } = React;

const BRAND = {
  ink: "#11161c", paper: "#f4f1ea", card: "#ffffff",
  accent: "#c0432f", accentSoft: "#f0d9d3",
  line: "#dcd6ca", muted: "#6b6459",
  green: "#3f7d5c", amber: "#c98a1e",
};
const STATUS = {
  open:     { label: "Open",        color: BRAND.accent, bg: BRAND.accentSoft },
  progress: { label: "In Progress", color: BRAND.amber,  bg: "#f6ecd4" },
  resolved: { label: "Resolved",    color: BRAND.green,  bg: "#dcebe2" },
};

// ─── API helper ────────────────────────────────────────────────────────────
async function api(path, opts = {}) {
  const r = await fetch(`/api/client${path}`, {
    credentials: 'include',
    headers: { 'Content-Type': 'application/json', ...(opts.headers || {}) },
    ...opts,
  });
  if (r.status === 401) { throw Object.assign(new Error('unauthorized'), { status: 401 }); }
  const ct = r.headers.get('content-type') || '';
  const body = ct.includes('json') ? await r.json() : await r.text();
  if (!r.ok) throw Object.assign(new Error(body.error || r.statusText), { status: r.status, body });
  return body;
}

// ─── Sign-in gate ──────────────────────────────────────────────────────────
// When the server has CLIENT_DEV_INSTANT_SIGNIN=true, we show a one-click
// "Sign in as Randy" / "Sign in as Designer" UI that bypasses the magic-link
// flow entirely.  This is the build-session affordance — production deploys
// flip the flag off and the real magic-link UI returns automatically.
function LoginGate({ onSignedIn }) {
  const [mode, setMode] = useState(null);   // null until /auth/mode answers
  const [email, setEmail] = useState("randy@lyleindustries.com");
  const [sent, setSent] = useState(false);
  const [fallback, setFallback] = useState(null);
  const [err, setErr] = useState(null);
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    api('/auth/me').then(onSignedIn).catch(() => {});
    api('/auth/mode').then(setMode).catch(() => setMode({ dev_instant_signin: false }));
  }, []);

  async function devSignIn(eAddr) {
    setErr(null); setBusy(true);
    try {
      await api('/auth/dev-signin', { method: 'POST', body: JSON.stringify({ email: eAddr || email }) });
      const me = await api('/auth/me');
      onSignedIn(me);
    } catch (e) { setErr(e.message); setBusy(false); }
  }

  async function submitMagic(e) {
    e && e.preventDefault();
    setErr(null); setBusy(true);
    try {
      const r = await api('/auth/request-link', { method: 'POST', body: JSON.stringify({ email }) });
      setSent(true);
      if (r.fallback_link) setFallback(r.fallback_link);
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  }

  if (mode === null) return <div style={{ padding:30, color:BRAND.muted }}>Loading…</div>;

  return (
    <div style={{ minHeight:"100vh", background:BRAND.ink, display:"flex", alignItems:"center", justifyContent:"center", padding:20 }}>
      <div style={{ width:440, background:BRAND.card, borderRadius:8, padding:36, boxShadow:"0 20px 60px rgba(0,0,0,.4)", textAlign:"center" }}>
        <img src="/assets/novare-logo.png" alt="Nóvare Digital"
          style={{ height:54, marginBottom:14 }}/>
        <div style={{ fontFamily:"Georgia, serif", fontSize:20, fontWeight:700, color:BRAND.ink }}>
          Client Website Review System
        </div>
        <div style={{ fontSize:12, color:BRAND.muted, marginTop:4, marginBottom:24, letterSpacing:0.5 }}>
          clientreview.NovareDigital.com
        </div>

        {mode.dev_instant_signin ? (
          <>
            <div style={{ background:"#f6ecd4", border:`1px solid ${BRAND.amber}`, color:"#92400e",
              padding:10, borderRadius:5, fontSize:11, fontWeight:600, marginBottom:18, lineHeight:1.5 }}>
              DEV MODE — magic-link suspended.  One-click sign in below.
              <br/>Production deploy will re-enable the real magic-link flow automatically.
            </div>
            <div style={{ fontSize:12, fontWeight:700, color:BRAND.muted, marginBottom:6 }}>
              Sign in as a demo user:
            </div>
            <button onClick={() => devSignIn('randy@lyleindustries.com')} disabled={busy}
              style={{ width:"100%", marginTop:6, padding:"12px 0", border:"none",
                background:BRAND.green, color:"#fff", borderRadius:5, fontWeight:700, fontSize:14,
                cursor:"pointer", opacity:busy?0.6:1 }}>
              ✓ Randy Vaughn (APPROVER)
            </button>
            <button onClick={() => devSignIn('designer@lyle-agency.com')} disabled={busy}
              style={{ width:"100%", marginTop:8, padding:"12px 0", border:"none",
                background:BRAND.amber, color:"#fff", borderRadius:5, fontWeight:700, fontSize:14,
                cursor:"pointer", opacity:busy?0.6:1 }}>
              ⚠ Designer (REVIEWER)
            </button>
            <div style={{ marginTop:18, padding:"10px 0", borderTop:`1px solid ${BRAND.line}` }}>
              <div style={{ fontSize:12, fontWeight:700, color:BRAND.muted, marginBottom:6 }}>
                Or sign in with any registered email:
              </div>
              <div style={{ display:"flex", gap:6 }}>
                <input value={email} onChange={(e) => setEmail(e.target.value)} type="email"
                  style={{ flex:1, padding:"8px 10px", border:`1px solid ${BRAND.line}`, borderRadius:5, fontSize:13 }}/>
                <button onClick={() => devSignIn(email)} disabled={busy}
                  style={{ padding:"8px 14px", border:"none", background:BRAND.ink, color:"#fff",
                    borderRadius:5, fontWeight:700, fontSize:13, cursor:"pointer" }}>
                  Sign in
                </button>
              </div>
            </div>
            {err && <div style={{ color:BRAND.accent, fontSize:12, marginTop:10 }}>{err}</div>}
          </>
        ) : !sent ? (
          <form onSubmit={submitMagic}>
            <label style={{ fontSize:12, fontWeight:700, color:BRAND.muted }}>Your email</label>
            <input value={email} onChange={(e) => setEmail(e.target.value)} type="email" required autoFocus
              style={{ width:"100%", marginTop:6, padding:"10px 12px", border:`1px solid ${BRAND.line}`, borderRadius:5, fontSize:14 }}/>
            {err && <div style={{ color:BRAND.accent, fontSize:12, marginTop:8 }}>{err}</div>}
            <button disabled={busy} type="submit" style={{ width:"100%", marginTop:16, padding:"11px 0", border:"none", background:BRAND.accent, color:"#fff", borderRadius:5, fontWeight:700, fontSize:14, cursor:"pointer", opacity:busy?0.6:1 }}>
              {busy ? "Sending…" : "Send me a secure link"}
            </button>
            <div style={{ fontSize:11, color:BRAND.muted, marginTop:16, lineHeight:1.6 }}>
              Magic-link sign-in — no password. You'll only see the projects shared with your account.
            </div>
          </form>
        ) : (
          <>
            <div style={{ background:"#dcebe2", color:BRAND.green, padding:14, borderRadius:5, fontSize:13, fontWeight:600, lineHeight:1.5 }}>
              A secure sign-in link has been sent to <strong>{email}</strong>. Check your inbox.
            </div>
            {fallback && (
              <div style={{ marginTop:14, padding:14, background:"#fef3c7", border:`1px solid ${BRAND.amber}`, borderRadius:5, fontSize:11, color:"#92400e" }}>
                <strong>DEV MODE:</strong> SMTP isn't configured. Use this one-time link:
                <div style={{ marginTop:8, wordBreak:"break-all" }}>
                  <a href={fallback} style={{ color:BRAND.accent, fontFamily:"monospace" }}>{fallback}</a>
                </div>
              </div>
            )}
          </>
        )}
      </div>
    </div>
  );
}

// ─── Role banner (shown on every project screen) ──────────────────────────
// Approver = green strip.  Reviewer = amber strip.  Always present so the
// signed-in user can never be confused about their authority level.
function RoleBanner({ project }) {
  if (!project) return null;
  const isApprover = project.your_role === 'approver';
  if (isApprover) {
    return (
      <div style={{ background:"#dcebe2", borderLeft:`4px solid ${BRAND.green}`,
        padding:"10px 16px", marginBottom:14, borderRadius:4, fontSize:13 }}>
        <strong style={{ color:BRAND.green }}>✓ You are the Approver.</strong>
        {" "}Final decisions on this draft are yours.  Additional reviewers may comment;
        your approval is what releases the work.
      </div>
    );
  }
  return (
    <div style={{ background:"#f6ecd4", borderLeft:`4px solid ${BRAND.amber}`,
      padding:"10px 16px", marginBottom:14, borderRadius:4, fontSize:13 }}>
      <strong style={{ color:BRAND.amber }}>⚠ You are a Reviewer.</strong>
      {" "}Your comments will be visible to everyone on this project.
      {" "}<strong>{project.approver_display || project.approver_email}</strong> is the
      designated Approver and makes the final decision.
    </div>
  );
}

// ─── First-sign-in acknowledgment modal for the Approver ─────────────────
// Shown when project.needs_approver_acknowledgment is true.  Cannot be dismissed
// without checking the box + clicking Accept — they accepted the role.  The
// exact paragraph shown is sent back to the server and stored verbatim for the
// legal record.
function ApproverAcknowledgmentModal({ project, onAccepted }) {
  const [checked, setChecked] = useState(false);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const ACKNOWLEDGMENT_TEXT = `I confirm that I am the designated Approver for the project "${project.name}". `
    + `I understand my approval releases this work for production. Additional reviewers may comment freely, `
    + `but the final decision on what changes to make — and when the work is ready to ship — is mine. `
    + `I have read and accept this responsibility.`;

  async function accept() {
    if (!checked || busy) return;
    setBusy(true); setErr(null);
    try {
      await api(`/projects/${project.id}/acknowledge-role`, {
        method: 'POST',
        body: JSON.stringify({ acknowledgment_text: ACKNOWLEDGMENT_TEXT }),
      });
      onAccepted();
    } catch (e) { setErr(e.message); setBusy(false); }
  }

  return (
    <div style={{ position:"fixed", inset:0, background:"rgba(17,22,28,.6)",
      zIndex:200, display:"flex", alignItems:"center", justifyContent:"center", padding:20 }}>
      <div style={{ width:540, maxWidth:"100%", background:"#fff", borderRadius:8, padding:30 }}>
        <div style={{ color:BRAND.green, fontWeight:700, fontSize:13, letterSpacing:1, marginBottom:8 }}>
          APPROVER ROLE — ACKNOWLEDGMENT REQUIRED
        </div>
        <h2 style={{ fontFamily:"Georgia, serif", margin:"0 0 14px", fontSize:22 }}>
          You're the Approver for this project.
        </h2>
        <div style={{ background:"#faf8f3", border:`1px solid ${BRAND.line}`, borderRadius:4,
          padding:16, fontSize:14, lineHeight:1.6, color:"#2a2a2a", marginBottom:16 }}>
          {ACKNOWLEDGMENT_TEXT}
        </div>
        <label style={{ display:"flex", alignItems:"flex-start", gap:10, fontSize:14, cursor:"pointer" }}>
          <input type="checkbox" checked={checked} onChange={(e) => setChecked(e.target.checked)}
            style={{ marginTop:3 }}/>
          <span>I have read the above and accept the Approver role for this project.</span>
        </label>
        {err && <div style={{ color:BRAND.accent, fontSize:13, marginTop:10 }}>{err}</div>}
        <div style={{ marginTop:20, display:"flex", justifyContent:"flex-end", gap:10 }}>
          <button onClick={accept} disabled={!checked || busy}
            style={{ padding:"10px 20px", border:"none", background:BRAND.green, color:"#fff",
              borderRadius:4, fontWeight:700, fontSize:14,
              cursor:(checked && !busy) ? "pointer" : "not-allowed",
              opacity:(checked && !busy) ? 1 : 0.5 }}>
            {busy ? "Accepting…" : "Accept Approver role"}
          </button>
        </div>
        <div style={{ marginTop:14, fontSize:11, color:BRAND.muted, lineHeight:1.5 }}>
          Your acceptance is timestamped and recorded for the project audit log.
          If you believe this role belongs to someone else, contact your Nóvare account
          manager before accepting.
        </div>
      </div>
    </div>
  );
}

// ─── Preview mode (#H) ────────────────────────────────────────────────────
// Full-screen modal showing the draft site at a selectable viewport width.
// No pin overlay, no comments, no commentary chrome — pure "what does this
// actually look like to a visitor."  Three presets + custom width.
//
// Per Mark's note: "it looks different depending on your screen" — there's
// always a disclaimer banner explaining real visitors may see minor variation.
const VIEWPORTS = [
  { id: 'desktop', label: 'Desktop',  w: 1440, h: 900,  icon: '🖥️' },
  { id: 'laptop',  label: 'Laptop',   w: 1280, h: 800,  icon: '💻' },
  { id: 'tablet',  label: 'Tablet',   w: 768,  h: 1024, icon: '📱' },
  { id: 'phone',   label: 'Phone',    w: 375,  h: 812,  icon: '📱' },
];

function PreviewModal({ project, version, onClose }) {
  const [vpId, setVpId] = useState('desktop');
  const [customW, setCustomW] = useState(1024);
  const [orientation, setOrientation] = useState('portrait'); // for tablet/phone
  const vp = VIEWPORTS.find(v => v.id === vpId);
  const siteAssets = (version.assets || []).filter(a => a.kind === 'site');
  const [activeAsset, setActiveAsset] = useState(siteAssets[0] || null);

  // Effective viewport — flip w/h for landscape on tablet/phone
  let frameW = vp ? vp.w : customW;
  let frameH = vp ? vp.h : 900;
  if ((vpId === 'tablet' || vpId === 'phone') && orientation === 'landscape') {
    [frameW, frameH] = [frameH, frameW];
  }
  if (vpId === 'custom') frameW = customW;

  if (siteAssets.length === 0) {
    return (
      <div style={{ position:"fixed", inset:0, background:"rgba(17,22,28,.9)", zIndex:300,
        display:"flex", alignItems:"center", justifyContent:"center", padding:30 }}>
        <div style={{ background:"#fff", padding:30, borderRadius:8, maxWidth:480, textAlign:"center" }}>
          <div style={{ fontSize:36, marginBottom:10 }}>📷</div>
          <h2 style={{ margin:"0 0 10px", fontFamily:"Georgia,serif" }}>No live preview available</h2>
          <p style={{ color:BRAND.muted, fontSize:14, lineHeight:1.5 }}>
            This version contains static design images (mockups), not an uploaded draft website.
            Once Nóvare uploads the clickable draft, the Preview button will show the real site here.
          </p>
          <button onClick={onClose}
            style={{ padding:"10px 20px", marginTop:14, border:"none", background:BRAND.accent,
              color:"#fff", borderRadius:4, fontWeight:700, cursor:"pointer" }}>
            Close
          </button>
        </div>
      </div>
    );
  }

  return (
    <div style={{ position:"fixed", inset:0, background:"#11161c", zIndex:300,
      display:"flex", flexDirection:"column" }}>
      {/* Top bar — viewport switcher */}
      <div style={{ background:BRAND.ink, color:"#fff", padding:"10px 20px",
        display:"flex", alignItems:"center", gap:14, borderBottom:"1px solid #2a2f36" }}>
        <div style={{ fontWeight:800, fontSize:14 }}>
          👁 Preview <span style={{ opacity:0.6, fontWeight:400, marginLeft:6 }}>
            · {project.name} {version.label}
          </span>
        </div>

        <div style={{ display:"flex", gap:6, marginLeft:20 }}>
          {VIEWPORTS.map(v => (
            <button key={v.id} onClick={() => setVpId(v.id)}
              style={{ padding:"6px 12px", fontSize:12, fontWeight:700, borderRadius:4,
                cursor:"pointer", border:`1px solid ${vpId === v.id ? BRAND.accent : "#444"}`,
                background: vpId === v.id ? BRAND.accent : "transparent",
                color:"#fff" }}>
              {v.icon} {v.label} <span style={{ opacity:0.7, fontWeight:400 }}>{v.w}px</span>
            </button>
          ))}
          <button onClick={() => setVpId('custom')}
            style={{ padding:"6px 12px", fontSize:12, fontWeight:700, borderRadius:4,
              cursor:"pointer", border:`1px solid ${vpId === 'custom' ? BRAND.accent : "#444"}`,
              background: vpId === 'custom' ? BRAND.accent : "transparent",
              color:"#fff" }}>
            ⚙ Custom
          </button>
        </div>

        {vpId === 'custom' && (
          <div style={{ display:"flex", alignItems:"center", gap:6, marginLeft:10 }}>
            <input type="range" min={320} max={1920} value={customW}
              onChange={(e) => setCustomW(parseInt(e.target.value, 10))}
              style={{ width:140 }}/>
            <span style={{ fontSize:12, fontFamily:"monospace", width:60 }}>{customW}px</span>
          </div>
        )}

        {(vpId === 'tablet' || vpId === 'phone') && (
          <div style={{ display:"flex", gap:4, marginLeft:10 }}>
            <button onClick={() => setOrientation('portrait')}
              style={{ padding:"5px 10px", fontSize:11, borderRadius:3, cursor:"pointer",
                border:`1px solid ${orientation === 'portrait' ? BRAND.accent : '#444'}`,
                background: orientation === 'portrait' ? BRAND.accent : 'transparent',
                color:"#fff" }}>▯ Portrait</button>
            <button onClick={() => setOrientation('landscape')}
              style={{ padding:"5px 10px", fontSize:11, borderRadius:3, cursor:"pointer",
                border:`1px solid ${orientation === 'landscape' ? BRAND.accent : '#444'}`,
                background: orientation === 'landscape' ? BRAND.accent : 'transparent',
                color:"#fff" }}>▭ Landscape</button>
          </div>
        )}

        {siteAssets.length > 1 && (
          <select value={activeAsset ? activeAsset.id : ''}
            onChange={(e) => setActiveAsset(siteAssets.find(a => a.id === parseInt(e.target.value, 10)))}
            style={{ marginLeft:10, padding:"5px 8px", background:"#222", color:"#fff",
              border:"1px solid #444", borderRadius:3, fontSize:12 }}>
            {siteAssets.map(a => <option key={a.id} value={a.id}>{a.filename}</option>)}
          </select>
        )}

        <div style={{ flex:1 }}/>
        <button onClick={onClose}
          style={{ padding:"7px 14px", border:"none", background:"#fff", color:BRAND.ink,
            borderRadius:4, fontWeight:700, fontSize:13, cursor:"pointer" }}>
          × Close preview
        </button>
      </div>

      {/* Disclaimer */}
      <div style={{ background:"#f6ecd4", borderBottom:`1px solid ${BRAND.amber}`,
        padding:"8px 20px", fontSize:12, color:"#92400e", display:"flex", alignItems:"center", gap:10 }}>
        <span style={{ fontWeight:700 }}>⚠ Real screens vary.</span>
        Your actual visitors may see minor differences in spacing, font rendering, or wrapping depending on their browser, operating system, and screen size.
        Mobile devices (iPhone, Android) sometimes need elements like watermarks or logos hidden or moved — let us know if you want a different behavior on a specific device.
      </div>

      {/* The framed iframe */}
      <div style={{ flex:1, overflow:"auto", padding:20,
        background:"repeating-conic-gradient(#1a1f25 0% 25%, #20262c 0% 50%) 50% / 24px 24px",
        display:"flex", justifyContent:"center", alignItems:"flex-start" }}>
        <div style={{
          width: frameW,
          maxWidth: '100%',
          height: vpId === 'custom' ? 'calc(100vh - 200px)' : frameH,
          background:"#fff",
          borderRadius: (vpId === 'phone' || vpId === 'tablet') ? 28 : 4,
          boxShadow:"0 20px 60px rgba(0,0,0,0.5)",
          overflow:"hidden",
          border: (vpId === 'phone' || vpId === 'tablet') ? "10px solid #2a2a2a" : "1px solid #444",
          position:"relative",
        }}>
          {activeAsset && (
            <iframe
              src={`/uploads/${activeAsset.stored_path}`}
              title={`Preview ${activeAsset.filename}`}
              sandbox="allow-scripts allow-forms allow-popups"
              style={{ width:"100%", height:"100%", border:"none", display:"block" }}/>
          )}
        </div>
      </div>

      {/* Footer note about the frame */}
      <div style={{ background:"#11161c", color:"#6b6459", padding:"6px 20px",
        fontSize:11, textAlign:"center", borderTop:"1px solid #2a2f36" }}>
        Showing at {frameW}×{vpId === 'custom' ? 'auto' : frameH}px
        {(vpId === 'phone' || vpId === 'tablet') && ` · ${orientation}`}
        {" · "}Click inside the frame to interact with the live draft site.
      </div>
    </div>
  );
}

// ─── FINAL APPROVAL modal (#K — Approver only) ────────────────────────────
// "I approve the website as presented and wish for it to be uploaded to my
// domain."  Distinct from feedback submission.  After this fires, staff sees
// a green panel on the project + can click Approve & Upload to go live.
function FinalApproveModal({ project, onClose, onApproved }) {
  const [checked, setChecked] = useState(false);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  const ACKNOWLEDGMENT_TEXT = `I, ${project.approver_display || project.approver_email}, `
    + `approve the "${project.name}" website as presented and wish for it to be uploaded to my domain. `
    + `I have reviewed the latest draft, all my feedback has been addressed, and I authorize Nóvare Digital `
    + `to publish this version live.`;

  async function go() {
    if (!checked || busy) return;
    setBusy(true); setErr(null);
    try {
      const r = await api(`/projects/${project.id}/final-approve`, {
        method: 'POST',
        body: JSON.stringify({ acknowledgment_text: ACKNOWLEDGMENT_TEXT }),
      });
      onApproved(r);
    } catch (e) { setErr(e.message); setBusy(false); }
  }

  return (
    <div style={{ position:"fixed", inset:0, background:"rgba(17,22,28,.7)", zIndex:200,
      display:"flex", alignItems:"center", justifyContent:"center", padding:20 }}>
      <div style={{ width:580, maxWidth:"100%", background:"#fff", borderRadius:8, padding:30 }}>
        <div style={{ color:BRAND.green, fontWeight:700, fontSize:13, letterSpacing:1, marginBottom:8 }}>
          ✓ FINAL APPROVAL — GO LIVE
        </div>
        <h2 style={{ fontFamily:"Georgia, serif", margin:"0 0 14px", fontSize:24 }}>
          Approve this website for production?
        </h2>
        <div style={{ background:"#fff8e6", border:`1px solid ${BRAND.amber}`, padding:14,
          borderRadius:4, fontSize:13, lineHeight:1.55, color:"#92400e", marginBottom:14 }}>
          <strong>This is a one-way action.</strong> Once you approve, Nóvare will review
          and upload the site to your domain.  Further changes will be treated as a new
          revision cycle.  Don't approve unless you're confident the site is ready.
        </div>
        <div style={{ background:"#faf8f3", border:`1px solid ${BRAND.line}`, borderRadius:4,
          padding:16, fontSize:14, lineHeight:1.6, color:"#2a2a2a", marginBottom:14 }}>
          {ACKNOWLEDGMENT_TEXT}
        </div>
        <label style={{ display:"flex", alignItems:"flex-start", gap:10, fontSize:14, cursor:"pointer" }}>
          <input type="checkbox" checked={checked} onChange={(e) => setChecked(e.target.checked)}
            style={{ marginTop:3 }}/>
          <span><strong>I approve the website as presented and wish for it to be uploaded to my domain.</strong></span>
        </label>
        {err && <div style={{ color:BRAND.accent, fontSize:13, marginTop:10 }}>{err}</div>}
        <div style={{ marginTop:20, display:"flex", justifyContent:"flex-end", gap:10 }}>
          <button onClick={onClose} disabled={busy}
            style={{ padding:"10px 18px", border:`1px solid ${BRAND.line}`, background:"#fff",
              borderRadius:4, fontWeight:700, cursor:"pointer" }}>Cancel</button>
          <button onClick={go} disabled={!checked || busy}
            style={{ padding:"12px 22px", border:"none", background:BRAND.green, color:"#fff",
              borderRadius:4, fontWeight:700, fontSize:15,
              cursor:(checked && !busy) ? "pointer" : "not-allowed",
              opacity:(checked && !busy) ? 1 : 0.5 }}>
            {busy ? "Submitting…" : "✓ Submit Final Approval"}
          </button>
        </div>
        <div style={{ marginTop:12, fontSize:11, color:BRAND.muted }}>
          Your approval is timestamped, recorded with your IP address, and stored
          permanently as part of the project's audit trail.
        </div>
      </div>
    </div>
  );
}

// ─── Submit-feedback bundle modal (Approver only) ─────────────────────────
// Shown when the Approver clicks "Submit feedback for V3".  Lists every open,
// unbundled comment on the version, shows the acknowledgment statement, and
// requires checkbox+click to submit.  Once submitted, all those comments
// become write-once (server enforces; UI shows lock badge).
function SubmitBundleModal({ project, version, onClose, onSubmitted }) {
  const [bundle, setBundle] = useState(null);
  const [checked, setChecked] = useState(false);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  const ACKNOWLEDGMENT_TEXT = `By submitting, I confirm these are the changes I'm requesting on the ${version.label} draft of "${project.name}". `
    + `I understand work will begin on this revision based on this exact list of comments. `
    + `Anything outside this list will be treated as new scope and addressed separately. `
    + `My acceptance is recorded with timestamp and IP for the project audit trail.`;

  useEffect(() => {
    api(`/projects/${project.id}/versions/${version.id}/pending-bundle`)
      .then(setBundle).catch(e => setErr(e.message));
  }, [project.id, version.id]);

  async function submit() {
    if (!checked || busy) return;
    setBusy(true); setErr(null);
    try {
      const r = await api(`/projects/${project.id}/versions/${version.id}/submit`, {
        method: 'POST', body: JSON.stringify({ acknowledgment_text: ACKNOWLEDGMENT_TEXT }),
      });
      onSubmitted(r);
    } catch (e) { setErr(e.message); setBusy(false); }
  }

  return (
    <div style={{ position:"fixed", inset:0, background:"rgba(17,22,28,.6)",
      zIndex:200, display:"flex", alignItems:"center", justifyContent:"center", padding:20 }}>
      <div style={{ width:680, maxWidth:"100%", maxHeight:"90vh", background:"#fff",
        borderRadius:8, padding:30, overflow:"auto" }}>
        <div style={{ color:BRAND.green, fontWeight:700, fontSize:13, letterSpacing:1, marginBottom:8 }}>
          SUBMIT FEEDBACK BUNDLE — {version.label}
        </div>
        <h2 style={{ fontFamily:"Georgia, serif", margin:"0 0 14px", fontSize:22 }}>
          Send these {bundle ? bundle.open_count : '…'} change requests to Nóvare?
        </h2>

        {!bundle && <div style={{ color:BRAND.muted }}>Loading…</div>}

        {bundle && bundle.open_count === 0 && (
          <div style={{ padding:16, background:"#faf8f3", borderRadius:4, color:BRAND.muted }}>
            No open comments to submit on {version.label}.  Comments must be in the "Open"
            state and not already in a previous submission.
          </div>
        )}

        {bundle && bundle.open_count > 0 && (
          <>
            <div style={{ fontSize:13, color:BRAND.muted, marginBottom:10 }}>
              {Object.entries(bundle.by_author).map(([k, v]) => `${v} from ${k}`).join(' · ')}
            </div>
            <div style={{ border:`1px solid ${BRAND.line}`, borderRadius:4, maxHeight:280,
              overflowY:"auto", marginBottom:14 }}>
              {bundle.open_comments.map((c, i) => (
                <div key={c.id} style={{ padding:"10px 14px", borderBottom: i < bundle.open_comments.length - 1 ? `1px solid ${BRAND.line}` : "none" }}>
                  <div style={{ fontSize:11, color:BRAND.muted }}>
                    #{i + 1} · {c.author_label} · pinned at {c.x_pct.toFixed(0)}%, {c.y_pct.toFixed(0)}%
                  </div>
                  <div style={{ fontSize:14, marginTop:2 }}>{c.text}</div>
                </div>
              ))}
            </div>
            <div style={{ background:"#faf8f3", border:`1px solid ${BRAND.line}`, borderRadius:4,
              padding:14, fontSize:13, lineHeight:1.55, marginBottom:14 }}>
              {ACKNOWLEDGMENT_TEXT}
            </div>
            <label style={{ display:"flex", alignItems:"flex-start", gap:10, fontSize:13, cursor:"pointer" }}>
              <input type="checkbox" checked={checked} onChange={(e) => setChecked(e.target.checked)}
                style={{ marginTop:3 }}/>
              <span>I confirm I've read and accept the statement above.</span>
            </label>
          </>
        )}

        {err && <div style={{ color:BRAND.accent, fontSize:13, marginTop:10 }}>{err}</div>}

        <div style={{ marginTop:20, display:"flex", justifyContent:"flex-end", gap:10 }}>
          <button onClick={onClose} disabled={busy}
            style={{ padding:"10px 18px", border:`1px solid ${BRAND.line}`, background:"#fff",
              borderRadius:4, fontWeight:700, fontSize:13, cursor:"pointer" }}>Cancel</button>
          {bundle && bundle.open_count > 0 && (
            <button onClick={submit} disabled={!checked || busy}
              style={{ padding:"10px 18px", border:"none", background:BRAND.green, color:"#fff",
                borderRadius:4, fontWeight:700, fontSize:13,
                cursor:(checked && !busy) ? "pointer" : "not-allowed",
                opacity:(checked && !busy) ? 1 : 0.5 }}>
              {busy ? "Submitting…" : `Submit ${bundle.open_count} change requests`}
            </button>
          )}
        </div>
      </div>
    </div>
  );
}

// ─── Pin marker ────────────────────────────────────────────────────────────
function Pin({ c, idx, active, onClick }) {
  const s = STATUS[c.status];
  return (
    <button onClick={(e) => { e.stopPropagation(); onClick(c.id); }}
      style={{ position:"absolute", left:`${c.x_pct}%`, top:`${c.y_pct}%`, transform:"translate(-50%,-50%)",
        width:26, height:26, borderRadius:"50% 50% 50% 2px", background:s.color, color:"#fff",
        border:"2px solid #fff", boxShadow:active?`0 0 0 4px ${s.bg}`:"0 2px 8px rgba(0,0,0,.3)",
        fontSize:12, fontWeight:700, cursor:"pointer", zIndex:active?30:10 }}
      title={c.text}>{idx + 1}</button>
  );
}

// ─── Asset (image OR uploaded draft site iframe) with pins overlaid ───────
// Two render modes:
//   kind='image' → <img> stretched to width, height auto
//   kind='site'  → <iframe sandbox="allow-scripts allow-forms"> showing the
//                  unpacked draft site.  Sandbox WITHOUT allow-same-origin =
//                  uploaded JS cannot read our cookies / DOM.  CSP at the
//                  /uploads/ static handler provides defense in depth.
//
// In both modes pins live in a transparent overlay div on top.  The overlay
// only captures clicks while in "placing" mode (cursor:crosshair) — otherwise
// pointer-events:none so the user can scroll/click the draft site normally.
function AssetCanvas({ asset, comments, placing, draft, onPlace, onPinClick, activeId, canPlace }) {
  const wrapperRef = useRef(null);
  const overlayActive = placing && canPlace;

  function handleOverlayClick(e) {
    if (!overlayActive) return;
    const rect = wrapperRef.current.getBoundingClientRect();
    const x = ((e.clientX - rect.left) / rect.width) * 100;
    const y = ((e.clientY - rect.top) / rect.height) * 100;
    onPlace({ asset_id: asset.id, x_pct: x, y_pct: y });
  }

  const isSite = asset.kind === 'site';
  const ours = comments.filter(c => c.asset_id === asset.id);

  return (
    <div style={{ marginBottom:18 }}>
      {isSite && (
        <div style={{ fontSize:11, color:BRAND.muted, marginBottom:6, fontStyle:"italic" }}>
          Draft website preview · scroll and click inside to explore · use "+ Add Comment" to pin feedback
        </div>
      )}
      <div ref={wrapperRef}
        style={{ position:"relative", border:`1px solid ${BRAND.line}`, borderRadius:6,
          overflow:"hidden", boxShadow:"0 4px 16px rgba(0,0,0,.06)",
          cursor: overlayActive ? "crosshair" : "default",
          background: isSite ? "#fff" : "transparent" }}>
        {isSite ? (
          <iframe
            src={`/uploads/${asset.stored_path}`}
            title={asset.filename}
            sandbox="allow-scripts allow-forms allow-popups"
            style={{ display:"block", width:"100%", height:"calc(100vh - 220px)",
              border:"none", background:"#fff" }}/>
        ) : (
          <img src={`/uploads/${asset.stored_path}`} alt={asset.filename} draggable={false}
            style={{ display:"block", width:"100%", height:"auto", userSelect:"none" }}/>
        )}
        {/* Click-catcher overlay: ALWAYS rendered so pins layer on top, but
            only captures pointer events while in "placing" mode.  Without
            this, clicks would go to the iframe and pin placement would fail. */}
        <div onClick={handleOverlayClick}
          style={{ position:"absolute", inset:0, zIndex:5,
            pointerEvents: overlayActive ? "auto" : "none" }}/>
        {ours.map((c) => (
          <Pin key={c.id} c={c} idx={comments.findIndex(x => x.id === c.id)}
            active={activeId === c.id} onClick={onPinClick}/>
        ))}
        {draft && draft.asset_id === asset.id && (
          <div style={{ position:"absolute", left:`${draft.x_pct}%`, top:`${draft.y_pct}%`,
            transform:"translate(-50%,-50%)", width:26, height:26,
            borderRadius:"50% 50% 50% 2px", background:BRAND.ink,
            border:"2px dashed #fff", zIndex:40, pointerEvents:"none" }}/>
        )}
      </div>
    </div>
  );
}

// ─── Comment row in sidebar ────────────────────────────────────────────────
function CommentRow({ c, idx, active, isApprover, onClick, onReply, onResolve }) {
  const s = STATUS[c.status];
  const [replyText, setReplyText] = useState("");
  const isLocked = !!c.submission_id;
  return (
    <div onClick={() => onClick(c.id)}
      style={{ padding:14, borderBottom:`1px solid ${BRAND.line}`, cursor:"pointer",
        background: active ? "#faf8f3" : (isLocked ? "#fafaf7" : "#fff") }}>
      <div style={{ display:"flex", alignItems:"center", gap:8, marginBottom:6 }}>
        <span style={{ width:20, height:20, borderRadius:"50%", background:s.color, color:"#fff",
          fontSize:11, fontWeight:700, display:"flex", alignItems:"center", justifyContent:"center" }}>
          {idx + 1}
        </span>
        <span style={{ fontSize:13, fontWeight:700 }}>{c.author_label}</span>
        {isLocked && (
          <span title={`Locked in submission #${c.submission_id} on ${new Date(c.locked_at).toLocaleString()}`}
            style={{ fontSize:10, fontWeight:700, padding:"2px 6px", borderRadius:3,
              background:BRAND.ink, color:"#fff" }}>🔒 SUBMITTED</span>
        )}
        <span style={{ marginLeft:"auto", fontSize:10, fontWeight:700, padding:"2px 8px",
          borderRadius:20, background:s.bg, color:s.color }}>{s.label}</span>
      </div>
      <div style={{ fontSize:13, lineHeight:1.5, color:"#2a2a2a" }}>{c.text}</div>
      {c.replies.map((r) => (
        <div key={r.id} style={{ marginTop:8, marginLeft:12, paddingLeft:10,
          borderLeft:`2px solid ${r.author_kind === 'staff' ? BRAND.accentSoft : BRAND.line}`,
          fontSize:12, color:BRAND.muted }}>
          <strong style={{ color: r.author_kind === 'staff' ? BRAND.accent : BRAND.ink }}>
            {r.author_label}:
          </strong> {r.text}
        </div>
      ))}
      {active && (
        <div onClick={(e) => e.stopPropagation()} style={{ marginTop:12 }}>
          <div style={{ display:"flex", gap:6 }}>
            <input value={replyText} onChange={(e) => setReplyText(e.target.value)}
              placeholder="Add a reply…"
              style={{ flex:1, padding:"6px 8px", border:`1px solid ${BRAND.line}`,
                borderRadius:4, fontSize:12 }}/>
            <button onClick={() => { if (replyText.trim()) { onReply(c.id, replyText.trim()); setReplyText(""); } }}
              style={{ padding:"6px 12px", border:"none", background:BRAND.ink, color:"#fff",
                borderRadius:4, fontSize:12, fontWeight:700, cursor:"pointer" }}>Reply</button>
          </div>
          {isApprover && !isLocked && c.status === 'open' && (
            <button onClick={() => onResolve(c.id)}
              style={{ marginTop:8, padding:"4px 10px", fontSize:11, fontWeight:700,
                border:`1px solid ${BRAND.green}`, background:"#fff", color:BRAND.green,
                borderRadius:4, cursor:"pointer" }}
              title="Mark resolved — excludes this comment from the next feedback bundle">
              ✓ Resolve (Approver)
            </button>
          )}
        </div>
      )}
    </div>
  );
}

// ─── Main app ──────────────────────────────────────────────────────────────
function App() {
  const [me, setMe] = useState(null);
  const [projects, setProjects] = useState([]);
  const [project, setProject] = useState(null);          // full project w/ versions+assets
  const [comments, setComments] = useState([]);
  const [version, setVersion] = useState(null);
  const [compare, setCompare] = useState(false);
  const [placing, setPlacing] = useState(false);
  const [draft, setDraft] = useState(null);
  const [draftText, setDraftText] = useState("");
  const [activeId, setActiveId] = useState(null);
  const [filter, setFilter] = useState("all");
  const [err, setErr] = useState(null);
  const [submitOpen, setSubmitOpen] = useState(false);
  const [submitted, setSubmitted] = useState(null); // post-submit success banner
  const [previewOpen, setPreviewOpen] = useState(false); // #H preview modal
  const [finalApproveOpen, setFinalApproveOpen] = useState(false); // #K
  const [finalApprovalState, setFinalApprovalState] = useState(null); // latest FA record
  const [finalApprovalJustSent, setFinalApprovalJustSent] = useState(null); // toast

  // Initial: check session.  If signed in, load projects.
  useEffect(() => {
    api('/auth/me').then((m) => { setMe(m); loadProjects(); }).catch(() => setMe(false));
  }, []);

  async function loadProjects() {
    try { setProjects(await api('/projects')); }
    catch (e) { setErr(e.message); }
  }
  async function openProject(id) {
    try {
      const p = await api(`/projects/${id}`);
      setProject(p);
      setVersion(p.versions[p.versions.length - 1] || null); // default to latest
      const cs = await api(`/projects/${id}/comments`);
      setComments(cs);
      setActiveId(null);
      window.history.pushState({}, '', `/project/${id}`);
      // #K — fetch latest final-approval state for this project
      api(`/projects/${id}/final-approval`).then(setFinalApprovalState).catch(() => setFinalApprovalState(null));
    } catch (e) { setErr(e.message); }
  }
  async function signOut() {
    await api('/auth/signout', { method: 'POST' });
    setMe(false); setProject(null); setProjects([]);
    window.history.pushState({}, '', '/');
  }

  const versionComments = useMemo(
    () => comments.filter(c => version && c.version_id === version.id),
    [comments, version]);
  const visibleComments = useMemo(
    () => versionComments.filter(c => filter === "all" || c.status === filter),
    [versionComments, filter]);
  const counts = useMemo(() => {
    const b = { open: 0, progress: 0, resolved: 0 };
    versionComments.forEach(c => b[c.status] !== undefined && b[c.status]++);
    return b;
  }, [versionComments]);

  function onPlace(p) { setDraft(p); setPlacing(false); }

  async function saveDraft() {
    if (!draftText.trim() || !draft) return;
    try {
      const created = await api('/comments', {
        method: 'POST',
        body: JSON.stringify({ ...draft, text: draftText.trim() }),
      });
      setComments(c => [...c, created]);
      setDraft(null); setDraftText(""); setActiveId(created.id);
    } catch (e) { setErr(e.message); }
  }
  async function resolveComment(commentId) {
    try {
      await api(`/comments/${commentId}/resolve`, { method: 'POST' });
      // refetch comments for the current project
      const cs = await api(`/projects/${project.id}/comments`);
      setComments(cs);
    } catch (e) { setErr(e.message); }
  }
  async function addReply(commentId, text) {
    try {
      const r = await api(`/comments/${commentId}/replies`, {
        method: 'POST', body: JSON.stringify({ text }),
      });
      setComments(cs => cs.map(c => c.id === commentId ? { ...c, replies: [...c.replies, r] } : c));
    } catch (e) { setErr(e.message); }
  }

  // Hook MUST be declared before any early returns (Rules of Hooks).
  // Auto-open a project if the URL is /project/N and we have the list loaded.
  useEffect(() => {
    if (me && !project && projects.length > 0) {
      const m = location.pathname.match(/^\/project\/(\d+)/);
      if (m) openProject(parseInt(m[1], 10));
    }
  }, [me, projects, project]);

  if (me === null) return <div style={{ padding:40, color:BRAND.muted }}>Loading…</div>;
  if (me === false) return <LoginGate onSignedIn={(m) => { setMe(m); loadProjects(); }}/>;

  return (
    <div style={{ background:BRAND.paper, minHeight:"100vh", color:BRAND.ink }}>
      {/* Top bar — 3-column: logo left, system name centered, user right */}
      <div style={{ borderBottom:`1px solid ${BRAND.line}`, background:BRAND.card,
        padding:"12px 26px", display:"flex", alignItems:"center", gap:14 }}>
        <div style={{ flex:1, display:"flex", alignItems:"center" }}>
          <a href="/" onClick={(e) => { e.preventDefault(); setProject(null); window.history.pushState({}, '', '/'); }}
             style={{ textDecoration:"none" }}>
            <img src="/assets/novare-logo.png" alt="Nóvare Digital"
              style={{ height:36, display:"block" }}/>
          </a>
        </div>
        <div style={{ flex:1, textAlign:"center" }}>
          <div style={{ fontFamily:"Georgia, serif", fontSize:18, fontWeight:700, color:BRAND.ink }}>
            Client Website Review System
          </div>
          <div style={{ fontSize:10, color:BRAND.muted, letterSpacing:1.5, textTransform:"uppercase" }}>
            clientreview.NovareDigital.com
          </div>
        </div>
        <div style={{ flex:1, display:"flex", alignItems:"center", gap:12, justifyContent:"flex-end" }}>
          {project && (
            <span style={{ fontSize:11, fontWeight:700, padding:"4px 10px", borderRadius:20,
              background: project.your_role === 'approver' ? "#dcebe2" : "#f6ecd4",
              color:      project.your_role === 'approver' ? BRAND.green : BRAND.amber,
              border: `1px solid ${project.your_role === 'approver' ? BRAND.green : BRAND.amber}` }}
              title={project.your_role === 'approver'
                ? "You are the Approver — your decisions are final."
                : `You are a Reviewer — ${project.approver_display || 'the Approver'} makes the final call.`}>
              {project.your_role === 'approver' ? "APPROVER" : "REVIEWER"}
            </span>
          )}
          <span style={{ fontSize:12, color:BRAND.muted }}>
            Signed in as <strong style={{ color:BRAND.ink }}>{me.email}</strong>
          </span>
          <button onClick={signOut}
            style={{ fontSize:12, color:BRAND.muted, background:"none", border:`1px solid ${BRAND.line}`,
              borderRadius:4, padding:"5px 10px", cursor:"pointer" }}>Sign out</button>
        </div>
      </div>

      {err && (
        <div style={{ padding:"10px 26px", background:"#fee", color:"#900", fontSize:13 }}>
          {err} <button onClick={() => setErr(null)} style={{ marginLeft:8, border:"none", background:"transparent", cursor:"pointer" }}>×</button>
        </div>
      )}

      {/* Project list (when no project open) */}
      {!project && (
        <div style={{ maxWidth:1100, margin:"0 auto", padding:26 }}>
          <h1 style={{ fontFamily:"Georgia, serif", fontSize:26, margin:"0 0 6px" }}>Your projects</h1>
          <div style={{ fontSize:13, color:BRAND.muted, marginBottom:20 }}>
            {projects.length === 0
              ? "No projects shared with your account yet. Novare will email you when one is ready."
              : `${projects.length} project${projects.length === 1 ? "" : "s"} available for review.`}
          </div>
          <div style={{ display:"grid", gridTemplateColumns:"repeat(auto-fill,minmax(280px,1fr))", gap:14 }}>
            {projects.map(p => (
              <button key={p.id} onClick={() => openProject(p.id)}
                style={{ textAlign:"left", background:"#fff", border:`1px solid ${BRAND.line}`,
                  borderRadius:6, padding:18, cursor:"pointer", boxShadow:"0 2px 8px rgba(0,0,0,.04)" }}>
                <div style={{ fontWeight:700, fontSize:16, marginBottom:4 }}>{p.name}</div>
                <div style={{ fontSize:12, color:BRAND.muted }}>{p.client_label || p.customer_name}</div>
                <div style={{ marginTop:10, fontSize:11, color:BRAND.muted }}>
                  Created {new Date(p.created_at).toLocaleDateString()}
                </div>
              </button>
            ))}
          </div>
        </div>
      )}

      {/* Approver acknowledgment modal — shown ONCE per Approver per project */}
      {project && project.needs_approver_acknowledgment && (
        <ApproverAcknowledgmentModal project={project}
          onAccepted={() => openProject(project.id)}/>
      )}

      {/* Preview modal (#H) — multi-viewport view of the actual draft site */}
      {project && previewOpen && version && (
        <PreviewModal project={project} version={version}
          onClose={() => setPreviewOpen(false)}/>
      )}

      {/* #K — Final Approval modal */}
      {project && finalApproveOpen && (
        <FinalApproveModal project={project}
          onClose={() => setFinalApproveOpen(false)}
          onApproved={(r) => {
            setFinalApproveOpen(false);
            setFinalApprovalJustSent(r);
            api(`/projects/${project.id}/final-approval`).then(setFinalApprovalState);
          }}/>
      )}

      {/* #K — Post-final-approval toast */}
      {finalApprovalJustSent && (
        <div style={{ position:"fixed", bottom:20, right:20, zIndex:150,
          background:BRAND.green, color:"#fff", padding:"14px 18px", borderRadius:6,
          boxShadow:"0 8px 24px rgba(0,0,0,.2)", maxWidth:380 }}>
          <strong>✓ Final approval submitted</strong> for {finalApprovalJustSent.project_name}.<br/>
          Nóvare has been notified and will upload your site to your domain shortly.
          You'll receive an email when it's live.
          <button onClick={() => setFinalApprovalJustSent(null)}
            style={{ marginLeft:12, background:"transparent", border:"none", color:"#fff", cursor:"pointer", fontSize:18 }}>×</button>
        </div>
      )}

      {/* #K — Status banner when a final approval is in flight or done */}
      {project && finalApprovalState && finalApprovalState.staff_decision === null && !finalApprovalJustSent && (
        <div style={{ background:"#dcebe2", borderLeft:`4px solid ${BRAND.green}`,
          padding:"12px 18px", margin:"0 26px 14px", borderRadius:4, fontSize:13, maxWidth:1346 }}>
          <strong style={{ color:BRAND.green }}>⏳ Final approval pending Nóvare review.</strong>
          {" "}You approved this website on {new Date(finalApprovalState.approved_at).toLocaleString()}.
          We'll upload it to your domain and email you when it's live.
        </div>
      )}
      {project && finalApprovalState && finalApprovalState.staff_decision === 'uploaded' && (
        <div style={{ background:"#dcebe2", borderLeft:`4px solid ${BRAND.green}`,
          padding:"12px 18px", margin:"0 26px 14px", borderRadius:4, fontSize:13, maxWidth:1346 }}>
          <strong style={{ color:BRAND.green }}>🎉 Your website is live!</strong>
          {" "}Uploaded to your domain on {new Date(finalApprovalState.reviewed_at || finalApprovalState.approved_at).toLocaleString()}.
        </div>
      )}

      {/* Submit-feedback bundle modal — Approver only */}
      {project && submitOpen && version && (
        <SubmitBundleModal project={project} version={version}
          onClose={() => setSubmitOpen(false)}
          onSubmitted={(r) => {
            setSubmitOpen(false);
            setSubmitted(r);
            // refresh comments so locked badges appear
            api(`/projects/${project.id}/comments`).then(setComments).catch(() => {});
          }}/>
      )}

      {/* Post-submit confirmation toast */}
      {submitted && (
        <div style={{ position:"fixed", bottom:20, right:20, zIndex:150,
          background:BRAND.green, color:"#fff", padding:"14px 18px", borderRadius:6,
          boxShadow:"0 8px 24px rgba(0,0,0,.2)", maxWidth:360 }}>
          <strong>Submitted ✓</strong> — bundle #{submitted.submission_id} locked
          {" "}({submitted.locked_comments} comments) on {submitted.version_label}.
          Nóvare has been notified.
          <button onClick={() => setSubmitted(null)}
            style={{ marginLeft:12, background:"transparent", border:"none", color:"#fff", cursor:"pointer", fontSize:18 }}>×</button>
        </div>
      )}

      {/* Project detail view */}
      {project && (
        <div style={{ display:"flex", gap:24, padding:26, alignItems:"flex-start", maxWidth:1400, margin:"0 auto" }}>
          <div style={{ flex:1, minWidth:0 }}>
            <RoleBanner project={project}/>
            <div style={{ marginBottom:16 }}>
              <h1 style={{ fontFamily:"Georgia, serif", fontSize:26, margin:0 }}>{project.name}</h1>
              <div style={{ fontSize:13, color:BRAND.muted, marginTop:4 }}>
                {project.client_label || project.customer_name}
              </div>
            </div>

            {/* How-to-use strip — visible on every project view so new reviewers
                aren't confused.  Three steps + Preview, color-coded to match
                the button that does each step. */}
            <div style={{ background:"#fff", border:`1px solid ${BRAND.line}`, borderRadius:6,
              padding:"14px 18px", marginBottom:16, display:"flex", gap:16, alignItems:"center", flexWrap:"wrap" }}>
              <div style={{ fontWeight:700, fontSize:12, color:BRAND.muted, letterSpacing:1,
                whiteSpace:"nowrap" }}>HOW TO USE:</div>
              <div style={{ display:"flex", gap:14, flexWrap:"wrap", flex:1, fontSize:13 }}>
                <div style={{ display:"flex", gap:8, alignItems:"center" }}>
                  <span style={{ background:BRAND.accent, color:"#fff", width:22, height:22,
                    borderRadius:"50%", display:"flex", alignItems:"center", justifyContent:"center",
                    fontWeight:700, fontSize:12 }}>1</span>
                  Click <strong>+ Add Comment</strong>
                </div>
                <div style={{ display:"flex", gap:8, alignItems:"center" }}>
                  <span style={{ background:BRAND.green, color:"#fff", width:22, height:22,
                    borderRadius:"50%", display:"flex", alignItems:"center", justifyContent:"center",
                    fontWeight:700, fontSize:12 }}>2</span>
                  Tap the spot on the design you want changed
                </div>
                <div style={{ display:"flex", gap:8, alignItems:"center" }}>
                  <span style={{ background:BRAND.ink, color:"#fff", width:22, height:22,
                    borderRadius:"50%", display:"flex", alignItems:"center", justifyContent:"center",
                    fontWeight:700, fontSize:12 }}>3</span>
                  Type your change and click <strong>Post Comment</strong>
                </div>
                <div style={{ display:"flex", gap:8, alignItems:"center", color:BRAND.muted, paddingLeft:10, borderLeft:`1px solid ${BRAND.line}` }}>
                  👁 Click <strong style={{ color:BRAND.ink }}>Preview</strong> anytime to see the draft live at desktop / tablet / phone sizes.
                </div>
              </div>
            </div>

            {project.versions.length === 0 ? (
              <div style={{ padding:30, background:"#fff", border:`1px solid ${BRAND.line}`, borderRadius:6, color:BRAND.muted }}>
                No design versions uploaded yet.
              </div>
            ) : (
              <>
                <div style={{ display:"flex", gap:8, marginBottom:16, flexWrap:"wrap", alignItems:"center" }}>
                  {project.versions.map(v => (
                    <button key={v.id}
                      onClick={() => { setVersion(v); setCompare(false); setActiveId(null); }}
                      style={{ padding:"7px 14px", borderRadius:4, fontSize:13, fontWeight:700, cursor:"pointer",
                        border:`1px solid ${version && version.id === v.id && !compare ? BRAND.ink : BRAND.line}`,
                        background: version && version.id === v.id && !compare ? BRAND.ink : "#fff",
                        color:    version && version.id === v.id && !compare ? "#fff" : BRAND.ink }}>
                      {v.label} · {new Date(v.created_at).toLocaleDateString()}
                    </button>
                  ))}
                  {project.versions.length >= 2 && (
                    <button onClick={() => setCompare(c => !c)}
                      style={{ padding:"7px 14px", borderRadius:4, fontSize:13, fontWeight:700, cursor:"pointer",
                        border:`1px solid ${compare ? BRAND.accent : BRAND.line}`,
                        background: compare ? BRAND.accent : "#fff", color: compare ? "#fff" : BRAND.ink }}>
                      ⇄ Compare last two
                    </button>
                  )}
                  <div style={{ flex:1 }}/>
                  <button onClick={() => setPreviewOpen(true)}
                    style={{ padding:"7px 16px", borderRadius:4, fontSize:13, fontWeight:700,
                      border:`1px solid ${BRAND.ink}`, background:"#fff", color:BRAND.ink, cursor:"pointer" }}
                    title="See the draft website live at different screen sizes — desktop, tablet, phone.">
                    👁 Preview
                  </button>
                  <button onClick={() => { setPlacing(p => !p); setDraft(null); }} disabled={compare}
                    style={{ padding:"7px 16px", borderRadius:4, fontSize:13, fontWeight:700,
                      cursor: compare ? "not-allowed" : "pointer", opacity: compare ? 0.4 : 1,
                      border:"none", background: placing ? BRAND.green : BRAND.accent, color:"#fff" }}>
                    {placing ? "Click a design to drop a pin…" : "+ Add Comment"}
                  </button>
                  {project.your_role === 'approver' && version && (
                    <button onClick={() => setSubmitOpen(true)}
                      style={{ padding:"7px 16px", borderRadius:4, fontSize:13, fontWeight:700,
                        border:"none", background:BRAND.green, color:"#fff", cursor:"pointer" }}
                      title="Bundle open comments into a write-once change request to Novare">
                      ✓ Submit feedback for {version.label}
                    </button>
                  )}
                  {/* #K — Final Approval button: Approver only, when there's a clickable draft
                       AND no pending/uploaded final approval exists yet */}
                  {project.your_role === 'approver' && version
                    && version.assets.some(a => a.kind === 'site')
                    && (!finalApprovalState || finalApprovalState.staff_decision === 'rejected') && (
                    <button onClick={() => setFinalApproveOpen(true)}
                      style={{ padding:"7px 18px", borderRadius:4, fontSize:13, fontWeight:800,
                        border:"none", background:BRAND.green, color:"#fff", cursor:"pointer",
                        boxShadow:"0 2px 8px rgba(63,125,92,0.4)" }}
                      title="Final approval — authorize Novare to publish this site to your live domain">
                      🚀 Approve Final Website
                    </button>
                  )}
                </div>

                <div style={{ display:"flex", gap:18 }}>
                  {compare ? (() => {
                    const last = project.versions.slice(-2);
                    return last.map(v => (
                      <div key={v.id} style={{ flex:1, minWidth:0 }}>
                        <div style={{ fontSize:12, fontWeight:700, color:BRAND.muted, marginBottom:8, letterSpacing:1 }}>
                          {v.label} · {new Date(v.created_at).toLocaleDateString()}
                        </div>
                        {v.assets.map(a => (
                          <AssetCanvas key={a.id} asset={a} comments={comments.filter(c=>c.version_id===v.id)}
                            placing={false} draft={null} onPlace={() => {}} canPlace={false}
                            activeId={activeId} onPinClick={setActiveId}/>
                        ))}
                      </div>
                    ));
                  })() : version && version.assets.map(a => (
                    <div key={a.id} style={{ flex:1, minWidth:0 }}>
                      <AssetCanvas asset={a} comments={versionComments.filter(c=>c.asset_id===a.id)}
                        placing={placing} draft={draft} onPlace={onPlace} canPlace={true}
                        activeId={activeId} onPinClick={(id) => setActiveId(activeId === id ? null : id)}/>
                    </div>
                  ))}
                </div>
                {/* Single-asset-per-row fallback: if only one asset, the loop above renders correctly. */}

                {draft && (
                  <div style={{ marginTop:16, background:"#fff", border:`1px solid ${BRAND.line}`, borderRadius:6, padding:16 }}>
                    <div style={{ fontSize:12, fontWeight:700, color:BRAND.muted, marginBottom:8 }}>
                      NEW COMMENT · pinned at {draft.x_pct.toFixed(0)}%, {draft.y_pct.toFixed(0)}%
                    </div>
                    <textarea autoFocus value={draftText} onChange={(e) => setDraftText(e.target.value)}
                      placeholder="Describe the change you'd like…"
                      style={{ width:"100%", minHeight:60, padding:10, border:`1px solid ${BRAND.line}`,
                        borderRadius:4, fontSize:14, fontFamily:"inherit", resize:"vertical" }}/>
                    <div style={{ display:"flex", gap:8, marginTop:10 }}>
                      <button onClick={saveDraft}
                        style={{ padding:"7px 16px", border:"none", background:BRAND.accent, color:"#fff",
                          borderRadius:4, fontWeight:700, fontSize:13, cursor:"pointer" }}>Post Comment</button>
                      <button onClick={() => { setDraft(null); setDraftText(""); }}
                        style={{ padding:"7px 16px", border:`1px solid ${BRAND.line}`, background:"#fff",
                          borderRadius:4, fontSize:13, cursor:"pointer" }}>Cancel</button>
                    </div>
                  </div>
                )}
              </>
            )}
          </div>

          {/* Sidebar */}
          <div style={{ width:340, flexShrink:0 }}>
            <div style={{ background:"#fff", border:`1px solid ${BRAND.line}`, borderRadius:6, overflow:"hidden" }}>
              <div style={{ padding:"14px 16px", borderBottom:`1px solid ${BRAND.line}` }}>
                <div style={{ fontWeight:800, fontSize:15 }}>Feedback</div>
                <div style={{ display:"flex", gap:6, marginTop:10, flexWrap:"wrap" }}>
                  {["all", "open", "progress", "resolved"].map(f => (
                    <button key={f} onClick={() => setFilter(f)}
                      style={{ padding:"4px 10px", borderRadius:20, fontSize:11, fontWeight:700, cursor:"pointer",
                        border:`1px solid ${filter === f ? BRAND.ink : BRAND.line}`,
                        background: filter === f ? BRAND.ink : "#fff",
                        color:    filter === f ? "#fff" : BRAND.muted }}>
                      {f === "all" ? "All" : STATUS[f].label}{f !== "all" ? ` ${counts[f]}` : ` ${versionComments.length}`}
                    </button>
                  ))}
                </div>
              </div>
              <div style={{ maxHeight:520, overflowY:"auto" }}>
                {visibleComments.length === 0 ? (
                  <div style={{ padding:24, textAlign:"center", color:BRAND.muted, fontSize:13 }}>
                    No comments in this view.
                  </div>
                ) : visibleComments.map((c, i) => (
                  <CommentRow key={c.id} c={c} idx={versionComments.indexOf(c)}
                    active={activeId === c.id}
                    isApprover={project.your_role === 'approver'}
                    onClick={(id) => setActiveId(activeId === id ? null : id)}
                    onReply={addReply}
                    onResolve={resolveComment}/>
                ))}
              </div>
              <div style={{ padding:"12px 16px", borderTop:`1px solid ${BRAND.line}`, fontSize:11, color:BRAND.muted }}>
                {counts.open} open · {counts.progress} in progress · {counts.resolved} resolved
              </div>
            </div>
            <div style={{ marginTop:14, fontSize:11, color:BRAND.muted, lineHeight:1.6 }}>
              Status changes and Novare replies happen on the staff side at <strong>nod.novaredigital.com</strong>.
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
