// MLuppens Website — Admin (Marieke-facing)
//
// Screens:
//   AdminLoginScreen       /admin/login
//   AdminDashboardScreen   /admin
//   AdminGalleryScreen     /admin/galleries/<slug>
//
// API contract:
//   POST   /api/admin/login
//   GET    /api/admin/galleries
//   POST   /api/admin/galleries
//   GET    /api/admin/galleries/:slug
//   PATCH  /api/admin/galleries/:slug
//   DELETE /api/admin/galleries/:slug
//   POST   /api/admin/photos/upload-url
//   PUT    /api/admin/photos/data/:photoId
//   POST   /api/admin/photos/commit
//   DELETE /api/admin/photos/:id
//   POST   /api/admin/sections
//   PATCH  /api/admin/sections/:id
//   DELETE /api/admin/sections/:id
//   POST   /api/admin/reorder
//   GET    /api/admin/testimonials
//   POST   /api/admin/testimonials
//   PATCH  /api/admin/testimonials/:id
//   DELETE /api/admin/testimonials/:id

// ── Utilities ────────────────────────────────────────────────

function slugify(name) {
  return (name || '').trim().toLowerCase()
    .replace(/&/g, ' and ').replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '').slice(0, 60) || 'untitled';
}

function genPassword(len = 8) {
  const chars = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
  const buf = new Uint32Array(len);
  crypto.getRandomValues(buf);
  return Array.from(buf, n => chars[n % chars.length]).join('');
}

function fmtBytes(n) {
  if (n < 1048576) return `${(n / 1024).toFixed(1)} KB`;
  return `${(n / 1048576).toFixed(1)} MB`;
}

function isImageFile(f) {
  return /^image\//i.test(f.type) || /\.(jpe?g|png|webp|avif)$/i.test(f.name);
}

// Given files with optional folder paths, group them by their immediate parent
// folder (auto-stripping a single common container). Returns each file annotated
// with `section` (string|null) — null means "no chapter."
function deriveSections(files) {
  const withPaths = files.map(f => ({
    file: f,
    path: f._relPath || f.webkitRelativePath || f.name,
  }));
  const parents = withPaths.map(fp => {
    const parts = fp.path.split('/'); parts.pop();
    return parts.join('/');
  });
  if (new Set(parents).size <= 1) {
    return withPaths.map(fp => ({ file: fp.file, section: null }));
  }
  // Longest common prefix among parents — usually the dropped container folder
  const sorted = [...new Set(parents)].sort();
  let prefix = sorted[0];
  for (const p of sorted) {
    while (prefix && !p.startsWith(prefix)) prefix = prefix.slice(0, -1);
  }
  if (prefix && !prefix.endsWith('/')) {
    const idx = prefix.lastIndexOf('/');
    prefix = idx === -1 ? '' : prefix.slice(0, idx + 1);
  }
  return withPaths.map((fp, i) => {
    let rel = parents[i].slice(prefix.length).replace(/^\/+/, '');
    const seg = rel ? rel.split('/')[0] : '';
    return { file: fp.file, section: cleanSectionName(seg) };
  });
}

function cleanSectionName(name) {
  if (!name) return null;
  // Strip leading numbers like "01 ", "1. ", "01-_"
  const stripped = name.replace(/^[0-9]+[\s\-_.]+/, '').trim();
  return stripped || name.trim() || null;
}

async function api(path, opts = {}) {
  const res = await fetch(path, { credentials: 'include', ...opts });
  if (res.status === 204) return null;
  return res;
}

function entryToFiles(entry, path = '') {
  return new Promise((resolve) => {
    if (entry.isFile) {
      entry.file((f) => {
        try { Object.defineProperty(f, '_relPath', { value: path + entry.name }); } catch (_) {}
        resolve([f]);
      }, () => resolve([]));
    } else if (entry.isDirectory) {
      const reader = entry.createReader();
      const all = [];
      const readBatch = () => {
        reader.readEntries(async (entries) => {
          if (!entries.length) { resolve(all); return; }
          for (const e of entries) all.push(...await entryToFiles(e, path + entry.name + '/'));
          readBatch();
        }, () => resolve(all));
      };
      readBatch();
    } else resolve([]);
  });
}

async function collectDroppedFiles(dt) {
  const out = [];
  if (dt?.items?.length && typeof dt.items[0].webkitGetAsEntry === 'function') {
    const entries = Array.from(dt.items).map(it => it.webkitGetAsEntry()).filter(Boolean);
    for (const e of entries) out.push(...await entryToFiles(e));
  } else if (dt?.files) {
    out.push(...Array.from(dt.files));
  }
  return out.filter(isImageFile);
}

function readImageDims(file) {
  return new Promise((resolve, reject) => {
    const url = URL.createObjectURL(file);
    const img = new Image();
    img.onload = () => { resolve({ width: img.naturalWidth, height: img.naturalHeight }); URL.revokeObjectURL(url); };
    img.onerror = () => { URL.revokeObjectURL(url); reject(); };
    img.src = url;
  });
}

// ── Shared chrome ────────────────────────────────────────────

function AdminChrome({ title, breadcrumbs, right }) {
  return (
    <div style={{
      borderBottom: '1px solid var(--hair)',
      padding: '20px 40px',
      display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24,
      background: 'var(--linen)',
      position: 'sticky', top: 0, zIndex: 100,
    }}>
      <div>
        {breadcrumbs && (
          <div style={{ fontFamily: 'var(--font-body)', fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: 'var(--ink-mute)', marginBottom: 4 }}>
            {breadcrumbs}
          </div>
        )}
        <h1 style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 300, fontSize: 28, margin: 0, color: 'var(--ink)', letterSpacing: '-0.01em' }}>
          {title}
        </h1>
      </div>
      <div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>{right}</div>
    </div>
  );
}

// Small italic "i" dot that toggles a popover with help text. Click anywhere
// else (or press Escape) to dismiss. Popover side auto-flips to whichever
// edge has more viewport room; pass `side` ('left'|'right') to force one.
function InfoDot({ children, side }) {
  const [open, setOpen]         = React.useState(false);
  const [resolved, setResolved] = React.useState(side || 'left');
  const wrapRef = React.useRef(null);
  const btnRef  = React.useRef(null);
  const POPOVER_W = 280;

  React.useEffect(() => {
    if (!open) return;
    if (side) {
      setResolved(side);
    } else if (btnRef.current) {
      const r = btnRef.current.getBoundingClientRect();
      const roomRight = window.innerWidth - r.right - 16;
      // 'left' anchors left edge of popover to dot → extends right; 'right' extends left.
      setResolved(roomRight >= POPOVER_W ? 'left' : 'right');
    }
    const onDown = e => { if (!wrapRef.current?.contains(e.target)) setOpen(false); };
    const onKey  = e => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDown);
      document.removeEventListener('keydown', onKey);
    };
  }, [open, side]);

  return (
    <span ref={wrapRef} style={{ position: 'relative', display: 'inline-flex', verticalAlign: 'middle', marginLeft: 8 }}>
      <button
        ref={btnRef}
        type="button"
        aria-label="What does this do?"
        onClick={e => { e.stopPropagation(); setOpen(o => !o); }}
        style={{
          width: 16, height: 16, borderRadius: '50%',
          border: '1px solid var(--hair)',
          background: open ? 'var(--ink)' : 'transparent',
          color: open ? 'var(--linen)' : 'var(--ink-mute)',
          fontFamily: 'var(--font-display)', fontStyle: 'italic',
          fontSize: 10, fontWeight: 400, lineHeight: 1,
          cursor: 'pointer', padding: 0,
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          transition: 'background .15s var(--ease), color .15s var(--ease)',
        }}
      >i</button>
      {open && (
        <div
          role="tooltip"
          style={{
            position: 'absolute', top: 'calc(100% + 8px)',
            [resolved]: 0, zIndex: 50,
            width: POPOVER_W, maxWidth: 'calc(100vw - 32px)',
            background: 'var(--linen)',
            border: '1px solid var(--hair)', padding: '14px 16px',
            fontFamily: 'var(--font-body)', fontSize: 12, lineHeight: 1.55,
            color: 'var(--ink-soft)', fontStyle: 'normal', textTransform: 'none', letterSpacing: 'normal',
            boxShadow: '0 6px 20px rgba(26,24,22,0.10)',
          }}
        >
          {children}
        </div>
      )}
    </span>
  );
}

