/* potionpost — main app
   The design step (brew → vessel/brew/ingredients/atmosphere) is this file's
   focus. The address → review → pay → sent machinery is shared with the sibling
   sites and lives in /shared/checkout.jsx (window.Checkout) — this app supplies
   the per-site copy, the dark "night" Stripe theme, the order payload, and the
   brand print layer (drawPotionFront), and renders the shared panels.
*/

const { useState, useRef, useEffect, useMemo, Fragment } = React;
const Checkout = window.Checkout;   // shared checkout hook + panels + composeFront

// ── vocab (read from window — set by potion.jsx) ──
const INGREDIENTS  = window.INGREDIENT_VOCAB;
const VESSELS_LIST = window.VESSEL_VOCAB;
const BREWS_LIST   = window.BREW_VOCAB;
const ATMOS_LIST   = window.ATMOS_VOCAB;

// Card background tints — moody, on-brand. The "midnight" default is a deep
// indigo so the brew glows; the others give you parchment / dusk / moss looks.
const BGS = [
  { id:'midnight', nm:'midnight', c:'#2a2335' },
  { id:'plum',     nm:'plum',     c:'#2f1f2c' },
  { id:'moss',     nm:'moss',     c:'#1f2a22' },
  { id:'parchment',nm:'parchment',c:'#e8dcb8' },
  { id:'bone',     nm:'bone',     c:'#d6c8a6' },
];

const MAX_INGS = 10;     // keep the brew elegant; past 10 it gets busy
const MAX_ATMOS = 3;
const NOTE_MAX = 220;
const FROM_MAX = 36;
const PRICE = 5;

// Pre-seeded "surprise me" recipes — pretty, varied, never random-garbage.
const SURPRISE_RECIPES = [
  { vessel:'cauldron', brew:'emerald',  ings:['mandrake','bay','toadstool','crystal','moonflower'], atm:['steam','sparks'] },
  { vessel:'flask',    brew:'violet',   ings:['nightshade','feather','starshard','petal'],          atm:['steam','moth']   },
  { vessel:'vial',     brew:'crimson',  ings:['petal','wax','crystal'],                             atm:['sparks','moon']  },
  { vessel:'gourd',    brew:'midnight', ings:['feather','starshard','spiral','crystal','moonflower'],atm:['moon','bats']  },
  { vessel:'jar',      brew:'gilded',   ings:['bay','acorn','starshard','moonflower'],              atm:['sparks','candle']},
  { vessel:'cauldron', brew:'midnight', ings:['eye','feather','nightshade','spiral','bone','bay'],  atm:['steam','raven']  },
  { vessel:'flask',    brew:'pearl',    ings:['moonflower','crystal','petal','starshard'],          atm:['steam','moth']   },
];

const SURPRISE_NOTES = [
  'a little something to keep the chill out.',
  'for clear skies on cloudy days.',
  'may this find you on a good night.',
  'stirred while thinking of you.',
  'one part fondness, two parts hope.',
  'brewed at moonrise, sealed with care.',
  'for the spell-bound in our lives.',
  'sip slowly. share if you must.',
  'a small charm against ordinary tuesdays.',
  'simmered low for someone wonderful.',
  'this one is for keeping, not curing.',
  'a draught of the warm-and-quiet variety.',
  'half potion, half postcard, all you.',
  'three drops at dusk; do not skip.',
  'made dark, finished sweet — like you.',
  'an entirely benign little hex.',
  'a sleeping-draught for noisy weeks.',
  'put a kettle on and keep me near.',
  'a brew for the small good things.',
  'mostly love. trace of moonlight.',
];

const STEPS = ['brew','address','review','sent'];

// ──────────────────────────────────────────────────────────────────
// Card preview — the physical postcard with potion art + handwritten note.
// ──────────────────────────────────────────────────────────────────

function CardPreview({ vessel, brew, ingredients, atmosphere, accentSeed, bg, note, from, small, innerRef }) {
  // On light tints, override the (light) ink so the note/signature stay legible —
  // matches the adaptive ink the print composite uses, so preview == mailed card.
  const light = Checkout.isLight(bg);
  const msgStyle = light ? { color: '#2a1f1a' } : null;
  const sigStyle = light ? { color: '#8a5a14' } : null;
  return (
    <div ref={innerRef} className={"postcard" + (small ? " sm" : "")} style={bg ? { background: bg } : null}>
      <span className="corner tl">№ vii</span>
      <span className="corner tr">est. 1899</span>
      <div className="art">
        <Potion vessel={vessel} brew={brew} ingredients={ingredients} atmosphere={atmosphere} accentSeed={accentSeed} />
      </div>
      <hr className="msg-divide" />
      <div className={"msg" + (note.trim() ? "" : " placeholder")} style={note.trim() ? msgStyle : null}>
        {note.trim() || "the incantation, in your own hand…"}
      </div>
      {from.trim() && <div className="sig" style={sigStyle}>— {from.trim()}</div>}
    </div>
  );
}

// ──────────────────────────────────────────────────────────────────
// Top bar
// ──────────────────────────────────────────────────────────────────

