// EUMORPH — home + Cécile + week rail + residence + member

const { useState: useS2, useEffect: useE2, useRef: useR2, useMemo: useM2, useCallback: useC2 } = React;

// ---------- Cécile portrait — reusable SVG, scales by `size` prop ----------
function CecilePortrait({ size = 96, ringed = false }) {
  return (
    <span className={`cp-wrap ${ringed ? "cp-ring" : ""}`} style={{ width: size, height: size }}>
      <svg viewBox="0 0 320 400" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
        <defs>
          <radialGradient id={`cp-vig-${size}`} cx="50%" cy="42%" r="62%">
            <stop offset="0%" stopColor="#4a3128" />
            <stop offset="100%" stopColor="#0d0807" />
          </radialGradient>
          <linearGradient id={`cp-hair-${size}`} x1="0" x2="0" y1="0" y2="1">
            <stop offset="0%" stopColor="#1c0f08" />
            <stop offset="100%" stopColor="#3a2316" />
          </linearGradient>
          <linearGradient id={`cp-skin-${size}`} x1="0" x2="1" y1="0" y2="1">
            <stop offset="0%" stopColor="#e7c9a8" />
            <stop offset="100%" stopColor="#a87a5a" />
          </linearGradient>
          <linearGradient id={`cp-collar-${size}`} x1="0" x2="0" y1="0" y2="1">
            <stop offset="0%" stopColor="#1a0908" />
            <stop offset="100%" stopColor="#0a0404" />
          </linearGradient>
        </defs>
        <rect width="320" height="400" fill={`url(#cp-vig-${size})`} />
        <path d="M 0 400 L 0 320 C 60 290, 110 280, 160 280 C 210 280, 260 290, 320 320 L 320 400 Z" fill={`url(#cp-collar-${size})`} />
        <path d="M 142 240 L 142 290 Q 160 300 178 290 L 178 240 Z" fill={`url(#cp-skin-${size})`} opacity="0.92" />
        <ellipse cx="160" cy="190" rx="56" ry="72" fill={`url(#cp-skin-${size})`} />
        <path d="M 100 175 C 95 130, 130 96, 160 96 C 190 96, 225 130, 220 175 C 218 165, 200 152, 162 152 C 124 152, 102 165, 100 175 Z" fill={`url(#cp-hair-${size})`} />
        <path d="M 100 175 C 92 215, 96 250, 110 280 L 122 270 C 116 240, 114 210, 116 188 Z" fill={`url(#cp-hair-${size})`} />
        <path d="M 220 175 C 228 215, 224 250, 210 280 L 198 270 C 204 240, 206 210, 204 188 Z" fill={`url(#cp-hair-${size})`} />
        <ellipse cx="130" cy="210" rx="14" ry="22" fill="#0a0504" opacity="0.18" />
        <path d="M 138 178 Q 148 174 156 178" stroke="#2a1810" strokeWidth="2.4" fill="none" strokeLinecap="round" />
        <path d="M 166 178 Q 174 174 184 178" stroke="#2a1810" strokeWidth="2.4" fill="none" strokeLinecap="round" />
        <ellipse cx="146" cy="190" rx="3" ry="2" fill="#1a0e08" />
        <ellipse cx="176" cy="190" rx="3" ry="2" fill="#1a0e08" />
        <path d="M 150 224 Q 161 232 172 224 Q 161 220 150 224 Z" fill="#7a2a26" />
      </svg>
    </span>
  );
}

// ---------- Cécile chat helpers — used by both the floating popover and the inline section ----------
const CECILE_SYS = `You are Cécile, the curator at Eumorph — a small editorial beauty house. You speak in unhurried, literary English with a faint Parisian formality. Sentences are short. You never use exclamation marks, "absolutely", "amazing", "I'd love to", or wellness-app vocabulary. You answer in 2–4 sentences, and you almost always recommend ONE product from the available list (never invent products). Reference a product by inserting the token [PROD:product-id] inline — the interface will render it as a card with an "Add to bag" button. You may also use *italics* for nuance and <Redact>BRAND</Redact> when naming a brand outside the [PROD] token. No greetings, no signoffs.`;

function buildCecilePrompt(userQ) {
  const products = (window.MC_DATA && window.MC_DATA.PRODUCTS) || [];
  const list = products.slice(0, 18).map(p => `  [PROD:${p.id}] = ${p.house} · ${p.name} — ${p.line || ""} ($${p.price})`).join("\n");
  return `${CECILE_SYS}

The available products you may recommend:
${list}

User asks: ${userQ}

Reply as Cécile, 2–4 sentences. End with one [PROD:id] token from the list above (the most fitting one). You may also use *italic* and <Redact>BRAND</Redact>. No greetings, no signoffs.`;
}

function parseCecileReply(text, opts = {}) {
  // Returns array of "blocks": text-runs and product-cards
  const blocks = [];
  const productRe = /\[PROD:([a-z0-9-]+)\]/gi;
  let last = 0, m;
  while ((m = productRe.exec(text)) !== null) {
    if (m.index > last) blocks.push({ kind: "text", value: text.slice(last, m.index) });
    blocks.push({ kind: "product", id: m[1] });
    last = m.index + m[0].length;
  }
  if (last < text.length) blocks.push({ kind: "text", value: text.slice(last) });
  return blocks;
}

function renderCecileText(text) {
  // Render <Redact>X</Redact> + *italic* in plain runs
  const out = [];
  const re = /<Redact>([^<]+)<\/Redact>/g;
  let last = 0, m, i = 0;
  while ((m = re.exec(text)) !== null) {
    if (m.index > last) out.push(text.slice(last, m.index));
    out.push(<Redact key={`r${i++}`}>{m[1]}</Redact>);
    last = m.index + m[0].length;
  }
  if (last < text.length) out.push(text.slice(last));
  return out.flatMap((p, idx) => {
    if (typeof p !== "string") return [p];
    const segs = [];
    const ire = /\*([^*]+)\*/g;
    let l = 0, im;
    while ((im = ire.exec(p)) !== null) {
      if (im.index > l) segs.push(p.slice(l, im.index));
      segs.push(<em key={`i${idx}-${l}`}>{im[1]}</em>);
      l = im.index + im[0].length;
    }
    if (l < p.length) segs.push(p.slice(l));
    return segs;
  });
}

// A small product-card that sits inside Cécile's reply.
function CecileProductCard({ id, dark, addToBag, go }) {
  const products = (window.MC_DATA && window.MC_DATA.PRODUCTS) || [];
  const p = products.find(x => x.id === id);
  if (!p) return null;
  const b = bottleFor(p);
  const [added, setAdded] = useS2(false);
  const onAdd = (e) => {
    e.stopPropagation();
    if (addToBag) addToBag(p);
    setAdded(true);
    setTimeout(() => setAdded(false), 1800);
  };
  return (
    <div className={`cpc ${dark ? "cpc-dark" : ""}`} onClick={() => go && go("pdp", { id: p.id })}>
      <div className="cpc-bottle">
        <Bottle kind={b.kind} color={b.color} height={dark ? 88 : 110} />
      </div>
      <div className="cpc-meta">
        <div className="cpc-house"><Redact>{p.house}</Redact></div>
        <div className="cpc-name"><em>{p.name}</em></div>
        <div className="cpc-line">{(p.line || "").split("—").pop().trim()} · {p.size}</div>
        <div className="cpc-row">
          <span className="cpc-price">${p.price}</span>
          <button className="cpc-add" onClick={onAdd}>{added ? "✓ added" : "Add to bag ↵"}</button>
        </div>
      </div>
    </div>
  );
}