// Small inline type badge pills
function TypeBadge({ label, active }) {
  if (!active) return null;
  return (
    <span style={{
      fontFamily: 'var(--font-body)', fontSize: 9, fontWeight: 500,
      letterSpacing: '0.2em', textTransform: 'uppercase',
      padding: '3px 8px', border: '1px solid var(--hair)',
      color: 'var(--ink-soft)', background: 'var(--bone)',
    }}>{label}</span>
  );
}

// ── LOGIN ────────────────────────────────────────────────────

function AdminLoginScreen({ go }) {
  const [password, setPassword] = React.useState('');
  const [error, setError] = React.useState('');
  const [submitting, setSubmitting] = React.useState(false);

  const onSubmit = async (e) => {
    e.preventDefault();
    if (!password.trim()) return;
    setSubmitting(true); setError('');
    try {
      const res = await fetch('/api/admin/login', {
        method: 'POST', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ password: password.trim() }),
      });
      if (res.ok) go('admin');
      else setError('Wrong password.');
    } catch { setError("Couldn't reach the server."); }
    finally { setSubmitting(false); }
  };

  return (
    <section style={{ minHeight: '88vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '120px 40px 80px' }}>
      <div style={{ maxWidth: 420, width: '100%', textAlign: 'center' }}>
        <Eyebrow>Admin</Eyebrow>
        <h1 className="display" style={{ marginTop: 24, marginBottom: 24, fontSize: 'clamp(36px, 4.5vw, 56px)' }}>
          Studio access.
        </h1>
        <Rule className="center" />
        <p className="body" style={{ color: 'var(--ink-soft)', marginTop: 24, marginBottom: 36, fontWeight: 300 }}>
          For Marieke only.
        </p>
        <form onSubmit={onSubmit} style={{ textAlign: 'left' }}>
          <input type="text" name="username" value="marieke" autoComplete="username" readOnly style={{ display: 'none' }} />
          <FormField label="Password" type="password" value={password} onChange={e => setPassword(e.target.value)} autoComplete="current-password" />
          {error && (
            <div style={{ marginTop: 8, padding: '12px 14px', background: 'rgba(92,34,48,0.08)', color: 'var(--velvet)', fontStyle: 'italic', fontSize: 13 }}>
              {error}
            </div>
          )}
          <div style={{ marginTop: 24 }}>
            <button type="submit" className="btn" disabled={submitting} style={{ width: '100%', justifyContent: 'center' }}>
              {submitting ? 'Opening…' : 'Sign in'}
            </button>
          </div>
        </form>
      </div>
    </section>
  );
}

// ── DASHBOARD ────────────────────────────────────────────────

const FILTERS = ['All', 'Wedding', 'Portfolio', 'Recent Work', 'Testimonials'];

function AdminDashboardScreen({ go }) {
  const [phase, setPhase] = React.useState('loading');
  const [galleries, setGalleries] = React.useState([]);
  const [filter, setFilter] = React.useState('All');
  const [showWizard, setShowWizard] = React.useState(false);
  const [ghostCount, setGhostCount] = React.useState(0);
  const [cleaning, setCleaning] = React.useState(false);

  const refresh = React.useCallback(() => {
    setPhase('loading');
    fetch('/api/admin/galleries', { credentials: 'include' })
      .then(async r => {
        if (r.status === 401) { go('admin-login'); return; }
        if (!r.ok) { setPhase('error'); return; }
        setGalleries(await r.json());
        setPhase('ready');
      })
      .catch(() => setPhase('error'));
    fetch('/api/admin/cleanup', { credentials: 'include' })
      .then(r => r.ok ? r.json() : { count: 0 })
      .then(d => setGhostCount(d.count || 0))
      .catch(() => {});
  }, [go]);

  React.useEffect(() => { refresh(); }, [refresh]);

  const cleanGhosts = async () => {
    setCleaning(true);
    await fetch('/api/admin/cleanup', { method: 'POST', credentials: 'include' });
    setGhostCount(0);
    setCleaning(false);
  };

  const visible = React.useMemo(() => {
    if (filter === 'Wedding')     return galleries.filter(g => g.is_wedding);
    if (filter === 'Portfolio')   return galleries.filter(g => g.is_portfolio);
    if (filter === 'Recent Work') return galleries.filter(g => g.is_recent_work);
    if (filter === 'Testimonials') return [];
    return galleries;
  }, [galleries, filter]);

  return (
    <>
      <AdminChrome
        title="Studio"
        right={
          filter !== 'Testimonials' && (
            <button className="btn" onClick={() => setShowWizard(true)}>New album</button>
          )
        }
      />

      {/* Filter strip */}
      <div style={{ borderBottom: '1px solid var(--hair)', padding: '0 40px', display: 'flex', gap: 0, alignItems: 'center' }}>
        {FILTERS.map(f => (
          <button
            key={f}
            onClick={() => setFilter(f)}
            style={{
              fontFamily: 'var(--font-body)', fontWeight: 500, fontSize: 11,
              letterSpacing: '0.2em', textTransform: 'uppercase',
              background: 'none', border: 'none', cursor: 'pointer',
              padding: '16px 20px',
              color: filter === f ? 'var(--ink)' : 'var(--ink-mute)',
              borderBottom: filter === f ? '2px solid var(--ink)' : '2px solid transparent',
              marginBottom: -1,
              transition: 'color .2s',
            }}
          >{f}</button>
        ))}
        <div style={{ marginLeft: 'auto' }}>
          <InfoDot side="right">
            <strong>Filter by where albums appear.</strong>
            <div style={{ marginTop: 6 }}>
              One album can live in multiple places — Wedding, Portfolio, Recent Work — at the same time.
              Set this when creating the album or in its Settings tab.
            </div>
          </InfoDot>
        </div>
      </div>

      <section style={{ padding: '32px 40px' }}>
        {phase === 'loading' && (
          <p className="small" style={{ color: 'var(--ink-mute)', fontStyle: 'italic' }}>Loading…</p>
        )}
        {phase === 'error' && (
          <p className="small" style={{ color: 'var(--velvet)', fontStyle: 'italic' }}>
            Couldn't load albums. The database may not be set up yet — apply <code>schema.sql</code> in the D1 console first.
          </p>
        )}

        {phase === 'ready' && filter === 'Testimonials' && (
          <AdminTestimonialsSection />
        )}

        {phase === 'ready' && filter !== 'Testimonials' && visible.length === 0 && (
          <div style={{ textAlign: 'center', padding: '80px 0', color: 'var(--ink-mute)' }}>
            <p className="body" style={{ fontStyle: 'italic' }}>No albums yet.</p>
            <button className="btn btn-rule" style={{ marginTop: 20 }} onClick={() => setShowWizard(true)}>
              Create your first one
            </button>
          </div>
        )}

        {phase === 'ready' && filter !== 'Testimonials' && visible.length > 0 && (
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 16 }}>
            {visible.map(g => (
              <GalleryCard key={g.slug} g={g} onClick={() => go('admin-gallery', g.slug)} />
            ))}
          </div>
        )}

        {phase === 'ready' && ghostCount > 0 && (
          <div style={{ marginTop: 48, paddingTop: 24, borderTop: '1px solid var(--hair)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}>
            <div className="small" style={{ color: 'var(--ink-mute)', fontStyle: 'italic' }}>
              {ghostCount} abandoned upload{ghostCount === 1 ? '' : 's'} from interrupted sessions.
              <InfoDot side="left">
                When an upload fails or a tab closes mid-upload, the photo leaves behind an empty record.
                These don't show in any album — this just clears them out of the database.
              </InfoDot>
            </div>
            <button className="btn-text-link" style={{ fontSize: 11 }} disabled={cleaning} onClick={cleanGhosts}>
              {cleaning ? 'Cleaning…' : 'Clean up'}
            </button>
          </div>
        )}
      </section>

      {showWizard && (
        <NewAlbumWizard
          onClose={() => setShowWizard(false)}
          onCreated={slug => { setShowWizard(false); go('admin-gallery', slug); }}
        />
      )}
    </>
  );
}

function GalleryCard({ g, onClick }) {
  return (
    <div
      onClick={onClick}
      style={{
        border: '1px solid var(--hair)', padding: 24, cursor: 'pointer',
        background: 'var(--linen)', transition: 'background .2s var(--ease)',
      }}
      onMouseEnter={e => { e.currentTarget.style.background = 'var(--bone)'; }}
      onMouseLeave={e => { e.currentTarget.style.background = 'var(--linen)'; }}
    >
      <div style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 300, fontSize: 22, color: 'var(--ink)', marginBottom: 10 }}>
        {g.display_name}
      </div>
      <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
        <TypeBadge label="Wedding"     active={g.is_wedding} />
        <TypeBadge label="Portfolio"   active={g.is_portfolio} />
        <TypeBadge label="Recent Work" active={g.is_recent_work} />
      </div>
      <div className="small" style={{ color: 'var(--ink-mute)' }}>
        {g.photo_count || 0} photos{g.event_date ? ` · ${g.event_date}` : ''}
      </div>
    </div>
  );
}

