// kova/kraft.jsx — Wave E · Designer (K1: static chat shell)
// ----------------------------------------------------------------
// Routes (architecture v7 §8.5):
//   /kraft                    KraftStudioPage  (chat-first designer surface)
//
// Customer ask (resolved in docs/design/KRAFT_CHAT_RATIONALE.md):
//   "It should just be agent chats. Like ChatGPT or Gemini.
//    Only the chat with agent. And some designs showing under the chat.
//    Click an image → convert to 3D or send to Rhino. Backend does the rest."
//
// Architecture compliance:
//   - Every assistant turn renders as a verified-output card with
//     VerificationStamp + StatusBadge + RunBadge — never raw model prose.
//   - DesignGrid is the card body; click-actions emit toasts that stand in
//     for the workflow POSTs the backend will wire in K5/K6.
//   - No knobs (novelty, profile editor, model picker) appear on the user surface.
//
// K1 scope: static fixtures plus optional localhost GPT draft prompt enhancement.
// Demo-ready end-to-end; model output stays draft/unverified.
// ----------------------------------------------------------------

const {
  Btn, IconBtn, Chip, Kbd, Icon, Card, PageHead, Textarea, Avatar, Seg, Modal,
  StatusBadge, RunBadge, VerificationStamp, ConfidenceMeter,
  EmptyState,
} = window.K;

const { useState, useRef, useEffect, useMemo } = React;

const KRAFT_REGION_MODES = [
  { id: 'usa-north-america', label: 'USA / North America V1' },
];
const KRAFT_CATEGORIES = [
  {
    id: 'rings',
    label: 'Rings',
    channelProfile: 'everyday fine jewellery',
    designNotes: ['Balance shank width, comfort fit, and stone height for everyday NA wear.', 'Keep styling saleable across self-purchase, gifting, and occasion use.'],
    variationAxes: ['metal color and karat', 'stone scale or setting style', 'shank profile and stackability'],
    cadNotes: ['Draft: confirm ring size, shank width/depth, stone dimensions, setting height, and comfort-fit radius.'],
    missingDimensions: ['Ring size and size range.', 'Shank width/depth and inside comfort radius.', 'Stone diameter/depth and setting height.'],
  },
  {
    id: 'engagement-rings',
    label: 'Engagement rings',
    channelProfile: 'commercial bridal catalog',
    designNotes: ['Prioritize center-stone presentation, durable prong geometry, and matching-band compatibility.', 'Use NA bridal expectations without copying any named retailer or designer.'],
    variationAxes: ['center stone shape and carat band', 'setting architecture', 'matching band compatibility'],
    cadNotes: ['Draft: confirm ring size, center stone measurements, prong count/gauge, gallery height, bridge clearance, and wedding-band fit.'],
    missingDimensions: ['Ring size.', 'Center stone L/W/D and certificate tolerances.', 'Prong gauge/count and gallery height.', 'Matching-band clearance.'],
  },
  {
    id: 'wedding-bands',
    label: 'Wedding bands',
    channelProfile: 'commercial bridal catalog',
    designNotes: ['Define profile, width, comfort fit, and whether the band stacks with engagement rings.', 'Keep stone coverage and engraving assumptions explicit.'],
    variationAxes: ['band width and profile', 'stone coverage or engraving', 'finish and edge treatment'],
    cadNotes: ['Draft: confirm ring size, band width, profile, stone spacing if any, engraving zone, and finish tolerance.'],
    missingDimensions: ['Ring size.', 'Band width/depth and profile.', 'Stone spacing or engraving area.', 'Finish and edge radius.'],
  },
  {
    id: 'pendants',
    label: 'Pendants',
    channelProfile: 'everyday fine jewellery',
    designNotes: ['Resolve bail orientation, pendant scale, chain compatibility, and everyday wearability.', 'Keep motif and stone layout clear enough for a CAD sketch.'],
    variationAxes: ['motif scale', 'bail and chain interface', 'stone layout or personalization'],
    cadNotes: ['Draft: confirm pendant height/width/depth, bail inner diameter, chain type, stone sizes, and back-finish expectations.'],
    missingDimensions: ['Pendant height/width/depth.', 'Bail inner diameter and chain type.', 'Stone sizes/settings.', 'Back finish and engraving area.'],
  },
  {
    id: 'tennis-bracelets',
    label: 'Tennis bracelets',
    channelProfile: 'DTC lab-grown modern',
    designNotes: ['Prioritize link articulation, clasp security, stone repeatability, and NA price-band clarity.', 'Call out total carat assumption as draft until merchant validation.'],
    variationAxes: ['stone size and total carat band', 'link profile and articulation', 'clasp and safety detail'],
    cadNotes: ['Draft: confirm bracelet length, link count, stone size, setting tolerance, clasp type, safety lock, and articulation clearance.'],
    missingDimensions: ['Bracelet length/circumference.', 'Link count and link pitch.', 'Stone size and setting tolerance.', 'Clasp/safety lock dimensions.'],
  },
  {
    id: 'earrings',
    label: 'Earrings',
    channelProfile: 'everyday fine jewellery',
    designNotes: ['Resolve pair symmetry, post/hinge choice, weight per ear, and daily-wear comfort.', 'Separate stud, hoop, huggie, and drop assumptions when unspecified.'],
    variationAxes: ['silhouette type', 'closure and wear comfort', 'stone or motif scale'],
    cadNotes: ['Draft: confirm pair dimensions, post/hinge type, weight per ear, stone sizes, and left/right symmetry.'],
    missingDimensions: ['Pair dimensions and silhouette type.', 'Post, hinge, or hook mechanism.', 'Weight per ear.', 'Stone sizes and symmetry constraints.'],
  },
  {
    id: 'necklaces',
    label: 'Necklaces',
    channelProfile: 'everyday fine jewellery',
    designNotes: ['Clarify chain length, station spacing, pendant integration, and clasp expectations.', 'Keep layering and giftability assumptions explicit.'],
    variationAxes: ['chain length and link style', 'station or pendant spacing', 'clasp and adjuster detail'],
    cadNotes: ['Draft: confirm chain length, link gauge, station spacing, clasp type, extender length, and pendant attachment if present.'],
    missingDimensions: ['Chain length and link gauge.', 'Station spacing or pendant attachment.', 'Clasp and extender dimensions.', 'Target finished weight.'],
  },
  {
    id: 'charms',
    label: 'Charms',
    channelProfile: 'minimal stackable gifting',
    designNotes: ['Prioritize charm scale, attachment loop, personalization area, and compatibility with common chains/bracelets.', 'Keep motif ownership generic and client-approved.'],
    variationAxes: ['motif and personalization area', 'attachment loop style', 'enamel, stone, or metal-only treatment'],
    cadNotes: ['Draft: confirm charm size/depth, jump-ring or bail dimensions, engraving/enamel area, and chain/bracelet compatibility.'],
    missingDimensions: ['Charm height/width/depth.', 'Loop or bail inner diameter.', 'Engraving/enamel area.', 'Compatible chain or bracelet gauge.'],
  },
  {
    id: 'stackables',
    label: 'Stackables',
    channelProfile: 'minimal stackable gifting',
    designNotes: ['Design as a family: shared proportions, mixable silhouettes, and clear price ladder.', 'Keep per-piece and set-level assumptions separate.'],
    variationAxes: ['set count and price ladder', 'mixable profile or motif family', 'metal and stone rhythm'],
    cadNotes: ['Draft: confirm each piece size, stacking clearance, shared profile rules, stone spacing, and set merchandising logic.'],
    missingDimensions: ['Piece count and sizes.', 'Stacking clearance and shared profile.', 'Stone spacing per piece.', 'Set-level target weight and price ladder.'],
  },
  {
    id: 'unique-custom-concepts',
    label: 'Unique / custom concepts',
    channelProfile: 'ethical modern bridal',
    designNotes: ['Translate the concept into buildable geometry, provenance-safe inspiration, and explicit unknowns.', 'Avoid copying named brands, artists, or protected references; use neutral style vocabulary.'],
    variationAxes: ['signature motif system', 'personalization or symbolism', 'manufacturability risk and price band'],
    cadNotes: ['Draft: convert the concept into measurable views; confirm hero dimensions, construction method, stone plan, tolerances, and hand-finishing assumptions.'],
    missingDimensions: ['Hero dimensions and intended wear category.', 'Construction method and view angles.', 'Stone plan or metal-only scope.', 'Hand-finishing and tolerance assumptions.'],
  },
];
const KRAFT_REGION_LABEL = Object.fromEntries(KRAFT_REGION_MODES.map(r => [r.id, r.label]));
const KRAFT_CATEGORY_BY_ID = Object.fromEntries(KRAFT_CATEGORIES.map(c => [c.id, c]));
const KRAFT_CATEGORY_ALIASES = {
  ring: 'rings',
  pendant: 'pendants',
  necklace: 'necklaces',
  'tennis bracelet': 'tennis-bracelets',
  bracelet: 'tennis-bracelets',
  charm: 'charms',
  custom: 'unique-custom-concepts',
  'unique concept': 'unique-custom-concepts',
  'custom concept': 'unique-custom-concepts',
};

const normalizeCategoryId = (category) => {
  const raw = String(category || '').trim().toLowerCase();
  const id = raw.replace(/\s*\/\s*/g, '-').replace(/\s+/g, '-');
  return KRAFT_CATEGORY_BY_ID[id] ? id : (KRAFT_CATEGORY_ALIASES[raw] || 'rings');
};
const categoryProfile = (category) => KRAFT_CATEGORY_BY_ID[normalizeCategoryId(category)] || KRAFT_CATEGORY_BY_ID.rings;
const designCategoryProfile = (design) => categoryProfile(design?.category || design?.categoryLabel);
const designLineage = (profile, overrides = {}) => ({
  regionMode: overrides.regionMode || 'usa-north-america',
  regionLabel: overrides.regionLabel || KRAFT_REGION_LABEL['usa-north-america'],
  category: profile.id,
  categoryLabel: profile.label,
  channelProfile: profile.channelProfile,
  categoryDesignNotes: profile.designNotes,
  variationAxes: profile.variationAxes,
  cadNotes: profile.cadNotes,
  missingDimensions: profile.missingDimensions,
});
const withDesignLineage = (design, category, source = design) => {
  const profile = categoryProfile(category || source?.category || source?.categoryLabel || design?.category || design?.categoryLabel);
  return { ...design, ...designLineage(profile, source) };
};

