// app.jsx — shell, backdrop wiring, exports, sharing and the view-only page

const { useState: useStateA, useEffect: useEffectA, useRef: useRefA } = React;

// A standard ID card (CR80) is 54 x 85.6 mm. At 600 dpi that is 1276 x 2022 px,
// so a downloaded side prints at exactly the right size.
const PRINT_W = 1276;
const CARD_RATIO = 85.6 / 54;

const loadImg = (src) => new Promise((res, rej) => {
  const img = new Image(); img.onload = () => res(img); img.onerror = rej; img.src = src;
});
const slug = (name) => (name || 'badge').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'badge';
const saveAs = (href, filename) => { const a = document.createElement('a'); a.download = filename; a.href = href; a.click(); };

// Colour a neutral fabric texture (white-ish RGB + alpha) with the card colour,
// the same multiply the CSS does, for drawing into the video.
function tintFabric(img, color) {
  const c = document.createElement('canvas'); c.width = img.width; c.height = img.height;
  const x = c.getContext('2d');
  x.fillStyle = color; x.fillRect(0, 0, c.width, c.height);
  x.globalCompositeOperation = 'multiply'; x.drawImage(img, 0, 0);
  x.globalCompositeOperation = 'destination-in'; x.drawImage(img, 0, 0);
  return c;
}

// Backdrop + page colours for whichever side of the card is showing.
function Shell({ card, cardSide, children }) {
  const office = getAura('pro', card.auraPro, card.auraProCustom);
  const aura = getAura('kid', card.auraKid, card.auraKidCustom);
  const lightPage = cardSide === 0 && isLightColor(office.color);
  const pageTokens = lightPage ? {
    '--page-bg': office.color, '--page-fg': '#141210', '--page-fg-dim': '#3d3832', '--page-muted': '#6b635a',
    '--page-line': 'rgba(20,18,16,.16)', '--page-line-strong': 'rgba(20,18,16,.3)',
  } : {
    '--page-bg': cardSide === 0 ? office.color : '#05060f', '--page-fg': '#f5efe2', '--page-fg-dim': '#d5cfc2', '--page-muted': '#a39c90',
    '--page-line': 'rgba(255,255,255,.16)', '--page-line-strong': 'rgba(255,255,255,.28)',
  };
  return (
    <div className={"shell " + (lightPage ? 'page-light' : 'page-dark')} style={pageTokens}>
      <Backdrop solid={office.color} colors={aura.colors} showAurora={cardSide === 1}/>
      {children}
    </div>
  );
}

// The page someone lands on from a shared link: just the badge, to flip and
// swing. No editing, no download or share bar.
function Viewer({ id }) {
  const [state, setState] = useStateA({ status: 'loading', card: null });
  const [cardSide, setCardSide] = useStateA(0);
  useEffectA(() => {
    let live = true;
    setState({ status: 'loading', card: null });
    fetchSharedCard(id)
      .then(card => live && setState(card ? { status: 'ok', card } : { status: 'missing', card: null }))
      .catch(() => live && setState({ status: 'error', card: null }));
    return () => { live = false; };
  }, [id]);
  const card = state.card || { ...newCard(), auraPro: 'paper' };
  const first = card.pro.name && card.pro.name !== 'Your Name' ? card.pro.name.trim().split(' ')[0] : '';
  const makeOwn = <a className="view-cta" href={location.pathname}>Make your own badge</a>;
  return (
    <Shell card={card} cardSide={cardSide}>
      {state.status === 'ok' ? (
        <div className="stage-layout is-view">
          <div className="stage-copy">
            <div className="eyebrow">♡ both sides of the same story</div>
            {first ? <h1>Meet <em className="tilted">{first}</em>.</h1> : <h1>Meet the <em className="tilted">kid</em> behind the job title.</h1>}
            <p className="lead">
              One side is who {first ? first + ' is' : 'they are'} at work. Flip the badge to meet who they were at six.
            </p>
            <p>
              Spot something you have in common? A hometown, a childhood dream, a strange obsession? That’s your opener. Go say hi.
            </p>
            <p className="hint">Tap the badge to flip it. Drag it sideways and let go to give it a swing.</p>
            {makeOwn}
          </div>
          <div className="stage-card"><Card card={card} large tilt onSideChange={setCardSide}/></div>
        </div>
      ) : (
        <div className="view-msg">
          <h1>{state.status === 'loading' ? 'Loading badge…' : state.status === 'missing' ? 'This badge doesn’t exist.' : 'Couldn’t load this badge.'}</h1>
          {state.status !== 'loading' && <p>{state.status === 'missing' ? 'The link may be mistyped.' : 'Check your connection and try again.'}</p>}
          {state.status !== 'loading' && makeOwn}
        </div>
      )}
    </Shell>
  );
}