// ── NEW ALBUM WIZARD ─────────────────────────────────────────

function NewAlbumWizard({ onClose, onCreated }) {
  const [step, setStep] = React.useState(1);
  const [displayName, setDisplayName] = React.useState('');
  const [slug, setSlug] = React.useState('');
  const [slugTouched, setSlugTouched] = React.useState(false);
  const [types, setTypes] = React.useState({ wedding: false, portfolio: false, recent_work: false });
  const [password, setPassword] = React.useState(() => genPassword());
  const [eventDate, setEventDate] = React.useState('');
  const [location, setLocation] = React.useState('');
  const [note, setNote] = React.useState('');
  const [submitting, setSubmitting] = React.useState(false);
  const [error, setError] = React.useState('');

  const onName = e => {
    setDisplayName(e.target.value);
    if (!slugTouched) setSlug(slugify(e.target.value));
  };

  const toggleType = key => setTypes(t => ({ ...t, [key]: !t[key] }));
  const anyType = types.wedding || types.portfolio || types.recent_work;

  const submit = async () => {
    setSubmitting(true); setError('');
    try {
      const res = await fetch('/api/admin/galleries', {
        method: 'POST', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          slug: slug.trim().toLowerCase(),
          display_name: displayName.trim(),
          password: types.wedding ? password.trim() : undefined,
          event_date: eventDate.trim() || null,
          location: location.trim() || null,
          note: note.trim() || null,
          is_wedding: types.wedding ? 1 : 0,
          is_portfolio: types.portfolio ? 1 : 0,
          is_recent_work: types.recent_work ? 1 : 0,
        }),
      });
      if (res.status === 201) {
        const { slug: created } = await res.json();
        onCreated(created);
      } else {
        const body = await res.json().catch(() => ({}));
        setError(body.error || 'Something went wrong.');
      }
    } catch { setError("Couldn't reach the server."); }
    finally { setSubmitting(false); }
  };

  return (
    <Modal onClose={onClose}>
      {/* Step indicator */}
      <div style={{ display: 'flex', gap: 8, marginBottom: 32 }}>
        {[1, 2, 3].map(n => (
          <div key={n} style={{
            flex: 1, height: 2,
            background: step >= n ? 'var(--ink)' : 'var(--hair)',
            transition: 'background .3s',
          }} />
        ))}
      </div>

      {step === 1 && (
        <>
          <Eyebrow>Step 1 of 3</Eyebrow>
          <h2 className="h2" style={{ marginTop: 12, marginBottom: 32, fontStyle: 'italic', fontWeight: 300 }}>
            Name the album.
            <InfoDot>
              <strong>Display name</strong> is what visitors see — write it the way you'd say it out loud.
              <div style={{ marginTop: 6 }}>
                <strong>URL slug</strong> is the address: lowercase, hyphens only, kept short.
                It becomes <code style={{ fontSize: 11 }}>/your-slug</code> on the site.
              </div>
            </InfoDot>
          </h2>
          <FormField
            label="Album name"
            placeholder="Thijs & Rianne · Sorrento"
            value={displayName}
            onChange={onName}
          />
          <FormField
            label="URL slug"
            placeholder="thijs-rianne"
            value={slug}
            onChange={e => { setSlug(e.target.value); setSlugTouched(true); }}
            helper={`mariekeluppens.com/${slug || '…'}`}
          />
          <div style={{ display: 'flex', gap: 12, marginTop: 32 }}>
            <button className="btn btn-rule" onClick={onClose}>Cancel</button>
            <button
              className="btn" style={{ flex: 1, justifyContent: 'center' }}
              disabled={!displayName.trim() || !slug.trim()}
              onClick={() => setStep(2)}
            >Next →</button>
          </div>
        </>
      )}

      {step === 2 && (
        <>
          <Eyebrow>Step 2 of 3</Eyebrow>
          <h2 className="h2" style={{ marginTop: 12, marginBottom: 8, fontStyle: 'italic', fontWeight: 300 }}>
            Where does it live?
            <InfoDot>
              <strong>Wedding</strong> → private, password-protected gallery you share with the couple.
              <div style={{ marginTop: 6 }}><strong>Portfolio</strong> → public, shown on <code style={{ fontSize: 11 }}>/portfolio</code>.</div>
              <div style={{ marginTop: 6 }}><strong>Recent Work</strong> → public, shown on <code style={{ fontSize: 11 }}>/recent-work</code> with editorial text.</div>
              <div style={{ marginTop: 8, fontStyle: 'italic' }}>One album can be more than one at a time — e.g. a wedding that's also in the portfolio.</div>
            </InfoDot>
          </h2>
          <p className="small" style={{ color: 'var(--ink-mute)', marginBottom: 32 }}>
            An album can appear in multiple places at once.
          </p>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
            {[
              { key: 'wedding',     label: 'Wedding',     desc: 'Private gallery — password-protected for the couple' },
              { key: 'portfolio',   label: 'Portfolio',   desc: 'Shown publicly on the Portfolio page' },
              { key: 'recent_work', label: 'Recent Work', desc: 'Shown publicly on the Recent Work page' },
            ].map(({ key, label, desc }) => (
              <div
                key={key}
                onClick={() => toggleType(key)}
                style={{
                  border: `1px solid ${types[key] ? 'var(--ink)' : 'var(--hair)'}`,
                  padding: '16px 20px', cursor: 'pointer',
                  background: types[key] ? 'var(--bone)' : 'var(--linen)',
                  display: 'flex', alignItems: 'center', gap: 16,
                  transition: 'all .2s var(--ease)',
                }}
              >
                <div style={{
                  width: 16, height: 16, border: `1px solid ${types[key] ? 'var(--ink)' : 'var(--hair)'}`,
                  background: types[key] ? 'var(--ink)' : 'transparent', flexShrink: 0,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                }}>
                  {types[key] && <span style={{ color: 'var(--linen)', fontSize: 10 }}>✓</span>}
                </div>
                <div>
                  <div style={{ fontFamily: 'var(--font-body)', fontWeight: 500, fontSize: 13, color: 'var(--ink)' }}>{label}</div>
                  <div style={{ fontFamily: 'var(--font-body)', fontSize: 12, color: 'var(--ink-mute)', marginTop: 2 }}>{desc}</div>
                </div>
              </div>
            ))}
          </div>
          <div style={{ display: 'flex', gap: 12, marginTop: 32 }}>
            <button className="btn btn-rule" onClick={() => setStep(1)}>← Back</button>
            <button
              className="btn" style={{ flex: 1, justifyContent: 'center' }}
              disabled={!anyType}
              onClick={() => setStep(3)}
            >Next →</button>
          </div>
        </>
      )}

      {step === 3 && (
        <>
          <Eyebrow>Step 3 of 3</Eyebrow>
          <h2 className="h2" style={{ marginTop: 12, marginBottom: 32, fontStyle: 'italic', fontWeight: 300 }}>
            Details.
            <InfoDot>
              Only wedding albums need a password — give it to the couple along with the URL.
              <div style={{ marginTop: 6 }}>Date and location appear inside the client's gallery — they're optional.</div>
              <div style={{ marginTop: 6 }}>You can change all of this later in the album's Settings tab.</div>
            </InfoDot>
          </h2>
          {types.wedding && (
            <>
              <FormField
                label="Client password"
                value={password}
                onChange={e => setPassword(e.target.value)}
                helper="Copy this and send it to the couple after creating the album."
              />
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
                <FormField label="Date" placeholder="May 2026" value={eventDate} onChange={e => setEventDate(e.target.value)} />
                <FormField label="Location" placeholder="Villa Astor, Sorrento" value={location} onChange={e => setLocation(e.target.value)} />
              </div>
              <FormField
                label="Personal note (optional)"
                multiline
                placeholder="Your day, gently kept."
                value={note}
                onChange={e => setNote(e.target.value)}
              />
            </>
          )}
          {!types.wedding && (
            <p className="small" style={{ color: 'var(--ink-mute)', fontStyle: 'italic', marginBottom: 24 }}>
              No password needed — this album is public.
            </p>
          )}
          {error && (
            <div style={{ padding: '12px 14px', background: 'rgba(92,34,48,0.08)', color: 'var(--velvet)', fontSize: 13, fontStyle: 'italic', marginBottom: 16 }}>
              {error}
            </div>
          )}
          <div style={{ display: 'flex', gap: 12, marginTop: 24 }}>
            <button className="btn btn-rule" onClick={() => setStep(2)}>← Back</button>
            <button
              className="btn" style={{ flex: 1, justifyContent: 'center' }}
              disabled={submitting}
              onClick={submit}
            >{submitting ? 'Creating…' : 'Create album'}</button>
          </div>
        </>
      )}
    </Modal>
  );
}