function TopBar({ step, onStep, admin }) {
  const stepIndex = STEPS.indexOf(step);
  const labels = ['brew', 'address', 'review', 'sent'];
  return (
    <header className="topbar">
      <div className="brandwrap">
        <Wordmark size={28} />
        <span className="tagline">small spells, by post.</span>
      </div>
      <div className="topright">
        <nav className="steps">
          {labels.map((s, i) => {
            const state = i === stepIndex ? 'on' : i < stepIndex ? 'done' : 'todo';
            return (
              <button key={s} className={"step " + state}
                      onClick={() => state === 'done' && onStep(s)}>
                <span className="num">{String(i + 1).padStart(2, '0')}</span>
                <span>{s}</span>
              </button>
            );
          })}
        </nav>
        {admin && admin.clientId && <Checkout.AdminControls admin={admin} />}
      </div>
    </header>
  );
}

// ──────────────────────────────────────────────────────────────────
// Brew panel — the design step's right column
// ──────────────────────────────────────────────────────────────────

function BrewPanel({
  vessel, setVessel,
  brew, setBrew,
  ingredients, addIng, removeIng,
  atmosphere, toggleAtmos,
  bg, setBg,
  note, setNote,
  from, setFrom,
  onReshuffle, onSurprise, atmosOpen, setAtmosOpen,
}) {
  return (
    <aside className="panel">
      <h2>brew your potion</h2>
      <p className="hint">pick a vessel, choose your brew, drop in what you fancy. <br/>everything is reversible.</p>
      <button className="seed randomize" onClick={onSurprise}>✦ surprise me</button>

      {/* ── vessel ── */}
      <div className="group">
        <div className="lab"><span>vessel</span><span className="ct">{VESSEL_META[vessel] && VESSEL_META[vessel].lt}</span></div>
        <div className="vessels">
          {VESSELS_LIST.map(v => (
            <button key={v.id} className={"vessel" + (vessel === v.id ? " sel" : "")} onClick={() => setVessel(v.id)} title={v.name}>
              <span className="vfig"><svg width="32" height="32" viewBox="0 0 36 36">{v.glyph}</svg></span>
              <span className="nm">{v.name}</span>
            </button>
          ))}
        </div>
      </div>

      {/* ── brew color ── */}
      <div className="group">
        <div className="lab"><span>brew</span><span className="ct">{BREW_META[brew] && BREW_META[brew].name}</span></div>
        <div className="brews">
          {BREWS_LIST.map(b => (
            <button key={b.id} className={"brew" + (brew === b.id ? " sel" : "")}
                    onClick={() => setBrew(b.id)} title={b.name}
                    style={{ background: `linear-gradient(180deg, ${b.top} 0%, ${b.mid} 60%, ${b.bot} 100%)` }}>
              <span className="lbl">{b.name}</span>
            </button>
          ))}
        </div>
      </div>

      {/* ── ingredients ── */}
      <div className="group">
        <div className="lab">
          <span>ingredients</span>
          <span className="ct">
            {ingredients.length} / {MAX_INGS}
            <button className="reshuffle" onClick={onReshuffle} disabled={ingredients.length < 2}> · reshuffle</button>
          </span>
        </div>
        <div className="ings">
          {INGREDIENTS.map(ing => {
            const isAtCap = ingredients.length >= MAX_INGS;
            return (
              <button key={ing.type} className="ing"
                      onClick={() => addIng(ing.type)} disabled={isAtCap}
                      title={`add ${ing.name}`}>
                <span className="swatch"><svg viewBox="-16 -16 32 32" width="22" height="22">{ing.glyph}</svg></span>
                <span className="col">
                  <span className="nm">{ing.name}</span>
                  <span className="lt">{ing.lt}</span>
                </span>
              </button>
            );
          })}
        </div>
        {ingredients.length > 0 && (
          <div className="chips">
            {ingredients.map(i => (
              <button key={i.id} className="chip" onClick={() => removeIng(i.id)}>
                {INGREDIENTS.find(x => x.type === i.type) ? INGREDIENTS.find(x => x.type === i.type).name : i.type}
                <span className="x">✕</span>
              </button>
            ))}
          </div>
        )}
      </div>

      {/* ── atmosphere (collapsible) ── */}
      <div className="group">
        <button className="disclose" onClick={() => setAtmosOpen(!atmosOpen)}>
          <span>
            atmosphere
            {atmosphere.length > 0 && <span className="cnt">{atmosphere.length}</span>}
          </span>
          <span className="chev">{atmosOpen ? '–' : '+'}</span>
        </button>
        {atmosOpen && (
          <>
            <p className="dischint">small flourishes around the vessel — up to {MAX_ATMOS}.</p>
            <div className="atmos">
              {ATMOS_LIST.map(a => {
                const isSel = atmosphere.includes(a.id);
                const atCap = atmosphere.length >= MAX_ATMOS && !isSel;
                return (
                  <button key={a.id} className={"atm" + (isSel ? " sel" : "")}
                          onClick={() => toggleAtmos(a.id)} disabled={atCap}>
                    <span className="afig"><svg width="24" height="24" viewBox="0 0 36 36">{a.glyph}</svg></span>
                    <span className="nm">{a.name}</span>
                  </button>
                );
              })}
            </div>
          </>
        )}
        {/* card-tint row — small, low visual weight */}
        <div className="bgs">
          <span className="bgs-lab">card tint</span>
          {BGS.map(b => (
            <button key={b.id} className={"swatchbtn" + (bg === b.id ? " sel" : "")}
                    title={b.nm} aria-label={b.nm}
                    style={{ background: b.c }}
                    onClick={() => setBg(b.id)} />
          ))}
        </div>
      </div>

      {/* ── incantation (note) ── */}
      <div className="group">
        <div className="lab"><span>incantation</span><span className="ct">{note.length} / {NOTE_MAX}</span></div>
        <textarea value={note} maxLength={NOTE_MAX}
                  placeholder="write a short spell, a wish, or a hello…"
                  onChange={e => setNote(e.target.value)} />
        <div className="spells">
          {SURPRISE_NOTES.slice(0, 4).map((s, i) => (
            <button key={i} className="spell" onClick={() => setNote(s)}>{s}</button>
          ))}
          <button className="spell" onClick={() => setNote(SURPRISE_NOTES[Math.floor(Math.random() * SURPRISE_NOTES.length)])}>
            ✦ try another
          </button>
        </div>
      </div>

      {/* ── signed by ── */}
      <div className="group">
        <div className="lab"><span>signed</span><span className="ct">{from.length} / {FROM_MAX}</span></div>
        <input className="from" value={from} maxLength={FROM_MAX}
               placeholder="your name, or alias of choice…"
               onChange={e => setFrom(e.target.value)} />
      </div>
    </aside>
  );
}