// ---------- FloatingCecile — bottom-right popover, with mini-chat + product recs ----------
const CECILE_THREAD_KEY = "eumorph.cecile.thread.v2";
function FloatingCecile({ onOpen, addToBag, go }) {
  const [open, setOpen] = useS2(false);
  const [hidden, setHidden] = useS2(false);
  const [shown, setShown] = useS2(false);
  const [thread, setThread] = useS2(() => {
    try {
      const raw = localStorage.getItem(CECILE_THREAD_KEY);
      return raw ? JSON.parse(raw) : [];
    } catch (e) { return []; }
  });
  const [val, setVal] = useS2("");
  const [thinking, setThinking] = useS2(false);
  const inputRef = useR2(null);
  const threadRef = useR2(null);

  // Persist thread on every change
  useE2(() => {
    try { localStorage.setItem(CECILE_THREAD_KEY, JSON.stringify(thread)); } catch (e) {}
  }, [thread]);

  // Reveal after some scroll
  useE2(() => {
    let scrolled = false;
    const onScroll = () => {
      if (!scrolled && window.scrollY > 480) {
        scrolled = true;
        setShown(true);
        window.removeEventListener("scroll", onScroll);
      }
    };
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  // Auto-scroll thread
  useE2(() => {
    if (threadRef.current) threadRef.current.scrollTop = threadRef.current.scrollHeight;
  }, [thread, thinking]);

  // Focus when opened
  useE2(() => {
    if (open && inputRef.current) setTimeout(() => inputRef.current?.focus(), 120);
  }, [open]);

  // Expose a global open handle so other parts of the page (TopBar,
  // inline Cécile section, chips) can drive this popover.
  useE2(() => {
    window.__openCecile = (seed) => {
      setHidden(false);
      setShown(true);
      setOpen(true);
      if (seed && typeof seed === "string") {
        setVal(seed);
        setTimeout(() => inputRef.current?.focus(), 140);
      }
    };
    return () => { try { delete window.__openCecile; } catch (e) {} };
  }, []);

  const ask = useC2(async (text) => {
    const q = (text ?? val).trim();
    if (!q || thinking) return;
    setVal("");
    setThread(t => [...t, { role: "u", text: q }]);
    setThinking(true);
    let reply = null;
    try {
      if (window.claude && window.claude.complete) {
        reply = await window.claude.complete({ messages: [{ role: "user", content: buildCecilePrompt(q) }] });
      }
    } catch (e) { reply = null; }
    if (!reply) {
      // Reasonable fallback that includes a product token so the UI still demoes recommendation cards.
      const products = (window.MC_DATA && window.MC_DATA.PRODUCTS) || [];
      const fallback = products.find(p => p.id === "ab-rich-cream") || products[0];
      reply = fallback
        ? `Then a quiet evening, *nothing acid*. Lay something occlusive on damp skin and sleep on it. [PROD:${fallback.id}]`
        : "A quiet evening, then. Nothing acid.";
    }
    setThread(t => [...t, { role: "c", text: reply }]);
    setThinking(false);
  }, [val, thinking]);

  const PROMPTS = [
    "It's cold and dry where I am.",
    "Build me a quiet morning.",
    "Something for an evening, nothing loud.",
    "What should I try if I don't usually wear scent?",
  ];

  if (hidden) return null;

  const renderBlocks = (text) => {
    const blocks = parseCecileReply(text);
    return blocks.map((b, i) => {
      if (b.kind === "text") return <span key={i}>{renderCecileText(b.value)}</span>;
      return <CecileProductCard key={i} id={b.id} dark addToBag={addToBag} go={go} />;
    });
  };

  return (
    <>
      {/* the small handle — always visible after scroll, hidden when popover open */}
      <div
        className={`fcv2-handle ${shown ? "fcv2-shown" : ""} ${open ? "fcv2-handle-hidden" : ""}`}
      >
        <button
          className="fcv2-dismiss"
          onClick={(e) => { e.stopPropagation(); setHidden(true); }}
          aria-label="dismiss"
        >×</button>
        <button className="fcv2-handle-btn" onClick={() => setOpen(true)}>
          <span className="fcv2-portrait"><CecilePortrait size={42} /></span>
          <span className="fcv2-line">
            <em>Cécile is in.</em>
            <span className="fcv2-sub"><span className="fcv2-mini-dot" /> until 23h CET · ask me anything</span>
          </span>
          <span className="fcv2-arr">→</span>
        </button>
      </div>

      {/* the popover panel */}
      <div className={`fcv2-pop ${open ? "fcv2-open" : ""}`} role="dialog" aria-label="Cécile, the curator">
        <header className="fcv2-pop-head">
          <div className="fcv2-pop-id">
            <span className="fcv2-pop-portrait"><CecilePortrait size={44} /></span>
            <div>
              <div className="fcv2-pop-name"><em>Cécile</em>, the curator</div>
              <div className="fcv2-pop-status"><span className="fcv2-dot" /> in · until 23h CET</div>
            </div>
          </div>
          <button className="fcv2-pop-close" onClick={() => setOpen(false)} aria-label="close">×</button>
        </header>
        <div className="fcv2-pop-body" ref={threadRef}>
          {thread.length === 0 && !thinking && (
            <div className="fcv2-opener">
              Tell me the room you're walking into and I'll tell you what to wear on your skin. I'll point you at one bottle, on the shelf, in the vault.
            </div>
          )}
          {thread.length > 0 && (
            <div className="fcv2-thread-bar">
              <span>— continued from {thread.length === 1 ? "1 message" : `${thread.length} messages`}</span>
              <button
                className="fcv2-clear"
                onClick={() => { if (confirm("Clear this conversation?")) setThread([]); }}
              >clear</button>
            </div>
          )}
          {thread.map((t, i) => (
            t.role === "u"
              ? <div key={i} className="fcv2-u">{t.text}</div>
              : <div key={i} className="fcv2-c">{renderBlocks(t.text)}</div>
          ))}
          {thinking && <div className="fcv2-c fcv2-thinking">Cécile is reading the file…</div>}
        </div>
        <div className="fcv2-pop-chips">
          {PROMPTS.map((p, i) => (
            <span key={i} className="fcv2-chip" onClick={() => ask(p)}>{p}</span>
          ))}
        </div>
        <div className="fcv2-pop-input">
          <input
            ref={inputRef}
            value={val}
            onChange={(e) => setVal(e.target.value)}
            onKeyDown={(e) => e.key === "Enter" && ask()}
            placeholder="Tell me what you'd like…"
          />
          <button className="fcv2-send" onClick={() => ask()}>Send ↵</button>
        </div>
      </div>
    </>
  );
}

// ---------- HERO (split: definition + a product still + scent-search) ----------
function HeroV2({ go, openCecile }) {
  const products = (window.MC_DATA && window.MC_DATA.PRODUCTS) || [];
  const featured = products.find(p => p.id === "mfk-baccarat") || products[0];
  const b = bottleFor(featured);
  const phrases = [
    "smoky vanilla, but quieter",
    "the library at midnight",
    "a Paris pharmacy in 1970",
    "fig tree in late August",
    "cold-day skin that doesn't crack",
  ];
  const [ph, setPh] = useS2(0);
  const [typed, setTyped] = useS2("");
  useE2(() => {
    const target = phrases[ph];
    let i = 0;
    setTyped("");
    const tick = setInterval(() => {
      i++;
      setTyped(target.slice(0, i));
      if (i >= target.length) {
        clearInterval(tick);
        setTimeout(() => setPh(p => (p + 1) % phrases.length), 2200);
      }
    }, 45);
    return () => clearInterval(tick);
  }, [ph]);

  return (
    <section className="hero-v2">
      <div className="container">
        <div className="hero-v2-grid">
          {/* LEFT — dictionary entry */}
          <div className="hero-left">
            <div className="dict-entry">
              <h1 className="dict-headword">
                <span className="syl">eu</span>
                <span className="syl-dot">·</span>
                <span className="syl">morph</span>
              </h1>
              <div className="dict-meta-row">
                <span className="dm-ipa">/&apos;yoo&#772;-môrf/</span>
                <span className="dm-pos">adj.</span>
                <span className="dm-etym">
                  Gk. <span className="grk">εὖ</span> well + <span className="grk">μορφή</span> form
                </span>
              </div>
              <div className="dict-defs">
                <div className="dict-def">
                  <span className="dict-def-num">1</span>
                  <p className="dict-def-text">
                    well-formed; of good shape; <em>what is correctly proportioned to the body that holds it.</em>
                  </p>
                </div>
                <div className="dict-def">
                  <span className="dict-def-num">2</span>
                  <p className="dict-def-text">
                    <span className="pos">fig.</span>
                    of beauty objects: chosen, sequenced, and dispatched in the right order — <em>not the loud one.</em>
                  </p>
                </div>
              </div>
              <p className="dict-usage">
                <span className="usage-mark">— usage</span>
                <em>"She kept a small shelf — three things, two cities, one perfumer she'd known since 1998."</em>
              </p>
            </div>
            <div className="hero-actions">
              <a href="#" onClick={(e) => { e.preventDefault(); go("this-week"); }} className="hero-cta">This week's seven →</a>
              <a href="#" onClick={(e) => { e.preventDefault(); openCecile && openCecile(); }} className="hero-cta alt">Talk to Cécile</a>
            </div>
          </div>

          {/* RIGHT — house-of-the-week card */}
          <div className="hero-right">
            <figure className="hero-still" onClick={() => go("pdp", { id: featured.id })}>
              <span className="hero-still-tag">Plate I · {featured.size}</span>
              <div className="hero-still-bottle">
                <Bottle kind={b.kind} color={b.color} height={360} />
              </div>
              <figcaption className="hero-still-foot">
                <div>
                  <div className="hero-still-mark" style={{
                    fontFamily: '"JetBrains Mono", monospace',
                    fontSize: 9, letterSpacing: '0.22em', textTransform: 'uppercase',
                    color: 'var(--muted)'
                  }}>— this week's object</div>
                  <div className="hero-still-name"><Redact>{featured.house}</Redact></div>
                  <div className="hero-still-prod"><em>{featured.name}</em></div>
                </div>
                <a className="hero-still-link" href="#" onClick={(e) => { e.preventDefault(); go("pdp", { id: featured.id }); }}>Open ↗</a>
              </figcaption>
            </figure>
          </div>
        </div>
      </div>

    </section>
  );
}

// ---------- CLIMATE — three products: morning, afternoon, night ----------
function ClimateRow({ go }) {
  const products = (window.MC_DATA && window.MC_DATA.PRODUCTS) || [];
  if (!products.length) return null;
  // Match catalog "climate" tags to slots when possible — cold/dry today.
  // Falls back to deterministic picks (by product id hash) so the slots
  // never shift on refresh and never duplicate.
  function tagged(want) {
    return products.filter(p => {
      const tags = (p.climate || []).map(t => String(t).toLowerCase());
      return tags.some(t => want.some(w => t.includes(w)));
    });
  }
  function hashIdx(id, salt) {
    const s = String(id || "") + ":" + salt;
    let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 9000;
    return h;
  }
  function pickFor(salt, prefer, taken) {
    const pool = (prefer.length ? prefer : products).filter(p => !taken.has(p.id));
    const list = pool.length ? pool : products.filter(p => !taken.has(p.id));
    if (!list.length) return null;
    return list[hashIdx(salt, salt) % list.length];
  }
  const taken = new Set();
  const morning = pickFor("morning", tagged(["morning", "day", "bright", "work"]), taken);
  if (morning) taken.add(morning.id);
  const afternoon = pickFor("afternoon", tagged(["office", "workday", "weekday", "day"]), taken);
  if (afternoon) taken.add(afternoon.id);
  const night = pickFor("night", tagged(["night", "evening", "cold", "dry", "occlusive"]), taken);
  const slots = [
    { tag: "Morning", time: "07h", note: "Soft, lifted, bright.", product: morning || products[0] },
    { tag: "Afternoon", time: "14h", note: "Quiet through the desk.", product: afternoon || products[1] || products[0] },
    { tag: "Night", time: "22h", note: "Occlusive, on damp skin.", product: night || products[2] || products[0] },
  ];
  return (
    <section className="climate-row">
      <div className="container">
        <ChapterMark numeral="III" label="The air, tonight" sub="Brooklyn · cold + dry" folio="pg. 18" />
        <div className="cv7-head">
          <div className="cv7-head-l">
            <div className="cv7-loc">Brooklyn, NY · <em>21h04</em></div>
            <h3 className="cv7-verdict"><em>Cold</em>, and <em>dry</em>. The air is pulling water out of skin.</h3>
          </div>
          <ul className="cv7-dials">
            <li><span className="cv7-dial-v">47<sup>°F</sup></span><span className="cv7-dial-k">temp</span></li>
            <li><span className="cv7-dial-v">28<sup>%</sup></span><span className="cv7-dial-k">humidity</span></li>
            <li><span className="cv7-dial-v">UV&nbsp;2</span><span className="cv7-dial-k">indoor</span></li>
          </ul>
        </div>
        <div className="cv7-row">
          {slots.map((s, i) => {
            const b = bottleFor(s.product);
            return (
              <article key={i} className="cv7-card" onClick={() => go("pdp", { id: s.product.id })}>
                <div className="cv7-card-tag">— {s.tag} · {s.time}</div>
                <div className="cv7-card-bottle"><Bottle kind={b.kind} color={b.color} height={120} /></div>
                <div className="cv7-card-meta">
                  <div className="cv7-card-house"><Redact>{s.product.house}</Redact></div>
                  <div className="cv7-card-name"><em>{s.product.name}</em></div>
                  <div className="cv7-card-note">{s.note}</div>
                </div>
                <div className="cv7-card-arr">→</div>
              </article>
            );
          })}
        </div>
      </div>
    </section>
  );
}

// ---------- THE OPENING STORY (single product, magazine-led) ----------
function OpeningStory({ go }) {
  const products = (window.MC_DATA && window.MC_DATA.PRODUCTS) || [];
  const p = products.find(x => x.id === "biologique-p50") || products[0];
  if (!p) return null;
  const b = bottleFor(p);
  return (
    <section className="story-section editorial-spread">
      <div className="container">
        <div className="es-running">
          <span>Eumorph · Issue {ISSUE.number}</span>
          <span className="rh-rule"></span>
          <span><em>The long read</em></span>
          <span className="rh-rule"></span>
          <span>pg. 24</span>
        </div>
        <div className="es-grid">
          <aside className="es-margin">
            <div className="es-folio"><em>II</em></div>
            <div className="es-margin-label">A note<br/>from<br/>the file</div>
            <div className="es-pull">
              "It travels in unmarked amber. The customs form lists it under <em>matiere premiere</em>."
            </div>
            <div className="es-footnote">
              <sup>1</sup> Phenol — yes, the same phenol used in deep peel work. At 1.5% it is keratolytic, not destructive. The 1970 formula is the original.
            </div>
            <div className="es-footnote">
              <sup>2</sup> The current EU label substitutes salicylic for the phenol. We carry the 1970, not the 2023.
            </div>
          </aside>
          <div className="es-headline">
            <h2>
              The chemist's wife<br/>
              who couldn't <em>tolerate</em><br/>
              any cosmetic on<br/>
              the market.
            </h2>
            <div className="es-byline">
              By Cécile Marchand · 04 February — <em>five-minute read</em>
            </div>
          </div>
          <div className="es-frame">
            <Bottle kind={b.kind} color={b.color} height={420} />
            <span className="es-frame-cap">Plate 02 · the 1970, photographed Tuesday</span>
          </div>
          <div className="es-body">
            <p className="es-lede dropcap">
              In 1970, in a small Paris laboratory, a biologist named Y. Allouche
              composed a formula for his own wife. She had reacted to every cream
              on the rue Saint-Honoré that year, and to most of the décennies before.
              He worked from a chemist's instinct — phenol<sup>1</sup>, niacinamide, vinegar,
              sulfur — diluted into a solution intended to be dabbed onto cotton,
              not poured into a palm.
            </p>
            <p className="es-body-p">
              It worked. She wore it. Her sister wore it. Her sister's hairdresser
              in the seventh arrondissement wore it. Within five years it had
              moved out of the laboratory and into a small clinic in the Marais,
              and from there — quietly, in unmarked amber bottles — into the
              dressing rooms of three couture houses whose names we are not at
              liberty to print here.
            </p>
            <p className="es-body-p">
              Today the original formula is illegal in EU cosmetics under its 1970
              name<sup>2</sup>. The reformulation softened it, and the softer thing is what
              most pharmacies sell. We source the 1970 through international
              distribution channels, in the bottle you see at left.
            </p>
            <div className="es-meta">
              <div><div className="k">Composed</div><div className="v"><em>Paris, 1970</em></div></div>
              <div><div className="k">By</div><div className="v"><em>Y. Allouche, biologist</em></div></div>
              <div><div className="k">House</div><div className="v"><Redact>{p.house}</Redact></div></div>
              <div><div className="k">Edition</div><div className="v"><em>The 1970</em></div></div>
            </div>
            <div className="es-cta-row">
              <a href="#" onClick={(e) => { e.preventDefault(); go("pdp", { id: p.id }); }} className="es-cta">
                Read the dossier <span className="es-arrow">→</span>
              </a>
              <span className="es-price">${p.price} · {p.size}</span>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

// ---------- THE EDIT — Cécile's three-product routine ----------
function TheEdit({ go, addToBag }) {
  const products = (window.MC_DATA && window.MC_DATA.PRODUCTS) || [];
  const ids = ["sisley-black-rose", "ab-rich-cream", "mfk-baccarat"];
  const lines = ids.map(id => products.find(p => p.id === id)).filter(Boolean);
  const subtotal = lines.reduce((s, p) => s + p.price, 0);
  return (
    <section className="edit-section">
      <div className="container">
        <ChapterMark numeral="III" label="The edit · this week" sub="Composed by Cécile" folio="pg. 14" />
        <div className="edit-grid">
          <div className="edit-img">
            <div className="edit-img-plate"><em>Plate 03</em></div>
            <div className="edit-img-cap">Still life · 04.iv.2026</div>
            <div className="edit-tray"></div>
            <div className="edit-bottles">
              {lines.map((p, i) => {
                const b = bottleFor(p);
                const heights = [200, 240, 220];
                return (
                  <div key={p.id} className="edit-bottle-slot">
                    <Bottle kind={b.kind} color={b.color} height={heights[i] || 220} />
                    <div className="ebs-label">{String(i + 1).padStart(2, "0")} · {p.name.split(/\s/)[0]}</div>
                  </div>
                );
              })}
            </div>
          </div>
          <div className="edit-body">
            <h3 style={{ fontSize: "clamp(56px, 6vw, 96px)", fontWeight: 300, lineHeight: 0.95, letterSpacing: "-0.025em", margin: "0 0 24px", fontVariationSettings: '"opsz" 144', color: "var(--paper)" }}>
              An evening<br/>in <em style={{ fontStyle: "italic", color: "var(--accent)" }}>amber</em>.
            </h3>
            <p className="edit-lede">
              A three-product sequence to be performed in order, with twenty
              minutes between the second and third. Composed after a week with
              old issues of <em>Vogue Paris</em>, 1978.
            </p>
            <div className="edit-list">
              {lines.map((p, i) => (
                <div key={p.id} className="edit-row" onClick={() => go("pdp", { id: p.id })}>
                  <span className="er-num">{String(i + 1).padStart(2, "0")}</span>
                  <div>
                    <div className="eyebrow" style={{ marginBottom: 4 }}>{p.line.split("—").pop().trim()}</div>
                    <div className="er-name"><Redact>{p.house}</Redact> · <em>{p.name}</em></div>
                  </div>
                  <span className="er-price">${p.price}</span>
                </div>
              ))}
            </div>
            <button className="btn-v2" onClick={() => { lines.forEach(addToBag); go("cart"); }}>
              Add the edit · ${subtotal}
            </button>
          </div>
        </div>
      </div>
    </section>
  );
}

// ---------- THREE RAILS — how it arrives ----------
function ThreeRails({ go }) {
  const rails = [
    { id: "vault", num: "I", name: "The Vault", blurb: "Held in our temperature-controlled rooms in Long Island City. Inspected, authenticated, dispatched within two business days.", lede: "Stocked & shipped from Brooklyn." },
    { id: "atelier", num: "II", name: "The Atelier", blurb: "Sourced from international distribution channels in Paris, Grasse, and Tokyo. Slightly longer transit; identical guarantee.", lede: "From abroad, to here." },
    { id: "lab", num: "III", name: "The Lab", blurb: "Routines, blends, and bespoke editions assembled by Cécile. Made-to-order; never stocked. Eight to twelve a year.", lede: "Composed for one." },
  ];
  return (
    <section className="rails-section">
      <div className="container">
        <ChapterMark numeral="IV" label="How it arrives" sub="Three paths · one shelf" folio="pg. 22" />
        <div className="section-head">
          <h2>From there,<br/><em>to here</em>.</h2>
          <p className="lede">
            Eumorph holds three rails — a vault for what we keep, an atelier for what travels from abroad, and a lab for what is made for one. Each ships in its own way; the guarantee is identical.
          </p>
        </div>
        <div className="rails-grid">
          {rails.map((r, i) => (
            <div key={r.id} className="rail-cell" onClick={() => go("her-shelf")}>
              <div className="rail-num">{r.num}</div>
              <div className="rail-name">{r.name}</div>
              <div className="rail-lede">{r.lede}</div>
              <p className="rail-blurb">{r.blurb}</p>
              <span className="rail-link">Enter →</span>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

// ---------- WEEK RAIL — compact horizontal strip of 7 small chips ----------
function WeekRail({ go }) {
  const products = (window.MC_DATA && window.MC_DATA.PRODUCTS) || [];
  const days = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"];
  const today = new Date().getDay();
  const todayIdx = (today + 6) % 7;
  // Pick 7 distinct products, looping through whatever the catalog provides.
  // No backend coupling — just rotates from the existing PRODUCTS list.
  const week = days.map((d, i) => ({
    day: d,
    product: products[i % products.length],
  }));
  return (
    <section className="week-section">
      <div className="container">
        <ChapterMark numeral="I" label="This week" sub="Seven arrivals" folio="pg. 04" />
        <div className="week-strip-head">
          <h2 className="week-strip-h2">One week.<br/>Seven <em>quiet</em> arrivals.</h2>
          <p className="week-strip-lede">
            One product per day. Drawn from the shelf, rotated weekly.
          </p>
        </div>
        <div className="week-strip">
          {week.map((d, i) => {
            if (!d.product) return null;
            const b = bottleFor(d.product);
            const isToday = i === todayIdx;
            return (
              <article
                key={i}
                className={`week-chip ${isToday ? "week-chip-today" : ""}`}
                onClick={() => go("pdp", { id: d.product.id })}
              >
                <div className="wc-day">{d.day}{isToday && <span className="wc-today-dot"></span>}</div>
                <div className="wc-bottle"><Bottle kind={b.kind} color={b.color} height={84} /></div>
                <div className="wc-house"><Redact>{d.product.house}</Redact></div>
                <div className="wc-name"><em>{d.product.name}</em></div>
              </article>
            );
          })}
        </div>
      </div>
    </section>
  );
}

// ---------- CÉCILE — inline, on the page ----------
const CECILE_OPENERS = [
  "Tell me the room you're walking into and I'll tell you what to wear on your skin.",
  "What's the weather where you are? I find half of skin failure is climate, not chemistry.",
  "If you'd like — describe a fragrance you wore once and loved. I'll find it again, or something near it.",
];
const CECILE_CHIPS = [
  "It's cold and dry where I am.",
  "I want something for an evening, nothing loud.",
  "What did the women in 1970s Paris actually use?",
  "I have an event Friday and tired skin.",
  "Build me a quiet morning.",
];

function CecileVoice({ inputRef, addToBag, go }) {
  // The inline panel is now a *launcher* into the floating chat.
  // Typing here, hitting Enter, clicking Send, or tapping any chip
  // opens the popover seeded with that text.
  const [opener] = useS2(() => CECILE_OPENERS[Math.floor(Math.random() * CECILE_OPENERS.length)]);
  const [val, setVal] = useS2("");

  const launch = useC2((text) => {
    const fallback = "Tell me what you'd like to find.";
    const seed = (text ?? val).trim() || fallback;
    setVal("");
    if (window.__openCecile) window.__openCecile(seed);
  }, [val]);

  return (
    <div className="cecile-panel">
      <div className="opener">{opener}</div>
      <div className="cecile-chips">
        {CECILE_CHIPS.map((c, i) => (
          <span key={i} className="cecile-chip" onClick={() => launch(c)}>{c}</span>
        ))}
      </div>
      <div className="cecile-input-row">
        <input
          ref={inputRef}
          value={val}
          onChange={(e) => setVal(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === "Enter") { e.preventDefault(); launch(); }
          }}
          onClick={(e) => {
            // First click into the inline box pops the floating chat —
            // it's the real conversation surface.
            if (!val) { e.currentTarget.blur(); launch(""); }
          }}
          placeholder="Tell me what you'd like to find…"
        />
        <span className="send" onClick={() => launch()}>Send ↵</span>
      </div>
      <div className="cecile-launch-note">
        <em>Conversation opens in the corner — Cécile remembers what you've asked her.</em>
      </div>
    </div>
  );
}

function CecileSection({ inputRef, addToBag, go }) {
  return (
    <section className="cecile-section" id="cecile">
      <div className="container">
        <ChapterMark numeral="VI" label="The curator" sub="Répondez tous les jours · jusqu'à 23h" folio="pg. 32" />
        <div className="cs-grid-v2">
          <aside className="cs-credentials">
            <figure className="cs-portrait-frame-v2">
              <CecilePortrait size={260} />
            </figure>
            <div className="cs-cred-meta">
              <span className="cs-portrait-num">Plate 04</span>
              <span className="cs-portrait-credit">Cécile Marchand · photographed for Eumorph, Brooklyn, January</span>
            </div>
            <h3 className="cs-cred-name">I'm&nbsp;<em>Cécile</em>.<br/>I keep <em>the&nbsp;file</em>.</h3>
            <p className="cs-cred-bio">
              Trained in Versailles. Eleven years at an independent house in
              the Marais before this one. I write the long reads, I taste the
              formulas, I answer when you write.
            </p>
            <ul className="cs-cred-list">
              <li><span className="cs-k">Trained</span><span className="cs-v">Versailles · 2006–08</span></li>
              <li><span className="cs-k">Worked</span><span className="cs-v">An independent house, Paris · 11 yrs</span></li>
              <li><span className="cs-k">Reads</span><span className="cs-v">≈ 40 letters / day</span></li>
              <li><span className="cs-k">Replies by</span><span className="cs-v">23h CET, same day</span></li>
            </ul>
            <div className="cs-cred-foot"><em>No algorithm. Only me, and what I've remembered about you.</em></div>
          </aside>
          <div className="cs-talk-v2">
            <div className="cs-talk-head-v2">
              <div className="eyebrow">— a quiet line, open until 23h CET</div>
              <h4 className="cs-talk-h">
                Tell me what you'd <em>like</em>.
              </h4>
              <p className="cs-talk-h-sub"><em>I'll find it, or something near it.</em></p>
            </div>
            <CecileVoice inputRef={inputRef} addToBag={addToBag} go={go} />
          </div>
        </div>
      </div>
    </section>
  );
}

// ---------- HOUSE GRID — the v1 product grid, brought back ----------
// ---------- THE VAULT — a long shelf list, not a calendar grid ----------
function HouseGrid({ go }) {
  const products = (window.MC_DATA && window.MC_DATA.PRODUCTS) || [];
  const items = products.slice(0, 6);
  // Each row gets a distinct, specific subline — not filler.
  const VAULT_NOTES = {
    "ab-rich-cream": "Triggering complex, on damp skin only.",
    "biologique-p50": "1970 phenol formula, sourced abroad.",
    "mfk-baccarat": "Saffron, ambergris, cedar — and a quiet math.",
    "diptyque-philosykos": "Fig leaf, fig wood, fig fruit. Nothing else.",
    "sisley-black-rose": "Forty minutes, a clay mask, and stillness.",
    "u-beauty-resurfacing": "Eight serums folded into one.",
    "byredo-mojave": "Sandalwood that doesn't reach past the desk.",
    "tomford-tobacco": "Smoke and vanilla, for an unhurried Sunday.",
    "111skin-bio-cellulose": "Sheet of bio-cellulose, twenty minutes.",
  };
  // categorize by line
  const cats = items.map(p => {
    const line = (p.line || "").toLowerCase();
    let cat = "Care";
    if (line.includes("parfum") || line.includes("fragrance") || line.includes("eau")) cat = "Scent";
    else if (line.includes("hair") || line.includes("shampoo")) cat = "Hair";
    else if (line.includes("body") || line.includes("bath")) cat = "Body";
    else if (line.includes("mask") || line.includes("sheet")) cat = "Mask";
    else if (line.includes("toner") || line.includes("exfoli")) cat = "Tonic";
    return { p, cat };
  });
  return (
    <section className="vault-section">
      <div className="container">
        <ChapterMark numeral="II" label="The vault · on the shelf" sub="Six, of forty-one" folio="pg. 10" />
        <div className="vault-head">
          <div className="vault-head-l">
            <span className="vault-head-mark">[ The vault — Long Island City ]</span>
            <h2 className="vault-h2">
              Six&nbsp;objects,<br/>
              <em>on&nbsp;a&nbsp;shelf</em>.
            </h2>
          </div>
          <p className="vault-head-r">
            Held in temperature-controlled rooms in Long Island City. Inspected,
            authenticated, dispatched within two business days. The list rotates
            slowly — once or twice a season. We don't carry forty-one at once
            because we can't keep track of more.
          </p>
        </div>
        <div className="vault-list">
          {cats.map(({ p, cat }, i) => {
            const b = bottleFor(p);
            return (
              <article key={p.id} className="vault-row" onClick={() => go("pdp", { id: p.id })}>
                <div className="vr-idx">{String(i + 1).padStart(2, "0")}.</div>
                <div className="vr-cat">{cat}</div>
                <div className="vr-bottle">
                  <Bottle kind={b.kind} color={b.color} height={120} />
                </div>
                <div className="vr-meta">
                  <div className="vr-line">{p.line.split("—").pop().trim()}</div>
                  <div className="vr-name">
                    <span className="vr-house"><Redact>{p.house}</Redact></span>
                    <span className="vr-prod"><em>{p.name}</em></span>
                  </div>
                  <div className="vr-note">{VAULT_NOTES[p.id] || p.notes_short || p.tagline || "—"}</div>
                </div>
                <div className="vr-foot">
                  <div className="vr-price">${p.price}</div>
                  <div className="vr-size">{p.size}</div>
                  <div className="vr-stock">— in vault</div>
                </div>
                <div className="vr-arr">→</div>
              </article>
            );
          })}
        </div>
        <div className="vault-foot">
          <span className="vault-foot-mark">— and thirty-five more, by request</span>
          <a href="#" className="vault-foot-link" onClick={(e) => { e.preventDefault(); go("her-shelf"); }}>
            Open the full vault →
          </a>
        </div>
      </div>
    </section>
  );
}

// ---------- ON THE SHELF — Atlas of What Grows ----------
// A hand-feel world map; cities are ink stains; pick one and a postcard
// reveals the terroir (what grows there) and the houses we carry from it.
function ResidenceSection({ go }) {
  // Equirectangular projection: lon → x [0..360], lat → y [0..180]
  // Map viewBox is 720 × 360 so 1 unit = 0.5°
  const project = (lat, lon) => ({
    x: (lon + 180) * 2,
    y: (90 - lat) * 2,
  });

  const cities = [
    {
      city: "Brooklyn", country: "United States", lat: 40.68, lon: -73.94,
      stamp: "USPS · NY 11217",
      grows: "Nothing grows here. The vault, the lab, the chair Cécile sits in.",
      terroir: [
        { what: "the vault", note: "11,000 sq ft · climate 17 °C, RH 55%" },
        { what: "the lab", note: "where we test every formula on Brooklyn tap water — the worst-case base." },
        { what: "Cécile's desk", note: "a green-shaded library lamp, lit until 23h." },
      ],
      houses: ["Le Labo", "U Beauty", "Eumorph (vault)"],
      cecile: "Where everything gets its passport stamped, Cécile says. The least romantic city on this map; the most useful.",
    },
    {
      city: "London", country: "United Kingdom", lat: 51.50, lon: -0.13,
      stamp: "ROYAL MAIL · SW1",
      grows: "An archive of British perfumery, kept under a Burlington Arcade glass roof.",
      terroir: [
        { what: "Penhaligon's archive", note: "Wellington Street, since 1870. Cécile reads the back-catalogue twice a year." },
        { what: "clinical skincare", note: "a tradition of bench-tested formulas; we read the published trials before we stock." },
      ],
      houses: ["Penhaligon's", "Augustinus Bader"],
      cecile: "London is for archives and bench-tested skincare. We never come here for sun.",
    },
    {
      city: "Paris", country: "France", lat: 48.85, lon: 2.35,
      stamp: "LA POSTE · 75006",
      grows: "Tuberose absolute, civet tincture, iris pallida (aged six years), a stubborn idea of taste.",
      terroir: [
        { what: "iris pallida", note: "rhizomes aged six years before distillation. one of the most expensive raw materials in perfumery." },
        { what: "tuberose absolute", note: "extracted at 30 °C; one kilo of flowers yields three grams." },
        { what: "the rive gauche", note: "where four of the six Paris houses on our shelf still keep ateliers within walking distance of each other." },
      ],
      houses: ["Diptyque", "Frédéric Malle", "Officine Universelle Buly", "Maison Francis Kurkdjian", "Byredo", "Maison de Mai"],
      cecile: "Paris isn't a place; it's an opinion about restraint. Six houses, all within fifteen minutes on foot.",
    },
    {
      city: "Grasse", country: "France", lat: 43.66, lon: 6.92,
      stamp: "PROVENCE · 06130",
      grows: "Centifolia rose (May, before 9am), jasmine grandiflorum (August nights), tuberose, mimosa.",
      terroir: [
        { what: "Rosa centifolia", note: "the May rose; picked at first light, before the sun cracks the petals open." },
        { what: "Jasmine grandiflorum", note: "harvested by hand, after midnight, when the indole is highest." },
        { what: "the fields", note: "the region we visit each spring; twenty-three hectares of working perfume agriculture." },
      ],
      houses: ["Maison de Mai", "Lab Marchand"],
      cecile: "If a perfume claims jasmine and isn't from Grasse, India, or Egypt — read the back of the bottle again.",
    },
    {
      city: "Florence", country: "Italy", lat: 43.77, lon: 11.25,
      stamp: "POSTE ITALIANE · 50100",
      grows: "Iris (orris root, four-year cure), bergamot from Calabria, monastic tinctures.",
      terroir: [
        { what: "Santa Maria Novella", note: "founded 1221. The world's oldest still-operating pharmacy." },
        { what: "orris root", note: "harvested, peeled, and aged four years in cellars below the Duomo's level." },
        { what: "Acqua di Rose", note: "a recipe unchanged since 1612. We carry the same SKU." },
      ],
      houses: ["Santa Maria Novella", "Officina Profumo"],
      cecile: "Florence sells you a bottle of rosewater that has outlasted seventeen popes. Buy two.",
    },
    {
      city: "Muscat", country: "Sultanate of Oman", lat: 23.59, lon: 58.40,
      stamp: "OMAN POST · 100",
      grows: "Boswellia sacra — the only place on earth where the finest frankincense resin still bleeds.",
      terroir: [
        { what: "Boswellia sacra", note: "the trees are tapped in Dhofar; resin hardens for fourteen days before harvest." },
        { what: "Hojari grade", note: "the highest grade of frankincense; the basis of Amouage's most prized compositions." },
        { what: "a royal house", note: "Amouage was founded in 1983 by decree of the late Sultan Qaboos." },
      ],
      houses: ["Amouage"],
      cecile: "If you've never smelled real frankincense — the resin, on a hot stone — you've never smelled the desert. Start with Interlude.",
    },
    {
      city: "Tokyo", country: "Japan", lat: 35.68, lon: 139.69,
      stamp: "日本郵便 · 〒107", 
      grows: "Hinoki cypress, yuzu (Kōchi prefecture), an obsession with the brief.",
      terroir: [
        { what: "Hinoki", note: "two-hundred-year-old cypress; the wood used in onsen bath panels and in some Japanese perfumery bases." },
        { what: "Yuzu zest", note: "cold-pressed within hours of picking; the most fragile citrus oil on our shelf." },
        { what: "the archive tradition", note: "Japanese houses keep meticulous formula archives; Cécile reads the public ones." },
      ],
      houses: ["Comme des Garçons Parfums", "Shiseido"],
      cecile: "Tokyo is the city where editing is taken seriously. Two houses; both teach restraint.",
    },
    {
      city: "Melbourne", country: "Australia", lat: -37.81, lon: 144.96,
      stamp: "AUSPOST · VIC 3000",
      grows: "Eucalyptus globulus, Tasmanian sandalwood (santalum spicatum), a calmer kind of design.",
      terroir: [
        { what: "Tasmanian sandalwood", note: "the sustainable alternative to overharvested Mysore; harvested at 18+ years." },
        { what: "Eucalyptus globulus", note: "the blue gum — every Aesop hand wash you've ever used has its DNA in it." },
        { what: "Collins Street", note: "Aesop's flagship; a model for how a brick-and-mortar should feel quiet." },
      ],
      houses: ["Aesop"],
      cecile: "If you want to learn what an honest store looks like — go to Collins Street. Stay for an hour. Buy nothing.",
    },
  ];

  // sort west→east so the postcard order matches walking the map
  const byLon = [...cities].sort((a, b) => a.lon - b.lon);
  const totalHouses = cities.reduce((s, c) => s + c.houses.length, 0);

  // Active city: auto-cycle, pause on user pick
  const [activeIdx, setActiveIdx] = React.useState(2); // start on Paris
  const [locked, setLocked] = React.useState(false);
  React.useEffect(() => {
    if (locked) return;
    const t = setInterval(() => {
      setActiveIdx((i) => (i + 1) % byLon.length);
    }, 11000);
    return () => clearInterval(t);
  }, [locked, byLon.length]);

  const active = byLon[activeIdx];

  return (
    <section className="atlas-section atlas-globe">
      <div className="container">
        <ChapterMark numeral="VIII" label="On the shelf" sub={`${totalHouses} houses · ${cities.length} cities`} folio="pg. 56" />
        <div className="atlas-head">
          <div className="atlas-head-l">
            <span className="atlas-head-mark">[ atlas of what grows · plate 09 ]</span>
            <h2 className="atlas-h2">
              An atlas<br/>
              of <em>what grows</em>.
            </h2>
          </div>
          <p className="atlas-head-r">
            Perfume is an agricultural product with a passport. Skincare,
            mostly, is a chemistry one. We carry {totalHouses} houses from {cities.length} cities,
            and we visit each at least once a year. This is what they grow there,
            and what we bottle from it.
          </p>
        </div>

        <div className="ag-wrap">
          {/* LEFT — the map */}
          <div className="ag-map-col">
            <div className="ag-map-frame">
              <div className="ag-map-corners">
                <span className="ag-corner tl">┌</span>
                <span className="ag-corner tr">┐</span>
                <span className="ag-corner bl">└</span>
                <span className="ag-corner br">┘</span>
              </div>
              <header className="ag-map-head">
                <span className="ag-tag">PLATE 09</span>
                <span className="ag-title"><em>Mappa Mundi Eumorpha</em></span>
                <span className="ag-meta">scale unspecified · projection equirectangular · ink &amp; tea on linen</span>
              </header>

              <svg className="ag-map" viewBox="0 0 720 360" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
                <defs>
                  <pattern id="ag-grain" x="0" y="0" width="2" height="2" patternUnits="userSpaceOnUse">
                    <rect width="2" height="2" fill="transparent"/>
                    <circle cx="0.5" cy="0.5" r="0.18" fill="rgba(28,26,23,0.05)"/>
                  </pattern>
                  <radialGradient id="ag-stain" cx="50%" cy="50%" r="50%">
                    <stop offset="0%" stopColor="#b04428" stopOpacity="0.95"/>
                    <stop offset="60%" stopColor="#b04428" stopOpacity="0.6"/>
                    <stop offset="100%" stopColor="#b04428" stopOpacity="0"/>
                  </radialGradient>
                  <radialGradient id="ag-tea" cx="50%" cy="50%" r="60%">
                    <stop offset="0%" stopColor="rgba(180,130,90,0.12)"/>
                    <stop offset="100%" stopColor="rgba(180,130,90,0)"/>
                  </radialGradient>
                </defs>

                {/* paper tone wash */}
                <rect x="0" y="0" width="720" height="360" fill="url(#ag-grain)"/>
                {/* tea stains for warmth */}
                <ellipse cx="190" cy="140" rx="120" ry="80" fill="url(#ag-tea)"/>
                <ellipse cx="500" cy="200" rx="160" ry="100" fill="url(#ag-tea)"/>

                {/* full graticule — every 30° */}
                <g className="ag-grat">
                  {/* lat lines */}
                  {[0, 30, 60, 120, 150].map(y => (
                    <line key={`la-${y}`} x1="0" y1={y * 2} x2="720" y2={y * 2}
                          strokeDasharray={y === 90 ? "0" : "1 5"}/>
                  ))}
                  {/* equator — solid */}
                  <line x1="0" y1="180" x2="720" y2="180" className="ag-grat-eq"/>
                  {/* tropics — dashed */}
                  <line x1="0" y1="133" x2="720" y2="133" strokeDasharray="2 4"/>
                  <line x1="0" y1="227" x2="720" y2="227" strokeDasharray="2 4"/>
                  {/* meridians every 30° */}
                  {[60, 120, 180, 240, 300, 360, 420, 480, 540, 600, 660].map(x => (
                    <line key={`me-${x}`} x1={x} y1="0" x2={x} y2="360"
                          strokeDasharray="1 6"/>
                  ))}
                  {/* prime meridian — solid */}
                  <line x1="360" y1="0" x2="360" y2="360" className="ag-grat-pm"/>
                </g>

                {/* graticule labels */}
                <g className="ag-grat-labels">
                  <text x="362" y="15" textAnchor="start">0°</text>
                  <text x="180" y="15" textAnchor="middle">90° W</text>
                  <text x="540" y="15" textAnchor="middle">90° E</text>
                  <text x="2" y="183" textAnchor="start">EQUATOR</text>
                  <text x="2" y="136" textAnchor="start">23°N · TROPIC OF CANCER</text>
                  <text x="2" y="230" textAnchor="start">23°S · TROPIC OF CAPRICORN</text>
                </g>

                {/* hand-lettered region names (no continents — just words on a grid) */}
                <g className="ag-region">
                  <text x="160" y="110" textAnchor="middle">NORTH</text>
                  <text x="160" y="125" textAnchor="middle">AMERICA</text>
                  <text x="240" y="270" textAnchor="middle">SOUTH</text>
                  <text x="240" y="285" textAnchor="middle">AMERICA</text>
                  <text x="395" y="240" textAnchor="middle">AFRICA</text>
                  <text x="335" y="118" textAnchor="middle">EUROPA</text>
                  <text x="565" y="115" textAnchor="middle">ASIA</text>
                  <text x="455" y="220" textAnchor="middle">ARABIA</text>
                  <text x="660" y="245" textAnchor="middle">OCEANIA</text>
                </g>

                <g className="ag-hemi">
                  <text x="100" y="35" textAnchor="middle">— THE WEST —</text>
                  <text x="600" y="35" textAnchor="middle">— THE EAST —</text>
                </g>

                {/* compass rose */}
                <g className="ag-compass" transform="translate(45,310)">
                  <circle cx="0" cy="0" r="20" fill="none" strokeWidth="0.6"/>
                  <circle cx="0" cy="0" r="14" fill="none" strokeWidth="0.4"/>
                  <path d="M 0,-20 L 3,0 L 0,20 L -3,0 Z" fill="#1c1a17"/>
                  <path d="M -20,0 L 0,3 L 20,0 L 0,-3 Z" fill="rgba(28,26,23,0.35)"/>
                  <text x="0" y="-25" textAnchor="middle">N</text>
                  <text x="25" y="3" textAnchor="start">E</text>
                  <text x="0" y="32" textAnchor="middle">S</text>
                  <text x="-25" y="3" textAnchor="end">W</text>
                </g>

                {/* legend */}
                <g className="ag-legend" transform="translate(620,310)">
                  <circle cx="0" cy="0" r="3" fill="#b04428"/>
                  <text x="8" y="3">on the shelf</text>
                  <line x1="-50" y1="14" x2="-30" y2="14" stroke="rgba(28,26,23,0.35)" strokeWidth="0.4" strokeDasharray="2 4"/>
                  <text x="-26" y="17">tropic line</text>
                </g>

                {/* cities — ink stains with smart label placement */}
                {byLon.map((c, i) => {
                  const { x, y } = project(c.lat, c.lon);
                  const isActive = i === activeIdx;
                  // Custom label offsets — Paris/London/Florence/Grasse cluster needs care
                  const labelOffsets = {
                    "London":    { dx: -28, dy: -22, anchor: "end"   },
                    "Paris":     { dx:  28, dy: -22, anchor: "start" },
                    "Grasse":    { dx: -28, dy:  18, anchor: "end"   },
                    "Florence":  { dx:  32, dy:  10, anchor: "start" },
                    "Brooklyn":  { dx: -10, dy:  -8, anchor: "end"   },
                    "Muscat":    { dx:  10, dy:   4, anchor: "start" },
                    "Tokyo":     { dx:  10, dy:  -8, anchor: "start" },
                    "Melbourne": { dx:  10, dy:  10, anchor: "start" },
                  };
                  const lo = labelOffsets[c.city] || { dx: 8, dy: -6, anchor: "start" };
                  const showLeader = Math.abs(lo.dx) >= 8 || Math.abs(lo.dy) >= 8;
                  return (
                    <g key={c.city}
                       className={`ag-pin ${isActive ? "is-active" : ""}`}
                       transform={`translate(${x},${y})`}
                       onClick={() => { setActiveIdx(i); setLocked(true); }}
                       style={{ cursor: "pointer" }}
                    >
                      {isActive && (
                        <circle r="22" fill="url(#ag-stain)" className="ag-pin-halo"/>
                      )}
                      {showLeader && (
                        <line x1="0" y1="0" x2={lo.dx * 0.7} y2={lo.dy * 0.7}
                              stroke="rgba(176,68,40,0.55)" strokeWidth="0.4"/>
                      )}
                      <circle r={isActive ? 4 : 2.4} fill="#b04428"/>
                      <circle r={isActive ? 7 : 4.5} fill="none" stroke="#b04428" strokeWidth="0.5" opacity={isActive ? 0.6 : 0.3}/>
                      <text
                        x={lo.dx}
                        y={lo.dy}
                        textAnchor={lo.anchor}
                        className="ag-pin-label"
                      >
                        {c.city.toLowerCase()}
                      </text>
                    </g>
                  );
                })}
              </svg>

              <footer className="ag-map-foot">
                <span>{cities.length} cities · {totalHouses} houses</span>
                <span>{locked ? "paused on " + active.city.toLowerCase() : "drifting · 11s"}</span>
                {locked && (
                  <button type="button" className="ag-resume" onClick={() => setLocked(false)}>resume drift →</button>
                )}
              </footer>
            </div>
          </div>

          {/* RIGHT — the postcard stack */}
          <div className="ag-postcard-col">
            <div className="ag-stack">
              {/* faux back cards */}
              <div className="ag-card-back ag-back-2" aria-hidden="true"></div>
              <div className="ag-card-back ag-back-1" aria-hidden="true"></div>

              <article className="ag-card" key={active.city}>
                <header className="ag-card-head">
                  <div className="ag-stamp">
                    <div className="ag-stamp-inner">
                      <div className="ag-stamp-num">{String(activeIdx + 1).padStart(2,"0")}/{byLon.length}</div>
                      <div className="ag-stamp-label">EUMORPH<br/>POSTAL</div>
                      <div className="ag-stamp-fee">9¢</div>
                    </div>
                  </div>
                  <div className="ag-postmark">
                    <div className="ag-postmark-ring">
                      <div className="ag-postmark-text">{active.stamp}</div>
                      <div className="ag-postmark-date">{new Date().toLocaleDateString("en-GB", { day:"2-digit", month:"short", year:"2-digit" }).toUpperCase()}</div>
                    </div>
                  </div>
                </header>

                <div className="ag-card-titleblock">
                  <span className="ag-card-coord">
                    {Math.abs(active.lat).toFixed(2)}°{active.lat >= 0 ? "N" : "S"} ·
                    {" "}{Math.abs(active.lon).toFixed(2)}°{active.lon >= 0 ? "E" : "W"}
                  </span>
                  <h3 className="ag-card-city">{active.city}</h3>
                  <span className="ag-card-country">{active.country}</span>
                </div>

                <div className="ag-card-body">
                  <p className="ag-grows-line">
                    <span className="ag-grows-label">what grows here</span>
                    <span className="ag-grows-text">{active.grows}</span>
                  </p>

                  <ul className="ag-terroir">
                    {active.terroir.map((t, i) => (
                      <li key={i}>
                        <span className="ag-ter-bullet">·</span>
                        <span className="ag-ter-what"><em>{t.what}</em></span>
                        <span className="ag-ter-note">{t.note}</span>
                      </li>
                    ))}
                  </ul>

                  <div className="ag-houses-block">
                    <div className="ag-houses-label">{active.houses.length === 1 ? "House" : "Houses"} on the shelf · {active.houses.length}</div>
                    <div className="ag-houses-list">
                      {active.houses.map((h, i) => (
                        <span key={i} className="ag-house"><Redact>{h}</Redact></span>
                      ))}
                    </div>
                  </div>

                  <div className="ag-cecile">
                    <span className="ag-cecile-mark">— CÉCILE</span>
                    <p className="ag-cecile-quote">"{active.cecile}"</p>
                  </div>
                </div>
              </article>
            </div>

            {/* city dock — tiny west→east strip showing all cities */}
            <div className="ag-dock">
              {byLon.map((c, i) => (
                <button
                  key={c.city}
                  type="button"
                  className={`ag-dock-btn ${i === activeIdx ? "is-active" : ""}`}
                  onClick={() => { setActiveIdx(i); setLocked(true); }}
                >
                  <span className="ag-dock-num">{String(i + 1).padStart(2,"0")}</span>
                  <span className="ag-dock-city">{c.city}</span>
                </button>
              ))}
            </div>
          </div>
        </div>

        <div className="atlas-foot">
          <span>Eight cities · forty-one houses on the shelf · refreshed monthly</span>
          <a href="#" onClick={(e) => { e.preventDefault(); go("in-residence"); }}>Open the full atlas →</a>
        </div>
      </div>
    </section>
  );
}

// ---------- MEMBERSHIP ----------
function MemberSection({ go }) {
  const perks = [
    { k: "The locked rate", v: "9% off, kept forever at the rate of the day you join. Not a coupon. A contract." },
    { k: "Four sample editions", v: "A small box, hand-tied, on each equinox and solstice. Composed for your skin and the season." },
    { k: "Cécile, by name", v: "She replies herself, until 23h CET. The file in front of her is the one she keeps for you." },
    { k: "Twenty-four hours early", v: "Restocks, vault rotations, and the back-in-stock list — before the public sees them." },
    { k: "$50, on your birthday", v: "Applied automatically the first of the month. No marketing email. No countdown. Just a credit." },
    { k: "Audited, once a year", v: "A written letter from Cécile, with the protocol adjusted. Includes a take-home box." },
  ];
  return (
    <section className="member-v3">
      <div className="container">
        <ChapterMark numeral="VII" label="The Circle · since 2024" sub="9% locked · until you cancel" folio="pg. 48" />
        <div className="mv3-grid">
          {/* LEFT — the card */}
          <div className="mv3-card">
            <div className="mv3-card-paper">
              <div className="mv3-card-row mv3-card-top">
                <span className="mv3-card-mark">— №</span>
                <span className="mv3-card-num">0001 · founder</span>
                <span className="mv3-card-rule"></span>
                <span className="mv3-card-stamp">EUMORPH · CIRCLE</span>
              </div>
              <div className="mv3-card-body">
                <div className="mv3-card-eyebrow">Membership · annual</div>
                <h3 className="mv3-card-title">
                  <em>The&nbsp;Circle</em>
                </h3>
                <div className="mv3-card-priceline">
                  <span className="mv3-card-price">$50</span>
                  <span className="mv3-card-price-unit"> / yr.</span>
                  <span className="mv3-card-price-strike">$149</span>
                </div>
                <p className="mv3-card-desc">
                  A locked rate, four sample editions, and a curator who answers
                  by name — kept on a single card, not in a database.
                </p>
              </div>
              <div className="mv3-card-row mv3-card-foot">
                <span className="mv3-card-foot-l">Issued · {ISSUE.monthLabel}</span>
                <span className="mv3-card-foot-c">—</span>
                <span className="mv3-card-foot-r">cancel anytime — rate stays</span>
              </div>
              {/* the wax seal */}
              <div className="mv3-seal" aria-hidden="true">
                <svg viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg">
                  <defs>
                    <radialGradient id="mv3-seal-g" cx="35%" cy="30%" r="70%">
                      <stop offset="0%" stopColor="#a7392f" />
                      <stop offset="60%" stopColor="#7a201a" />
                      <stop offset="100%" stopColor="#4a110d" />
                    </radialGradient>
                  </defs>
                  <circle cx="40" cy="40" r="34" fill="url(#mv3-seal-g)" />
                  <circle cx="40" cy="40" r="30" fill="none" stroke="rgba(255,220,200,0.35)" strokeWidth="0.5" />
                  <text x="40" y="36" textAnchor="middle" fill="#f6dcc8" style={{ fontFamily: '"Fraunces", serif', fontSize: 14, fontStyle: "italic" }}>e</text>
                  <text x="40" y="50" textAnchor="middle" fill="rgba(246,220,200,0.7)" style={{ fontFamily: '"JetBrains Mono", monospace', fontSize: 5, letterSpacing: "0.2em" }}>CIRCLE</text>
                </svg>
              </div>
            </div>
            <div className="mv3-card-cta-row">
              <button className="mv3-cta" onClick={() => go("membership")}>Take a seat · $50</button>
              <span className="mv3-cta-aside">No marketing email · cancel anytime</span>
            </div>
            <div className="mv3-card-fineprint">
              <span>— Pays for itself at the second order, on average. From the 2025 cohort: members spent $830 with us on average and saved $74.70.<sup style={{ fontSize: "0.7em", marginLeft: 2 }}>1</sup></span>
              <span>For perfumers, MUAs and aestheticians: <a href="#" onClick={(e) => { e.preventDefault(); go("pro"); }}>The Founding 100 →</a></span>
            </div>
            <div style={{ fontFamily: "JetBrains Mono, monospace", fontSize: 9, letterSpacing: "0.06em", color: "var(--muted)", marginTop: 12, lineHeight: 1.5, fontStyle: "italic" }}>
              <sup>1</sup> n=412 active members, calendar 2025. Internal figures — audited annually by our accountants. Past members are not future members; your mileage will vary.
            </div>
          </div>

          {/* RIGHT — perks (kept light, paper-toned) */}
          <div className="mv3-perks-wrap">
            <div className="mv3-perks-head">
              <div className="eyebrow">— What it is, in plain English</div>
              <h4 className="mv3-perks-h">Six things, <em>kept</em>.</h4>
            </div>
            <ol className="mv3-perks">
              {perks.map((p, i) => (
                <li key={i} className="mv3-perk">
                  <span className="mv3-perk-n">{String(i + 1).padStart(2, "0")}</span>
                  <div className="mv3-perk-body">
                    <div className="mv3-perk-k">{p.k}</div>
                    <div className="mv3-perk-v">{p.v}</div>
                  </div>
                </li>
              ))}
            </ol>
          </div>
        </div>
      </div>
    </section>
  );
}

// ---------- BEAUTY INDEX — what Cécile has read ----------
function BeautyIndex({ go }) {
  // The index has three layers: dossiers (long-form), traces (live citations), readers (who's pulled it)
  const dossiers = [
    { num: "047", title: "Phenol, in dilution", sub: "the chemistry of the 1970 Lotion P50, & why the EU rewrote it in 2023", words: "12,400", reads: "8,212", route: "pdp", routeId: "biologique-p50" },
    { num: "046", title: "Baccarat 540, decoded", sub: "saffron · ambergris · cedar — and the math of \"a cathedral fragrance\"", words: "8,800", reads: "23,118", route: "pdp", routeId: "mfk-baccarat" },
    { num: "045", title: "TFC8®, in plain English", sub: "Augustinus Bader, the protein the patent calls a triggering complex", words: "6,200", reads: "5,041", route: "pdp", routeId: "ab-rich-cream" },
    { num: "044", title: "What 1970s Paris actually wore", sub: "from Jean Patou's archives, Hermès' couriers, and a hairdresser in the 7th", words: "9,300", reads: "11,602", route: "in-residence", routeId: null },
  ];
  const traces = [
    { src: "Wikipedia · Lotion P50",   note: "cited in the article footnote · stable since Mar 2026" },
    { src: "Common Crawl · CC-MAIN-26-13", note: "indexed 2026-03-14 · 8,412 docs scraped" },
    { src: "Claude (Sonnet 4.5)",       note: "answered 47× in last 30 days when asked about \"P50\"" },
    { src: "Perplexity · pro tier",     note: "linked in 12 reasoning traces · attribution intact" },
    { src: "Google · AI Overviews",     note: "surfaced for 6 queries · still attributed (sometimes)" },
  ];
  const stats = [
    { v: "8,412", k: "Dossiers in the index", note: "Published; CC-BY" },
    { v: "47", k: "Credentialed authors", note: "Perfumers · chemists · editors" },
    { v: "14,228", k: "AI citations · 30 days", note: "Across four major models" },
    { v: "32–52", k: "Structured attributes / SKU", note: "INCI · provenance · pH · notes" },
  ];

  return (
    <section className="bx-section">
      <div className="container">
        <ChapterMark numeral="IX" label="The beauty index · open access" sub="CC-BY · read by every model" folio="pg. 54" />
        <div className="bx-head">
          <div className="bx-head-l">
            <span className="bx-head-mark">[ open access · creative commons by 4.0 ]</span>
            <h2 className="bx-h2">
              What Cécile<br/>has <em>read</em>.
            </h2>
          </div>
          <p className="bx-head-r">
            Eight thousand long-form dossiers on ingredients, perfumers, founders,
            climates, and concerns. Published openly, read by Wikipedia and every
            model trained after April 2026. The shelf is downstream of the index.
          </p>
        </div>

        {/* MAIN — split: dossiers on left (4 rows), live traces on right */}
        <div className="bx-main">
          <div className="bx-dossiers">
            <div className="bx-mini-head">
              <span>The four most-read this month</span>
              <a href="#" onClick={(e) => e.preventDefault()}>Open the index →</a>
            </div>
            <ol className="bx-doss-list">
              {dossiers.map((d, i) => (
                <li key={d.num} className="bx-doss" onClick={(e) => {
                  e.preventDefault();
                  if (d.route === "pdp" && d.routeId) go("pdp", { id: d.routeId });
                  else if (d.route) go(d.route);
                }} style={{ cursor: "pointer" }}>
                  <div className="bx-doss-num">№ {d.num}</div>
                  <div className="bx-doss-body">
                    <h4 className="bx-doss-title">{d.title}</h4>
                    <p className="bx-doss-sub">{d.sub}</p>
                    <div className="bx-doss-meta">
                      <span>{d.words} words</span>
                      <span>·</span>
                      <span>{d.reads} reads</span>
                      <span>·</span>
                      <span><em>read by Cécile, twice</em></span>
                    </div>
                  </div>
                  <div className="bx-doss-arr">→</div>
                </li>
              ))}
            </ol>
          </div>
          <aside className="bx-traces">
            <div className="bx-mini-head">
              <span>Where it shows up</span>
              <span className="bx-live"><span className="bx-live-dot"></span>live</span>
            </div>
            <ul className="bx-trace-list">
              {traces.map((t, i) => (
                <li key={i} className="bx-trace">
                  <div className="bx-trace-src">{t.src}</div>
                  <div className="bx-trace-note">— {t.note}</div>
                </li>
              ))}
            </ul>
            <div className="bx-trace-foot">
              <em>Most of the answers about beauty on the open web are now downstream of someone's index. We made ours public.</em>
            </div>
          </aside>
        </div>

        {/* footer stats */}
        <div className="bx-stats">
          {stats.map((s, i) => (
            <div key={i} className="bx-stat">
              <div className="bx-stat-v">{s.v}</div>
              <div className="bx-stat-k">{s.k}</div>
              <div className="bx-stat-note">— {s.note}</div>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

Object.assign(window, { HeroV2, ClimateRow, OpeningStory, TheEdit, ThreeRails, WeekRail, CecileSection, CecileVoice, BeautyIndex, ResidenceSection, MemberSection, HouseGrid, FloatingCecile });