// ── GALLERY SCREEN ───────────────────────────────────────────

function AdminGalleryScreen({ slug, go }) {
  const [phase, setPhase] = React.useState('loading');
  const [data, setData] = React.useState(null);   // { gallery, items[] }
  const [tab, setTab] = React.useState('photos'); // photos | settings
  const [uploads, setUploads] = React.useState([]);
  const [isDragOver, setIsDragOver] = React.useState(false);

  const load = React.useCallback(() => {
    if (!slug) return;
    fetch(`/api/admin/galleries/${encodeURIComponent(slug)}`, { credentials: 'include' })
      .then(async r => {
        if (r.status === 401) { go('admin-login'); return; }
        if (r.status === 404) { setPhase('missing'); return; }
        if (!r.ok) { setPhase('error'); return; }
        setData(await r.json());
        setPhase('ready');
      })
      .catch(() => setPhase('error'));
  }, [slug, go]);

  React.useEffect(() => { load(); }, [load]);

  // ── Upload pipeline (3-step: url → PUT → commit) ──────────

  const uploadFile = async (entry) => {
    const patch = p => setUploads(arr => arr.map(u => u.id === entry.id ? { ...u, ...p } : u));
    try {
      patch({ status: 'starting', progress: 0 });
      const initRes = await fetch('/api/admin/photos/upload-url', {
        method: 'POST', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          slug,
          filename: entry.file.name,
          mime: entry.file.type || 'image/jpeg',
          size: entry.file.size,
          sectionName: entry.section || null,
        }),
      });
      if (!initRes.ok) throw new Error(`init ${initRes.status}`);
      const { photoId, putUrl } = await initRes.json();

      await new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('PUT', putUrl);
        xhr.withCredentials = true;
        xhr.setRequestHeader('Content-Type', entry.file.type || 'image/jpeg');
        xhr.upload.onprogress = e => { if (e.lengthComputable) patch({ status: 'uploading', progress: e.loaded / e.total }); };
        xhr.onload = () => xhr.status < 300 ? resolve() : reject(new Error(`PUT ${xhr.status}`));
        xhr.onerror = () => reject(new Error('network error'));
        xhr.send(entry.file);
      });

      const dims = await readImageDims(entry.file).catch(() => ({ width: null, height: null }));
      const commitRes = await fetch('/api/admin/photos/commit', {
        method: 'POST', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ photoId, width: dims.width, height: dims.height }),
      });
      if (!commitRes.ok) throw new Error(`commit ${commitRes.status}`);
      patch({ status: 'done', progress: 1 });
    } catch (err) {
      patch({ status: 'error', error: err.message || 'failed' });
    }
  };

  // Concurrency-limited drain (max 3 in flight)
  React.useEffect(() => {
    const pending  = uploads.filter(u => u.status === 'queued');
    const inFlight = uploads.filter(u => u.status === 'starting' || u.status === 'uploading');
    if (!pending.length) {
      const busy = uploads.some(u => ['queued','starting','uploading'].includes(u.status));
      if (!busy && uploads.length) load();
      return;
    }
    pending.slice(0, Math.max(0, 3 - inFlight.length)).forEach(entry => {
      setUploads(arr => arr.map(u => u.id === entry.id ? { ...u, status: 'starting' } : u));
      uploadFile(entry);
    });
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [uploads]);

  const enqueue = async files => {
    if (!files?.length) return;
    const annotated = deriveSections(Array.from(files));
    const sectionNames = [...new Set(annotated.map(e => e.section).filter(Boolean))];

    if (sectionNames.length) {
      try {
        await fetch('/api/admin/sections/ensure', {
          method: 'POST', credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ slug, names: sectionNames }),
        });
        load(); // refresh items so the new section rows show up immediately
      } catch (_) {}
    }

    setUploads(arr => [...arr, ...annotated.map(({ file, section }) => ({
      id: crypto.randomUUID(), file, section, status: 'queued', progress: 0,
    }))]);
  };

  const onDragOver  = e => { e.preventDefault(); setIsDragOver(true); };
  const onDragLeave = ()  => setIsDragOver(false);
  const onDrop = async e => {
    e.preventDefault(); setIsDragOver(false);
    enqueue(await collectDroppedFiles(e.dataTransfer));
  };

  const retryUpload = (id) => {
    setUploads(arr => arr.map(u => u.id === id ? { ...u, status: 'queued', error: undefined, progress: 0 } : u));
  };

  const clearFinished = () => {
    setUploads(arr => arr.filter(u => !['done', 'error'].includes(u.status)));
  };

  // ── Sorted items state (local, synced to server on reorder) ─

  const [items, setItems] = React.useState([]);
  React.useEffect(() => { if (data) setItems(data.items); }, [data]);

  const saveOrder = React.useCallback(async (newItems) => {
    await fetch('/api/admin/reorder', {
      method: 'POST', credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ slug, order: newItems.map(it => ({ type: it.type, id: it.id })) }),
    });
  }, [slug]);

  const addSection = async () => {
    const title = prompt('Section title:');
    if (!title?.trim()) return;
    const res = await fetch('/api/admin/sections', {
      method: 'POST', credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ slug, title: title.trim(), sort_order: (items.length + 1) * 1000 }),
    });
    if (res.ok) load();
  };

  const deletePhoto = async (id) => {
    if (!confirm('Delete this photo?')) return;
    await fetch(`/api/admin/photos/${id}`, { method: 'DELETE', credentials: 'include' });
    setItems(it => it.filter(i => !(i.type === 'photo' && i.id === id)));
  };

  const deleteSection = async (id) => {
    await fetch(`/api/admin/sections/${id}`, { method: 'DELETE', credentials: 'include' });
    setItems(it => it.filter(i => !(i.type === 'section' && i.id === id)));
  };

  const [editingSection, setEditingSection] = React.useState(null);

  const saveSection = async (id, patch) => {
    await fetch(`/api/admin/sections/${id}`, {
      method: 'PATCH', credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(patch),
    });
    setItems(it => it.map(i => i.type === 'section' && i.id === id ? { ...i, ...patch } : i));
    setEditingSection(null);
  };

  const setCover = async (photoId) => {
    await fetch(`/api/admin/galleries/${slug}`, {
      method: 'PATCH', credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ cover_photo_id: photoId }),
    });
    setData(d => ({ ...d, gallery: { ...d.gallery, cover_photo_id: photoId } }));
  };

  // ── Drag-to-sort ──────────────────────────────────────────

  const dragIndex = React.useRef(null);

  const onItemDragStart = (e, i) => {
    dragIndex.current = i;
    e.dataTransfer.effectAllowed = 'move';
  };

  const onItemDragOver = (e, i) => {
    e.preventDefault();
    if (dragIndex.current === null || dragIndex.current === i) return;
    const newItems = [...items];
    const [moved] = newItems.splice(dragIndex.current, 1);
    newItems.splice(i, 0, moved);
    dragIndex.current = i;
    setItems(newItems);
  };

  const onItemDrop = (e) => {
    e.preventDefault();
    dragIndex.current = null;
    saveOrder(items);
  };

  // ── Loading states ────────────────────────────────────────

  if (phase === 'loading') return (
    <section style={{ minHeight: '60vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <p className="small" style={{ color: 'var(--ink-mute)', fontStyle: 'italic' }}>Loading…</p>
    </section>
  );
  if (phase === 'missing') return (
    <section style={{ minHeight: '60vh', display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center', padding: 40 }}>
      <div>
        <p className="body" style={{ fontStyle: 'italic', color: 'var(--ink-soft)' }}>This album doesn't exist.</p>
        <button className="btn btn-rule" style={{ marginTop: 16 }} onClick={() => go('admin')}>Back</button>
      </div>
    </section>
  );
  if (phase === 'error') return (
    <section style={{ minHeight: '60vh', display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center', padding: 40 }}>
      <div>
        <p className="body" style={{ color: 'var(--velvet)', fontStyle: 'italic' }}>Couldn't load this album.</p>
        <button className="btn btn-rule" style={{ marginTop: 16 }} onClick={load}>Retry</button>
      </div>
    </section>
  );

  const { gallery } = data;
  const activeCount = uploads.filter(u => !['done','error'].includes(u.status)).length;
  const doneCount   = uploads.filter(u => u.status === 'done').length;
  const errorCount  = uploads.filter(u => u.status === 'error').length;
  const totalBytes  = uploads.reduce((s, u) => s + (u.file.size || 0), 0);
  const totalUploadedBytes = uploads.reduce((s, u) => {
    if (u.status === 'done')      return s + u.file.size;
    if (u.status === 'uploading') return s + u.file.size * (u.progress || 0);
    return s;
  }, 0);
  const overallPct = totalBytes > 0 ? totalUploadedBytes / totalBytes : 0;
  const shareUrl    = `${window.location.origin}/${gallery.slug}`;

  return (
    <>
      <AdminChrome
        title={gallery.display_name}
        breadcrumbs={<span style={{ cursor: 'pointer' }} onClick={() => go('admin')}>← Studio</span>}
        right={
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            <TypeBadge label="Wedding"     active={gallery.is_wedding} />
            <TypeBadge label="Portfolio"   active={gallery.is_portfolio} />
            <TypeBadge label="Recent Work" active={gallery.is_recent_work} />
            {!!gallery.is_wedding && (
              <a className="btn-text-link" href={shareUrl} target="_blank" rel="noreferrer" style={{ marginLeft: 8 }}>
                View as client ↗
              </a>
            )}
          </div>
        }
      />

      {/* Tabs */}
      <div style={{ borderBottom: '1px solid var(--hair)', padding: '0 40px', display: 'flex' }}>
        {['photos', 'settings'].map(t => (
          <button key={t} onClick={() => setTab(t)} style={{
            fontFamily: 'var(--font-body)', fontWeight: 500, fontSize: 11,
            letterSpacing: '0.2em', textTransform: 'uppercase',
            background: 'none', border: 'none', cursor: 'pointer',
            padding: '14px 20px',
            color: tab === t ? 'var(--ink)' : 'var(--ink-mute)',
            borderBottom: tab === t ? '2px solid var(--ink)' : '2px solid transparent',
            marginBottom: -1, transition: 'color .2s',
          }}>{t === 'photos' ? `Photos (${items.filter(i => i.type === 'photo').length})` : 'Settings'}</button>
        ))}
      </div>

      {tab === 'photos' && (
        <section style={{ padding: '32px 40px' }}>
          {/* Share panel — wedding only */}
          {!!gallery.is_wedding && (
            <div style={{ background: 'var(--bone)', padding: '20px 24px', marginBottom: 32, border: '1px solid var(--hair)', display: 'flex', gap: 40, flexWrap: 'wrap' }}>
              <div>
                <div className="small" style={{ color: 'var(--ink-mute)', marginBottom: 4 }}>Client URL</div>
                <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
                  <code style={{ fontSize: 13, color: 'var(--ink)' }}>{shareUrl}</code>
                  <button className="btn-text-link" onClick={() => navigator.clipboard.writeText(shareUrl)}>Copy</button>
                </div>
              </div>
              <div>
                <div className="small" style={{ color: 'var(--ink-mute)', marginBottom: 4 }}>Password</div>
                <div className="small" style={{ fontStyle: 'italic', color: 'var(--ink-mute)' }}>
                  Set at creation · change in Settings tab
                </div>
              </div>
            </div>
          )}

          {/* Drop zone */}
          <div
            onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
            style={{
              border: `2px dashed ${isDragOver ? 'var(--ink)' : 'var(--hair)'}`,
              background: isDragOver ? 'var(--bone)' : 'transparent',
              padding: '48px 40px', textAlign: 'center',
              transition: 'background .15s var(--ease), border-color .15s var(--ease)',
              marginBottom: 32,
            }}
          >
            <div style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 300, fontSize: 24, color: 'var(--ink)', marginBottom: 6 }}>
              Drop photos or a folder here.
              <InfoDot>
                Drag a flat batch of photos, or a folder with subfolders.
                <div style={{ marginTop: 6 }}>
                  <strong>Subfolders become chapters</strong> — a folder called <code style={{ fontSize: 11 }}>01 Ceremony</code> creates a "Ceremony" chapter (the leading number is stripped).
                </div>
                <div style={{ marginTop: 6 }}>Only JPEG, PNG, WebP and AVIF are uploaded — sidecar files like .xmp are skipped.</div>
              </InfoDot>
            </div>
            <p className="small" style={{ color: 'var(--ink-mute)', marginBottom: 20, fontStyle: 'italic' }}>
              Your Lightroom exports, exactly as they are.
            </p>
            <div style={{ display: 'flex', gap: 12, justifyContent: 'center', flexWrap: 'wrap' }}>
              <label className="btn btn-rule" style={{ cursor: 'pointer' }}>
                Pick files
                <input type="file" multiple accept="image/*" style={{ display: 'none' }}
                  onChange={e => { enqueue(Array.from(e.target.files).filter(isImageFile)); e.target.value = ''; }} />
              </label>
              <label className="btn btn-rule" style={{ cursor: 'pointer' }}>
                Pick a folder
                <input type="file" multiple webkitdirectory="" directory="" style={{ display: 'none' }}
                  onChange={e => { enqueue(Array.from(e.target.files).filter(isImageFile)); e.target.value = ''; }} />
              </label>
            </div>
          </div>

          {/* Upload queue */}
          {uploads.length > 0 && (
            <div style={{ marginBottom: 40 }}>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, marginBottom: 10 }}>
                <div className="small" style={{ color: 'var(--ink-soft)' }}>
                  {activeCount > 0
                    ? `Uploading ${activeCount} of ${uploads.length}… · ${fmtBytes(totalUploadedBytes)} of ${fmtBytes(totalBytes)}`
                    : errorCount > 0
                      ? `${doneCount} done · ${errorCount} failed · ${uploads.length} total`
                      : `${doneCount} of ${uploads.length} uploaded · ${fmtBytes(totalBytes)}`}
                </div>
                {activeCount === 0 && (
                  <button className="btn-text-link" style={{ fontSize: 10 }} onClick={clearFinished}>Clear</button>
                )}
              </div>
              <div style={{ height: 4, background: 'var(--hair)', marginBottom: 16 }}>
                <div style={{ height: '100%', background: 'var(--amber)', width: `${overallPct * 100}%`, transition: 'width .2s linear' }} />
              </div>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 8 }}>
                {uploads.map(u => (
                  <div key={u.id} style={{ border: '1px solid var(--hair)', padding: 10, fontSize: 12, fontFamily: 'var(--font-body)' }}>
                    <div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginBottom: 4, color: 'var(--ink)' }}>{u.file.name}</div>
                    <div style={{ color: u.status === 'error' ? 'var(--velvet)' : 'var(--ink-mute)', fontSize: 10, letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 4 }}>
                      {u.status === 'queued' && 'queued'}
                      {u.status === 'starting' && 'starting…'}
                      {u.status === 'uploading' && `${Math.round(u.progress * 100)}%`}
                      {u.status === 'done' && '✓ done'}
                      {u.status === 'error' && `error: ${u.error}`}
                      {' · '}{fmtBytes(u.file.size)}
                    </div>
                    {u.status === 'uploading' && (
                      <div style={{ height: 2, background: 'var(--hair)' }}>
                        <div style={{ height: '100%', background: 'var(--amber)', width: `${u.progress * 100}%`, transition: 'width .15s linear' }} />
                      </div>
                    )}
                    {u.status === 'error' && (
                      <button className="btn-text-link" style={{ fontSize: 10, padding: 0, marginTop: 2 }} onClick={() => retryUpload(u.id)}>
                        Retry
                      </button>
                    )}
                  </div>
                ))}
              </div>
            </div>
          )}

          {/* Sortable list: sections + photos interleaved */}
          {items.length > 0 && (
            <div>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
                <h3 className="h3">
                  Story order
                  <InfoDot>
                    This is the exact order photos appear in the public album.
                    <div style={{ marginTop: 6 }}>Drag the <span style={{ fontFamily: 'var(--font-body)' }}>⠿</span> handle to move any row up or down.</div>
                    <div style={{ marginTop: 6 }}>Chapters break photos into named sections — click <em>Edit</em> on a chapter to add a small editorial line under its title.</div>
                  </InfoDot>
                </h3>
                <button className="btn btn-rule" style={{ fontSize: 10, padding: '10px 16px' }} onClick={addSection}>
                  + Add section
                </button>
              </div>
              <p className="small" style={{ color: 'var(--ink-mute)', marginBottom: 20, fontStyle: 'italic' }}>
                Drag to reorder. Sections act as chapter headers in the album.
              </p>
              <div style={{ border: '1px solid var(--hair)' }}>
                {items.map((item, i) => (
                  <SortableRow
                    key={`${item.type}-${item.id}`}
                    item={item}
                    isCover={item.type === 'photo' && item.id === gallery.cover_photo_id}
                    index={i}
                    onDragStart={e => onItemDragStart(e, i)}
                    onDragOver={e => onItemDragOver(e, i)}
                    onDrop={onItemDrop}
                    onDelete={() => item.type === 'photo' ? deletePhoto(item.id) : deleteSection(item.id)}
                    onEdit={() => setEditingSection(item)}
                    onSetCover={() => setCover(item.id)}
                  />
                ))}
              </div>
            </div>
          )}
        </section>
      )}

      {tab === 'settings' && (
        <GallerySettingsPanel gallery={gallery} slug={slug} onSaved={load} onDeleted={() => go('admin')} />
      )}

      {editingSection && (
        <SectionEditModal
          section={editingSection}
          onClose={() => setEditingSection(null)}
          onSave={patch => saveSection(editingSection.id, patch)}
        />
      )}
    </>
  );
}