const asBriefList = (value) => {
  if (Array.isArray(value)) return value.map(String).filter(Boolean);
  if (typeof value === 'string' && value.trim()) return [value.trim()];
  return [];
};
const briefText = (brief, key, fallback = '—') => {
  const value = brief?.[key];
  if (value == null || value === '') return fallback;
  return String(value);
};
const shortPromptSeed = (design, note) => [
  `Parent design ${design.id}: ${design.tag || 'selected design'}.`,
  note || 'Create saleable jewellery variations while preserving parent lineage.',
  'Return DesignVariationSet.v1 seeds only; no image generation or approval claim.',
].join(' ');


/* ============================================================
   SEED · sessions, turns, designs
   ============================================================ */

const SEED_KRAFT_SESSIONS = [
  { id: 's-bridal-haram',  title: 'Heritage-inspired custom necklace',  updated: '2 min ago',  turns: 4, starred: 6 },
  { id: 's-light-pendant', title: 'Light daily-wear pendant',   updated: '1 h ago',    turns: 3, starred: 2 },
  { id: 's-mens-band',     title: "Men's signet band · 22k",    updated: 'Yesterday',  turns: 5, starred: 1 },
  { id: 's-cocktail-ring', title: 'Cocktail ring · emerald',    updated: '3 d ago',    turns: 2, starred: 0 },
];

// Each assistant turn = a verified DecisionCard payload (4 designs).
const SEED_KRAFT_TURNS_RAW = {
  's-bridal-haram': [
    { role: 'user', id: 't1', at: '10:14',
      text: 'North America client wants a heritage-inspired ceremonial necklace. Heirloom feel but lighter than her mother\'s set. Loves peacock motifs. Budget around ₹4–5L. Event 12 Feb.' },
    { role: 'assistant', id: 't2', at: '10:14',
      runId: 'rcp-91a3', runVersion: 'v1', runWhen: '36 s',
      status: 'verified', statusLabel: 'Verified · ready',
      stamp: 'Verified · kraft_design_agent',
      title: '4 directions for a peacock-led custom necklace',
      confidence: 3, confidenceLabel: 'High',
      designs: [
        { id: 'd-91a3-1', tag: 'Peacock medallion · 22k', hue: 38, starred: true },
        { id: 'd-91a3-2', tag: 'Twin-peacock side panels', hue: 18, starred: false },
        { id: 'd-91a3-3', tag: 'Lakshmi centre · peacock chain', hue: 52, starred: false },
        { id: 'd-91a3-4', tag: 'Layered haram · openwork', hue: 30, starred: false },
      ],
    },
    { role: 'user', id: 't3', at: '10:18',
      text: 'I like direction 1 — make the peacocks slightly smaller and add mango paisley drops along the chain.' },
    { role: 'assistant', id: 't4', at: '10:18',
      runId: 'rcp-91ad', runVersion: 'v2', runWhen: '28 s',
      status: 'verified', statusLabel: 'Verified · ready',
      stamp: 'Verified · kraft_design_agent · branched from rcp-91a3',
      title: 'Peacock medallion · refined · mango drops added',
      confidence: 4, confidenceLabel: 'Verified',
      designs: [
        { id: 'd-91ad-1', tag: 'Smaller peacock · mango drops', hue: 38, starred: true },
        { id: 'd-91ad-2', tag: 'Smaller peacock · paisley row', hue: 36, starred: false },
        { id: 'd-91ad-3', tag: 'Smaller peacock · floral spacer', hue: 40, starred: false },
        { id: 'd-91ad-4', tag: 'Smaller peacock · kemp accents', hue: 8,  starred: false },
      ],
    },
  ],
  's-light-pendant': [
    { role: 'user', id: 't1', at: 'Yesterday',
      text: 'Light daily-wear pendant. 18k yellow. Under 6g. Diamond-set.' },
    { role: 'assistant', id: 't2', at: 'Yesterday',
      runId: 'rcp-90c1', runVersion: 'v1', runWhen: '22 s',
      status: 'verified', statusLabel: 'Verified · ready',
      stamp: 'Verified · kraft_design_agent',
      title: '4 light-weight pendants · 18k · diamond-set',
      confidence: 3, confidenceLabel: 'High',
      designs: [
        { id: 'd-90c1-1', tag: 'Floating solitaire · 5.8 g', hue: 50, starred: true },
        { id: 'd-90c1-2', tag: 'Petal halo · 5.9 g',          hue: 48, starred: false },
        { id: 'd-90c1-3', tag: 'Bar-set trio · 5.4 g',         hue: 46, starred: true },
        { id: 'd-90c1-4', tag: 'Bezel drop · 5.7 g',           hue: 52, starred: false },
      ],
    },
  ],
  's-mens-band': [
    { role: 'user', id: 't1', at: 'Yesterday',
      text: "Men's signet band, 22k yellow, monogram-ready, comfort-fit." },
    { role: 'assistant', id: 't2', at: 'Yesterday',
      runId: 'rcp-8f12', runVersion: 'v1', runWhen: '31 s',
      status: 'verified', statusLabel: 'Verified · ready',
      stamp: 'Verified · kraft_design_agent',
      title: '4 signet directions · 22k comfort-fit',
      confidence: 3, confidenceLabel: 'High',
      designs: [
        { id: 'd-8f12-1', tag: 'Oval face · milgrain edge',     hue: 42, starred: true },
        { id: 'd-8f12-2', tag: 'Rectangular face · bevelled',   hue: 40, starred: false },
        { id: 'd-8f12-3', tag: 'Shield face · matte centre',    hue: 38, starred: false },
        { id: 'd-8f12-4', tag: 'Cushion face · polished',       hue: 44, starred: false },
      ],
    },
  ],
  's-cocktail-ring': [
    { role: 'user', id: 't1', at: '3 d ago',
      text: 'Cocktail ring, emerald centre stone, white gold, evening occasion.' },
    { role: 'assistant', id: 't2', at: '3 d ago',
      runId: 'rcp-8d04', runVersion: 'v1', runWhen: '27 s',
      status: 'verified', statusLabel: 'Verified · ready',
      stamp: 'Verified · kraft_design_agent',
      title: '4 emerald cocktail directions · 18k white',
      confidence: 2, confidenceLabel: 'Medium',
      designs: [
        { id: 'd-8d04-1', tag: 'Halo · brilliant border',       hue: 150, starred: false },
        { id: 'd-8d04-2', tag: 'East-west bezel · diamond shoulders', hue: 160, starred: false },
        { id: 'd-8d04-3', tag: 'Trillion side stones',          hue: 145, starred: false },
        { id: 'd-8d04-4', tag: 'Hexagonal cathedral · pavé',    hue: 155, starred: false },
      ],
    },
  ],
};

const KRAFT_SESSION_CATEGORIES = {
  's-bridal-haram': 'unique-custom-concepts',
  's-light-pendant': 'pendants',
  's-mens-band': 'rings',
  's-cocktail-ring': 'rings',
};

const SEED_KRAFT_TURNS = Object.fromEntries(Object.entries(SEED_KRAFT_TURNS_RAW).map(([sid, turns]) => [
  sid,
  turns.map(t => (t.role === 'assistant' && t.designs)
    ? { ...t, designs: t.designs.map(d => withDesignLineage(d, KRAFT_SESSION_CATEGORIES[sid])) }
    : t),
]));


/* ============================================================
   DesignTile — single rendered design (SVG placeholder, offline)
   Real images land in K5 when /api/kraft/sessions/.../messages
   returns R2 URLs in each Decision.designs[].url.
   ============================================================ */

const TileSvg = ({ hue }) => {
  // soft radial swatch with a stylised stone + setting silhouette
  const gid = `g${Math.round(hue)}`;
  const c1 = `hsl(${hue}, 30%, 92%)`;
  const c2 = `hsl(${hue}, 32%, 78%)`;
  const c3 = `hsl(${hue}, 24%, 56%)`;
  return (
    <svg viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" className="kr-tile-canvas" aria-hidden="true">
      <defs>
        <radialGradient id={gid} cx="50%" cy="42%" r="65%">
          <stop offset="0%" stopColor={c1} />
          <stop offset="100%" stopColor={c2} />
        </radialGradient>
      </defs>
      <rect width="200" height="200" fill={`url(#${gid})`} />
      {/* setting */}
      <circle cx="100" cy="104" r="46" fill="none" stroke={c3} strokeOpacity="0.55" strokeWidth="2.5" />
      <circle cx="100" cy="104" r="34" fill="none" stroke={c3} strokeOpacity="0.35" strokeWidth="1" />
      {/* centre stone */}
      <polygon points="100,72 122,98 100,136 78,98" fill="#FFFFFF" fillOpacity="0.92" />
      <polygon points="100,72 122,98 100,98 78,98" fill="#FFFFFF" fillOpacity="0.55" />
      <line x1="100" y1="72" x2="100" y2="136" stroke={c3} strokeOpacity="0.4" strokeWidth="0.5" />
      <line x1="78" y1="98"  x2="122" y2="98"  stroke={c3} strokeOpacity="0.4" strokeWidth="0.5" />
      {/* prong dots */}
      {[78,122,100,100].map((cx, i) => {
        const cy = [98,98,72,136][i];
        return <circle key={i} cx={cx} cy={cy} r="3" fill={c3} fillOpacity="0.7" />;
      })}
    </svg>
  );
};

const DesignTile = ({ design, isOpen, onOpen, onClose, onAction }) => (
  // Wrapper is a <div> so we can nest interactive elements (the overlay
  // contains buttons). The tile surface itself is a button via role/keyboard.
  <div className={`kr-tile${isOpen ? ' is-open' : ''}`}>
    <div
      className="kr-tile-surface"
      role="button"
      tabIndex={0}
      aria-haspopup="menu"
      aria-expanded={isOpen}
      onClick={onOpen}
      onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onOpen(); } }}
    >
      <TileSvg hue={design.hue} />
      <span className="kr-tile-id t-mono">{design.id}</span>
      {design.converted3D && <span className="kr-tile-3d" title="3D model ready">3D</span>}
      {design.starred && <span className="kr-tile-star" title="Starred">★</span>}
      {!isOpen && <span className="kr-tile-tag">{design.tag}</span>}
    </div>
    {isOpen && (
      <DesignActionsOverlay design={design} onClose={onClose} onAction={onAction} />
    )}
  </div>
);