// ──────────────────────────────────────────────────────────────────
// Step panels — each owns its own markup/prose, wired to the shared
// useCheckout hook (co.*) and using Checkout.Field.
// ──────────────────────────────────────────────────────────────────

// Dark "night" Stripe Payment Element theme — candle-gold on aubergine.
const STRIPE_APPEARANCE = { theme: 'night', variables: {
  colorPrimary: '#d6a847', colorBackground: '#2a2335', colorText: '#f0e2c1',
  colorTextSecondary: '#8a7c5c', colorDanger: '#c87a4a',
  fontFamily: 'Newsreader, Georgia, serif', borderRadius: '10px',
}};

function AddressPanel({ co }) {
  const { Field } = Checkout;
  const errors = co.errors;
  return (
    <aside className="panel">
      <h2>where shall it land?</h2>
      <p className="hint">a 6 × 9 in art-postcard, printed on thick coated stock and mailed first-class. <br/>arrives in 4–7 days.</p>

      {co.sendError && <div className="notice">{co.sendError}</div>}

      <div className="subhead">
        recipient
        <span className="sub">where the potion ends up. include their full name.</span>
      </div>
      <Checkout.PasteAddress label="paste an address" placeholder="paste a full address here — we’ll sort it into the fields below" onParsed={p => co.setTo({ ...co.to, ...p })} />
      <Field label="full name" value={co.to.name} onChange={v => co.setTo({ ...co.to, name:v })} err={errors.to && errors.to.name} placeholder="margaret hawthorne" />
      <Checkout.StreetField label="street" value={co.to.line1} err={errors.to && errors.to.line1} placeholder="221b baker street"
        ctx={{ city: co.to.city, state: co.to.state, zip: co.to.zip }}
        onChange={v => co.setTo({ ...co.to, line1:v })}
        onPick={s => co.setTo({ ...co.to, line1:s.line1, city:s.city, state:s.state, zip:s.zip })} />
      <Field label="apt / unit (optional)" value={co.to.line2} onChange={v => co.setTo({ ...co.to, line2:v })} placeholder="apt 3a" />
      <div className="row">
        <Field label="city"  value={co.to.city}  onChange={v => co.setTo({ ...co.to, city:v  })} err={errors.to && errors.to.city}  placeholder="portland" />
        <Field label="state" value={co.to.state} onChange={v => co.setTo({ ...co.to, state:v.toUpperCase() })} err={errors.to && errors.to.state} maxLength={2} placeholder="OR" />
        <Field label="zip"   value={co.to.zip}   onChange={v => co.setTo({ ...co.to, zip:v   })} err={errors.to && errors.to.zip}   placeholder="97214" />
      </div>

      {co.tos.slice(1).map((t, j) => {
        const i = j + 1;
        const te = errors.tos && errors.tos[i] || {};
        return (
          <React.Fragment key={i}>
            <div className="subhead" style={{display:'flex',justifyContent:'space-between',alignItems:'baseline'}}>
              <span>recipient {i + 1}</span>
              <button type="button" className="linkbtn quiet" onClick={() => co.removeRecipient(i)}>remove</button>
            </div>
            <Checkout.PasteAddress label="paste an address" placeholder="paste a full address here — we'll sort it into the fields below" onParsed={p => co.setRecipient(i, {...t, ...p})} />
            <Field label="full name" value={t.name} err={te.name} placeholder="margaret hawthorne" onChange={v => co.setRecipient(i, {...t, name:v})} />
            <Checkout.StreetField label="street" value={t.line1} err={te.line1} placeholder="221b baker street"
              ctx={{ city: t.city, state: t.state, zip: t.zip }}
              onChange={v => co.setRecipient(i, {...t, line1:v})}
              onPick={s => co.setRecipient(i, {...t, line1:s.line1, city:s.city, state:s.state, zip:s.zip})} />
            <Field label="apt / unit (optional)" value={t.line2} onChange={v => co.setRecipient(i, {...t, line2:v})} />
            <div className="row">
              <Field label="city"  value={t.city}  onChange={v => co.setRecipient(i, {...t, city:v})}  err={te.city}  placeholder="portland" />
              <Field label="state" value={t.state} onChange={v => co.setRecipient(i, {...t, state:v.toUpperCase()})} err={te.state} maxLength={2} placeholder="OR" />
              <Field label="zip"   value={t.zip}   onChange={v => co.setRecipient(i, {...t, zip:v})}   err={te.zip}   placeholder="97214" />
            </div>
          </React.Fragment>
        );
      })}
      {co.tos.length < co.maxRecipients && (
        <p className="addrcpt">
          <button type="button" className="linkbtn" onClick={co.addRecipient}>+ send this card to another address</button>
          <span className="muted">${PRICE} each — same card, mailed separately</span>
        </p>
      )}

      <div className="subhead">return address</div>
      <p className="savehint">we print potionpost's return address by default — nothing to fill in.</p>
      <label className="optret">
        <input type="checkbox" checked={co.useOwnReturn} onChange={e => co.setUseOwnReturn(e.target.checked)} />
        <span>print my own return address instead</span>
      </label>
      {co.useOwnReturn && (
        <div className="retform">
          <p className="savehint" style={{ marginTop: '10px' }}>we'll remember this for next time — only on this browser</p>
          <Field label="full name" value={co.ret.name} onChange={v => co.setRet({ ...co.ret, name:v })} err={errors.ret && errors.ret.name} placeholder="your name" />
          <Checkout.StreetField label="street" value={co.ret.line1} err={errors.ret && errors.ret.line1} placeholder="123 willow lane"
            ctx={{ city: co.ret.city, state: co.ret.state, zip: co.ret.zip }}
            onChange={v => co.setRet({ ...co.ret, line1:v })}
            onPick={s => co.setRet({ ...co.ret, line1:s.line1, city:s.city, state:s.state, zip:s.zip })} />
          <Field label="apt / unit (optional)" value={co.ret.line2} onChange={v => co.setRet({ ...co.ret, line2:v })} />
          <div className="row">
            <Field label="city"  value={co.ret.city}  onChange={v => co.setRet({ ...co.ret, city:v  })} err={errors.ret && errors.ret.city}  />
            <Field label="state" value={co.ret.state} onChange={v => co.setRet({ ...co.ret, state:v.toUpperCase() })} err={errors.ret && errors.ret.state} maxLength={2} />
            <Field label="zip"   value={co.ret.zip}   onChange={v => co.setRet({ ...co.ret, zip:v   })} err={errors.ret && errors.ret.zip}   />
          </div>
        </div>
      )}
    </aside>
  );
}

