/* tidepool — the app.
 *
 * Design step is a real wet-on-wet watercolour pool (/shared/tidepool.js, WebGL2);
 * everything downstream — address, Stripe, Lob, tracking — is the shared checkout
 * module (/shared/checkout.jsx). Per the shared-checkout design rule we own the
 * panel markup and let the hook own all the money-touching machinery.
 *
 * The card is the painting: full-bleed art, no frame, no imprint. Only the buyer's
 * signature sits on the front (bottom-right, like an artist signs a real sheet).
 * The written message lives on the BACK, via the shared `allowMessage` path.
 */
const { useState, useRef, useEffect, useCallback } = React;
const { PIGMENTS, PAPERS } = window.Tidepool;

const PRICE = 5;                       // mirrors PRICE_CENTS in server.js
const STEPS = ['paint', 'address', 'review', 'sent'];
const STEP_LABEL = { paint: 'paint', address: 'address', review: 'review', sent: 'sent' };
const MAX_SIG = 60;
const MAX_MSG = 400;

// Stripe's Payment Element, themed to the studio's deep water.
const STRIPE_APPEARANCE = {
  theme: 'night',
  variables: {
    colorPrimary: '#7fd0d8', colorBackground: '#132427', colorText: '#e7e2d6',
    colorDanger: '#c08a3e', fontFamily: 'Newsreader, Georgia, serif',
    borderRadius: '9px', spacingUnit: '4px',
  },
};

// One ink rule for the signature, used by BOTH the on-screen card and the print
// plate — a preview that lies about the mailed card is worse than no preview.
const SIG_DARK = 'rgba(38,46,52,.86)', SIG_LIGHT = 'rgba(245,242,233,.92)';
const sigInk = (lum) => (lum > 132 ? SIG_DARK : SIG_LIGHT);
// The signature's box, as a fraction of the card. Mirrored by the .sig CSS.
const SIG_BOX = { x0: 0.40, y0: 0.035, x1: 0.94, y1: 0.115 };  // uv, y from bottom

const hx = (h) => { const n = parseInt(h.slice(1), 16); return [n >> 16 & 255, n >> 8 & 255, n & 255]; };
const tint = (h, a) => 'rgb(' + hx(h).map((v) => Math.round(v + (255 - v) * a)).join(',') + ')';
const shade = (h, a) => 'rgb(' + hx(h).map((v) => Math.round(v * (1 - a))).join(',') + ')';

// ──────────────────────────────────────────────────────────────────
// Print render — the painting IS the card, so the art goes edge to edge and the
// only thing drawn over it is the signature. composeFront hands us the portrait
// 1875×2775 plate and rotates it onto Lob's landscape 4×6 afterwards.
// ──────────────────────────────────────────────────────────────────
function drawTidepoolFront(x, env, { sig }) {
  const { PR_W, PR_H } = env.dims;
  const art = env.art;
  if (art) x.drawImage(art, 0, 0, PR_W, PR_H);   // full bleed — the wash is the card
  const text = (sig || '').trim();
  if (!text) return;

  // Inset well inside Lob's trim so a signature can never be cut off at the deckle.
  const padX = PR_W * 0.085, padY = PR_H * 0.062;
  const size = Math.round(PR_W * 0.034);
  x.font = `600 ${size}px 'Caveat', cursive`;
  x.textAlign = 'right'; x.textBaseline = 'alphabetic';
  const w = Math.min(x.measureText(text).width, PR_W * 0.6);

  // Ink is chosen from what the wash actually did underneath — a painting can end
  // up dark or pale in that corner, and a signature nobody can read is a defect.
  let lum = 255;
  if (art) {
    try {
      const ax = art.getContext('2d');
      const bw = Math.max(2, Math.round(w)), bh = Math.max(2, Math.round(size * 1.5));
      const bx = Math.max(0, Math.round(PR_W - padX - bw));
      const by = Math.max(0, Math.round(PR_H - padY - bh));
      const d = ax.getImageData(bx, by, Math.min(bw, art.width - bx), Math.min(bh, art.height - by)).data;
      let sum = 0, n = 0;
      for (let i = 0; i < d.length; i += 16) { sum += 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2]; n++; }
      lum = n ? sum / n : 255;
    } catch (e) { /* tainted or empty canvas — fall back to dark ink on light paper */ }
  }
  x.fillStyle = sigInk(lum);
  x.fillText(text, PR_W - padX, PR_H - padY);
}