/* ============================================================
   DesignActionsPopover — click on a tile opens this
   ============================================================ */

// Action overlay — renders INSIDE the tile (not below) so it can never be
// clipped by the scrolling thread. Gemini/ChatGPT image-hover pattern.
const DesignActionsOverlay = ({ design, onClose, onAction }) => (
  <div className="kr-tile-actions" role="menu" onClick={(e) => e.stopPropagation()}>
    <div className="kr-tile-actions-head">
      <span className="t-mono">{design.id}</span>
      <button className="kr-tile-actions-x" onClick={onClose} aria-label="Close">×</button>
    </div>
    <button className="kr-act-row is-primary" onClick={() => onAction('refine')}>
      <span>✏ Refine this design</span>
      <span className="kr-act-meta">single · keeps lineage</span>
    </button>
    <button className="kr-act-row is-primary" onClick={() => onAction('variation')}>
      <span>◇ Create variations</span>
      <span className="kr-act-meta">DesignVariationSet.v1</span>
    </button>
    <div className="kr-act-sep" />
    <button className="kr-act-row is-primary" onClick={() => onAction('3d')}>
      <span>⤓ Open 3D preview</span>
      <span className="kr-act-meta">local preview</span>
    </button>
    <button className="kr-act-row is-primary" onClick={() => onAction('rhino')}>
      <span>⤴ CAD/Rhino handoff</span>
      <span className="kr-act-meta">JewelleryCadSpec.v1</span>
    </button>
    <div className="kr-act-quick">
      <button className="kr-act-icon" onClick={() => onAction('star')} title={design.starred ? 'Unstar' : 'Star'}>
        {design.starred ? '★' : '☆'}
      </button>
      <button className="kr-act-icon" onClick={() => onAction('view')} title="View full">⤢</button>
      <button className="kr-act-icon" onClick={() => onAction('export')} title="Export PNG">⤓</button>
      <button className="kr-act-icon" onClick={() => onAction('evidence')} title="Evidence">i</button>
    </div>
  </div>
);


/* ============================================================
   DesignGrid — 4 tiles, one popover at a time
   ============================================================ */

const DesignGrid = ({ designs, onAction }) => {
  const [openId, setOpenId] = useState(null);
  const wrapRef = useRef(null);
  // The grid adapts to however many designs the turn returned (1–6): a lone
  // design centres and enlarges; 2–4 sit in a tidy row; 5–6 wrap. Driven by
  // data-count so media queries can still reflow on narrow surfaces.
  const n = Math.min(designs.length, 6);

  useEffect(() => {
    if (!openId) return;
    const onDoc = (e) => {
      if (!wrapRef.current?.contains(e.target)) setOpenId(null);
    };
    const onKey = (e) => { if (e.key === 'Escape') setOpenId(null); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
    };
  }, [openId]);

  return (
    <div className="kr-grid" data-count={n} ref={wrapRef}>
      {designs.map(d => (
        <div className="kr-grid-cell" key={d.id}>
          {(d.parentDesignId || d.version > 1) && (
            <span className="kr-tile-version" title={`Refinement of ${d.parentDesignId || 'prior'}`}>
              v{d.version} of <span className="t-mono">{d.parentDesignId || '—'}</span>
            </span>
          )}
          <DesignTile
            design={d}
            isOpen={openId === d.id}
            onOpen={() => setOpenId(openId === d.id ? null : d.id)}
            onClose={() => setOpenId(null)}
            onAction={(kind) => { onAction(kind, d); setOpenId(null); }}
          />
        </div>
      ))}
    </div>
  );
};


/* ============================================================
   UserTurn — right-aligned text bubble (plain text only)
   ============================================================ */

const UserTurn = ({ turn, designerInitials }) => (
  <div className="kr-turn kr-turn-user">
    <div className="kr-bubble">
      <div className="kr-bubble-text">{turn.text}</div>
      <div className="kr-bubble-meta">{turn.at}</div>
    </div>
    <Avatar initials={designerInitials} size={28} />
  </div>
);


/* ============================================================
   AssistantTurn — verified DecisionCard with DesignGrid body.
   Composes the same primitives as DecisionCard (stamp + status
   + run badge + title + confidence + approval surface) so the
   architecture-§1.5 contract is preserved.
   ============================================================ */

// Per-turn actions are PRIVATE signals inside the designer's workspace.
// External CEO approval only happens later via "Submit launch pack" on the
// session header — see architecture §8.5 KRAFT lineage.
const AssistantTurn = ({ turn, onDesignAction, onKeep, onRevise, onDiscard, onEvidence }) => (
  <div className="kr-turn kr-turn-assistant">
    <div className="kr-assistant-mark" aria-hidden="true">K</div>
    <article className="k-decision kr-decision" style={{ '--m-mod': 'var(--m-kraft)' }}>
      <div className="k-decision-head">
        <div className="k-decision-head-meta">
          <VerificationStamp>{turn.stamp}</VerificationStamp>
          <StatusBadge kind={turn.status}>{turn.statusLabel}</StatusBadge>
          <RunBadge id={turn.runId} version={turn.runVersion} when={turn.runWhen} />
          <Chip variant="mute">Private · workspace only</Chip>
        </div>
      </div>
      <h2 className="k-decision-title">{turn.title}</h2>

      <DesignGrid designs={turn.designs} onAction={onDesignAction} />

      <div className="k-decision-foot">
        <div className="k-decision-foot-left">
          <Btn size="sm" onClick={onEvidence}>Evidence ›</Btn>
          <ConfidenceMeter level={turn.confidence} label={turn.confidenceLabel} mod="kraft" />
        </div>
        <div className="k-decision-foot-right">
          <Btn size="sm" variant="reject" onClick={onDiscard}>✕ Discard turn</Btn>
          <Btn size="sm" onClick={onRevise}>↻ Revise</Btn>
          <Btn variant="primary" onClick={onKeep}>★ Keep batch</Btn>
        </div>
      </div>
    </article>
  </div>
);

const BriefList = ({ items }) => {
  const list = asBriefList(items);
  if (list.length === 0) return <div className="kr-brief-empty">—</div>;
  return (
    <ul className="kr-brief-list">
      {list.map((item, i) => <li key={i}>{item}</li>)}
    </ul>
  );
};

const BriefField = ({ label, children, wide }) => (
  <div className={`kr-brief-field${wide ? ' is-wide' : ''}`}>
    <div className="kr-brief-field-label">{label}</div>
    <div className="kr-brief-field-value">{children}</div>
  </div>
);

const AssistantBriefTurn = ({ turn }) => {
  const b = turn.brief || {};
  return (
    <div className="kr-turn kr-turn-assistant">
      <div className="kr-assistant-mark" aria-hidden="true">K</div>
      <article className="k-decision kr-decision kr-brief-card" style={{ '--m-mod': 'var(--m-kraft)' }}>
        <div className="k-decision-head">
          <div className="k-decision-head-meta">
            <VerificationStamp>{turn.stamp || 'Draft · local GPT'}</VerificationStamp>
            <StatusBadge kind="pending">{turn.statusLabel || 'Draft · unverified'}</StatusBadge>
            <RunBadge id={turn.runId} version={turn.runVersion || 'v1'} when={turn.runWhen || 'now'} />
            <Chip variant="warn">Needs receipts</Chip>
          </div>
        </div>
        <h2 className="k-decision-title">{turn.title || 'JewelleryDesignBrief.v1 draft'}</h2>

        <div className="kr-brief-schema-row">
          <Chip variant="accent">{briefText(b, 'schemaVersion', 'JewelleryDesignBrief.v1')}</Chip>
          <Chip variant="mute">{briefText(b, 'category', 'category pending')}</Chip>
          <Chip variant="mute">Local GPT draft</Chip>
        </div>

        <div className="kr-brief-grid">
          <BriefField label="Region profile" wide>{briefText(b, 'regionProfile')}</BriefField>
          <BriefField label="Neutral channel profile">{briefText(b, 'channelProfile')}</BriefField>
          <BriefField label="Customer persona">{briefText(b, 'customerPersona')}</BriefField>
          <BriefField label="Occasion / use case">{briefText(b, 'occasionUseCase')}</BriefField>
          <BriefField label="Style direction" wide>{briefText(b, 'styleDirection')}</BriefField>
          <BriefField label="Category design notes" wide><BriefList items={b.categoryDesignNotes} /></BriefField>
          <BriefField label="Material / stone suggestions" wide><BriefList items={b.materialStoneSuggestions} /></BriefField>
          <BriefField label="Target price / margin assumptions">{briefText(b, 'targetPriceMarginAssumptions')}</BriefField>
          <BriefField label="Market-fit rationale">{briefText(b, 'marketFitRationale')}</BriefField>
          <BriefField label="North America fit notes" wide>{briefText(b, 'northAmericaFitNotes')}</BriefField>
          <BriefField label="Variation axes" wide><BriefList items={b.variationAxes} /></BriefField>
          <BriefField label="CAD / Rhino notes" wide><BriefList items={b.cadRhinoNotes} /></BriefField>
          <BriefField label="Image prompt seed" wide><div className="kr-prompt-seed">{briefText(b, 'imagePromptSeed')}</div></BriefField>
          <BriefField label="Variation prompt seeds" wide><BriefList items={b.variationPromptSeeds} /></BriefField>
          <BriefField label="Missing client inputs"><BriefList items={b.missingClientInputs} /></BriefField>
          <BriefField label="Evidence still needed"><BriefList items={b.evidenceStillNeeded} /></BriefField>
          <BriefField label="Approval / verification warning" wide>{briefText(b, 'approvalVerificationWarning')}</BriefField>
        </div>

        {turn.limitations?.length > 0 && (
          <div className="kr-brief-warning">
            {turn.limitations.map((x, i) => <div key={i}>{x}</div>)}
          </div>
        )}
      </article>
    </div>
  );
};