function SectionEditModal({ section, onClose, onSave }) {
  const [title, setTitle]                 = React.useState(section.title || '');
  const [description, setDescription]     = React.useState(section.description || '');
  const [descriptionNl, setDescriptionNl] = React.useState(section.description_nl || '');
  const [saving, setSaving]               = React.useState(false);

  const submit = async () => {
    if (!title.trim()) return;
    setSaving(true);
    await onSave({
      title: title.trim(),
      description: description.trim() || null,
      description_nl: descriptionNl.trim() || null,
    });
    setSaving(false);
  };

  return (
    <Modal onClose={onClose}>
      <Eyebrow>Chapter</Eyebrow>
      <h2 className="h2" style={{ marginTop: 12, marginBottom: 32, fontStyle: 'italic', fontWeight: 300 }}>
        Edit chapter.
      </h2>
      <FormField label="Title" value={title} onChange={e => setTitle(e.target.value)} placeholder="Ceremony" />
      <FormField
        label="Description (EN)" multiline
        value={description} onChange={e => setDescription(e.target.value)}
        placeholder="A quiet vow under the olive tree."
      />
      <FormField
        label="Description (NL)" multiline
        value={descriptionNl} onChange={e => setDescriptionNl(e.target.value)}
        placeholder="Een stille belofte onder de olijfboom."
      />
      <div style={{ display: 'flex', gap: 12, marginTop: 24 }}>
        <button className="btn btn-rule" onClick={onClose}>Cancel</button>
        <button
          className="btn" style={{ flex: 1, justifyContent: 'center' }}
          onClick={submit} disabled={saving || !title.trim()}
        >{saving ? 'Saving…' : 'Save chapter'}</button>
      </div>
    </Modal>
  );
}

