// Shared collection-page shell.
// Renders the nav + header + toolbar + pieces grid + outro for any collection.
// The collection-specific page file just feeds it a name + lede + 4 piece components.
//
// Provides on window:
//   PALETTES, signVars, E, Removable, Piece, PaletteSwatch, CustomSwatch,
//   CollectionShell, hex2rgba

const { useState, useEffect, useRef } = React;

// ─────────── shared palettes ───────────
const PALETTES = {
  neutral: { label: 'Neutral', paper: '#FFFFFF', ink: '#1A1612', accent: '#1A1612',
             rule: 'rgba(26,22,18,0.32)', muted: 'rgba(26,22,18,0.55)',
             chip: ['#FFFFFF', '#1A1612'] },
  spring:  { label: 'Spring',  paper: '#FFFFFF', ink: '#37472F', accent: '#B97D81',
             rule: 'rgba(55,71,47,0.28)', muted: 'rgba(55,71,47,0.6)',
             chip: ['#FFFFFF', '#37472F', '#B97D81'] },
  fall:    { label: 'Fall',    paper: '#FFFFFF', ink: '#26170D', accent: '#A35530',
             rule: 'rgba(38,23,13,0.3)',  muted: 'rgba(38,23,13,0.55)',
             chip: ['#FFFFFF', '#26170D', '#A35530'] },
};

// ─────────── editable text wrapper ───────────
// `children` is a literal string so React's diff treats it as stable and
// never overwrites the user's in-browser edits on re-render.
const E = ({ children, className }) => (
  <span contentEditable suppressContentEditableWarning className={`tpl-edit ${className || ''}`}>{children}</span>
);

// ─────────── CSS-var helper for the sign root ───────────
function signVars(p, w, h) {
  return {
    width: w, height: h,
    '--paper':  p.paper,
    '--ink':    p.ink,
    '--accent': p.accent,
    '--rule':   p.rule,
    '--muted':  p.muted,
  };
}

function hex2rgba(hex, a) {
  const h = hex.replace('#', '');
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  return `rgba(${r},${g},${b},${a})`;
}

// ─────────── Removable hover-× wrapper ───────────
function Removable({ children, onRemove, label = 'Remove this line' }) {
  return (
    <div className="rm">
      {children}
      <button type="button" className="rm__x" onClick={onRemove} aria-label={label} title={label}>
        <span aria-hidden="true">×</span>
      </button>
    </div>
  );
}

function PaletteSwatch({ palette, active, onClick }) {
  return (
    <button type="button" className={`tpl-swatch ${active ? 'is-active' : ''}`} onClick={onClick}>
      <span className="tpl-swatch__chip">
        {palette.chip.map((c, i) => <i key={i} style={{ background: c }} />)}
      </span>
      <span>{palette.label}</span>
    </button>
  );
}

function CustomSwatch({ active, onClick, ink, accent }) {
  return (
    <button type="button" className={`tpl-swatch ${active ? 'is-active' : ''}`} onClick={onClick}>
      <span className="tpl-swatch__chip">
        <i style={{ background: '#FFFFFF' }} />
        <i style={{ background: ink }} />
        <i style={{ background: accent }} />
      </span>
      <span>Custom</span>
    </button>
  );
}