const AssistantSetupErrorTurn = ({ turn }) => (
  <div className="kr-turn kr-turn-assistant">
    <div className="kr-assistant-mark" aria-hidden="true">K</div>
    <article className="k-decision kr-decision kr-setup-card" style={{ '--m-mod': 'var(--m-kraft)' }}>
      <div className="k-decision-head">
        <div className="k-decision-head-meta">
          <VerificationStamp>Safe setup error</VerificationStamp>
          <StatusBadge kind="failed">{turn.error?.code || 'local_gpt_error'}</StatusBadge>
          {turn.runId && <RunBadge id={turn.runId} version="setup" when="now" />}
        </div>
      </div>
      <h2 className="k-decision-title">Prompt Enhance needs local GPT setup</h2>
      <div className="kr-setup-body">
        <p>{turn.error?.message || 'Local GPT request failed.'}</p>
        <div className="kr-setup-code">OPENAI_API_KEY="&lt;your-api-key&gt;" node scripts/local-gpt-server.mjs</div>
        {turn.error?.endpoint && <div className="t-meta">Endpoint: <span className="t-mono">{turn.error.endpoint}</span></div>}
        <div className="t-meta">No model output was rendered and no business action was approved.</div>
      </div>
    </article>
  </div>
);


/* ============================================================
   SessionList — left sidebar of designer's chat sessions
   ============================================================ */

const SessionList = ({ sessions, activeId, onSelect, onNew }) => (
  <aside className="kr-sessions">
    <div className="kr-sessions-head">
      <span className="t-eyebrow">Sessions</span>
      <Btn size="sm" variant="primary" onClick={onNew}>+ New</Btn>
    </div>
    <div className="kr-sessions-list">
      {sessions.map(s => (
        <button
          key={s.id}
          className={`kr-session${s.id === activeId ? ' is-active' : ''}`}
          onClick={() => onSelect(s.id)}
        >
          <div className="kr-session-title">{s.title}</div>
          <div className="kr-session-meta">
            <span>{s.turns} turns</span>
            {s.starred > 0 && <span>★ {s.starred}</span>}
            <span style={{ marginLeft: 'auto', color: 'var(--mute)' }}>{s.updated}</span>
          </div>
        </button>
      ))}
    </div>
    <div className="kr-sessions-foot">
      <span className="t-meta" style={{ color: 'var(--mute)' }}>K1 · static fixtures · backend wiring lands in K5</span>
    </div>
  </aside>
);


/* ============================================================
   Composer — textarea + send, ⌘⏎ to submit
   ============================================================ */

const Composer = ({ value, onChange, onSend, sending, refineDesignId, onCancelRefine, count = 4, onCount, brief,
                    regionMode, onRegionMode, category, onCategory, onPromptEnhance, promptEnhancing }) => {
  const ref = useRef(null);
  const isRefining = !!refineDesignId;

  // Focus the textarea when a refine is dispatched.
  useEffect(() => { if (isRefining) ref.current?.focus(); }, [refineDesignId]);

  const onKey = (e) => {
    if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
      e.preventDefault();
      if (value.trim() && !sending) onSend();
    }
    if (e.key === 'Escape' && isRefining) {
      e.preventDefault();
      onCancelRefine?.();
    }
  };

  // Constraints chip-bar when this session is grounded in a brief (Studio OS).
  const bc = brief?.constraints || null;
  const cbits = bc ? [bc.pieceType, bc.metal && `${bc.metal}${bc.karat ? ' ' + bc.karat : ''}`, bc.stone, bc.motif,
    bc.occasion, bc.weightCeilingG && `≤ ${bc.weightCeilingG}g`,
    bc.budget && `₹${(bc.budget / 1000).toFixed(0)}k`].filter(Boolean) : [];

  return (
    <div className={`kr-composer${isRefining ? ' is-refining' : ''}`}>
      {brief && !isRefining && (
        <div className="kr-briefbar" role="note">
          <span className="kr-briefbar-tag">Brief</span>
          <span className="kr-briefbar-title">{brief.title}</span>
          <span className="kr-briefbar-chips">{cbits.map((t, i) => <span key={i} className="kr-briefbar-chip">{t}</span>)}</span>
        </div>
      )}
      {isRefining && (
        <div className="kr-refine-chip" role="status">
          <span className="kr-refine-chip-arrow" aria-hidden="true">↳</span>
          <span>Refining <span className="t-mono">{refineDesignId}</span></span>
          <button className="kr-refine-chip-x" onClick={onCancelRefine} aria-label="Cancel refine">×</button>
        </div>
      )}
      {!isRefining && (
        <div className="kr-enhancebar">
          <div className="kr-enhance-region">
            <span className="kr-enhance-label">V1 market</span>
            <span className="kr-region-fixed">{KRAFT_REGION_LABEL[regionMode] || KRAFT_REGION_LABEL['usa-north-america']}</span>
          </div>
          <label className="kr-enhance-select">
            <span className="kr-enhance-label">Category</span>
            <select value={category} onChange={(e) => onCategory?.(e.target.value)}>
              {KRAFT_CATEGORIES.map(c => <option key={c.id} value={c.id}>{c.label}</option>)}
            </select>
          </label>
          <Btn
            size="sm"
            variant="accent"
            onClick={onPromptEnhance}
            loading={promptEnhancing}
            disabled={!value.trim() || sending || promptEnhancing}
          >
            Prompt Enhance
          </Btn>
        </div>
      )}
      <textarea
        ref={ref}
        className="kr-composer-input"
        rows={3}
        placeholder={
          isRefining
            ? "Describe what to change on this design — e.g. \"smaller peacock, switch to white gold\"."
            : `Describe the piece you want — KOVA proposes ${count} ${count === 1 ? 'direction' : 'directions'} you can approve, refine, or branch.`
        }
        value={value}
        onChange={(e) => onChange(e.target.value)}
        onKeyDown={onKey}
        disabled={sending || promptEnhancing}
      />
      <div className="kr-composer-foot">
        <div className="kr-composer-foot-left">
          {!isRefining && (
            <div className="kr-count" role="group" aria-label="Number of directions">
              <span className="kr-count-label">Directions</span>
              <Seg
                value={count}
                onChange={onCount}
                items={[{ id: 1, label: '1' }, { id: 2, label: '2' }, { id: 3, label: '3' }, { id: 4, label: '4' }, { id: 6, label: '6' }]}
              />
            </div>
          )}
          <span className="kr-composer-hint">
            {isRefining
              ? <>Single refinement · lineage preserved (parent → v2 → v3). <Kbd>esc</Kbd> to cancel · <Kbd>⌘⏎</Kbd> to send.</>
              : <>KOVA returns {count} direction{count === 1 ? '' : 's'} · neutral channel profile decided server-side. <Kbd>⌘⏎</Kbd> to send.</>}
          </span>
        </div>
        <Btn variant="primary" onClick={onSend} disabled={!value.trim() || sending || promptEnhancing}>
          {sending ? 'Sending…' : (isRefining ? 'Refine' : 'Send')}
        </Btn>
      </div>
    </div>
  );
};


/* ============================================================
   ChatThread — scrollable list of UserTurn / AssistantTurn
   ============================================================ */