function SortableRow({ item, isCover, index, onDragStart, onDragOver, onDrop, onDelete, onEdit, onSetCover }) {
  const [dragging, setDragging] = React.useState(false);

  if (item.type === 'section') {
    return (
      <div
        draggable
        onDragStart={e => { setDragging(true); onDragStart(e); }}
        onDragEnd={() => setDragging(false)}
        onDragOver={onDragOver}
        onDrop={onDrop}
        style={{
          display: 'flex', alignItems: 'flex-start', gap: 12,
          padding: '12px 16px',
          background: dragging ? 'var(--bone)' : 'var(--linen)',
          borderBottom: '1px solid var(--hair)',
          cursor: 'grab',
        }}
      >
        <span style={{ color: 'var(--ink-mute)', fontSize: 14, userSelect: 'none', paddingTop: 2 }}>⠿</span>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{
            fontFamily: 'var(--font-body)', fontWeight: 500, fontSize: 10,
            letterSpacing: '0.28em', textTransform: 'uppercase', color: 'var(--ink-soft)',
          }}>
            {item.title}
          </div>
          {item.description && (
            <div style={{
              fontFamily: 'var(--font-display)', fontStyle: 'italic', fontWeight: 300,
              fontSize: 13, color: 'var(--ink-soft)', marginTop: 4, lineHeight: 1.5,
              overflow: 'hidden', textOverflow: 'ellipsis', display: '-webkit-box',
              WebkitLineClamp: 2, WebkitBoxOrient: 'vertical',
            }}>
              {item.description}
            </div>
          )}
        </div>
        <button className="btn-text-link" style={{ fontSize: 10, padding: '4px 0' }} onClick={onEdit}>Edit</button>
        <button className="btn-text-link" style={{ fontSize: 10, padding: '4px 0', color: 'var(--velvet)' }} onClick={onDelete}>Remove</button>
      </div>
    );
  }

  return (
    <div
      draggable
      onDragStart={e => { setDragging(true); onDragStart(e); }}
      onDragEnd={() => setDragging(false)}
      onDragOver={onDragOver}
      onDrop={onDrop}
      style={{
        display: 'flex', alignItems: 'center', gap: 12,
        padding: '8px 16px',
        background: dragging ? 'var(--bone)' : 'transparent',
        borderBottom: '1px solid var(--hair)',
        cursor: 'grab',
      }}
    >
      <span style={{ color: 'var(--ink-mute)', fontSize: 14, userSelect: 'none' }}>⠿</span>
      {/* Thumbnail */}
      <div style={{
        width: 40, height: 40, flexShrink: 0,
        background: 'var(--bone)',
        backgroundImage: item.url ? `url(${item.url})` : 'none',
        backgroundSize: 'cover', backgroundPosition: 'center',
      }} />
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontFamily: 'var(--font-body)', fontSize: 12, color: 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          {item.original_name || `Photo ${item.id}`}
        </div>
        {item.width && (
          <div style={{ fontSize: 11, color: 'var(--ink-mute)' }}>{item.width} × {item.height}</div>
        )}
      </div>
      {isCover && (
        <span style={{ fontFamily: 'var(--font-body)', fontSize: 9, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'var(--amber)', paddingRight: 4 }}>Cover</span>
      )}
      {!isCover && (
        <button className="btn-text-link" style={{ fontSize: 10, padding: '4px 0' }} onClick={onSetCover}>Set cover</button>
      )}
      <button className="btn-text-link" style={{ fontSize: 10, padding: '4px 0', color: 'var(--velvet)' }} onClick={onDelete}>Delete</button>
    </div>
  );
}