// ──────────────────────────────────────────────────────────────────
// The studio panel — pans, tools, paper, signature, back message.
// ──────────────────────────────────────────────────────────────────
function StudioPanel({ pigment, onPigment, tool, onTool, paper, onPaper,
                       sig, setSig, message, setMessage, canUndo,
                       onUndo, onClear, onDry, onSurprise, msgOpen, setMsgOpen }) {
  const TOOL_BLURB = {
    brush: null,
    water: 'clean water — drop it into a damp wash and watch the colour bloom back',
    stir:  'no pigment, just current — reshape what’s already floating',
    lift:  'a thirsty brush — lifts colour back off the sheet',
  };
  const pigName = (PIGMENTS.find((p) => p.id === pigment) || {}).name;
  return (
    <aside className="panel">
      <h2>the pool</h2>
      <p className="hint">tap a colour, then drag on the sheet. drag slowly for a fat wet
        stroke, quickly for a dry scratch. the water keeps moving after you let go.</p>

      <span className="subhead">pigments</span>
      <div className="pigname">{tool === 'brush' ? pigName : TOOL_BLURB[tool]}</div>
      <div className="pans">
        {PIGMENTS.map((p) => (
          <button key={p.id} className="pan" title={p.name} aria-label={p.name}
            aria-pressed={p.id === pigment && tool === 'brush'}
            onClick={() => onPigment(p.id)}
            style={{ background: `linear-gradient(165deg, ${tint(p.hex, .30)} 0%, ${p.hex} 46%, ${shade(p.hex, .34)} 100%)` }} />
        ))}
      </div>

      <span className="subhead">water</span>
      <div className="grp">
        {['brush', 'water', 'stir', 'lift'].map((t) => (
          <button key={t} className="tool" aria-pressed={tool === t} onClick={() => onTool(t)}>{t}</button>
        ))}
      </div>

      <span className="subhead">paper</span>
      <div className="papers">
        {PAPERS.map((p) => (
          <button key={p.id} className="paper" aria-pressed={paper === p.id} onClick={() => onPaper(p.id)}>
            <i style={{ background: p.hex }} />{p.name}
          </button>
        ))}
      </div>

      <div className="acts">
        <button className="act primary" onClick={onSurprise}>surprise me</button>
        <button className="act" onClick={onDry}>let it dry</button>
        <button className="act ghost" onClick={onUndo} disabled={!canUndo}>undo</button>
        <button className="act ghost" onClick={onClear}>start the sheet over</button>
      </div>

      <span className="subhead">sign it
        <span className="sub">goes in the corner of the painting, in your hand. optional.</span>
      </span>
      <div className="field">
        <input value={sig} maxLength={MAX_SIG} placeholder="— m." onChange={(e) => setSig(e.target.value)} />
      </div>

      <span className="subhead">write on the back</span>
      {!msgOpen && !message ? (
        <p className="savehint">
          <button type="button" className="linkbtn" onClick={() => setMsgOpen(true)}>+ add a message</button>
          <span style={{ display: 'block' }}>leave it blank and the back carries a small copy of the painting instead.</span>
        </p>
      ) : (
        <div className="field">
          <textarea value={message} maxLength={MAX_MSG} placeholder="the message side — write to them here"
            onChange={(e) => setMessage(e.target.value)} />
          <div className="counter">{message.length} / {MAX_MSG}</div>
        </div>
      )}
    </aside>
  );
}

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

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

      <span className="subhead">recipient
        <span className="sub">where the painting ends up. include their full name.</span>
      </span>
      <Checkout.PasteAddress label="paste an address" 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}>
            <span 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>
            </span>
            <Checkout.PasteAddress label="paste an address" 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 painting to another address</button>
          <span className="muted">${PRICE} each — same painting, mailed separately</span>
        </p>
      )}

      <span className="subhead">return address</span>
      <p className="savehint">we print Picpost’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">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>
      )}
      {co.showErrors && !co.addrReady && <p className="err">a few fields still need filling in.</p>}
    </aside>
  );
}

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

      <div className="rev">
        <span className="subhead">to</span>
        {co.tos.map((t, i) => (
          <div className="addr" key={i} style={i > 0 ? { marginTop: 12, paddingTop: 12, borderTop: '1px solid var(--line2)' } : 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">
        <span className="subhead">summary</span>
        {many && <div className="line"><span>{co.tos.length} cards × ${PRICE.toFixed(2)} — same painting, mailed separately</span></div>}
        <div className="line total">
          <span>{many ? `${co.tos.length} 4 × 6 paintings` : 'one 4 × 6 painting'} — printing &amp; postage included</span>
          <span>{co.freeApplied ? <><s style={{ opacity: .6, marginRight: 8 }}>${(PRICE * co.tos.length).toFixed(2)}</s>free</> : `$${dollars}`}</span>
        </div>
        <p className="muted">{many ? `${co.tos.length} separate mailings` : `a single mailing to ${co.to.name || 'them'}`}. nothing else.</p>
      </div>

      <div className="rev">
        <span className="subhead">for the receipt
          <span className="sub">one email when it ships. no list, no upsell.</span>
        </span>
        <Checkout.Field label="email" value={co.email} onChange={co.setEmail} err={co.errors.email} placeholder="you@somewhere.com" />
      </div>

      {co.isAdminFree ? (
        <div className="rev">
          <span className="subhead">payment</span>
          <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">
          <span className="subhead">payment</span>
          <p className="muted" style={{ color: 'var(--pop)' }}>gift code applied — {many ? 'these cards ship' : 'this card ships'} free.{typeof co.freeRemaining === 'number' ? ` (${co.freeRemaining} left on this code)` : ''}</p>
        </div>
      ) : co.payEnabled ? (
        <div className="rev">
          <span className="subhead">payment</span>
          <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)' }}>promo applied — your total is ${dollars}.</p>}
          {co.promoTried && !co.promoApplied && !co.creatingOrder && co.promo.trim() && <p className="muted" style={{ color: 'var(--warn-deep)' }}>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 — “send it” simulates the order to test the print pipeline.</p>
      )}
    </aside>
  );
}

function SentPanel({ co, onAnother }) {
  const many = co.tos.length > 1;
  return (
    <aside className="panel">
      <div className="done">
        <span className="badge">off the easel</span>
        <h2>it’s in the post <span style={{ color: 'var(--pop)' }}>💧</span></h2>
        <p>{many
          ? <>your painting has been printed in <strong>{co.tos.length} copies</strong> — addressed to {co.tos.map((t) => t.name).filter(Boolean).join(', ')} — and sent to press.</>
          : <>your painting is addressed to <em>{co.to.name || 'them'}</em> and 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.</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 }}>paint another</button></div>
      </div>
    </aside>
  );
}

// ──────────────────────────────────────────────────────────────────
function App() {
  const canvasRef = useRef(null);
  const sheetRef = useRef(null);
  const tpRef = useRef(null);

  const [ready, setReady] = useState(false);
  const [unsupported, setUnsupported] = useState(false);
  const [pigment, setPigment] = useState('ultra');
  const [tool, setTool] = useState('brush');
  const [paper, setPaper] = useState('cotton');
  const [sig, setSig] = useState('');
  const [message, setMessage] = useState('');
  const [msgOpen, setMsgOpen] = useState(false);
  const [touched, setTouched] = useState(false);
  const [canUndo, setCanUndo] = useState(false);
  const [wet, setWet] = useState(0);
  const [sigLum, setSigLum] = useState(255);
  const [lost, setLost] = useState(false);
  const [, forceTick] = useState(0);

  // The pool is created ONCE and lives for the session — the painting has to
  // survive walking to address/review and back, so the canvas never unmounts.
  useEffect(() => {
    const tp = new window.Tidepool.Tidepool(canvasRef.current, {
      aspect: 2 / 3, paper: 'cotton', pigment: 'ultra',
    });
    if (tp.unsupported) { setUnsupported(true); return; }
    tpRef.current = tp;
    tp.onChange = () => { setCanUndo(tp.canUndo); setLost(!!tp.lost); forceTick((n) => n + 1); };
    setReady(true);
    const ro = new ResizeObserver(() => tp.resize());
    ro.observe(sheetRef.current);
    const iv = setInterval(() => {
      setWet(tp.wetness);
      // Keep the on-screen signature's ink in step with the print plate's.
      setSigLum(tp.probeLuminance(SIG_BOX.x0, SIG_BOX.y0, SIG_BOX.x1, SIG_BOX.y1));
    }, 320);
    const onOrient = () => setTimeout(() => tp.resize(), 250);
    window.addEventListener('orientationchange', onOrient);
    return () => { clearInterval(iv); ro.disconnect(); window.removeEventListener('orientationchange', onOrient); tp.stop(); };
  }, []);

  const tp = tpRef.current;
  const markTouched = useCallback(() => setTouched(true), []);

  function pickPigment(id) { setPigment(id); setTool('brush'); if (tp) { tp.setPigment(id); tp.setTool('brush'); } }
  function pickTool(t) { setTool(t); if (tp) tp.setTool(t); }
  function pickPaper(id) { setPaper(id); if (tp) tp.setPaper(id); }

  const paperHex = (PAPERS.find((p) => p.id === paper) || PAPERS[0]).hex;
  // "Ready" means pigment actually reached the paper — not merely that a pan is lit.
  const designReady = !!(tp && tp.usedPigments().length > 0);

  async function renderComposite() {
    const t = tpRef.current;
    const front = await Checkout.composeFront({
      bgHex: paperHex,
      fonts: ["600 96px 'Caveat'"],
      // The pool renders its own print plate at full size — same shader as the
      // on-screen sheet, so the mailed card is exactly what the buyer painted.
      loadArt: () => t.exportCanvas(1875, 2775),
      drawPortrait: (x, env) => drawTidepoolFront(x, env, { sig }),
      // A full-bleed painting as PNG is 5–15 MB and blows the 6 MB order-POST cap.
      imageType: 'image/jpeg', imageQuality: 0.92,
    });
    const names = t.usedPigments().map((id) => (PIGMENTS.find((p) => p.id === id) || {}).name).filter(Boolean);
    const previewBack = await Checkout.composeBack({
      bgHex: paperHex, echoPng: front.bouquet,
      fonts: ["italic 46px 'Newsreader'", "26px 'Newsreader'", "600 40px 'Caveat'"],
      left: { kind: message.trim() ? 'note' : 'art', text: message.trim(),
              fontCss: "'Caveat', cursive", invLabel: 'painted with', items: names },
      mark: { word1: 'tide', word2: 'pool', accent: '#2e6b7a', accentWord: 2 }, caption: 'Picpost · scan to paint your own', glyph: '💧',
      qrUrl: '/tidepool/api/order/card/qr.svg',
    });
    return { ...front, previewBack };
  }

  const co = Checkout.useCheckout({
    price: PRICE,
    designStep: 'paint',
    sentStep: 'sent',
    localStorageKey: 'tp_ret',
    stripeAppearance: STRIPE_APPEARANCE,
    returnUrl: window.location.origin + window.location.pathname,
    buildPayload: () => ({
      paint: { pigments: tpRef.current ? tpRef.current.usedPigments() : [], paper },
      note: sig.trim(), message: message.trim(), messageFont: 'hand', bg: paperHex,
    }),
    renderComposite,
  });
  const { step, setStep } = co;

  // Painting is only possible on the design step — elsewhere the sheet is a preview.
  const locked = step !== 'paint';

  function startOver() {
    co.startOver(() => {
      if (tpRef.current) tpRef.current.clear();
      setSig(''); setMessage(''); setMsgOpen(false); setTouched(false);
    });
  }

  const stepIndex = STEPS.indexOf(step);
  const cta = (() => {
    if (step === 'paint') return { label: designReady ? 'continue to address' : 'paint something first', disabled: !designReady };
    if (step === 'address') return { label: co.verifying ? (co.tos.length > 1 ? 'checking addresses…' : 'checking address…') : 'review the painting', disabled: co.verifying };
    if (step === 'review') {
      const needEmail = !co.emailValid;
      if (co.isAdminFree) return { label: co.paying ? 'mailing…' : 'mail it free 💧', disabled: co.paying || needEmail };
      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 * co.tos.length * 100) / 100).toFixed(2);
      if (co.payEnabled && !co.clientSecret)
        return { label: co.creatingOrder ? 'preparing…' : 'continue to payment', disabled: co.paying || co.creatingOrder || needEmail };
      return { label: co.paying ? 'processing…' : `send it · $${dollars}`, disabled: co.paying || co.creatingOrder || needEmail };
    }
    return null;
  })();

  const sum = (() => {
    if (step === 'paint') {
      if (!designReady) return 'dip a colour and drag on the sheet to begin';
      const n = tp ? tp.usedPigments().length : 0;
      return `${n} pigment${n === 1 ? '' : 's'} · ${(PAPERS.find((p) => p.id === paper) || {}).name}`;
    }
    if (step === 'address') return co.addrReady ? 'address looks complete' : 'fill the recipient address';
    if (step === 'review') return 'one card · printing & postage included';
    return '';
  })();

  const recipeNode = (() => {
    const names = tp ? tp.usedPigments().map((id) => (PIGMENTS.find((p) => p.id === id) || {}).name).filter(Boolean) : [];
    return (
      <div className="rev">
        <span className="subhead">the painting</span>
        <div className="recipe">
          a wash on <em>{(PAPERS.find((p) => p.id === paper) || {}).name}</em>
          {names.length > 0 && <>, painted with <em>{names.join(', ')}</em></>}.
          <br /><br />
          <span className="rcap">signed</span>
          {sig.trim() ? <em>{sig.trim()}</em> : <span style={{ color: 'var(--dimmer)' }}>(unsigned)</span>}
          <span className="rcap">on the back</span>
          {message.trim()
            ? `"${message.trim()}"`
            : <span style={{ color: 'var(--dimmer)' }}>(a small copy of the painting)</span>}
        </div>
      </div>
    );
  })();

  if (unsupported) {
    return (
      <div className="shell">
        <div className="top"><span className="mark">tide<b>pool</b></span></div>
        <p className="nogl">tidepool needs WebGL2 — it’s a real fluid simulation, not a filter.
          Try Safari 15+, Chrome, or Firefox on a recent device.</p>
      </div>
    );
  }

  return (
    <div className="shell">
      <header className="top">
        <a className="mark" href="./">tide<b>pool</b></a>
        <nav className="rail">
          {STEPS.slice(0, 3).map((s, i) => (
            <button key={s} aria-current={step === s} disabled={i > stepIndex || step === 'sent'}
              onClick={() => i < stepIndex && setStep(s)}>{STEP_LABEL[s]}</button>
          ))}
        </nav>
      </header>

      <div className="work">
        <section className="stage">
          {step === 'paint' && (
            <div className="intro">
              <p className="kick">real water <span className="glyph">·</span> real pigment
                <span className="glyph">·</span> printed &amp; mailed for you</p>
              <h1>drop a colour in. <em>let the water decide.</em></h1>
              <p>this isn’t a brush that draws shapes — it’s a shallow pool. you nudge it,
                the current keeps turning, and it dries into something you couldn’t have drawn.
                <span className="pop"> we print it and mail it.</span></p>
            </div>
          )}

          {lost && (
            <div className="notice" style={{ maxWidth: 420, margin: '0 auto 12px' }}>
              the pool lost its graphics context — that usually means the browser ran
              low on memory. reload the page to start a fresh sheet.
            </div>
          )}
          <div className="sheetwrap">
            <div ref={sheetRef} className={'sheet' + (locked ? ' locked' : '') + (step !== 'paint' ? ' small' : '')}
              onPointerDown={() => { if (!locked) { markTouched(); sheetRef.current.classList.add('painting'); } }}
              onPointerUp={() => sheetRef.current && sheetRef.current.classList.remove('painting')}
              onPointerLeave={() => sheetRef.current && sheetRef.current.classList.remove('painting')}>
              <canvas ref={canvasRef} />
              {sig.trim() && <span className="sig" style={{ color: sigInk(sigLum) }}>{sig.trim()}</span>}
              {!touched && ready && step === 'paint' && (
                <div className="hint">
                  <div className="big">drag slowly for a loaded wet stroke</div>
                  <div className="small">then switch to water and drop it in</div>
                </div>
              )}
            </div>
          </div>

          <p className="spec">4 × 6 in <span className="glyph">·</span> thick coated card stock
            <span className="glyph">·</span> printed &amp; mailed for you</p>
          {step === 'paint' && (
            <div className="wetmeter">
              <i className={'drop' + (wet > 0.04 ? ' wet' : '')} />
              {wet > 0.5 ? 'flooded' : wet > 0.18 ? 'wet' : wet > 0.04 ? 'damp' : 'dry'}
            </div>
          )}
        </section>

        {step === 'paint' && (
          <StudioPanel
            pigment={pigment} onPigment={pickPigment}
            tool={tool} onTool={pickTool}
            paper={paper} onPaper={pickPaper}
            sig={sig} setSig={setSig}
            message={message} setMessage={setMessage}
            msgOpen={msgOpen} setMsgOpen={setMsgOpen}
            canUndo={canUndo}
            onUndo={() => tp && tp.undo()}
            onClear={() => { if (tp) tp.clear(); setTouched(false); }}
            onDry={() => tp && tp.dryNow()}
            onSurprise={() => { if (tp) { tp.compose(); markTouched(); } }}
          />
        )}
        {step === 'address' && <AddressPanel co={co} />}
        {step === 'review' && <ReviewPanel co={co} summary={recipeNode} />}
        {step === 'sent' && <SentPanel co={co} onAnother={startOver} />}

        {cta && (
          <div className="actionbar">
            <span className="sum">
              {step !== 'paint' && <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>

      <footer>Tidepool · a Picpost studio</footer>
    </div>
  );
}

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