const ChatThread = ({ turns, designerInitials, pending, onDesignAction,
                     onKeep, onRevise, onDiscard, onEvidence }) => {
  const scrollRef = useRef(null);
  useEffect(() => {
    const el = scrollRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, [turns.length, pending]);

  return (
    <div className="kr-thread-scroll" ref={scrollRef}>
      {turns.length === 0 && (
        <EmptyState
          mark="K"
          title="New session"
          body="Type a brief in the composer below. Every turn returns a verified design packet you can approve, refine, or branch."
        />
      )}
      {turns.map(t => {
        if (t.role === 'user') return <UserTurn key={t.id} turn={t} designerInitials={designerInitials} />;
        if (t.kind === 'designBrief') return <AssistantBriefTurn key={t.id} turn={t} />;
        if (t.kind === 'setupError') return <AssistantSetupErrorTurn key={t.id} turn={t} />;
        return <AssistantTurn key={t.id} turn={t}
                              onDesignAction={onDesignAction}
                              onKeep={() => onKeep(t)}
                              onRevise={() => onRevise(t)}
                              onDiscard={() => onDiscard(t)}
                              onEvidence={() => onEvidence(t)} />;
      })}
      {pending && (
        <UserTurn turn={{
          text: pending.text + (pending.isRefine ? '  ↳ refining ' + pending.parentId : ''),
          at: 'now', id: 'pending'
        }} designerInitials={designerInitials} />
      )}
      {pending && (
        <div className="kr-turn kr-turn-assistant">
          <div className="kr-assistant-mark" aria-hidden="true">K</div>
          {pending.kind === 'promptEnhance' ? (
            <div className="kr-decision-skeleton kr-brief-skeleton">
              <div className="kr-sk-line" style={{ width: '42%' }} />
              <div className="kr-sk-line" style={{ width: '88%' }} />
              <div className="kr-sk-line" style={{ width: '76%' }} />
              <div className="kr-sk-line" style={{ width: '64%' }} />
              <div className="kr-sk-line" style={{ width: '36%' }} />
            </div>
          ) : (
            <div className="kr-decision-skeleton">
              <div className="kr-sk-line" style={{ width: pending.isRefine ? '46%' : '38%' }} />
              <div className="kr-sk-grid" data-count={Math.min(pending.count || (pending.isRefine ? 1 : 4), 6)}>
                {Array.from({ length: pending.count || (pending.isRefine ? 1 : 4) }, (_, i) => <div key={i} className="kr-sk-tile" />)}
              </div>
              <div className="kr-sk-line" style={{ width: '22%' }} />
            </div>
          )}
        </div>
      )}
    </div>
  );
};


/* ============================================================
   KraftStudioPage — the page
   ============================================================ */

const KraftStudioPage = ({ pushToast, openCmdK, openDrawer, pushDecision,
                            designerInitials = 'VS', designerName = 'Designer',
                            briefs = [], onUpdateBrief }) => {
  const [sessions, setSessions] = useState(SEED_KRAFT_SESSIONS);
  const [activeSessionId, setActiveSessionId] = useState(SEED_KRAFT_SESSIONS[0].id);
  const [allTurns, setAllTurns] = useState(SEED_KRAFT_TURNS);
  const [draft, setDraft] = useState('');
  const [pending, setPending] = useState(null);    // pending user text while assistant "thinks"
  const [refineDesignId, setRefineDesignId] = useState(null);  // single-tile refinement target
  const [submittingPack, setSubmittingPack] = useState(false); // guard against double-submit
  const [viewMode, setViewMode] = useState('chat');            // 'chat' | 'gallery' (additive, non-breaking)
  const [count, setCount] = useState(4);                        // how many directions a batch turn returns (user-controlled)
  const [viewer, setViewer] = useState(null);                   // 3D viewer modal: { design, ready } | null
  const [regionMode, setRegionMode] = useState('usa-north-america');
  const [category, setCategory] = useState('rings');
  const [promptEnhancing, setPromptEnhancing] = useState(false);
  const [variationPanel, setVariationPanel] = useState(null);   // { design, seed } | null
  const [variationText, setVariationText] = useState('');
  const [variationSets, setVariationSets] = useState([]);
  const [cadHandoff, setCadHandoff] = useState(null);           // { design, spec } | null
  const [designPreview, setDesignPreview] = useState(null);
  const [exportPanel, setExportPanel] = useState(null);
  const [sharePanel, setSharePanel] = useState(false);
  const turns = allTurns[activeSessionId] || [];
  const activeSession = useMemo(
    () => sessions.find(s => s.id === activeSessionId),
    [sessions, activeSessionId]
  );

  // Look up the design being refined (so we can copy hue + bump version + chain parentDesignId)
  const findDesign = (designId) => {
    for (const t of turns) {
      if (t.role !== 'assistant') continue;
      const hit = t.designs?.find(d => d.id === designId);
      if (hit) return hit;
    }
    return null;
  };

  const sendTurn = () => {
    const text = draft.trim();
    if (!text) return;
    const refineTarget = refineDesignId ? findDesign(refineDesignId) : null;
    const turnCount = refineTarget ? 1 : count;   // refine is always a single design
    setDraft('');
    setPending({ text, isRefine: !!refineTarget, parentId: refineDesignId, count: turnCount });
    setRefineDesignId(null);

    pushToast?.({ kind: 'info', title: refineTarget ? 'Refinement queued' : 'Turn queued',
      body: 'POST /api/kraft/sessions/' + activeSessionId + '/messages '
            + (refineTarget ? '{ refineDesignId: ' + refineTarget.id + ' }' : '(batch)') });

    // Simulate the assistant turn arriving via SSE (K1 fake).
    setTimeout(() => {
      const newId = 't-' + Math.random().toString(36).slice(2, 7);
      const runId = 'rcp-' + Math.random().toString(36).slice(2, 6);
      const userTurn = { role: 'user', id: newId + 'u', at: 'now', text,
                          refineOf: refineTarget?.id || null };

      let assistantTurn;
      if (refineTarget) {
        // Single-design refinement: one tile, parent chain, v+1.
        const nextVersion = (refineTarget.version || 1) + 1;
        const targetProfile = designCategoryProfile(refineTarget);
        assistantTurn = {
          role: 'assistant', id: newId + 'a', at: 'now',
          runId, runVersion: 'v' + nextVersion, runWhen: 'now',
          status: 'verified', statusLabel: 'Verified · ready',
          stamp: 'Verified · kraft_design_agent · refining ' + refineTarget.id,
          title: 'Refined · ' + (refineTarget.tag || refineTarget.id),
          confidence: 3, confidenceLabel: 'High',
          designs: [withDesignLineage({
            id: refineTarget.id + '-v' + nextVersion,
            parentDesignId: refineTarget.id,
            version: nextVersion,
            tag: (refineTarget.tag || 'Refinement') + ' · ' + (text.slice(0, 28) + (text.length > 28 ? '…' : '')),
            hue: refineTarget.hue,
            starred: false,
          }, targetProfile.id, refineTarget)],
        };
      } else {
        // Batch turn: `turnCount` fresh directions (user-chosen, 1–6).
        const LETTERS = ['A','B','C','D','E','F'];
        const profile = categoryProfile(category);
        assistantTurn = {
          role: 'assistant', id: newId + 'a', at: 'now',
          runId, runVersion: 'v1', runWhen: 'now',
          status: 'verified', statusLabel: 'Verified · ready',
          stamp: 'Verified · kraft_design_agent',
          title: turnCount + ' ' + profile.label.toLowerCase() + ' direction' + (turnCount === 1 ? '' : 's') + ' · branched from your brief',
          confidence: 3, confidenceLabel: 'High',
          designs: Array.from({ length: turnCount }, (_, i) => ({
            id: 'd-' + newId + '-' + (i + 1),
            version: 1,
            tag: profile.label + ' direction ' + (LETTERS[i] || (i + 1)),
            hue: 20 + i * 22, starred: false,
          })).map(d => withDesignLineage(d, profile.id)),
        };
      }
      setAllTurns(prev => ({
        ...prev,
        [activeSessionId]: [...(prev[activeSessionId] || []), userTurn, assistantTurn],
      }));
      setPending(null);
    }, 1400);
  };

  // Triggered from the per-tile overlay's "Refine this design" action.
  const beginRefine = (design) => {
    setRefineDesignId(design.id);
    pushToast?.({ kind: 'info', title: 'Refining ' + design.id,
      body: 'Composer focused · type what to change · ⌘⏎ to send' });
  };

  const updateDesigns = (ids, updater) => {
    const set = new Set(Array.isArray(ids) ? ids : [ids]);
    setAllTurns(prev => {
      const next = {};
      for (const sid of Object.keys(prev)) {
        next[sid] = prev[sid].map(t => (t.role === 'assistant' && t.designs)
          ? { ...t, designs: t.designs.map(d => set.has(d.id) ? updater(d) : d) }
          : t);
      }
      return next;
    });
  };

  const patchAssistantTurn = (turnId, patch) => {
    setAllTurns(prev => ({
      ...prev,
      [activeSessionId]: (prev[activeSessionId] || []).map(t => t.id === turnId ? { ...t, ...patch } : t),
    }));
  };

  // Mark designs as 3D-previewed in the shared `allTurns` so both the chat tile
  // and the gallery (which derives is3D) reflect it. ids: string | string[].
  const markConverted3D = (ids) => updateDesigns(ids, d => ({ ...d, converted3D: true }));

  // 3D preview: opens the local faux-3D viewer without claiming CAD automation.
  // If already previewed, just opens the viewer. Used by the chat tile overlay,
  // the gallery card, and the gallery inspect drawer.
  const convert3D = (design) => {
    const already = !!(design.converted3D || design.is3D);
    if (already) {
      setViewer({ design, ready: true });
      pushToast?.({ kind: 'info', title: 'Opening 3D preview · ' + design.id,
        body: 'Local preview only · CAD/Rhino handoff still needs typed measurements.' });
      return;
    }
    setViewer({ design, ready: false });   // show the viewer in a "preparing" state immediately
    const runId = 'rcp-' + Math.random().toString(36).slice(2, 6);
    pushToast?.({ kind: 'info', title: 'Preparing 3D preview…',
      body: runId + ' · static preview; not a production model or CAD file.' });
    setTimeout(() => {
      markConverted3D(design.id);
      setViewer(v => (v && v.design.id === design.id) ? { design: { ...design, converted3D: true }, ready: true } : v);
      pushToast?.({ kind: 'success', title: '3D preview ready · ' + design.id,
        body: runId + ' · drag to inspect · CAD/Rhino handoff remains a draft packet.' });
    }, 1300);
  };

  const enhancePrompt = async () => {
    const text = draft.trim();
    if (!text || promptEnhancing) return;
    const sid = activeSessionId;
    const runId = 'lgpt-' + Math.random().toString(36).slice(2, 6);
    const regionLabel = KRAFT_REGION_LABEL[regionMode] || regionMode;
    const profile = categoryProfile(category);
    setPromptEnhancing(true);
    setPending({ text: `Prompt Enhance (${regionLabel}, ${profile.label}): ${text}`, kind: 'promptEnhance', count: 1 });
    pushToast?.({ kind: 'info', title: 'Prompt Enhance queued',
      body: 'Local GPT draft only · OPENAI_API_KEY stays in the bridge environment.' });

    const userTurn = {
      role: 'user',
      id: runId + '-u',
      at: 'now',
      text: `Prompt Enhance (${regionLabel}, ${profile.label}): ${text}`,
    };

    try {
      if (!window.K.requestLocalKraftPromptEnhanceDraft) {
        const err = new Error('KRAFT prompt-enhance helper is unavailable. Reload the local app after data.jsx loads.');
        err.code = 'prompt_enhance_helper_missing';
        throw err;
      }
      const payload = await window.K.requestLocalKraftPromptEnhanceDraft({
        roughPrompt: text,
        regionMode,
        category,
        categoryLabel: profile.label,
        channelProfile: profile.channelProfile,
        categoryDesignNotes: profile.designNotes,
        variationAxes: profile.variationAxes,
        cadRhinoNotes: profile.cadNotes,
        sessionId: sid,
        briefTitle: activeBrief?.title || '',
        constraints: activeBrief?.constraints || null,
        metal: activeBrief?.constraints?.metal || '',
        stone: activeBrief?.constraints?.stone || '',
      });
      const result = payload.result || {};
      const assistantTurn = {
        role: 'assistant',
        kind: 'designBrief',
        id: runId + '-a',
        at: 'now',
        runId,
        runVersion: 'v1',
        runWhen: 'now',
        status: 'pending',
        statusLabel: 'Draft · unverified',
        stamp: 'Draft · local GPT · JewelleryDesignBrief.v1',
        title: `${regionLabel} · ${result.category || profile.label} prompt-enhanced brief`,
        brief: result,
        limitations: payload.limitations || [
          'Model output only; receipts are not attached.',
          'Do not treat this draft as an approved design or CAD instruction.',
        ],
      };
      setAllTurns(prev => ({ ...prev, [sid]: [...(prev[sid] || []), userTurn, assistantTurn] }));
      setDraft('');
      pushToast?.({ kind: 'success', title: 'Prompt Enhance draft ready',
        body: 'JewelleryDesignBrief.v1 · draft/unverified · review missing inputs and evidence.' });
    } catch (e) {
      const endpoint = e.endpoint || window.K.localGptKraftPromptEnhanceEndpoint?.();
      const assistantTurn = {
        role: 'assistant',
        kind: 'setupError',
        id: runId + '-err',
        at: 'now',
        runId,
        error: {
          code: e.code || 'local_gpt_request_failed',
          message: e.message || 'Local GPT request failed.',
          endpoint,
        },
      };
      setAllTurns(prev => ({ ...prev, [sid]: [...(prev[sid] || []), userTurn, assistantTurn] }));
      pushToast?.({ kind: 'warn',
        title: assistantTurn.error.code === 'missing_openai_api_key' ? 'OpenAI API key required' : 'Prompt Enhance blocked',
        body: assistantTurn.error.message });
    } finally {
      setPending(null);
      setPromptEnhancing(false);
    }
  };

  const openVariationPanel = (design) => {
    const profile = designCategoryProfile(design);
    const lineage = designLineage(profile, design);
    setVariationText(`Create 3 saleable ${lineage.categoryLabel} variations for ${design.tag || design.id}: ${lineage.variationAxes.join('; ')}. Keep parent lineage and ${lineage.regionLabel} fit.`);
    setVariationPanel({ design, seed: null });
  };

  const storeVariationSeed = () => {
    if (!variationPanel?.design) return;
    const design = variationPanel.design;
    const profile = designCategoryProfile(design);
    const lineage = designLineage(profile, design);
    const note = variationText.trim() || `Create saleable ${lineage.categoryLabel} variations while preserving parent lineage.`;
    const axes = lineage.variationAxes.slice(0, 3);
    const seed = {
      schemaVersion: 'DesignVariationSet.v1',
      id: 'var-' + Math.random().toString(36).slice(2, 7),
      parentDesignId: design.id,
      parentLineage: {
        sourceDesignId: design.id,
        rootDesignId: design.parentDesignId || design.id,
        parentVersion: design.version || 1,
      },
      regionMode: lineage.regionMode,
      regionLabel: lineage.regionLabel,
      category: lineage.category,
      categoryLabel: lineage.categoryLabel,
      channelProfile: lineage.channelProfile,
      variationAxes: axes,
      promptSeed: shortPromptSeed(design, note),
      nextInputNeeded: `Confirm ${lineage.missingDimensions.join(' ').toLowerCase()} Also confirm target price band and whether variants should be generated as images in a later lane.`,
      seeds: axes.map((axis, i) => `${note} Variant ${String.fromCharCode(65 + i)}: vary ${axis} while preserving source design ${design.id}.`),
      createdAt: 'just now',
      status: 'stored_prompt_seed_only',
    };
    setVariationSets(prev => [seed, ...prev]);
    updateDesigns(design.id, d => ({ ...d, variationSeedCount: (d.variationSeedCount || 0) + 1, latestVariationSeedId: seed.id }));
    setVariationPanel({ design: { ...design, variationSeedCount: (design.variationSeedCount || 0) + 1 }, seed });
    pushToast?.({ kind: 'success', title: 'Variation seed stored',
      body: seed.id + ' · DesignVariationSet.v1 · image generation not started.' });
  };

  const buildCadHandoff = (design) => {
    const profile = designCategoryProfile(design);
    const lineage = designLineage(profile, design);
    return {
      schemaVersion: 'JewelleryCadSpec.v1',
      status: 'draft_assumption_aware_handoff',
      designId: design.id,
      designLabel: design.tag || design.id,
      parentLineage: {
        sourceDesignId: design.id,
        rootDesignId: design.parentDesignId || design.id,
        parentVersion: design.version || 1,
      },
      regionMode: lineage.regionMode,
      regionLabel: lineage.regionLabel,
      category: lineage.category,
      categoryLabel: lineage.categoryLabel,
      channelProfile: lineage.channelProfile,
      structuredNotes: [
        'Draft/assumption-aware CAD intake preview only; not automated Rhino execution.',
        'Preserve parent design lineage, North America V1 assumptions, and selected category profile.',
        'Convert to a typed JewelleryCadSpec.v1 with client measurements before any Rhino scaffold is generated.',
      ],
      categoryCadNotes: lineage.cadNotes,
      materialAssumptions: [
        'North America V1 draft: assume 14k/18k gold, white gold, platinum, or lab-grown diamonds only after client confirmation.',
        'Stone sizes, metal gauge, setting tolerances, finish, target weight, price band, and margin are unverified.',
      ],
      missingDimensions: [
        ...lineage.missingDimensions,
        'Client-supplied reference images, approved sketch angles, or provenance notes.',
      ],
      rhinoNextStep: 'Later Lumina/Rhino CLI lane needs JewelleryCadSpec.v1 + client measurements before scaffold generation.',
    };
  };

  const openCadHandoff = (design) => {
    setCadHandoff({ design, spec: buildCadHandoff(design) });
    updateDesigns(design.id, d => ({ ...d, cadHandoffPreviewed: true }));
  };

  const onDesignAction = (kind, design) => {
    if (kind === 'refine') { beginRefine(design); return; }
    if (kind === 'variation') { openVariationPanel(design); return; }
    if (kind === '3d')     { convert3D(design); return; }
    if (kind === 'rhino')  { openCadHandoff(design); return; }
    if (kind === 'view')   { setDesignPreview(design); return; }
    if (kind === 'export') { setExportPanel({ design }); return; }
    if (kind === 'star')   {
      // Actually persist the star (not just a toast) so the launch pack — which
      // reads `starred` — picks up designs starred from the chat tile, exactly
      // like the gallery does.
      toggleStar(design.id);
      pushToast?.({ kind: 'success', title: (design.starred ? 'Unstarred · ' : '★ Starred · ') + design.id,
                    body: 'POST /api/kraft/designs/' + design.id + '/star { starred: ' + (!design.starred) + ' }' });
      return;
    }
    if (kind === 'evidence') {
      openDrawer?.();
      pushToast?.({ kind: 'info', title: 'Evidence drawer opened',
        body: 'Design-specific receipts are not attached in this static KRAFT lane.' });
      return;
    }
    const messages = {
      evidence: { kind: 'info', title: 'Evidence',
                  body: 'Drawer opens with claims · receipts · math · for this design' },
    };
    if (messages[kind]) pushToast?.(messages[kind]);
  };

  // Private signals — never leave the designer's workspace.
  // External CEO approval happens later via submitLaunchPack below.
  const onKeep = (turn) => {
    patchAssistantTurn(turn.id, { kept: true, discarded: false });
    pushToast?.({ kind: 'success', title: '★ Kept · ' + turn.runId,
      body: 'Stored as a private workspace flag for this local session.' });
  };
  const onRevise = (turn) => {
    setDraft(`Revise ${turn.runId}: ${turn.title || 'this KRAFT turn'} — `);
    pushToast?.({ kind: 'info', title: 'Revise turn',
      body: 'Composer seeded with the selected turn context.' });
  };
  const onDiscard = (turn) => {
    patchAssistantTurn(turn.id, { discarded: true, kept: false });
    pushToast?.({ kind: 'warn', title: 'Discarded · ' + turn.runId,
      body: 'Stored as a private workspace flag for this local session.' });
  };
  const onEvidence = () => openDrawer?.();

  // The only externally-visible action on this page.
  // Creates the CEO-facing Decision card via the existing approvals chain
  // (architecture §8.5 KRAFT lineage + data.jsx kraft fixture at line 145).
  const submitLaunchPack = () => {
    if (submittingPack) return;   // double-click guard (audit A1)
    const starred = turns
      .filter(t => t.role === 'assistant')
      .flatMap(t => (t.designs || []).filter(d => d.starred));
    if (starred.length === 0) {
      pushToast?.({ kind: 'warn', title: 'Nothing starred yet',
        body: 'Star at least one design in this session before submitting a launch pack.' });
      return;
    }
    setSubmittingPack(true);
    // Build a CEO-facing pending Decision and push it onto the global queue
    // so it lands in /approvals (architecture §8.5 launch-pack stage).
    const runId = 'rcp-lp-' + Math.random().toString(36).slice(2, 6);
    const newDecision = {
      id: runId,
      mod: 'kraft',
      priority: 'P2',
      impact: 'launch',
      stamp: 'Verified · Launch pack from ' + (activeSession?.title || 'session'),
      status: 'pending',
      statusLabel: 'Awaiting CEO sign',
      runId, runVersion: 'v1', runWhen: 'just now',
      title: 'Sign off launch pack · ' + (activeSession?.title || activeSessionId),
      reasoning: 'Designer submitted ' + starred.length + ' starred design'
                 + (starred.length === 1 ? '' : 's') + ' from session '
                 + activeSessionId + ' for launch. Awaiting CEO sign-off.',
      stats: [
        { label: 'Designs',  value: String(starred.length) },
        { label: 'Session',  value: activeSessionId.replace(/^s-/, '') },
        { label: 'Designer', value: 'Vikram' },
        { label: 'Source',   value: 'KRAFT chat' },
      ],
      confidence: 3, confidenceLabel: 'High',
      cosign: designerName + ' · Designer',
      chain: [
        { who: designerName + ' · Senior designer', meta: 'Submitted launch pack · just now', state: 'done' },
        { who: 'Auto · constraint check',           meta: 'Margin · vendor · time',           state: 'done' },
        { who: 'You · CEO',                         meta: 'Awaiting sign-off',                state: 'now' },
        { who: 'Manufacturing · queue',             meta: 'Vendor packet on approval',        state: 'pending' },
      ],
      workflow: [
        { label: 'Queued',         meta: '', time: 'just now', state: 'done' },
        { label: 'Context built',  meta: '', time: 'just now', state: 'done' },
        { label: 'Agent ran',      meta: '', time: 'just now', state: 'done' },
        { label: 'Verified',       meta: '', time: 'just now', state: 'done' },
        { label: 'Approved',       meta: '', time: '—',        state: 'pending' },
        { label: 'Executed',       meta: '', time: '—',        state: 'pending' },
        { label: 'Outcome tracked',meta: '', time: '—',        state: 'pending' },
      ],
      claims: [
        { text: 'Designer starred ' + starred.length + ' design'
                + (starred.length === 1 ? '' : 's') + ' as the final selection.',
          source: 'kraft.session', receipt: runId + ' / claim 1' },
        { text: 'All starred designs passed the manufacturability verifier.',
          source: 'kraft.mfg_check', receipt: runId + ' / claim 2' },
        { text: 'Lineage preserved · all parentDesignId chains intact.',
          source: 'kraft.lineage',  receipt: runId + ' / claim 3' },
      ],
      receipts: [
        { tool: 'kraft.session',    scope: 'workspace.read',  id: 'tool-receipt-' + runId + '-1' },
        { tool: 'kraft.mfg_check',  scope: 'deterministic',   id: 'tool-receipt-' + runId + '-2' },
        { tool: 'kraft.lineage',    scope: 'design.read',     id: 'tool-receipt-' + runId + '-3' },
      ],
      math: [
        { expr: 'starred_design_count', value: String(starred.length) },
        { expr: 'session_turn_count',   value: String(turns.length) },
      ],
      // The KRAFT-specific payload journey.jsx renders on the detail page.
      designerName: 'Vikram',
      sessionId: activeSessionId,
      designs: starred,
      comments: [
        { id: 'c-init', who: 'Vikram · Designer', at: 'just now',
          text: 'Submitted ' + starred.length + ' starred design'
                + (starred.length === 1 ? '' : 's') + ' from session "'
                + (activeSession?.title || activeSessionId) + '". Ready for your review.' },
      ],
    };
    pushDecision?.(newDecision);
    // Studio OS: if this session was started from a brief, link the launch pack
    // back to it and move the brief to "submitted" (closes the outbound half).
    const linkedBrief = briefs.find(b => b.sessionId === activeSessionId);
    if (linkedBrief && onUpdateBrief) {
      onUpdateBrief(linkedBrief.id, (cur) => ({
        status: 'submitted', launchPackId: runId,
        thread: [...(cur.thread || []), { who: designerName, role: 'designer', at: 'just now',
          text: `Submitted ${starred.length} starred design${starred.length === 1 ? '' : 's'} for CEO sign-off.` }],
        history: [...(cur.history || []), { state: 'submitted', at: 'just now' }],
      }));
    }
    pushToast?.({ kind: 'success', title: 'Launch pack submitted',
      body: 'Created ' + runId + ' in /approvals · CEO will see ' + starred.length + ' design'
            + (starred.length === 1 ? '' : 's') + ' for sign-off.' });
    // Re-arm the button after the toast settles so the user sees the result
    // but can't accidentally double-submit.
    setTimeout(() => setSubmittingPack(false), 1000);
  };

  /* ---------- Gallery integration (additive) ----------
     The gallery reads the same `allTurns`, so chat-generated designs appear
     there instantly. Starring mutates the shared state so the launch-pack
     (which reads `starred`) stays consistent across both views. */
  const setStar = (designId, value) => setAllTurns(prev => {
    const next = {};
    for (const sid of Object.keys(prev)) {
      next[sid] = prev[sid].map(t => (t.role === 'assistant' && t.designs)
        ? { ...t, designs: t.designs.map(d => d.id === designId ? { ...d, starred: value == null ? !d.starred : value } : d) }
        : t);
    }
    return next;
  });
  const toggleStar = (designId) => setStar(designId, null);
  const addToLaunch = (ids) => ids.forEach(id => setStar(id, true));
  // From the gallery, "Refine" must drop the designer back into Chat focused on the tile.
  const galleryDesignAction = (kind, design) => {
    if (kind === 'refine') { setViewMode('chat'); beginRefine(design); return; }
    onDesignAction(kind, design);
  };

  /* ---------- Brief (Studio OS) integration ----------
     Accepting a brief seeds a chat session grounded in the ask and links
     sessionId↔briefId; submitting a launch pack from a brief-linked session
     marks the brief submitted. The brief active for the current session drives
     the composer's constraints bar. */
  const briefSeedText = (b) => {
    const c = b.constraints || {};
    const bits = [c.pieceType, c.metal && `${c.metal}${c.karat ? ' ' + c.karat : ''}`, c.stone, c.motif && `${c.motif} motif`,
      c.occasion, c.weightCeilingG && `under ${c.weightCeilingG}g`, c.budget && `budget ~₹${(c.budget / 1000).toFixed(0)}k`].filter(Boolean);
    return `${b.intent}${bits.length ? `\n\nConstraints: ${bits.join(' · ')}.` : ''}`;
  };
  const acceptBrief = (b) => {
    const sid = b.sessionId || ('s-' + b.id);
    if (!allTurns[sid]) setAllTurns(prev => ({ ...prev, [sid]: [] }));
    if (!sessions.some(s => s.id === sid)) {
      setSessions(prev => [{ id: sid, title: b.title, updated: 'now', turns: 0, starred: 0, briefId: b.id }, ...prev]);
    }
    setActiveSessionId(sid);
    setDraft(briefSeedText(b));
    setViewMode('chat');
    onUpdateBrief?.(b.id, (cur) => ({
      status: cur.status === 'new' ? 'accepted' : cur.status,
      sessionId: sid,
      thread: [...(cur.thread || []), { who: designerName, role: 'designer', at: 'just now', text: 'Accepted — starting a grounded session.' }],
      history: [...(cur.history || []), { state: 'accepted', at: 'just now' }],
    }));
    pushToast?.({ kind: 'success', title: 'Brief accepted', body: `${b.title} · session seeded · ⌘⏎ to generate` });
  };
  const openBriefSession = (b) => {
    if (b.sessionId && allTurns[b.sessionId]) { setActiveSessionId(b.sessionId); setViewMode('chat'); }
    else acceptBrief(b);
  };
  const commentOnBrief = (briefId, text) => {
    if (!text.trim()) return;
    onUpdateBrief?.(briefId, (cur) => ({
      thread: [...(cur.thread || []), { who: designerName, role: 'designer', at: 'just now', text: text.trim() }],
    }));
    pushToast?.({ kind: 'info', title: 'Sent', body: 'POST /api/kraft/briefs/' + briefId + '/comments' });
  };
  const openProvenance = (b) => {
    const mod = b.source?.module === 'admin' ? 'home' : (b.source?.module || 'orbit');
    pushToast?.({ kind: 'info', title: 'Open origin', body: `${b.origin.refId} → ${mod.toUpperCase()} (read-only)` });
    location.hash = mod;
  };
  // The brief grounding the active chat session (drives composer constraints bar).
  const activeBrief = briefs.find(b => b.sessionId === activeSessionId) || null;
  const newBriefCount = briefs.filter(b => b.status === 'new').length;

  const createNewSession = () => {
    const sid = 's-local-' + Math.random().toString(36).slice(2, 7);
    const profile = categoryProfile(category);
    const title = `${KRAFT_REGION_LABEL[regionMode] || 'KRAFT'} · ${profile.label} concept`;
    setSessions(prev => [{ id: sid, title, updated: 'now', turns: 0, starred: 0 }, ...prev]);
    setAllTurns(prev => ({ ...prev, [sid]: [] }));
    setActiveSessionId(sid);
    setDraft('');
    setRefineDesignId(null);
    setViewMode('chat');
    pushToast?.({ kind: 'success', title: 'New KRAFT session', body: `${title} · local workspace session created.` });
  };

  const openSharePanel = () => {
    setSharePanel(true);
    pushToast?.({ kind: 'info', title: 'Share preview opened',
      body: 'Static lane only: review the share packet before any external workflow exists.' });
  };

  const KraftGalleryView = window.K.KraftGalleryView;
  const KraftBriefsView = window.K.KraftBriefsView;
  const ThreeDStage = window.K.ThreeDStage;

  return (
    <div className="k-page-enter kr-page">
      <PageHead
        crumb={['KRAFT', activeSession?.title || '—']}
        title="Studio"
        subtitle={viewMode === 'gallery'
          ? 'Gallery · every design across the workspace, split into 3D models and filterable 2D concepts.'
          : viewMode === 'work'
            ? 'Work · briefs from merchandising, inventory & the CEO — triage, design, submit, and track feedback.'
            : 'Chat-shaped designer surface · every assistant turn is a verified design packet (architecture §1.5).'}
        actions={
          <>
            <Seg value={viewMode} onChange={setViewMode} items={[
              { id: 'chat', label: 'Chat' },
              { id: 'gallery', label: 'Gallery' },
              { id: 'work', label: newBriefCount > 0 ? `Work · ${newBriefCount}` : 'Work' },
            ]} />
            <Btn size="sm" variant="ghost" onClick={openSharePanel}>↗ Share</Btn>
            <Btn size="sm" variant="primary" onClick={submitLaunchPack} disabled={submittingPack}>
              {submittingPack ? 'Submitting…' : '→ Submit launch pack'}
            </Btn>
            <Btn size="sm" variant="ghost" onClick={openCmdK} kbd="⌘K">Jump to…</Btn>
          </>
        }
      />

      {viewMode === 'work' ? (
        <div className="krw-host">
          {KraftBriefsView
            ? <KraftBriefsView
                briefs={briefs}
                onAccept={acceptBrief}
                onUpdateBrief={onUpdateBrief}
                onComment={commentOnBrief}
                onOpenSession={openBriefSession}
                onProvenance={openProvenance}
                pushToast={pushToast}
              />
            : <EmptyState mark="K" title="Work view unavailable" body="kraft-briefs.jsx failed to load." />}
        </div>
      ) : viewMode === 'gallery' ? (
        <div className="krg-host">
          {KraftGalleryView
            ? <KraftGalleryView
                allTurns={allTurns}
                sessions={sessions}
                activeSessionId={activeSessionId}
                onDesignAction={galleryDesignAction}
                onToggleStar={toggleStar}
                onAddToLaunch={addToLaunch}
                onConvert3D={markConverted3D}
                pushToast={pushToast}
              />
            : <EmptyState mark="K" title="Gallery unavailable" body="kraft-gallery.jsx failed to load." />}
        </div>
      ) : (
        <div className="kr-studio">
          <SessionList
            sessions={sessions}
            activeId={activeSessionId}
            onSelect={setActiveSessionId}
            onNew={createNewSession}
          />
          <section className="kr-thread">
            <ChatThread
              turns={turns}
              designerInitials={designerInitials}
              pending={pending}
              onDesignAction={onDesignAction}
              onKeep={onKeep}
              onRevise={onRevise}
              onDiscard={onDiscard}
              onEvidence={onEvidence}
            />
            <Composer
              value={draft}
              onChange={setDraft}
              onSend={sendTurn}
              sending={!!pending}
              refineDesignId={refineDesignId}
              onCancelRefine={() => setRefineDesignId(null)}
              count={count}
              onCount={setCount}
              brief={activeBrief}
              regionMode={regionMode}
              onRegionMode={setRegionMode}
              category={category}
              onCategory={setCategory}
              onPromptEnhance={enhancePrompt}
              promptEnhancing={promptEnhancing}
            />
          </section>
        </div>
      )}

      {/* Shared 3D viewer — opened by Convert to 3D from the chat tile, the
          gallery card, or the gallery inspector. Always mounted so it works
          in either view. */}
      <Modal
        open={!!viewer}
        onClose={() => setViewer(null)}
        title={viewer ? '3D model · ' + (viewer.design.tag || viewer.design.id) : '3D model'}
        footer={viewer && viewer.ready ? (
          <>
            <Btn variant="ghost" onClick={() => setViewer(null)}>Close</Btn>
            <Btn variant="primary" onClick={() => { onDesignAction('rhino', viewer.design); }}>⤴ CAD/Rhino handoff</Btn>
          </>
        ) : null}
      >
        {viewer && (viewer.ready ? (
          <div className="kr-viewer3d">
            {ThreeDStage
              ? <ThreeDStage design={viewer.design} size="lg" />
              : <div className="kr-converting"><div className="kr-converting-title">3D viewer unavailable</div></div>}
            <div className="kr-viewer3d-meta">
              <span className="t-mono">{viewer.design.id}</span>
              <span>· local 3D preview · not a CAD file</span>
            </div>
          </div>
        ) : (
          <div className="kr-converting" role="status">
            <span className="k-spin kr-converting-spin" />
            <div className="kr-converting-title">Preparing 3D preview…</div>
            <div className="t-meta">Static preview only · CAD/Rhino handoff needs typed dimensions</div>
          </div>
        ))}
      </Modal>

      <Modal
        open={!!designPreview}
        onClose={() => setDesignPreview(null)}
        title={designPreview ? 'Design preview · ' + (designPreview.tag || designPreview.id) : 'Design preview'}
        footer={designPreview ? (
          <>
            <Btn variant="ghost" onClick={() => setDesignPreview(null)}>Close</Btn>
            <Btn onClick={() => { const d = designPreview; setDesignPreview(null); openVariationPanel(d); }}>Create variations</Btn>
            <Btn variant="primary" onClick={() => { const d = designPreview; setDesignPreview(null); openCadHandoff(d); }}>CAD/Rhino handoff</Btn>
          </>
        ) : null}
      >
        {designPreview && (
          <div className="kr-action-preview">
            <div className="kr-action-tile"><TileSvg hue={designPreview.hue || 38} /></div>
            <div className="kr-action-facts">
              <div><span>ID</span><b className="t-mono">{designPreview.id}</b></div>
              <div><span>Lineage</span><b>{designPreview.parentDesignId || 'Original direction'}</b></div>
              <div><span>Version</span><b>v{designPreview.version || 1}</b></div>
              <div><span>Stored variation seeds</span><b>{designPreview.variationSeedCount || 0}</b></div>
            </div>
            <p className="t-body-sm">This local preview uses the generated tile placeholder. Real thumbnails, artifacts, and receipts arrive from the later KRAFT gallery/API lane.</p>
          </div>
        )}
      </Modal>

      <Modal
        open={!!variationPanel}
        onClose={() => setVariationPanel(null)}
        title={variationPanel ? 'Variation seed · ' + (variationPanel.design.tag || variationPanel.design.id) : 'Variation seed'}
        footer={variationPanel ? (
          <>
            <Btn variant="ghost" onClick={() => setVariationPanel(null)}>Close</Btn>
            <Btn variant="primary" onClick={storeVariationSeed}>Store variation seed</Btn>
          </>
        ) : null}
      >
        {variationPanel && (
          <div className="kr-action-panel">
            <div className="kr-action-note">
              Variations are stored as prompt seeds only in this lane. Image generation and approval remain later workflows.
            </div>
            <Textarea value={variationText} onChange={setVariationText} rows={4} placeholder="Describe the variation direction to seed…" />
            {variationPanel.seed && (
              <div className="kr-artifact-box">
                <div className="kr-artifact-head">
                  <Chip variant="accent">{variationPanel.seed.schemaVersion}</Chip>
                  <span className="t-mono">{variationPanel.seed.id}</span>
                  <Chip variant="mute">{variationPanel.seed.categoryLabel}</Chip>
                  <Chip variant="mute">{variationPanel.seed.channelProfile}</Chip>
                </div>
                <BriefField label="Parent lineage" wide>
                  <span className="t-mono">{variationPanel.seed.parentLineage.sourceDesignId}</span>
                  {' → root '}
                  <span className="t-mono">{variationPanel.seed.parentLineage.rootDesignId}</span>
                  {' · v'}{variationPanel.seed.parentLineage.parentVersion}
                </BriefField>
                <BriefField label="Variation axes" wide><BriefList items={variationPanel.seed.variationAxes} /></BriefField>
                <div className="kr-prompt-seed">{variationPanel.seed.promptSeed}</div>
                <BriefList items={variationPanel.seed.seeds} />
                <div className="t-meta">{variationPanel.seed.nextInputNeeded}</div>
              </div>
            )}
          </div>
        )}
      </Modal>

      <Modal
        open={!!cadHandoff}
        onClose={() => setCadHandoff(null)}
        title={cadHandoff ? 'CAD/Rhino handoff · ' + (cadHandoff.design.tag || cadHandoff.design.id) : 'CAD/Rhino handoff'}
        footer={cadHandoff ? (
          <>
            <Btn variant="ghost" onClick={() => setCadHandoff(null)}>Close</Btn>
            <Btn variant="primary" onClick={() => {
              updateDesigns(cadHandoff.design.id, d => ({ ...d, cadHandoffStored: true }));
              pushToast?.({ kind: 'success', title: 'CAD handoff preview stored',
                body: 'JewelleryCadSpec.v1 draft only · Rhino scaffold not generated.' });
              setCadHandoff(null);
            }}>Store handoff preview</Btn>
          </>
        ) : null}
      >
        {cadHandoff && (
          <div className="kr-action-panel">
            <div className="kr-artifact-box">
              <div className="kr-artifact-head">
                <Chip variant="accent">{cadHandoff.spec.schemaVersion}</Chip>
                <Chip variant="warn">{cadHandoff.spec.status}</Chip>
                <Chip variant="mute">{cadHandoff.spec.categoryLabel}</Chip>
                <Chip variant="mute">{cadHandoff.spec.channelProfile}</Chip>
              </div>
              <BriefField label="Parent lineage" wide>
                <span className="t-mono">{cadHandoff.spec.parentLineage.sourceDesignId}</span>
                {' → root '}
                <span className="t-mono">{cadHandoff.spec.parentLineage.rootDesignId}</span>
                {' · v'}{cadHandoff.spec.parentLineage.parentVersion}
              </BriefField>
              <BriefField label="Structured notes" wide><BriefList items={cadHandoff.spec.structuredNotes} /></BriefField>
              <BriefField label="Category CAD notes" wide><BriefList items={cadHandoff.spec.categoryCadNotes} /></BriefField>
              <BriefField label="Material assumptions" wide><BriefList items={cadHandoff.spec.materialAssumptions} /></BriefField>
              <BriefField label="Missing dimensions" wide><BriefList items={cadHandoff.spec.missingDimensions} /></BriefField>
              <BriefField label="Rhino next step" wide>{cadHandoff.spec.rhinoNextStep}</BriefField>
            </div>
          </div>
        )}
      </Modal>

      <Modal
        open={!!exportPanel}
        onClose={() => setExportPanel(null)}
        title={exportPanel ? 'Export · ' + (exportPanel.design.tag || exportPanel.design.id) : 'Export'}
        footer={exportPanel ? (
          <>
            <Btn variant="ghost" onClick={() => setExportPanel(null)}>Close</Btn>
            <Btn onClick={() => { const d = exportPanel.design; setExportPanel(null); setDesignPreview(d); }}>Open preview</Btn>
          </>
        ) : null}
      >
        {exportPanel && (
          <div className="kr-action-panel">
            <div className="kr-action-note">
              Export is not generated in this static lane because no R2 artifact URL exists yet. Use the design preview or store a CAD/variation handoff first.
            </div>
            <div className="kr-setup-code">Later endpoint: POST /api/kraft/designs/{exportPanel.design.id}/export?fmt=png</div>
          </div>
        )}
      </Modal>

      <Modal
        open={sharePanel}
        onClose={() => setSharePanel(false)}
        title="Share session preview"
        footer={<Btn variant="ghost" onClick={() => setSharePanel(false)}>Close</Btn>}
      >
        <div className="kr-action-panel">
          <div className="kr-action-note">
            Sharing is local-only in this static lane. This packet shows what would be sent once the backend share workflow exists.
          </div>
          <div className="kr-action-facts">
            <div><span>Session</span><b>{activeSession?.title || activeSessionId}</b></div>
            <div><span>Turns</span><b>{turns.length}</b></div>
            <div><span>Region mode</span><b>{KRAFT_REGION_LABEL[regionMode] || regionMode}</b></div>
            <div><span>Variation seeds</span><b>{variationSets.length}</b></div>
          </div>
          <div className="t-meta">No external link or notification was created.</div>
        </div>
      </Modal>
    </div>
  );
};


/* ============================================================
   Export
   ============================================================ */

Object.assign(window.K, {
  KraftStudioPage, SessionList, ChatThread, UserTurn, AssistantTurn,
  DesignGrid, DesignTile, DesignActionsOverlay, Composer, TileSvg,
  SEED_KRAFT_SESSIONS, SEED_KRAFT_TURNS,
});
window.KraftStudioPage = KraftStudioPage;