function ReviewPanel({ co, summary }) {
  const dollars = ((co.payEnabled ? co.payAmount : PRICE * co.tos.length * 100) / 100).toFixed(2);
  return (
    <aside className="panel">
      <h2>one last look</h2>
      <p className="hint">we'll print this card and drop it in the post within one business day.</p>
      {co.sendError && <div className="notice">{co.sendError}</div>}

      <div className="rev">
        <div className="subhead">to</div>
        {co.tos.map((t, i) => (
          <div className="addr" key={i} style={i > 0 ? {marginTop:'12px',paddingTop:'12px',borderTop:'1px solid var(--line)'} : null}>
            <span className="who">{t.name}</span>
            {t.line1}{t.line2 ? `, ${t.line2}` : ''}<br/>
            {t.city}, {t.state} {t.zip}
          </div>
        ))}
      </div>

      {summary}

      <div className="rev">
        <div className="subhead">summary</div>
        {co.tos.length > 1 && (
          <div className="line"><span>{co.tos.length} cards × ${PRICE.toFixed(2)} — same design, mailed separately</span></div>
        )}
        <div className="line total">
          <span>{co.tos.length > 1 ? `${co.tos.length} 6 × 9 art-postcards` : 'one 6 × 9 art-postcard'} — printing &amp; postage included</span>
          <span>{co.freeApplied ? <><s style={{opacity:.6,marginRight:'8px'}}>${(PRICE * co.tos.length).toFixed(2)}</s>free</> : `$${dollars}`}</span>
        </div>
        <p className="muted">{co.tos.length > 1 ? `${co.tos.length} separate mailings` : `a single mailing to ${co.to.name || 'them'}`}. nothing else.</p>
      </div>

      {!co.campaignMode && (
        <div className="rev">
          <div className="subhead">
            for the receipt
            <span className="sub">a single email when the postcard ships. no list, no upsell.</span>
          </div>
          <Checkout.Field label="email" value={co.email} onChange={co.setEmail} err={co.errors.email} placeholder="you@somewhere.com" />
        </div>
      )}

      {co.isAdminFree ? (
        <div className="rev">
          <div className="subhead">payment</div>
          <p className="muted" style={{ color: 'var(--pop)' }}>admin free-send — no charge. this prints &amp; mails a <strong>real</strong> card.</p>
        </div>
      ) : co.freeApplied ? (
        <div className="rev">
          <div className="subhead">payment</div>
          <p className="muted" style={{ color: 'var(--pop-deep)' }}>gift code applied — {co.tos.length > 1 ? 'these cards ship' : 'this card ships'} free.{typeof co.freeRemaining === 'number' ? ` (${co.freeRemaining} left on this code)` : ''}</p>
          <p className="muted">no payment needed — we’ll prepare {co.tos.length > 1 ? 'these cards' : 'this card'} and mail {co.tos.length > 1 ? 'them' : 'it'} for you.</p>
        </div>
      ) : co.payEnabled ? (
        <div className="rev">
          <div className="subhead">payment</div>
          <div className="promo">
            <input value={co.promo} maxLength={24} placeholder="promo code" onChange={e => co.setPromo(e.target.value)} />
            <button className="btn ghost" onClick={co.applyPromo} disabled={co.creatingOrder || !co.promo.trim()}>
              {co.creatingOrder ? '…' : 'apply'}
            </button>
          </div>
          {co.promoApplied && <p className="muted" style={{ color: 'var(--pop-deep)' }}>promo applied — your total is ${dollars}.</p>}
          {co.promoTried && !co.promoApplied && !co.creatingOrder && co.promo.trim() && <p className="muted" style={{ color: 'var(--warn)' }}>that code isn't valid.</p>}
          <div id="payment-element" style={{ marginTop: 14 }}></div>
          {co.creatingOrder && <p className="muted">preparing secure checkout…</p>}
          {!co.clientSecret && !co.creatingOrder && <p className="muted">enter your email above, then “continue to payment”.</p>}
          {co.testMode && <p className="muted">test mode — card <strong>4242 4242 4242 4242</strong>, any future date, any CVC &amp; ZIP.</p>}
        </div>
      ) : (
        <p className="muted">payment isn’t configured — "seal &amp; send" simulates the order to test the print pipeline.</p>
      )}
    </aside>
  );
}

function SentPanel({ co, onAnother }) {
  return (
    <aside className="panel">
      <div className="done">
        <span className="badge">brewed &amp; bottled</span>
        <h2>off it goes <span style={{color:'var(--pop)'}}>✦</span></h2>
        <p>{co.tos.length > 1
          ? <>your potion has been bottled in <strong>{co.tos.length} copies</strong> — addressed to {co.tos.map(t => t.name).filter(Boolean).join(', ')} — and sent to press.</>
          : <>your potion has been sealed, addressed to <em>{co.to.name || 'them'}</em>, and is on its way by post.</>}</p>
        {co.email && <p className="muted">we've emailed your tracking link to <em>{co.email}</em>.</p>}
        <ol className="next-steps">
          <li>printed on thick coated stock at our press in the morning.</li>
          <li>handed to the post that afternoon.</li>
          <li>lands in their mailbox in 4–7 business days, looking very much itself.</li>
        </ol>
        {co.orderId
          ? <a className="btn" style={{ display:'inline-block', textDecoration:'none', marginTop: 24 }}
               href={`track.html?o=${encodeURIComponent(co.orderId)}`}>track your card →</a>
          : <p className="muted" style={{ marginTop: 16 }}>check your email for the tracking link.</p>}
        <div><button className="btn ghost" onClick={onAnother} style={{ marginTop: 22 }}>brew another</button></div>
      </div>
    </aside>
  );
}

// ──────────────────────────────────────────────────────────────────
// Print render: bake the potion card into one rotated image for Lob.
// Lob's 4×6 is LANDSCAPE-only and ignores CSS transforms; composeFront fills the
// portrait canvas + rotates it, and this draws the potionpost-specific layer
// (corner labels, wordmark, signature, incantation, ✦ divider, the potion art).
// ──────────────────────────────────────────────────────────────────

function drawPotionFront(x, env, { note, from }) {
  const { PR_W, PR_H, left, right, top, bottom, cw } = env.dims;
  const { light, bgHex, wrapLines, art } = env;

  // Adaptive ink so the note reads on dark *and* light tints (matches CardPreview).
  const inkNote = light ? '#2a1f1a' : '#e8dcc0';
  const inkSig  = light ? '#8a5a14' : '#f1c970';
  const inkMeta = light ? '#6e5a36' : '#8a7c5c';
  const hair    = light ? 'rgba(40,28,20,.22)' : 'rgba(214,190,138,.26)';

  // gold inset frame (echoes the card's inset border)
  if (x.roundRect) { x.save(); x.strokeStyle = hair; x.lineWidth = 3;
    x.beginPath(); x.roundRect(left * 0.62, top * 0.62, PR_W - left * 1.24, PR_H - top * 1.24, 22); x.stroke(); x.restore(); }

  // corner labels
  x.fillStyle = inkMeta; x.textBaseline = 'alphabetic';
  x.font = "italic 400 34px 'IM Fell English', Georgia, serif";
  x.textAlign = 'left'; x.fillText('№ vii', left, top + 4);
  x.textAlign = 'right'; x.fillText('est. 1899', right, top + 4);
  x.textAlign = 'left';

  // bottom-up: wordmark, signature, note; the potion fills the remaining top.
  let yb = bottom;
  x.fillStyle = inkMeta; x.textAlign = 'center'; x.font = "500 30px 'JetBrains Mono', monospace";
  if ('letterSpacing' in x) x.letterSpacing = '7px';
  x.fillText('POTIONPOST.IO', PR_W / 2, yb);
  if ('letterSpacing' in x) x.letterSpacing = '0px';
  yb -= 30 + 44;

  if (from && from.trim()) {
    x.fillStyle = inkSig; x.textAlign = 'right'; x.font = "500 78px 'Caveat', cursive";
    x.fillText('— ' + from.trim(), right, yb);
    yb -= 78 + 26;
  }

  // note — auto-fit (92→50px) so it never crowds the potion
  const artTop = top + 84;
  const noteBandBottom = yb;
  const avail = noteBandBottom - artTop;
  const maxNoteH = avail * 0.42;
  const noteText = (note || '').trim();
  let fs = 92, lines = [];
  for (; fs >= 50; fs -= 6) { x.font = `500 ${fs}px 'Caveat', cursive`; lines = wrapLines(x, noteText, cw); if (lines.length * Math.round(fs * 1.22) <= maxNoteH) break; }
  const lh = Math.round(fs * 1.22);
  x.fillStyle = inkNote; x.textAlign = 'left'; x.font = `500 ${fs}px 'Caveat', cursive`;
  let ny = noteBandBottom;
  for (let i = lines.length - 1; i >= 0; i--) { x.fillText(lines[i], left, ny); ny -= lh; }
  const noteTop = ny + lh;

  // The potion fills the space above the note. Matches petalpost's proven front:
  // no ✦ divider rule — it collided with the Caveat ascenders; the art just sits
  // above the note with a small gap.
  const artBottom = noteText ? noteTop - 40 : yb;
  if (art && artBottom > artTop) {
    const aw = cw, ah = artBottom - artTop, r = art.height / art.width;
    let dw = aw, dh = aw * r; if (dh > ah) { dh = ah; dw = ah / r; }
    x.drawImage(art, left + (aw - dw) / 2, artTop + (ah - dh) / 2, dw, dh);
  }
}

// ──────────────────────────────────────────────────────────────────
// App
// ──────────────────────────────────────────────────────────────────

// The design-tool "Tweaks" panel is stripped for production; the values the
// design landed on are baked in here (midnight palette, IM Fell display, corner
// labels on, full vessel glow). The look is set once on <html> at mount — the
// index.html also sets data-palette="midnight" up front so there's no flash.
const LOOK = { palette: 'midnight', display: 'fell', corners: true, glow: 1 };

function App() {
  useEffect(() => {
    const r = document.documentElement;
    r.setAttribute('data-palette', LOOK.palette);
    r.style.setProperty('--display', LOOK.display === 'fell' ? "'IM Fell English', 'Newsreader', Georgia, serif" : "'Newsreader', Georgia, serif");
    r.style.setProperty('--glow-mult', String(LOOK.glow));
    r.classList.toggle('no-corners', !LOOK.corners);
  }, []);

  // design state
  const [vessel, setVessel] = useState('cauldron');
  const [brew, setBrew] = useState('emerald');
  const [ingredients, setIngredients] = useState([]);
  const [atmosphere, setAtmosphere] = useState([]);
  const [atmosOpen, setAtmosOpen] = useState(false);
  const [bg, setBg] = useState('midnight');
  const [accentSeed, setAccentSeed] = useState(() => (Math.random() * 1e6) | 0);
  const [note, setNote] = useState('');
  const [from, setFrom] = useState('');
  const idRef = useRef(0);
  const previewRef = useRef(null);   // the live CardPreview, rasterized for the print

  const bgHex = (BGS.find(b => b.id === bg) || BGS[0]).c;

  // brew step actions
  function addIng(type) {
    if (ingredients.length >= MAX_INGS) return;
    idRef.current += 1;
    const id = idRef.current;
    setIngredients(s => [...s, { id, type }]);
  }
  const removeIng = (id) => setIngredients(s => s.filter(x => x.id !== id));
  function toggleAtmos(id) {
    setAtmosphere(a => a.includes(id) ? a.filter(x => x !== id) : a.length >= MAX_ATMOS ? a : [...a, id]);
  }
  function reshuffle() { setAccentSeed((Math.random() * 1e6) | 0); }
  function surprise() {
    const r = SURPRISE_RECIPES[Math.floor(Math.random() * SURPRISE_RECIPES.length)];
    setVessel(r.vessel); setBrew(r.brew);
    const newIngs = r.ings.map(type => { idRef.current += 1; return { id: idRef.current, type }; });
    setIngredients(newIngs);
    setAtmosphere(r.atm);
    setAtmosOpen(true);
    setAccentSeed((Math.random() * 1e6) | 0);
    // Roll a fresh incantation only if the note is empty or one of our suggested
    // lines — never clobber something the sender wrote by hand.
    setNote(n => (!n.trim() || SURPRISE_NOTES.includes(n))
      ? SURPRISE_NOTES[Math.floor(Math.random() * SURPRISE_NOTES.length)] : n);
  }

  const designReady = ingredients.length > 0;

  // product-specific order payload (the shared hook adds image/preview/addresses)
  const potionPayload = () => ({ vessel, brew, ingredients: ingredients.map(i => ({ type: i.type })), atmosphere });

  // Returns { image, preview, bouquet, previewBack } via the shared composers + the
  // potion layer. previewBack is the clean mailing-side echo for the emails.
  async function renderComposite() {
    const front = await Checkout.composeFront({
      bgHex,
      fonts: ["500 96px 'Caveat'", "500 30px 'JetBrains Mono'", "italic 400 34px 'IM Fell English'"],
      loadArt: () => Checkout.loadArtImage(previewRef.current && previewRef.current.querySelector('.art svg'), 1400, 0.03),
      drawPortrait: (x, env) => drawPotionFront(x, env, { note, from }),
    });
    const ingNames = ingredients.map(i => (INGREDIENTS.find(x => x.type === i.type) || {}).name).filter(Boolean);
    const previewBack = await Checkout.composeBack({
      bgHex, echoPng: front.bouquet,
      fonts: ["italic 46px 'Newsreader'", "26px 'Newsreader'"],
      left: { kind:'art', invLabel:'in this brew', items: ingNames },
      mark: { word1:'potion', word2:'post.io' }, caption:'scan to brew your own', glyph:'✦',
      qrUrl:'/api/order/card/qr.svg',
    });
    return { ...front, previewBack };
  }

  // ── shared checkout machinery (step + address + payment + order pipe) ──
  const co = Checkout.useCheckout({
    price: PRICE,
    designStep: 'brew',
    localStorageKey: 'pp_ret',
    stripeAppearance: STRIPE_APPEARANCE,
    returnUrl: window.location.origin + '/',
    buildPayload: () => ({ potion: potionPayload(), note, from, bg: bgHex }),
    renderComposite,
  });
  const { step, setStep } = co;

  function startOver() {
    co.startOver(() => {
      setIngredients([]); setAtmosphere([]); setVessel('cauldron'); setBrew('emerald');
      setNote(''); setFrom('');
    });
  }

  const stepIndex = STEPS.indexOf(step);
  const cta = (() => {
    if (step === 'brew')    return { label: designReady ? 'continue to address' : 'add an ingredient', disabled: !designReady };
    if (step === 'address') return { label: co.verifying ? (co.tos.length > 1 ? 'checking addresses…' : 'checking address…') : 'review the potion', disabled: co.verifying };
    if (step === 'review') {
      // email gates every non-campaign path (paid / admin-free / no-Stripe dev)
      const needEmail = !co.campaignMode && !co.emailValid;
      if (co.isAdminFree) return { label: co.paying ? 'mailing…' : 'mail it free ✦', disabled: co.paying || needEmail };
      // Free gift code: no payment element — one CTA holds the order server-side.
      if (co.freeApplied) return { label: (co.paying || co.creatingOrder) ? 'preparing…' : 'mail it free ✦', disabled: co.paying || co.creatingOrder || needEmail };
      const dollars = ((co.payEnabled ? co.payAmount : PRICE * 100) / 100).toFixed(2);
      // Two-phase paid flow: confirm the email first (opens the PaymentIntent), then pay.
      if (co.payEnabled && !co.campaignMode && !co.clientSecret)
        return { label: co.creatingOrder ? 'preparing…' : 'continue to payment', disabled: co.paying || co.creatingOrder || needEmail };
      return {
        label: co.paying ? 'processing…' : `seal & send · $${dollars}`,
        disabled: co.paying || co.creatingOrder || needEmail,
      };
    }
    return null;
  })();

  // summary line in the action bar
  const sum = (() => {
    if (step === 'brew') {
      const v = VESSEL_META[vessel];
      const b = BREW_META[brew];
      if (!designReady) return 'pick a vessel · drop in an ingredient to begin';
      return `${ingredients.length} ingredient${ingredients.length === 1 ? '' : 's'} · ${b.name} in a ${v.name}`;
    }
    if (step === 'address') return co.addrReady ? 'address looks complete' : 'fill the recipient address';
    if (step === 'review')  return 'one card · printing & postage included';
    return '';
  })();

  // the per-site recipe block shown on the review step (shared ReviewPanel renders it)
  const recipeNode = (() => {
    const ingNames = ingredients.map(i => (INGREDIENTS.find(x => x.type === i.type) || {}).name).filter(Boolean);
    const v = VESSEL_META[vessel];
    const b = BREW_META[brew];
    const atmNames = atmosphere.map(id => (ATMOS_LIST.find(a => a.id === id) || {}).name).filter(Boolean);
    // "a"/"an" by leading vowel sound — "an emerald brew", "a cauldron".
    const article = (w) => /^[aeiou]/i.test(String(w || '')) ? 'an' : 'a';
    return (
      <div className="rev">
        <div className="subhead">the recipe</div>
        <div className="recipe">
          {article(b && b.name)} <em>{b && b.name}</em> brew, stirred in {article(v && v.name)} <em>{v && v.name}</em>,
          {ingNames.length > 0 && <> with <em>{ingNames.join(', ')}</em></>}
          {atmNames.length > 0 && <> · {atmNames.join(', ')} for atmosphere</>}.
          <br/><br/>
          <span className="rcap">incantation</span>
          {note ? `"${note}"` : <em style={{color:'var(--taupe)'}}>(no note — just the brew)</em>}
          {from && <><br/>— <em>{from}</em></>}
        </div>
      </div>
    );
  })();

  return (
    <div className="shell">
      <TopBar step={step} onStep={setStep} admin={co.admin} />

      {step !== 'sent' ? (
        <div className="work">
          <section className="stage">
            {step === 'brew' && (
              <div className="intro">
                <p className="kick">small spells <span className="glyph">✦</span> mailed first-class <span className="glyph">✦</span> printed on thick stock</p>
                <h1>brew a little something. <em>send it by post.</em></h1>
                <p>compose a potion — vessel, brew, a handful of ingredients — and a few honest words.<br/>
                  <span className="pop">we print &amp; mail the card.</span> they get something pretty in the post.
                </p>
              </div>
            )}

            <CardPreview
              vessel={vessel} brew={brew} ingredients={ingredients} atmosphere={atmosphere}
              accentSeed={accentSeed} bg={bgHex} note={note} from={from}
              small={step !== 'brew'} innerRef={previewRef}
            />

            <p className="spec">
              6 × 9 in <span className="glyph">✦</span> thick coated card stock <span className="glyph">✦</span> printed &amp; mailed for you
            </p>

            {step === 'brew' && !designReady && (
              <button className="seed" style={{ marginTop: 18 }} onClick={surprise}>✦ surprise me with a recipe</button>
            )}
          </section>

          {step === 'brew' && (
            <BrewPanel
              vessel={vessel} setVessel={setVessel}
              brew={brew} setBrew={setBrew}
              ingredients={ingredients} addIng={addIng} removeIng={removeIng}
              atmosphere={atmosphere} toggleAtmos={toggleAtmos}
              bg={bg} setBg={setBg}
              note={note} setNote={setNote}
              from={from} setFrom={setFrom}
              onReshuffle={reshuffle} onSurprise={surprise}
              atmosOpen={atmosOpen} setAtmosOpen={setAtmosOpen}
            />
          )}
          {step === 'address' && <AddressPanel co={co} />}
          {step === 'review' && <ReviewPanel co={co} summary={recipeNode} />}

          {cta && (
            <div className="actionbar">
              <span className="sum">
                {step !== 'brew' && <button className="btn ghost" onClick={co.goBack} style={{ marginRight: 12 }}>back</button>}
                {sum}
              </span>
              <button className="btn" onClick={() => co.goNext(designReady)} disabled={cta.disabled}>{cta.label}</button>
            </div>
          )}
        </div>
      ) : (
        <SentPanel co={co} onAnother={startOver} />
      )}

      <Footer />
    </div>
  );
}

// ──────────────────────────────────────────────────────────────────
// Footer
// ──────────────────────────────────────────────────────────────────

// Cross-product "family" row, built from the shared registry in /shared/family.js
// (window.familyOthers). React-safe: we render the markup ourselves so React owns
// the DOM; family.js only supplies the data + the injected .fam-* styles.
function FamilyRow() {
  var others = (typeof window !== 'undefined' && window.familyOthers)
    ? window.familyOthers('potionpost') : [];
  if (!others.length) return null;
  return (
    <div className="fam" style={{ '--fam-max': '1100px' }}>
      <span className="fam-lead">more from the same little press</span>
      <div className="fam-row">
        {others.map(function (s) {
          return (
            <a key={s.id} className="fam-card" href={'https://' + s.host + '/?ref=potionpost'}
               title={s.pitch} style={{ '--fa': s.accent }}>
              <img className="fam-og" src={'/' + s.id + '/og.png'} alt={s.name + ' — ' + s.pitch}
                   loading="lazy" decoding="async" />
              <span className="fam-cap">
                <span className="fam-sw"></span>
                <span className="fam-nm">{s.name}</span>
              </span>
            </a>
          );
        })}
      </div>
    </div>
  );
}

function Footer() {
  return (
    <footer className="pp-footer">
      <div className="inner">
        <div className="col brandcol">
          <Wordmark size={22} />
          <p>a small stall for handmade mail, by post. brewed, printed, &amp; sent on your behalf.</p>
        </div>
        <div className="col">
          <h4>the brews</h4>
          <a href="#">how it works</a>
          <a href="#">ingredients</a>
          <a href="#">sample card</a>
          <a href="#">tracking</a>
        </div>
        <div className="col">
          <h4>the apothecary</h4>
          <a href="#">about</a>
          <a href="#">our press</a>
          <a href="#">faq</a>
          <a href="#">contact</a>
        </div>
        <div className="col">
          <h4>the small print</h4>
          <a href="#">privacy</a>
          <a href="#">refunds</a>
          <a href="#">terms</a>
        </div>
      </div>
      <FamilyRow />
      <div className="bar">
        <div className="inner2">
          <span>© potionpost &amp; co. · printed in oregon</span>
          <span>made small &amp; mailed slow</span>
        </div>
      </div>
    </footer>
  );
}

// vessel/brew display metadata maps (id → { id, name, lt, glyph }).
// These mirror the picker vocab — the renderer keeps the real geometry maps.
const VESSEL_META = Object.fromEntries(VESSELS_LIST.map(v => [v.id, v]));
const BREW_META   = Object.fromEntries(BREWS_LIST.map(b => [b.id, b]));

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