function App() {
  const [card, setCard] = useStateA(() => loadMyCard());
  const [mode, setMode] = useStateA('edit');          // 'edit' | 'preview'
  const [cardSide, setCardSide] = useStateA(0);        // 0 = pro, 1 = kid
  const [toast, setToast] = useStateA(null);
  const [busy, setBusy] = useStateA(null);             // null | 'png' | 'video'
  const [progress, setProgress] = useStateA(0);
  const [askVideo, setAskVideo] = useStateA(false);    // the "with or without background" dialog
  const [publishing, setPublishing] = useStateA(false);

  useEffectA(() => { saveMyCard(card); }, [card]);
  useEffectA(() => { fitBadge(); const t = setTimeout(fitBadge, 60); return () => clearTimeout(t); }, [mode]);   // Preview has a bottom bar

  const showToast = (msg) => { setToast(msg); setTimeout(() => setToast(null), msg.length > 45 ? 5200 : 2600); };

  // Nothing is saved or shared until the badge is complete.
  const missing = cardMissing(card);
  const notReady = () => {
    if (!missing.length) return false;
    showToast(`Finish your badge first — ${missing.length} thing${missing.length > 1 ? 's' : ''} to go`);
    return true;
  };

  // A clean, still, off-screen copy of the card. Every export is taken from
  // this one, so edit-mode outlines and the swing never end up in a file.
  const captureRef = useRefA(null);

  // Capture each face on its own, flat and un-mirrored.
  const captureFaces = async (outWidth = PRINT_W, slot = false, square = false) => {
    const root = captureRef.current;
    if (!root || !window.htmlToImage) return null;
    if (document.fonts && document.fonts.ready) await document.fonts.ready;
    const snap = (sel) => {
      const el = root.querySelector(sel);
      return window.htmlToImage.toPng(el, {
        pixelRatio: outWidth / el.offsetWidth,
        cacheBust: true,
        width: el.offsetWidth, height: el.offsetHeight,
        style: { transform: 'none', opacity: '1', position: 'static', boxShadow: 'none', transition: 'none', margin: '0',
          // print and share images are a solid card; only the video keeps the lanyard hole
          ...(slot ? {} : { mask: 'none', webkitMask: 'none', WebkitMask: 'none' }),
          // print-shop sheets: no rounded corners or edge line, the shop trims and rounds the card itself
          ...(square ? { borderRadius: '0', border: 'none' } : {}) },
      });
    };
    const frontUrl = await snap('.card-face.front');
    const backUrl = await snap('.card-face.back');
    return { frontUrl, backUrl };
  };

  const downloadPng = async () => {
    if (busy || notReady()) return;
    setBusy('png');
    try {
      const faces = await captureFaces();
      if (!faces) throw new Error('capture failed');
      const nm = slug(card?.pro?.name);
      saveAs(faces.frontUrl, `tinyyou-${nm}-front.png`);
      await new Promise(r => setTimeout(r, 250)); // so the browser doesn't merge the two downloads
      saveAs(faces.backUrl, `tinyyou-${nm}-back.png`);
      showToast('Saved front and back at ID-card print size');
    } catch (err) { console.error(err); showToast('Save failed — try again'); }
    finally { setBusy(null); }
  };

  // Print-shop file: one CMYK PDF, front and back, each page 61 x 91 mm. That is the finished
  // 55 x 85 mm card plus 3 mm on every side that gets trimmed off (the "bleed"), as Printo and
  // similar shops ask for. The badge is drawn undistorted inside the finished area; the rest is
  // the card's own colour. Small text is enlarged for this file only (see .for-print in the CSS).
  const buildPrintPdf = async () => {
    const rig = captureRef.current;
    rig.classList.add('for-print');
    let faces;
    try {
      await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
      faces = await captureFaces(PRINT_W, false, true);
    } finally { rig.classList.remove('for-print'); }
    if (!faces) throw new Error('capture failed');
    const mm = (v) => Math.round(v / 25.4 * 600);
    const W = mm(61), H = mm(91), fh = mm(85), fw = Math.round(fh * 54 / 85.6);
    const sheet = async (url, side) => {
      const img = await loadImg(url);
      const c = document.createElement('canvas'); c.width = W; c.height = H;
      const ctx = c.getContext('2d', { willReadFrequently: true });
      ctx.fillStyle = getTheme(card, side).bg; ctx.fillRect(0, 0, W, H);
      ctx.imageSmoothingQuality = 'high';
      ctx.drawImage(img, Math.round((W - fw) / 2), Math.round((H - fh) / 2), fw, fh);
      return { rgba: ctx.getImageData(0, 0, W, H).data, width: W, height: H };
    };
    return buildCmykPdf([await sheet(faces.frontUrl, 0), await sheet(faces.backUrl, 1)], { widthMm: 61, heightMm: 91, bleedMm: 3 });
  };
  const downloadPrint = async () => {
    if (busy || notReady()) return;
    setBusy('print');
    try {
      const pdf = await buildPrintPdf();
      const url = URL.createObjectURL(new Blob([pdf], { type: 'application/pdf' }));
      saveAs(url, `tinyyou-${slug(card?.pro?.name)}-print.pdf`);
      setTimeout(() => URL.revokeObjectURL(url), 8000);
      showToast('Saved PDF to print: CMYK, front and back, with trim margin');
    } catch (err) { console.error(err); showToast('Save failed — try again'); }
    finally { setBusy(null); }
  };
  window.__buildPrintPdf = buildPrintPdf;
  window.__downloadPrint = downloadPrint;

  // Build the video as a scene we can draw at any moment in time: backdrop,
  // strap, hook and a card that swings and flips from the professional side
  // to the childhood side and back.
  const VIDEO_MS = 6200;
  const buildScene = async (withBg) => {
    const faces = await captureFaces(900, true);
    if (!faces) return null;
    const [front, back, metal, fabricSrc, tileSrc] = await Promise.all([
      loadImg(faces.frontUrl), loadImg(faces.backUrl),
      loadImg('assets/lanyard-metal.png?v=125'), loadImg('assets/lanyard-fabric.png'), loadImg('assets/strap-tile.png'),
    ]);
    // one strap, one colour on both sides
    const theme = getTheme(card), strapHex = getStrap(card);
    const fabric = tintFabric(fabricSrc, strapHex), tile = tintFabric(tileSrc, strapHex);

    const W = 1080, H = 1920;
    const canvas = document.createElement('canvas'); canvas.width = W; canvas.height = H;
    const ctx = canvas.getContext('2d');
    const cw = 620, ch = cw * CARD_RATIO, cardTop = 600;
    // lanyard geometry, as fractions of the card width (same numbers as the CSS)
    const endW = cw * 0.428, endH = endW * 226 / 160, endX = (W - endW) / 2;
    const endTop = cardTop - endH * (180 / 226);       // the card's top edge crosses the hook here
    const strapW = endW * 0.7, strapX = endX + endW * 0.494 - strapW / 2, tileH = strapW * 240 / 224;
    const metalPivot = endTop + endH * 0.196;
    const pivotY = -260;
    const ease = (t) => t < .5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
    const bd = window.__backdrop;

    const draw = (ms) => {
      const t = ms / 1000;
      // flip timeline: 0 = professional, 1 = childhood
      let f = 0;
      if (ms < 1100) f = 0; else if (ms < 1900) f = ease((ms - 1100) / 800);
      else if (ms < 4100) f = 1; else if (ms < 4900) f = 1 - ease((ms - 4100) / 800); else f = 0;

      if (withBg && bd) {
        bd.renderAt(f);
        const s = Math.max(W / bd.canvas.width, H / bd.canvas.height);
        const dw = bd.canvas.width * s, dh = bd.canvas.height * s;
        ctx.drawImage(bd.canvas, (W - dw) / 2, (H - dh) / 2, dw, dh);
      } else {
        ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, W, H);
      }

      // gentle sway, plus a knock each time the card flips
      const knock = (start) => { const d = (ms - start) / 1000; return d > 0 ? Math.exp(-d * 2.2) * Math.sin(d * 7.5) : 0; };
      const a1 = 0.022 * Math.sin(t * 1.7) + 0.02 * (knock(1100) - knock(4100));
      const a2 = 0.03 * Math.sin(t * 2.3 + 1) + 0.07 * (knock(1150) - knock(4150));

      ctx.save();
      ctx.translate(W / 2, pivotY); ctx.rotate(a1); ctx.translate(-W / 2, -pivotY);
      for (let y = endTop + 1 - tileH; y > pivotY - 400; y -= tileH) ctx.drawImage(tile, strapX, y, strapW, tileH + 1);
      ctx.drawImage(fabric, endX, endTop, endW, endH);

      // card: swings from the hook, flips around its vertical centre line
      ctx.save();
      ctx.translate(W / 2, cardTop + 20); ctx.rotate(a2); ctx.translate(-W / 2, -(cardTop + 20));
      const rot = f * Math.PI;
      const sx = Math.max(0.002, Math.abs(Math.cos(rot)));
      const img = rot > Math.PI / 2 ? back : front;
      ctx.translate(W / 2, cardTop - Math.sin(rot) * 14);
      ctx.scale(sx, 1);
      // Keep the card's shadow out of the slot, so the hole shows the clean background
      // (same numbers as the slot mask in the CSS).
      const sw = cw * 0.224, sh = sw / 4, sy = (ch - sh) * 0.034;
      ctx.beginPath();
      ctx.rect(-W, -H, W * 2, H * 3);
      ctx.roundRect(-sw / 2, sy, sw, sh, sh / 2);
      ctx.clip('evenodd');
      ctx.shadowColor = 'rgba(0,0,0,.38)'; ctx.shadowBlur = 46; ctx.shadowOffsetY = 26;
      ctx.drawImage(img, -cw / 2, 0, cw, ch);
      ctx.restore();

      // the metal sits on top of the card and follows half of its swing
      ctx.save();
      ctx.translate(W / 2, metalPivot); ctx.rotate(a2 * 0.5); ctx.translate(-W / 2, -metalPivot);
      ctx.shadowColor = 'rgba(0,0,0,.3)'; ctx.shadowBlur = 6; ctx.shadowOffsetY = 3;
      // the ring stays put; the clip below the swivel turns with the card
      const cut = 0.483, mh = metal.height, axis = endX + endW * 0.488, turn = Math.cos(rot);
      ctx.drawImage(metal, 0, 0, metal.width, mh * cut, endX, endTop, endW, endH * cut);
      if (Math.abs(turn) > 0.03) {
        ctx.translate(axis, 0); ctx.scale(turn, 1); ctx.translate(-axis, 0);
        ctx.drawImage(metal, 0, mh * cut, metal.width, mh * (1 - cut), endX, endTop + endH * cut, endW, endH * (1 - cut));
      }
      ctx.restore();
      ctx.restore();
    };
    return { canvas, draw, done: () => bd && bd.release() };
  };
  window.__buildScene = buildScene; // lets a single frame be checked without recording
  window.__captureFaces = captureFaces;

  const captureVideo = async (withBg = true) => {
    if (!window.MediaRecorder) return null;
    const scene = await buildScene(withBg);
    if (!scene) return null;
    const candidates = ['video/mp4;codecs=avc1.42E01E', 'video/mp4;codecs=avc1', 'video/mp4', 'video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm'];
    const mimeType = candidates.find(m => window.MediaRecorder.isTypeSupported(m)) || 'video/webm';
    const ext = mimeType.startsWith('video/mp4') ? 'mp4' : 'webm';
    const recorder = new window.MediaRecorder(scene.canvas.captureStream(30), { mimeType, videoBitsPerSecond: 8_000_000 });
    const chunks = [];
    recorder.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); };
    return new Promise((resolve) => {
      recorder.onstop = () => { scene.done(); resolve({ blob: new Blob(chunks, { type: mimeType }), ext }); };
      scene.draw(0);
      recorder.start();
      let t0;
      const tick = (now) => {
        if (t0 == null) t0 = now;
        const ms = now - t0;
        setProgress(Math.min(100, Math.round((ms / VIDEO_MS) * 100)));
        scene.draw(Math.min(ms, VIDEO_MS));
        if (ms < VIDEO_MS) requestAnimationFrame(tick);
        else setTimeout(() => recorder.stop(), 80);
      };
      requestAnimationFrame(tick);
    });
  };

  window.__captureVideo = captureVideo;

  const downloadVideo = async (withBg) => {
    setAskVideo(false);
    if (busy) return;
    if (!window.MediaRecorder) { showToast('Video isn’t supported in this browser'); return; }
    setBusy('video'); setProgress(0);
    try {
      showToast('Recording… keep this tab open');
      const result = await captureVideo(withBg);
      if (!result) throw new Error('record failed');
      const url = URL.createObjectURL(result.blob);
      saveAs(url, `tinyyou-${slug(card?.pro?.name)}.${result.ext}`);
      setTimeout(() => URL.revokeObjectURL(url), 4000);
      showToast(result.ext === 'mp4' ? 'Saved video (MP4)' : 'Saved video (WebM — this browser can’t make MP4)');
    } catch (err) { console.error(err); showToast('Recording failed — try again'); }
    finally { setBusy(null); setProgress(0); }
  };

  // The card's own link. Photos are uploaded and the card saved the first time;
  // after that the same link is updated in place.
  const link = shareState(card);
  const ensureLink = async () => {
    if (link.upToDate) return link.link;
    const out = await publishCard(card);
    setCard(out.card);
    return out.link;
  };
  const getLink = async () => {
    if (publishing || notReady()) return;
    if (!shareReady()) { showToast('Links aren’t set up yet — see config.js'); return; }
    setPublishing(true);
    try {
      const url = await ensureLink();
      try { await navigator.clipboard.writeText(url); showToast('Link copied'); }
      catch (e) { showToast('Link ready — copy it from the Save & share panel'); }
    } catch (err) { console.error(err); showToast(err.message === 'name-taken' ? 'That link name is taken — try another' : 'Couldn’t create the link — try again'); }
    finally { setPublishing(false); }
  };

  const shareCaption = (url) =>
    `There’s a ${Math.min(15, Math.max(5, parseInt(card.kid.age, 10) || 6))}-year-old sitting behind every job title. Here’s mine.\n\n` +
    'Networking events are awkward, especially if you’re an introvert. So I made a badge that starts the conversation for me. Flip it and ask me anything on the back.' +
    (url ? `\n\nTry the link to check it out ↓\n${url}` : '');
  const dataUrlToBlob = (dataUrl) => {
    const [meta, b64] = dataUrl.split(',');
    const mime = (meta.match(/:(.*?);/) || [, 'image/png'])[1];
    const bin = atob(b64); const arr = new Uint8Array(bin.length);
    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
    return new Blob([arr], { type: mime });
  };

  const handleShare = async (target) => {
    if (target === 'copylink') {
      try { await navigator.clipboard.writeText(shareCaption(link.link)); showToast('Caption copied'); }
      catch (e) { showToast('Couldn’t copy — sorry'); }
      return;
    }
    if (notReady()) return;
    if (target === 'download') return downloadPng();
    if (target === 'print') return downloadPrint();
    if (target === 'video') { setAskVideo(true); return; }
    if (busy) return;

    // LinkedIn / Instagram: make a PNG and a video, hand them to the phone's
    // share sheet if there is one, otherwise save them and open the compose page.
    // The caption carries the card's link when links are set up.
    setBusy('video'); setProgress(0);
    try {
      showToast('Preparing your badge…');
      let url = null;
      if (shareReady()) { try { url = await ensureLink(); } catch (e) { console.error(e); } }
      const faces = await captureFaces();
      if (!faces) throw new Error('capture failed');
      const nm = slug(card?.pro?.name);
      const caption = shareCaption(url);
      const pngFile = new File([dataUrlToBlob(faces.frontUrl)], `tinyyou-${nm}.png`, { type: 'image/png' });
      let videoFile = null, vid = null;
      try { vid = await captureVideo(true); if (vid) videoFile = new File([vid.blob], `tinyyou-${nm}.${vid.ext}`, { type: vid.blob.type }); } catch (e) {}
      const files = videoFile ? [pngFile, videoFile] : [pngFile];

      try {
        if (navigator.canShare && navigator.share && navigator.canShare({ files })) {
          await navigator.share({ files, title: 'Tiny You | my badge', text: caption });
          return;
        }
      } catch (e) { /* cancelled or unsupported — fall through to the desktop flow */ }

      saveAs(faces.frontUrl, pngFile.name);
      if (vid) { const u = URL.createObjectURL(vid.blob); saveAs(u, videoFile.name); setTimeout(() => URL.revokeObjectURL(u), 4000); }
      try { await navigator.clipboard.writeText(caption); } catch (e) {}
      if (target === 'linkedin') {
        window.open(`https://www.linkedin.com/feed/?shareActive=true&text=${encodeURIComponent(caption)}`, '_blank', 'noopener,noreferrer');
        showToast('LinkedIn post opened — add the saved video or image.');
      } else {
        // Instagram's website has no link that opens "new post" directly.
        window.open('https://www.instagram.com/', '_blank', 'noopener,noreferrer');
        showToast('Instagram opened — press + Create and add the saved file. Caption is copied.');
      }
    } catch (err) { console.error(err); showToast('Couldn’t prepare the badge — try again'); }
    finally { setBusy(null); setProgress(0); }
  };

  const resetCard = () => {
    if (confirm('Reset the badge and start over?')) { setCard(newCard()); showToast('Fresh badge'); }
  };

  const office = getAura('pro', card.auraPro, card.auraProCustom);
  const aura = getAura('kid', card.auraKid, card.auraKidCustom);
  const linkLabel = publishing ? 'Saving…' : link.upToDate ? 'Copy link' : link.link ? 'Update link' : 'Get link';
  const exportProps = { onShare: handleShare, busy, progress, link, publishing, onGetLink: getLink, missing,
    linkName: card.linkName || '', linkHint: (!card.share || !card.share.published) && autoLinkNames(card)[0] || 'your-name', onLinkName: (v) => setCard(c => ({ ...c, linkName: String(v).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/-{2,}/g, '-').replace(/^-+/, '').slice(0, 24) })) };

  return (
    <Shell card={card} cardSide={cardSide}>
      <nav className="nav">
        <div className="nav-tabs">
          <button className={mode === 'edit' ? 'active' : ''} onClick={() => setMode('edit')}>Edit</button>
          <button className={mode === 'preview' ? 'active' : ''} onClick={() => setMode('preview')}>Preview</button>
        </div>
        <div className="nav-right">
          <button className="nav-btn" onClick={resetCard}>Reset</button>
        </div>
      </nav>

      {mode === 'edit' && (
        <Editor card={card} onUpdate={setCard} onPreview={() => setMode('preview')} onSideChange={setCardSide} {...exportProps}/>
      )}

      {mode === 'preview' && (
        <div className="stage-layout is-preview">
          <div className="stage-copy">
            <div className="eyebrow">♡ both sides of the same story</div>
            <h1>Meet the <em className="tilted">kid</em> behind the job title.</h1>
            <p>
              Be kind to yourself. The professional you doesn't need it all figured out — little you didn't either, and look how far that kid came.
            </p>
            <p className="hint">Tap the badge to flip it. Drag it sideways and let go to give it a swing.</p>
          </div>
          <div className="stage-card">
            <Card card={card} large tilt onSideChange={setCardSide}/>
          </div>
        </div>
      )}

      <div className="capture-rig" aria-hidden="true" ref={captureRef}>
        <Card card={card} large still side={0}/>
      </div>

      {askVideo && (
        <div className="modal-veil" onClick={() => setAskVideo(false)} onKeyDown={e => e.key === 'Escape' && setAskVideo(false)}>
          <div className="modal" role="dialog" aria-modal="true" aria-labelledby="ask-video-title" onClick={e => e.stopPropagation()}>
            <h2 id="ask-video-title">Save video</h2>
            <p>A 6-second vertical video of your badge flipping. Choose how you want it.</p>
            <div className="modal-opts" style={{ '--strap': getStrap(card) }}>
              <button className="modal-opt" autoFocus onClick={() => downloadVideo(true)}>
                <span className="modal-thumb" style={{ background: `linear-gradient(180deg, ${office.color} 0 34%, #0a1030 46%, ${aura.colors[1]} 68%, ${aura.colors[0]} 82%, #070b1e)` }}/>
                With background
              </button>
              <button className="modal-opt" onClick={() => downloadVideo(false)}>
                <span className="modal-thumb" style={{ background: '#fff' }}/>
                Without background
              </button>
            </div>
            <button className="modal-cancel" onClick={() => setAskVideo(false)}>Cancel</button>
          </div>
        </div>
      )}

      {toast && <div className="toast">{toast}</div>}
    </Shell>
  );
}