function GallerySettingsPanel({ gallery, slug, onSaved, onDeleted }) {
  const [displayName, setDisplayName]     = React.useState(gallery.display_name);
  const [displayNameNl, setDisplayNameNl] = React.useState(gallery.display_name_nl || '');
  const [isWedding, setIsWedding]         = React.useState(!!gallery.is_wedding);
  const [isPortfolio, setIsPortfolio]     = React.useState(!!gallery.is_portfolio);
  const [isRecentWork, setIsRecentWork]   = React.useState(!!gallery.is_recent_work);
  const [password, setPassword]           = React.useState('');
  const [eventDate, setEventDate]         = React.useState(gallery.event_date || '');
  const [location, setLocation]           = React.useState(gallery.location || '');
  const [note, setNote]                   = React.useState(gallery.note || '');
  const [noteNl, setNoteNl]               = React.useState(gallery.note_nl || '');
  const [textSide, setTextSide]           = React.useState(gallery.text_side || 'right');
  const [saving, setSaving]               = React.useState(false);
  const [deleting, setDeleting]           = React.useState(false);
  const [error, setError]                 = React.useState('');
  const [saved, setSaved]                 = React.useState(false);

  const save = async () => {
    setSaving(true); setError(''); setSaved(false);
    const body = {
      display_name: displayName.trim(),
      display_name_nl: displayNameNl.trim() || null,
      is_wedding: isWedding ? 1 : 0,
      is_portfolio: isPortfolio ? 1 : 0,
      is_recent_work: isRecentWork ? 1 : 0,
      event_date: eventDate.trim() || null,
      location: location.trim() || null,
      note: note.trim() || null,
      note_nl: noteNl.trim() || null,
      text_side: textSide,
    };
    if (password.trim()) body.password = password.trim();
    try {
      const res = await fetch(`/api/admin/galleries/${slug}`, {
        method: 'PATCH', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      if (res.ok) { setSaved(true); onSaved(); }
      else setError('Save failed.');
    } catch { setError("Couldn't reach the server."); }
    finally { setSaving(false); }
  };

  const deleteGallery = async () => {
    if (!confirm(`Delete "${gallery.display_name}" and all its photos? This cannot be undone.`)) return;
    setDeleting(true);
    try {
      await fetch(`/api/admin/galleries/${slug}`, { method: 'DELETE', credentials: 'include' });
      onDeleted();
    } catch { setError("Couldn't delete."); setDeleting(false); }
  };

  return (
    <section style={{ padding: '32px 40px', maxWidth: 600 }}>
      <FormField label="Album name (EN)" value={displayName} onChange={e => setDisplayName(e.target.value)} />

      <div style={{ margin: '24px 0 8px', display: 'flex', alignItems: 'center', fontFamily: 'var(--font-body)', fontSize: 11, fontWeight: 500, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--ink-soft)' }}>
        Album type
        <InfoDot>
          Check any combination — this controls where the album appears on the public site.
          <div style={{ marginTop: 6 }}><strong>Wedding</strong> → private gallery, needs a password.</div>
          <div style={{ marginTop: 6 }}><strong>Portfolio</strong> → public, listed on <code style={{ fontSize: 11 }}>/portfolio</code>.</div>
          <div style={{ marginTop: 6 }}><strong>Recent Work</strong> → public, with editorial text on <code style={{ fontSize: 11 }}>/recent-work</code>.</div>
        </InfoDot>
      </div>
      {[
        { key: 'wedding',     label: 'Wedding',     val: isWedding,    set: setIsWedding },
        { key: 'portfolio',   label: 'Portfolio',   val: isPortfolio,  set: setIsPortfolio },
        { key: 'recent_work', label: 'Recent Work', val: isRecentWork, set: setIsRecentWork },
      ].map(({ key, label, val, set }) => (
        <label key={key} style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8, cursor: 'pointer' }}>
          <input type="checkbox" checked={val} onChange={e => set(e.target.checked)} />
          <span style={{ fontFamily: 'var(--font-body)', fontSize: 14 }}>{label}</span>
        </label>
      ))}

      {isWedding && (
        <>
          <div style={{ marginTop: 24 }}>
            <FormField
              label="New client password (leave blank to keep current)"
              value={password}
              onChange={e => setPassword(e.target.value)}
            />
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
            <FormField label="Date" placeholder="May 2026" value={eventDate} onChange={e => setEventDate(e.target.value)} />
            <FormField label="Location" placeholder="Villa Astor, Sorrento" value={location} onChange={e => setLocation(e.target.value)} />
          </div>
          {!isRecentWork && <FormField label="Personal note" multiline value={note} onChange={e => setNote(e.target.value)} />}
        </>
      )}

      {isRecentWork && (
        <div style={{ marginTop: 24, padding: '20px 24px', background: 'var(--bone)', border: '1px solid var(--hair)' }}>
          <div style={{ display: 'flex', alignItems: 'center', fontFamily: 'var(--font-body)', fontSize: 11, fontWeight: 500, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--ink-soft)', marginBottom: 16 }}>
            Recent Work content
            <InfoDot>
              These fields only matter when the album shows on <code style={{ fontSize: 11 }}>/recent-work</code>.
              <div style={{ marginTop: 6 }}><strong>Dutch name</strong> shows when a visitor's language is set to NL.</div>
              <div style={{ marginTop: 6 }}><strong>Editorial text</strong> appears next to the cover photo — keep it short and poetic.</div>
              <div style={{ marginTop: 6 }}><strong>Image position</strong> alternates the visual rhythm down the page.</div>
            </InfoDot>
          </div>
          <FormField label="Dutch name" placeholder="Romantiek aan zee" value={displayNameNl} onChange={e => setDisplayNameNl(e.target.value)} />
          <FormField label="Editorial text (EN)" multiline placeholder="A lace veil, the wind off the water…" value={note} onChange={e => setNote(e.target.value)} />
          <FormField label="Editorial text (NL)" multiline placeholder="Een kanten sluier, de wind van zee…" value={noteNl} onChange={e => setNoteNl(e.target.value)} />
          <div style={{ marginTop: 8 }}>
            <div style={{ fontFamily: 'var(--font-body)', fontSize: 11, fontWeight: 500, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--ink-soft)', marginBottom: 10 }}>Image position</div>
            {[['right', 'Image on right, text on left'], ['left', 'Image on left, text on right']].map(([val, lbl]) => (
              <label key={val} style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8, cursor: 'pointer' }}>
                <input type="radio" name="text_side" value={val} checked={textSide === val} onChange={() => setTextSide(val)} />
                <span style={{ fontFamily: 'var(--font-body)', fontSize: 14 }}>{lbl}</span>
              </label>
            ))}
          </div>
        </div>
      )}

      {error && (
        <div style={{ padding: '12px 14px', background: 'rgba(92,34,48,0.08)', color: 'var(--velvet)', fontSize: 13, fontStyle: 'italic', marginTop: 16 }}>
          {error}
        </div>
      )}
      {saved && (
        <div style={{ padding: '12px 14px', background: 'rgba(126,124,97,0.12)', color: 'var(--olive)', fontSize: 13, marginTop: 16 }}>
          Saved.
        </div>
      )}

      <div style={{ marginTop: 24, display: 'flex', gap: 12 }}>
        <button className="btn" onClick={save} disabled={saving}>{saving ? 'Saving…' : 'Save changes'}</button>
      </div>

      <div style={{ marginTop: 64, paddingTop: 32, borderTop: '1px solid var(--hair)' }}>
        <div className="small" style={{ color: 'var(--ink-mute)', marginBottom: 12 }}>
          Deleting this album removes all photos permanently. This cannot be undone.
        </div>
        <button
          onClick={deleteGallery} disabled={deleting}
          style={{
            fontFamily: 'var(--font-body)', fontSize: 11, fontWeight: 500,
            letterSpacing: '0.18em', textTransform: 'uppercase',
            background: 'none', border: '1px solid var(--velvet)',
            color: 'var(--velvet)', padding: '12px 24px', cursor: 'pointer',
          }}
        >{deleting ? 'Deleting…' : 'Delete album'}</button>
      </div>
    </section>
  );
}

// ── TESTIMONIALS ─────────────────────────────────────────────

function AdminTestimonialsSection() {
  const [phase, setPhase] = React.useState('loading');
  const [items, setItems] = React.useState([]);
  const [editing, setEditing] = React.useState(null); // null | 'new' | testimonial object

  const load = React.useCallback(() => {
    fetch('/api/admin/testimonials', { credentials: 'include' })
      .then(async r => { setItems(await r.json()); setPhase('ready'); })
      .catch(() => setPhase('error'));
  }, []);

  React.useEffect(() => { load(); }, [load]);

  const toggleVisible = async (t) => {
    await fetch(`/api/admin/testimonials/${t.id}`, {
      method: 'PATCH', credentials: 'include',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ visible: t.visible ? 0 : 1 }),
    });
    setItems(arr => arr.map(i => i.id === t.id ? { ...i, visible: i.visible ? 0 : 1 } : i));
  };

  const deleteT = async (id) => {
    if (!confirm('Delete this testimonial?')) return;
    await fetch(`/api/admin/testimonials/${id}`, { method: 'DELETE', credentials: 'include' });
    setItems(arr => arr.filter(i => i.id !== id));
  };

  const moveUp = async (i) => {
    if (i === 0) return;
    const next = [...items];
    [next[i - 1], next[i]] = [next[i], next[i - 1]];
    setItems(next);
    await Promise.all(next.map((t, idx) =>
      fetch(`/api/admin/testimonials/${t.id}`, {
        method: 'PATCH', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ sort_order: (idx + 1) * 1000 }),
      })
    ));
  };

  const moveDown = async (i) => {
    if (i === items.length - 1) return;
    const next = [...items];
    [next[i], next[i + 1]] = [next[i + 1], next[i]];
    setItems(next);
    await Promise.all(next.map((t, idx) =>
      fetch(`/api/admin/testimonials/${t.id}`, {
        method: 'PATCH', credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ sort_order: (idx + 1) * 1000 }),
      })
    ));
  };

  if (phase === 'loading') return <p className="small" style={{ color: 'var(--ink-mute)', fontStyle: 'italic' }}>Loading…</p>;
  if (phase === 'error')   return <p className="small" style={{ color: 'var(--velvet)', fontStyle: 'italic' }}>Couldn't load testimonials.</p>;

  return (
    <>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 24 }}>
        <h3 className="h3">
          Testimonials
          <InfoDot>
            Visible testimonials appear on the homepage in this order.
            <div style={{ marginTop: 6 }}>Use the up/down arrows to reorder. Toggle <strong>Visible</strong> to hide one without deleting.</div>
            <div style={{ marginTop: 6 }}><strong>Quote</strong> is the short preview; <strong>Full quote</strong> is revealed when a visitor expands it.</div>
            <div style={{ marginTop: 6 }}>NL translations are optional — if blank, the English version is shown to Dutch visitors.</div>
          </InfoDot>
        </h3>
        <button className="btn" onClick={() => setEditing('new')}>Add testimonial</button>
      </div>

      {items.length === 0 && (
        <p className="body" style={{ fontStyle: 'italic', color: 'var(--ink-mute)' }}>No testimonials yet.</p>
      )}

      <div style={{ border: '1px solid var(--hair)' }}>
        {items.map((t, i) => (
          <div key={t.id} style={{
            display: 'flex', alignItems: 'flex-start', gap: 16, padding: '20px 20px',
            borderBottom: i < items.length - 1 ? '1px solid var(--hair)' : 'none',
            background: t.visible ? 'transparent' : 'rgba(140,135,128,0.06)',
          }}>
            {/* Up/down */}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 4, paddingTop: 2 }}>
              <button onClick={() => moveUp(i)} style={{ background: 'none', border: '1px solid var(--hair)', width: 24, height: 24, cursor: 'pointer', fontSize: 10, color: 'var(--ink-mute)' }}>↑</button>
              <button onClick={() => moveDown(i)} style={{ background: 'none', border: '1px solid var(--hair)', width: 24, height: 24, cursor: 'pointer', fontSize: 10, color: 'var(--ink-mute)' }}>↓</button>
            </div>

            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontFamily: 'var(--font-display)', fontStyle: 'italic', fontSize: 15, color: t.visible ? 'var(--ink)' : 'var(--ink-mute)', marginBottom: 4, lineHeight: 1.5 }}>
                "{t.quote.length > 120 ? t.quote.slice(0, 120) + '…' : t.quote}"
              </div>
              <div className="small" style={{ color: 'var(--ink-mute)' }}>
                — {t.attr}{t.location ? ` · ${t.location}` : ''}
              </div>
            </div>

            <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexShrink: 0 }}>
              <button onClick={() => toggleVisible(t)} style={{
                fontFamily: 'var(--font-body)', fontSize: 9, fontWeight: 500,
                letterSpacing: '0.18em', textTransform: 'uppercase',
                padding: '5px 10px', cursor: 'pointer',
                background: t.visible ? 'var(--ink)' : 'transparent',
                color: t.visible ? 'var(--linen)' : 'var(--ink-mute)',
                border: '1px solid var(--hair)',
              }}>{t.visible ? 'Visible' : 'Hidden'}</button>
              <button className="btn-text-link" style={{ fontSize: 10 }} onClick={() => setEditing(t)}>Edit</button>
              <button className="btn-text-link" style={{ fontSize: 10, color: 'var(--velvet)' }} onClick={() => deleteT(t.id)}>Delete</button>
            </div>
          </div>
        ))}
      </div>

      {editing !== null && (
        <TestimonialModal
          t={editing === 'new' ? null : editing}
          onClose={() => setEditing(null)}
          onSaved={() => { setEditing(null); load(); }}
        />
      )}
    </>
  );
}