// ─────────── QR uploader ───────────
// Square drop-zone for the couple's QR code image. Uses regular DOM (NOT
// shadow DOM) so html2canvas captures the dropped image when we render a PDF.
// Persists across reloads via localStorage keyed by the slot id.
function QRUploader({ id, size = 200, placeholder = 'Drop your QR code' }) {
  const STORAGE_KEY = `cedar-hills-qr-${id}`;
  const [src, setSrc] = useState(() => {
    try { return localStorage.getItem(STORAGE_KEY) || ''; } catch { return ''; }
  });
  const inputRef = React.useRef(null);

  const handleFile = (file) => {
    if (!file || !/^image\//.test(file.type)) return;
    const reader = new FileReader();
    reader.onload = (e) => {
      const url = e.target.result;
      setSrc(url);
      try { localStorage.setItem(STORAGE_KEY, url); } catch {}
    };
    reader.readAsDataURL(file);
  };

  const onDrop = (e) => {
    e.preventDefault();
    e.currentTarget.classList.remove('is-dropping');
    handleFile(e.dataTransfer.files?.[0]);
  };
  const onDragOver  = (e) => { e.preventDefault(); e.currentTarget.classList.add('is-dropping'); };
  const onDragLeave = (e) => { e.currentTarget.classList.remove('is-dropping'); };
  const onClear = (e) => {
    e.stopPropagation();
    setSrc('');
    try { localStorage.removeItem(STORAGE_KEY); } catch {}
  };

  return (
    <div
      className={`qr-slot ${src ? 'is-filled' : 'is-empty'}`}
      style={{ width: size, height: size }}
      onClick={() => !src && inputRef.current?.click()}
      onDrop={onDrop}
      onDragOver={onDragOver}
      onDragLeave={onDragLeave}
      role="button"
      tabIndex={0}
    >
      {src ? (
        <>
          <img className="qr-slot__img" src={src} alt="QR code" />
          <button
            type="button"
            className="qr-slot__clear"
            onClick={onClear}
            aria-label="Remove QR code"
            title="Remove QR code"
          >×</button>
        </>
      ) : (
        <div className="qr-slot__empty" aria-hidden="true">
          <svg viewBox="0 0 100 100" className="qr-slot__pattern">
            <g>
              {/* finders */}
              <rect x="6"  y="6"  width="24" height="24" fill="none" strokeWidth="3" />
              <rect x="12" y="12" width="12" height="12" />
              <rect x="70" y="6"  width="24" height="24" fill="none" strokeWidth="3" />
              <rect x="76" y="12" width="12" height="12" />
              <rect x="6"  y="70" width="24" height="24" fill="none" strokeWidth="3" />
              <rect x="12" y="76" width="12" height="12" />
              {/* data-ish dots */}
              <rect x="38" y="38" width="6" height="6" />
              <rect x="48" y="40" width="6" height="6" />
              <rect x="58" y="36" width="6" height="6" />
              <rect x="40" y="50" width="6" height="6" />
              <rect x="52" y="52" width="6" height="6" />
              <rect x="62" y="48" width="6" height="6" />
              <rect x="38" y="62" width="6" height="6" />
              <rect x="50" y="64" width="6" height="6" />
              <rect x="60" y="60" width="6" height="6" />
              <rect x="72" y="42" width="6" height="6" />
              <rect x="76" y="56" width="6" height="6" />
              <rect x="68" y="74" width="6" height="6" />
              <rect x="78" y="78" width="6" height="6" />
              <rect x="42" y="76" width="6" height="6" />
              <rect x="54" y="80" width="6" height="6" />
            </g>
          </svg>
          <div className="qr-slot__hint">
            <strong>{placeholder}</strong>
            <span>or click to browse</span>
          </div>
        </div>
      )}
      <input
        ref={inputRef}
        type="file"
        accept="image/*"
        onChange={(e) => handleFile(e.target.files?.[0])}
        style={{ display: 'none' }}
      />
    </div>
  );
}

// Print-size presets — actual paper dimensions the user feeds the printer.
// We compute a transform: scale() so the sign fills as much of the printable
// area as possible without breaking its 3:4 / 4:5 aspect.
// `printable: false` hides the in-browser Print button — the size is too big
// for a home printer, so we only offer the download path.
const PRINT_SIZES = {
  letter: { w: 8.5, h: 11, margin: 0.25, label: '8½ × 11',     slug: 'letter', printable: true },
  '4x6':  { w: 4,   h: 6,  margin: 0.2,  label: '4 × 6 frame', slug: '4x6',    printable: true },
};

// Print-shop / full-size option — varies by piece (posters print at 18×24,
// framed pieces at 8×10). Same shape as PRINT_SIZES entries.
const PRINT_SHOP_SIZE = {
  poster: { w: 18, h: 24, margin: 0, label: '18 × 24 poster',  slug: '18x24', printable: false, note: 'send to a print shop' },
  framed: { w: 8,  h: 10, margin: 0, label: '8 × 10 framed',   slug: '8x10',  printable: false, note: 'send to a print shop' },
};

function slugify(s) {
  return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
}

function Piece({ name, dim, page = 'poster', children, verified = true, requestVerify }) {
  const pieceRef = React.useRef(null);
  const [busy, setBusy] = React.useState(null); // sizeKey of in-progress download

  // Build the size list dynamically so each piece offers its native print-shop size.
  const SIZES = {
    ...PRINT_SIZES,
    shop: PRINT_SHOP_SIZE[page] || PRINT_SHOP_SIZE.poster,
  };

  // Capture the sign's pixel dimensions in a way both handlers share.
  const readSignBox = () => {
    const signEl = pieceRef.current?.querySelector('.sign');
    if (!signEl) return null;
    return {
      el: signEl,
      w: parseFloat(signEl.style.width)  || (page === 'poster' ? 540 : 400),
      h: parseFloat(signEl.style.height) || (page === 'poster' ? 720 : 500),
    };
  };

  const doPrint = (sizeKey) => {
    const box = readSignBox();
    if (!box) return;
    const cfg = PRINT_SIZES[sizeKey] || PRINT_SIZES.letter;

    // 4×6 gets special treatment: print on letter paper so the margins give
    // room for corner crop marks; the sign fills the 4×6 area exactly.
    const is4x6 = sizeKey === '4x6';
    const pageW = is4x6 ? 8.5 : cfg.w;
    const pageH = is4x6 ? 11  : cfg.h;

    // Scale sign to fill the 4×6 target (no inner margin — crop marks are the guide).
    const availW = (is4x6 ? cfg.w : cfg.w - 2 * cfg.margin) * 96;
    const availH = (is4x6 ? cfg.h : cfg.h - 2 * cfg.margin) * 96;
    const scale  = Math.min(availW / box.w, availH / box.h);

    // Print through an isolated about:blank popup so:
    //   - Main page chrome can't leak in
    //   - The print footer URL field shows "about:blank" not your domain
    //   - @page rule sizes the page exactly to the user's paper choice
    // Falls back to a hidden iframe if popups are blocked.
    const cssLinks = [...document.querySelectorAll('link[rel="stylesheet"]')]
      .map((l) => `<link rel="stylesheet" href="${l.href}">`)
      .join('\n');

    // Crop-mark CSS — injected only for 4×6 prints.
    const cropMarkCss = is4x6 ? `
      /* Cut guide: 4×6 box centred on letter paper with corner crop marks */
      .cut-guide {
        position: absolute; top: 50%; left: 50%;
        transform: translate(-50%, -50%);
        width: 4in; height: 6in;
      }
      /* Four corners — each corner has two arms (horizontal + vertical) */
      .cut-corner { position: absolute; display: block; }
      .cut-corner::before, .cut-corner::after {
        content: ''; position: absolute; background: #888;
        -webkit-print-color-adjust: exact; print-color-adjust: exact;
      }
      .cut-corner::before { width: 0.2in; height: 0.5pt; }
      .cut-corner::after  { width: 0.5pt; height: 0.2in; }
      /* top-left */
      .cut-corner--tl { top: -0.15in; left: -0.15in; }
      .cut-corner--tl::before { top: 0; left: 0; }
      .cut-corner--tl::after  { top: 0; left: 0; }
      /* top-right */
      .cut-corner--tr { top: -0.15in; right: -0.15in; }
      .cut-corner--tr::before { top: 0; right: 0; }
      .cut-corner--tr::after  { top: 0; right: 0; }
      /* bottom-left */
      .cut-corner--bl { bottom: -0.15in; left: -0.15in; }
      .cut-corner--bl::before { bottom: 0; left: 0; }
      .cut-corner--bl::after  { bottom: 0; left: 0; }
      /* bottom-right */
      .cut-corner--br { bottom: -0.15in; right: -0.15in; }
      .cut-corner--br::before { bottom: 0; right: 0; }
      .cut-corner--br::after  { bottom: 0; right: 0; }
      /* "cut" label — tiny, grey, in the top margin */
      .cut-label {
        position: absolute; top: -0.3in; left: 50%;
        transform: translateX(-50%);
        font-family: Arial, sans-serif; font-size: 6pt;
        color: #999; letter-spacing: 0.12em; text-transform: uppercase;
        white-space: nowrap;
        -webkit-print-color-adjust: exact; print-color-adjust: exact;
      }
    ` : '';

    const cropMarkHtml = is4x6 ? `
      <div class="cut-guide">
        <div class="sign-print-wrap">${box.el.outerHTML}</div>
        <span class="cut-corner cut-corner--tl"></span>
        <span class="cut-corner cut-corner--tr"></span>
        <span class="cut-corner cut-corner--bl"></span>
        <span class="cut-corner cut-corner--br"></span>
        <span class="cut-label">✂ cut on marks &nbsp;·&nbsp; 4 × 6 in</span>
      </div>
    ` : `<div class="sign-print-wrap">${box.el.outerHTML}</div>`;

    const html = [
      '<!DOCTYPE html>',
      '<html>',
      '<head>',
      '<meta charset="UTF-8">',
      // Empty title → no page-title string in print header
      '<title></title>',
      cssLinks,
      '<style>',
      `@page { size: ${pageW}in ${pageH}in; margin: 0; }`,
      'html, body { margin: 0; padding: 0; background: #fff; overflow: hidden; }',
      // position:absolute keeps the sign out of document flow so transform:scale
      // doesn't cause a blank second page via layout overflow.
      `body { width: ${pageW}in; height: ${pageH}in; position: relative; overflow: hidden; }`,
      // For non-4x6: sign is absolute-centred directly in the body.
      // For 4x6: sign is absolute-centred inside .cut-guide (which is centred in body).
      `.sign-print-wrap { position: absolute; top: 50%; left: 50%;`,
      `                   transform: translate(-50%, -50%) scale(${scale});`,
      '                   transform-origin: center center; }',
      '.sign { box-shadow: none !important; }',
      '.rm__x, .bar-item__x, .bar-menu__add { display: none !important; }',
      '.tpl-edit { outline: none !important; background: transparent !important; box-shadow: none !important; }',
      cropMarkCss,
      '</style>',
      '</head>',
      '<body>',
      cropMarkHtml,
      '<script>',
      'window.addEventListener("load", function () {',
      '  var go = function () { setTimeout(function () { window.focus(); window.print(); }, 250); };',
      '  if (document.fonts && document.fonts.ready) { document.fonts.ready.then(go); } else { go(); }',
      '});',
      'window.addEventListener("afterprint", function () { setTimeout(function () { window.close(); }, 200); });',
      '</script>',
      '</body>',
      '</html>',
    ].join('\n');

    // Try popup first (cleanest URL in print footer)
    const w = window.open('about:blank', '_blank', 'width=620,height=820,scrollbars=no,resizable=yes');
    if (w && !w.closed) {
      w.document.open();
      w.document.write(html);
      w.document.close();
      return;
    }

    // Fallback: hidden iframe (URL footer will show this page's URL)
    document.getElementById('tpl-print-iframe')?.remove();
    const iframe = document.createElement('iframe');
    iframe.id = 'tpl-print-iframe';
    iframe.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:0;visibility:hidden;';
    iframe.srcdoc = html;
    document.body.appendChild(iframe);
    const cleanup = () => document.getElementById('tpl-print-iframe')?.remove();
    setTimeout(cleanup, 120000);
  };

  const handlePrint = (sizeKey) => (e) => {
    e.preventDefault();
    if (!verified) { requestVerify(() => doPrint(sizeKey)); return; }
    doPrint(sizeKey);
  };

  // Render the sign to a real PDF at the chosen paper size, then download it.
  // Uses html2canvas (DOM → PNG) + jsPDF (PNG → PDF). Both are loaded as globals
  // from the host HTML's <script> tags.
  const doDownloadPDF = (sizeKey) => async () => {
    if (busy) return;
    const box = readSignBox();
    if (!box) return;
    const cfg = SIZES[sizeKey] || SIZES.letter;

    if (typeof window.html2canvas !== 'function' || !window.jspdf?.jsPDF) {
      alert('PDF tools are still loading — give it a second and try again.');
      return;
    }

    setBusy(sizeKey);
    try {
      // Wait for any web fonts so the captured render isn't fallback-typeset.
      if (document.fonts?.ready) await document.fonts.ready;

      // Pick a render scale that yields ~200 DPI at the target paper size.
      // Floor of 3× for crisp text on small sizes; capped at 8× for memory.
      const dpiScale = Math.ceil((cfg.w * 200) / box.w);
      const captureScale = Math.min(8, Math.max(3, dpiScale));

      const canvas = await window.html2canvas(box.el, {
        scale: captureScale,
        backgroundColor: '#ffffff',
        useCORS: true,
        logging: false,
      });

      const { jsPDF } = window.jspdf;
      const pdf = new jsPDF({
        orientation: cfg.h >= cfg.w ? 'portrait' : 'landscape',
        unit: 'in',
        format: [cfg.w, cfg.h],
      });

      // Fit the sign inside (page − 2·margin), preserving aspect.
      const availW = cfg.w - 2 * cfg.margin;
      const availH = cfg.h - 2 * cfg.margin;
      const signInW = box.w / 96;
      const signInH = box.h / 96;
      const fit = Math.min(availW / signInW, availH / signInH);
      const drawW = signInW * fit;
      const drawH = signInH * fit;
      const x = (cfg.w - drawW) / 2;
      const y = (cfg.h - drawH) / 2;

      pdf.addImage(canvas.toDataURL('image/png'), 'PNG', x, y, drawW, drawH);
      pdf.save(`cedar-hills-${slugify(name)}-${cfg.slug}.pdf`);
    } catch (err) {
      console.error('PDF download failed', err);
      alert("Sorry — couldn't build the PDF. Try the Print option and choose 'Save as PDF' there.");
    } finally {
      setBusy(null);
    }
  };

  const handleDownloadPDF = (sizeKey) => async (e) => {
    e.preventDefault();
    if (!verified) { requestVerify(() => doDownloadPDF(sizeKey)()); return; }
    await doDownloadPDF(sizeKey)();
  };

  return (
    <figure ref={pieceRef} className={`piece piece--${page}`}>
      <div className="piece__board">{children}</div>
      <figcaption className="piece__label">
        <div className="piece__title">
          <span className="piece__name">{name}</span>
          <span className="piece__sep">·</span>
          <span className="piece__dim" dangerouslySetInnerHTML={{ __html: dim }} />
        </div>
        <div className="piece__sizes">
          {Object.entries(SIZES).map(([key, cfg]) => (
            <div key={key} className={`piece__size ${cfg.printable ? '' : 'piece__size--shop'}`}>
              <span className="piece__size-lbl">
                {cfg.label}
                {cfg.note && <em className="piece__size-note"> · {cfg.note}</em>}
              </span>
              {cfg.printable && (
                <>
                  <a className="piece__action" href="#" onClick={handlePrint(key)}>Print</a>
                  <span className="piece__action-sep">·</span>
                </>
              )}
              <a
                className={`piece__action ${busy === key ? 'is-busy' : ''}`}
                href="#"
                onClick={handleDownloadPDF(key)}
                aria-busy={busy === key || undefined}
              >
                {busy === key ? 'Building PDF…' : 'Download PDF'}
              </a>
            </div>
          ))}
        </div>
      </figcaption>
    </figure>
  );
}

// Shared "Removable" hover-× wrapper — provided by collection-shell.jsx.
// (No local definition here — keep one source of truth.)

// CollectionShell — the whole collection-detail page layout.
// Props:
//   no          string  e.g. "01"
//   name        string  e.g. "Editorial"
//   descriptor  string  optional kicker
//   lede        string|node
//   pieces      array of { name, dim, Component }  — first two go solo, rest pair up
//   outro       node    closing copy in the dark footer
//   customDefault  optional { ink, accent }
function CollectionShell({ no, name, descriptor, lede, pieces, outro, initialPalette = 'neutral', customDefault = { ink: '#2A4053', accent: '#C39A5B' } }) {
  const [palKey, setPalKey] = useState(initialPalette);
  const [custom, setCustom] = useState(customDefault);
  const [resetKey, setResetKey] = useState(0);
  const [bandOpen, setBandOpen] = useState(false);
  const [navOpen, setNavOpen] = useState(false);
  const headerRef = useRef(null);

  useEffect(() => {
    function handleKey(e) { if (e.key === 'Escape') { setBandOpen(false); setNavOpen(false); } }
    document.addEventListener('keydown', handleKey);
    return () => document.removeEventListener('keydown', handleKey);
  }, []);

  useEffect(() => {
    function handleClick(e) {
      if (headerRef.current && !headerRef.current.contains(e.target)) setBandOpen(false);
    }
    document.addEventListener('mousedown', handleClick);
    return () => document.removeEventListener('mousedown', handleClick);
  }, []);

  // Force a sign-text auto-fit pass any time the deck remounts (initial load,
  // palette change, reset). The global pass already watches input + viewport
  // resize, but multiple direct calls here cover the React-mount timing
  // (children mount asynchronously and CSS-applied widths settle a tick later).
  useEffect(() => {
    if (typeof window.fitSignText !== 'function') return;
    const timers = [
      requestAnimationFrame(() => window.fitSignText()),
      setTimeout(() => window.fitSignText(), 50),
      setTimeout(() => window.fitSignText(), 300),
      setTimeout(() => window.fitSignText(), 1000),
    ];
    return () => {
      cancelAnimationFrame(timers[0]);
      timers.slice(1).forEach(clearTimeout);
    };
  }, [palKey, custom.ink, custom.accent, resetKey]);

  const palette = palKey === 'custom'
    ? {
        label: 'Custom',
        paper: '#FFFFFF',
        ink: custom.ink,
        accent: custom.accent,
        rule: hex2rgba(custom.ink, 0.3),
        muted: hex2rgba(custom.ink, 0.55),
        chip: ['#FFFFFF', custom.ink, custom.accent],
      }
    : PALETTES[palKey];

  const [showResetConfirm, setShowResetConfirm] = useState(false);

  const resetAll = () => setShowResetConfirm(true);
  const confirmReset = () => { setResetKey((k) => k + 1); setShowResetConfirm(false); };
  const cancelReset  = () => setShowResetConfirm(false);

  // ── Couple verification ──────────────────────────────────────────────────
  const [verified, setVerified] = useState(() => {
    try { return sessionStorage.getItem('chf-couple-verified') === '1'; } catch { return false; }
  });
  const [showVerifyModal, setShowVerifyModal] = useState(false);
  const [verifyPending, setVerifyPending] = useState(null);
  const [accessCode, setAccessCode] = useState('');
  const [verifyError, setVerifyError] = useState('');
  const [verifyLoading, setVerifyLoading] = useState(false);

  const requestVerify = (callback) => {
    setVerifyPending(() => callback);
    setAccessCode('');
    setVerifyError('');
    setShowVerifyModal(true);
  };

  const doVerify = async () => {
    if (!accessCode.trim() || verifyLoading) return;
    setVerifyLoading(true);
    setVerifyError('');
    try {
      const res = await fetch('/api/verify-couple-access', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ code: accessCode.trim() }),
      });
      const data = await res.json();
      if (data.valid) {
        try { sessionStorage.setItem('chf-couple-verified', '1'); } catch {}
        setVerified(true);
        setShowVerifyModal(false);
        setAccessCode('');
        if (verifyPending) verifyPending();
      } else {
        setVerifyError("That code doesn’t match — check your booking confirmation and try again.");
      }
    } catch {
      setVerifyError('Something went wrong. Please try again.');
    }
    setVerifyLoading(false);
  };

  const solo = pieces.filter((pc) => (pc.page || 'framed') === 'poster');
  const row  = pieces.filter((pc) => (pc.page || 'framed') !== 'poster');

  return (
    <div className="col-page">
      {showResetConfirm && (
        <div className="col-reset-overlay" onClick={cancelReset}>
          <div className="col-reset-dialog" onClick={(e) => e.stopPropagation()}>
            <p className="col-reset-dialog__msg">Reset all signs to defaults? Your edits and any removed lines will be restored.</p>
            <div className="col-reset-dialog__btns">
              <button className="col-reset-dialog__btn col-reset-dialog__btn--cancel" onClick={cancelReset}>Cancel</button>
              <button className="col-reset-dialog__btn col-reset-dialog__btn--ok" onClick={confirmReset}>Reset</button>
            </div>
          </div>
        </div>
      )}

      {showVerifyModal && (
        <div className="col-verify-overlay" onClick={() => setShowVerifyModal(false)}>
          <div className="col-verify-dialog" onClick={(e) => e.stopPropagation()}>
            <p className="col-verify-dialog__eyebrow">Cedar Hills Farm · Couples Only</p>
            <h3 className="col-verify-dialog__title">Enter your access code to print</h3>
            <p className="col-verify-dialog__body">
              Printing is reserved for our couples. You'll find your access code in your Cedar Hills Farm booking confirmation.
            </p>
            <input
              className="col-verify-dialog__input"
              type="text"
              placeholder="Access code"
              value={accessCode}
              onChange={(e) => { setAccessCode(e.target.value); setVerifyError(''); }}
              onKeyDown={(e) => e.key === 'Enter' && doVerify()}
              autoFocus
            />
            {verifyError && <p className="col-verify-dialog__error">{verifyError}</p>}
            <div className="col-reset-dialog__btns">
              <button className="col-reset-dialog__btn col-reset-dialog__btn--cancel" onClick={() => setShowVerifyModal(false)}>Cancel</button>
              <button
                className="col-reset-dialog__btn col-reset-dialog__btn--ok"
                onClick={doVerify}
                disabled={!accessCode.trim() || verifyLoading}
              >
                {verifyLoading ? 'Checking…' : 'Unlock & Continue'}
              </button>
            </div>
          </div>
        </div>
      )}

      <header className="tmpl-nav" ref={headerRef}>
        <a href="/" className="tmpl-nav__logo" aria-label="Cedar Hills Farm — Home">
          <img src="../photos/logo.png" alt="Cedar Hills Farm" />
        </a>
        <nav className="tmpl-nav__links">
          <a href="/whats-included" className="tmpl-nav__link">The Details</a>
          <a href="/pricing" className="tmpl-nav__link">Pricing</a>
          <a href="/availability" className="tmpl-nav__link">Availability</a>
          <a href="/gallery" className="tmpl-nav__link">Gallery</a>
          <a href="/faq" className="tmpl-nav__link">FAQ</a>
          <a href="/contact" className="tmpl-nav__link">Inquire</a>
          <button
            className={`tmpl-nav__link tmpl-couples-trigger${bandOpen ? ' tmpl-couples-trigger--open' : ''} tmpl-nav__link--active`}
            aria-expanded={bandOpen}
            onClick={() => setBandOpen(v => !v)}
          >
            For Couples
            <span className="tmpl-couples-trigger__chevron" aria-hidden="true" />
          </button>
        </nav>

        {/* Hamburger — mobile only */}
        <button className="tmpl-nav__hamburger" aria-label="Open menu" onClick={() => setNavOpen(true)}>
          <span /><span /><span />
        </button>

        <div className={`tmpl-band${bandOpen ? ' tmpl-band--open' : ''}`} role="menu">
          <div className="tmpl-band__inner">
            <a href="/plan-your-layout" className="tmpl-band__item" role="menuitem" onClick={() => setBandOpen(false)}>
              <span className="tmpl-band__num">01</span>
              <span className="tmpl-band__label">Layout Planner</span>
              <span className="tmpl-band__desc">Design your reception floor plan</span>
            </a>
            <a href="/album" className="tmpl-band__item" role="menuitem" onClick={() => setBandOpen(false)}>
              <span className="tmpl-band__num">02</span>
              <span className="tmpl-band__label">Wedding Album</span>
              <span className="tmpl-band__desc">View &amp; download your photos</span>
            </a>
            <a href="index.html" className="tmpl-band__item" role="menuitem" onClick={() => setBandOpen(false)}>
              <span className="tmpl-band__num">03</span>
              <span className="tmpl-band__label">Template Library</span>
              <span className="tmpl-band__desc">Day-of signage, yours to customize</span>
            </a>
          </div>
        </div>
      </header>

      {/* Mobile overlay */}
      {navOpen && (
        <div className="tmpl-mobile-overlay">
          <button className="tmpl-mobile-overlay__close" onClick={() => setNavOpen(false)} aria-label="Close menu">×</button>
          <a href="/whats-included" className="tmpl-mobile-overlay__link">The Details</a>
          <a href="/pricing" className="tmpl-mobile-overlay__link">Pricing</a>
          <a href="/availability" className="tmpl-mobile-overlay__link">Availability</a>
          <a href="/gallery" className="tmpl-mobile-overlay__link">Gallery</a>
          <a href="/faq" className="tmpl-mobile-overlay__link">FAQ</a>
          <a href="/contact" className="tmpl-mobile-overlay__link">Inquire</a>
          <div className="tmpl-mobile-overlay__cta">
            <a href="/contact" className="tmpl-mobile-overlay__btn">Schedule a Tour</a>
          </div>
          <div className="tmpl-mobile-overlay__group">
            <span className="tmpl-mobile-overlay__group-label">For Couples</span>
            <a href="/plan-your-layout" className="tmpl-mobile-overlay__sub">Layout Planner</a>
            <a href="/album" className="tmpl-mobile-overlay__sub">Wedding Album</a>
            <a href="index.html" className="tmpl-mobile-overlay__sub">Template Library</a>
          </div>
        </div>
      )}

      <header className="col-header">
        <div className="col-header__eyebrow">COLLECTION · NO. {no}</div>
        <h1 className="col-header__title">{name}</h1>
        {descriptor && <div className="col-header__descriptor">{descriptor}</div>}
        <p className="col-header__lede">{lede}</p>
      </header>

      <section className="col-toolbar">
        <div className="col-toolbar__row">
          <div className="col-toolbar__group">
            <span className="col-toolbar__label">Palette</span>
            <div className="col-toolbar__swatches">
              {Object.entries(PALETTES).map(([k, pal]) => (
                <PaletteSwatch key={k} palette={pal} active={palKey === k} onClick={() => setPalKey(k)} />
              ))}
              <CustomSwatch
                active={palKey === 'custom'}
                onClick={() => setPalKey('custom')}
                ink={custom.ink}
                accent={custom.accent}
              />
            </div>
          </div>
          <div className="col-toolbar__hint">
            <i className="dot" />
            Click any text to edit &middot; Hover a line for &times; to drop it &middot; Palette recolors all four
            <button type="button" className="col-toolbar__reset" onClick={resetAll} title="Restore all signs to defaults">
              &#10227; Reset signs
            </button>
          </div>
        </div>
        <div className="col-toolbar__printtip">
          <strong>Tip:</strong> Each sign offers <em>Print</em> (sends straight to your printer) or <em>Download PDF</em> at <em>8½ × 11</em>, <em>4 × 6</em>, or the full <em>18 × 24 / 8 × 10</em> print-shop size. The big size is download-only — too large for most home printers — but it's the PDF to hand off to a local print shop.
        </div>
        {palKey === 'custom' && (
          <div className="col-toolbar__custom">
            <span className="col-toolbar__custom-label">YOUR COLORS</span>
            <label className="color-input">
              <span className="color-input__lbl">Ink</span>
              <input type="color" value={custom.ink} onChange={(e) => setCustom((c) => ({ ...c, ink: e.target.value }))} />
              <span className="color-input__hex">{custom.ink.toUpperCase()}</span>
            </label>
            <label className="color-input">
              <span className="color-input__lbl">Accent</span>
              <input type="color" value={custom.accent} onChange={(e) => setCustom((c) => ({ ...c, accent: e.target.value }))} />
              <span className="color-input__hex">{custom.accent.toUpperCase()}</span>
            </label>
            <span className="col-toolbar__custom-note">Paper stays white &mdash; ink &amp; accent are yours.</span>
          </div>
        )}
      </section>

      <main className="col-pieces">
        {solo.map((piece) => {
          const C = piece.Component;
          return (
            <div key={piece.name} className="col-pieces__solo">
              <Piece name={piece.name} dim={piece.dim} page={piece.page || 'poster'} verified={verified} requestVerify={requestVerify}>
                <C key={resetKey} p={palette} />
              </Piece>
            </div>
          );
        })}
        {row.length > 0 && (
          <div className="col-pieces__row">
            {row.map((piece) => {
              const C = piece.Component;
              return (
                <Piece key={piece.name} name={piece.name} dim={piece.dim} page={piece.page || 'framed'} verified={verified} requestVerify={requestVerify}>
                  <C key={resetKey} p={palette} />
                </Piece>
              );
            })}
          </div>
        )}
      </main>

      <footer className="col-outro">
        <div className="col-outro__inner">
          <div className="col-outro__eyebrow">&mdash; THAT'S THE KIT &mdash;</div>
          <h3>Ready to print.</h3>
          <p>{outro}</p>
          <a className="col-outro__cta" href="index.html">&larr; Browse other collections</a>
        </div>
      </footer>
    </div>
  );
}