// Phones: size the badge to the screen. It is the main thing on the page, so it
// takes all the room between the title and the settings sheet, on any phone.
// The badge is 360 px wide and 742 px tall including its ring and hook.
function fitBadge() {
  const root = document.documentElement;
  if (window.innerWidth > 760) {
    // Laptops and desktops: shrink the badge just enough that it, and the flip button under it,
    // fit the window height, and cap the settings panel so the page itself never needs to scroll.
    const hud = document.querySelector('.hud');                       // the bar along the bottom in Preview
    const room = window.innerHeight - 62 - 50 - (hud ? 70 : 12);      // strap start above, flip button below
    root.style.setProperty('--card-zoom', Math.max(0.6, Math.min(1, room / 744)).toFixed(3));
    const tray = document.querySelector('.tray');
    if (tray) {
      const top = tray.getBoundingClientRect().top + window.scrollY;
      root.style.setProperty('--tray-max', Math.max(240, window.innerHeight - top - 16) + 'px');
    }
    window.dispatchEvent(new Event('tinyyou:fit'));
    return;
  }
  const ae = document.activeElement;
  if (ae && /INPUT|TEXTAREA/.test(ae.tagName) && root.style.getPropertyValue('--card-zoom')) return; // keyboard is up: hold still
  const byWidth = (window.innerWidth - 28) / 360;
  const short = window.innerHeight < 700;                    // short phones get a smaller title (see styles.css)
  const byHeight = (window.innerHeight - (short ? 114 : 150) - 76) / 742;   // title above, settings sheet below
  const z = Math.max(0.55, Math.min(1.15, byWidth, byHeight));
  root.style.setProperty('--card-zoom', z.toFixed(3));
  window.dispatchEvent(new Event('tinyyou:fit'));
}
fitBadge();
window.addEventListener('resize', fitBadge);
window.addEventListener('orientationchange', fitBadge);
window.addEventListener('load', fitBadge);
if (document.fonts && document.fonts.ready) document.fonts.ready.then(fitBadge);

// A link like …/#/c/ab12cd34ef opens the view-only page; anything else is the maker.
const viewIdFromHash = () => { const m = location.hash.match(/^#\/c\/([a-z0-9-]+)/); return m ? m[1] : null; };
function Root() {
  const [viewId, setViewId] = useStateA(viewIdFromHash);
  useEffectA(() => {
    const onHash = () => setViewId(viewIdFromHash());
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);
  return viewId ? <Viewer id={viewId}/> : <App/>;
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Root/>);