function TestimonialModal({ t, onClose, onSaved }) {
  const isNew = !t;
  const [quote, setQuote]               = React.useState(t?.quote || '');
  const [fullQuote, setFullQuote]       = React.useState(t?.full_quote || '');
  const [quoteNl, setQuoteNl]           = React.useState(t?.quote_nl || '');
  const [fullQuoteNl, setFullQuoteNl]   = React.useState(t?.full_quote_nl || '');
  const [attr, setAttr]                 = React.useState(t?.attr || '');
  const [location, setLocation]         = React.useState(t?.location || '');
  const [saving, setSaving]             = React.useState(false);
  const [error, setError]               = React.useState('');

  const save = async () => {
    if (!quote.trim() || !attr.trim()) { setError('Quote and name are required.'); return; }
    setSaving(true); setError('');
    const body = {
      quote: quote.trim(), full_quote: fullQuote.trim() || null,
      quote_nl: quoteNl.trim() || null, full_quote_nl: fullQuoteNl.trim() || null,
      attr: attr.trim(), location: location.trim() || null,
    };
    try {
      const url  = isNew ? '/api/admin/testimonials' : `/api/admin/testimonials/${t.id}`;
      const method = isNew ? 'POST' : 'PATCH';
      const res  = await fetch(url, {
        method, credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      if (res.ok || res.status === 201) onSaved();
      else setError('Save failed.');
    } catch { setError("Couldn't reach the server."); }
    finally { setSaving(false); }
  };

  return (
    <Modal onClose={onClose} wide>
      <Eyebrow>{isNew ? 'New testimonial' : 'Edit testimonial'}</Eyebrow>
      <h2 className="h2" style={{ marginTop: 12, marginBottom: 32, fontStyle: 'italic', fontWeight: 300 }}>
        {isNew ? 'Add a kind word.' : 'Update.'}
      </h2>
      <FormField label="Quote (short, shown first)" multiline value={quote} onChange={e => setQuote(e.target.value)} placeholder='"She perfectly captured every moment…"' />
      <FormField label="Full quote (optional, revealed on expand)" multiline value={fullQuote} onChange={e => setFullQuote(e.target.value)} />
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
        <FormField label="Name" placeholder="Rianne & Thijs" value={attr} onChange={e => setAttr(e.target.value)} />
        <FormField label="Location (optional)" placeholder="Amsterdam" value={location} onChange={e => setLocation(e.target.value)} />
      </div>
      <div style={{ marginTop: 8, padding: '12px 16px', background: 'var(--bone)', borderLeft: '2px solid var(--hair)' }}>
        <div className="small" style={{ color: 'var(--ink-mute)', marginBottom: 12 }}>Dutch translations (optional)</div>
        <FormField label="Quote NL" multiline value={quoteNl} onChange={e => setQuoteNl(e.target.value)} />
        <FormField label="Full quote NL" multiline value={fullQuoteNl} onChange={e => setFullQuoteNl(e.target.value)} />
      </div>
      {error && (
        <div style={{ padding: '12px 14px', background: 'rgba(92,34,48,0.08)', color: 'var(--velvet)', fontSize: 13, fontStyle: 'italic', marginTop: 16 }}>
          {error}
        </div>
      )}
      <div style={{ display: 'flex', gap: 12, marginTop: 24 }}>
        <button className="btn btn-rule" onClick={onClose}>Cancel</button>
        <button className="btn" style={{ flex: 1, justifyContent: 'center' }} onClick={save} disabled={saving}>
          {saving ? 'Saving…' : (isNew ? 'Add testimonial' : 'Save changes')}
        </button>
      </div>
    </Modal>
  );
}

// ── Shared modal wrapper ─────────────────────────────────────

function Modal({ children, onClose, wide }) {
  return (
    <div
      onClick={onClose}
      style={{ position: 'fixed', inset: 0, zIndex: 200, background: 'rgba(26,24,22,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32 }}
    >
      <div
        onClick={e => e.stopPropagation()}
        style={{ background: 'var(--linen)', padding: 48, maxWidth: wide ? 720 : 560, width: '100%', maxHeight: '90vh', overflowY: 'auto' }}
      >
        {children}
      </div>
    </div>
  );
}

Object.assign(window, { AdminLoginScreen, AdminDashboardScreen, AdminGalleryScreen });