Object.assign(window, { PALETTES, E, signVars, hex2rgba, Removable, Piece, PaletteSwatch, CustomSwatch, QRUploader, CollectionShell });

// ─────────── Universal text auto-fit ───────────
// Display text on signs (large script names, oversized welcome words, etc.)
// uses fixed font-sizes + white-space: nowrap. When a couple edits the text
// to something longer, it overflows the sign frame. This pass watches the
// known "big text" classes across all collections and applies a two-stage
// strategy: if the text has multiple words, first try wrapping to two lines
// (preserves the declared size); otherwise — or if wrapping still doesn't
// fit — shrink the font-size proportionally so the text sits inside the
// container. Re-runs on edit (input), on resize, and when new sign
// elements mount (palette changes / piece-add / reset).
(function initSignTextFit() {
  const FIT_SELECTOR = [
    // Editorial — sign-a
    '.sign-a__welcome',
    '.sign-a__big-name',
    // Heirloom — sign-b
    '.sign-b__welcome',
    '.sign-b__names',
    // Almanac — sign-c
    '.sign-c__line--welcome',
    '.sign-c__line--welcome-sm',
    '.sign-c__line--welcome-md',
    '.sign-c__line--names',
    // Meadow — sign-d
    '.sign-d__big-name',
    '.sign-d__welcome',
    // Harbor — sign-f (Lyric's hero has its own ScriptHero logic)
    '.sign-f__script-name',
    '.sign-f__script-single',
  ].join(',');

  const MIN_RATIO = 0.25; // never shrink below 25% of declared size
  const SLACK_PX  = 2;    // small tolerance for sub-px rendering

  function fitOne(el) {
    if (!el || !el.isConnected) return;
    // The fit element may sit inside an intermediate flex column (e.g.
    // .sign-f__script-stack on Harbor's welcome poster) that has min-width:
    // auto and bloats along with overflowing children. Use .sign__inner as
    // the bounded container (its width is anchored to the sign frame).
    const bound = el.closest('.sign__inner') || el.parentElement;
    if (!bound) return;

    // Reset to a clean baseline (declared size, single line). Cap the
    // element's max-width to the bounded container so flex bloat doesn't
    // mask overflow.
    el.style.fontSize = '';
    el.style.minWidth = '0';
    el.style.maxWidth = bound.clientWidth + 'px';
    el.style.whiteSpace = 'nowrap';

    const cs = getComputedStyle(el);
    const baseSize = parseFloat(cs.fontSize);
    if (!baseSize) return;

    const avail = bound.clientWidth - SLACK_PX;
    const text = (el.textContent || '').trim();
    const multiWord = /\s/.test(text);

    // Already fits on one line at the declared size — done.
    if (el.scrollWidth <= avail + SLACK_PX) return;

    // Reference single-line height (declared size, nowrap) for line counting.
    const singleLineH = el.offsetHeight;

    if (multiWord) {
      // Try wrapping at the declared size first — preserve the visual weight
      // if a two-line break is enough.
      el.style.whiteSpace = 'normal';
      const wrappedW = el.scrollWidth;
      const wrappedLines = Math.round(el.offsetHeight / Math.max(singleLineH, 1));
      if (wrappedLines <= 2 && wrappedW <= avail + SLACK_PX) return;

      // Two-line wrap at declared size doesn't fit (either 3+ lines or one
      // word is wider than the container). Binary-search a font-size that
      // keeps the wrapped text at <= 2 lines and within the width.
      let lo = baseSize * MIN_RATIO;
      let hi = baseSize;
      let best = lo;
      for (let i = 0; i < 10; i++) {
        const mid = (lo + hi) / 2;
        el.style.fontSize = mid + 'px';
        const lineHAtMid = singleLineH * mid / baseSize;
        const lines = Math.round(el.offsetHeight / Math.max(lineHAtMid, 1));
        const widthOK = el.scrollWidth <= avail + SLACK_PX;
        if (lines <= 2 && widthOK) {
          best = mid;
          lo = mid; // try larger
        } else {
          hi = mid; // too big
        }
      }
      el.style.fontSize = Math.floor(best * 100) / 100 + 'px';
      return;
    }

    // Single-word text — shrink the nowrap line to fit.
    el.style.whiteSpace = 'nowrap';
    const natural = el.scrollWidth;
    const ratio = avail / natural;
    const next = Math.max(baseSize * MIN_RATIO, baseSize * ratio);
    el.style.fontSize = Math.floor(next * 100) / 100 + 'px';
  }

  function fitAll(root) {
    const scope = root && root.querySelectorAll ? root : document;
    scope.querySelectorAll(FIT_SELECTOR).forEach(fitOne);
  }

  let rafId = null;
  function scheduleFitAll() {
    if (rafId) return;
    rafId = requestAnimationFrame(() => { rafId = null; fitAll(); });
  }

  function ready(fn) {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', fn, { once: true });
    } else {
      fn();
    }
  }

  ready(() => {
    fitAll();
    if (document.fonts?.ready) document.fonts.ready.then(() => requestAnimationFrame(fitAll));

    // Re-fit when text is edited inline via contentEditable
    document.addEventListener('input', (e) => {
      const t = e.target;
      if (!t || !t.closest) return;
      const host = t.closest(FIT_SELECTOR);
      if (host) fitOne(host);
    });

    // Re-fit when sign elements mount/remount (palette change, reset, etc.)
    const mo = new MutationObserver((muts) => {
      for (const m of muts) {
        for (const n of m.addedNodes) {
          if (n.nodeType !== 1) continue;
          if (n.matches?.(FIT_SELECTOR) || n.querySelector?.(FIT_SELECTOR)) {
            scheduleFitAll();
            return;
          }
        }
      }
    });
    mo.observe(document.body, { childList: true, subtree: true });

    // Re-fit on viewport resize (sign cards reflow on small screens)
    window.addEventListener('resize', scheduleFitAll);
  });

  // Expose for any caller that wants to force a refit (e.g. after async work)
  window.fitSignText = fitAll;
})();
