// spacegen-app.jsx
// Core SpaceGenerator prototype — accepts theme tokens and renders the
// full upload → presets → generate → stacked feed flow.
// All variations share the same layout & state machine; only tokens differ.

const SG_PRESETS = [
  { id: 'creative',  name: 'Creative Studio',     tag: 'WORK',     hue: 28,  blurb: 'Open-plan studio. Concrete + warm wood + plants. Daylight pour.',
    variants: [
      'Long timber benches with iMacs, a model-making corner with foam cutters, large mood-board wall.',
      'Standing desks in a U-formation, plotter printers along one wall, swatch library in open shelving.',
      'Low partitions with felt acoustic panels, lounge nook with Eames chairs, pinboard runs full length.',
    ] },
  { id: 'design',    name: 'Design Studio',       tag: 'WORK',     hue: 50,  blurb: 'Architecture / industrial-design office. Drafting tables, material library, pin-up walls.',
    variants: [
      'Tilted drafting tables under daylight pendants, a long material library wall (samples in trays), pin-up board running full length.',
      'CNC mill and 3D printers in a glass-walled fab corner, prototype shelves, big tracing-paper rolls on stands.',
      'Sit-stand desks with dual monitors, large physical site model on a central plinth, blueprint flat-files.',
    ] },
  { id: 'bike',      name: 'Bike Shop',           tag: 'RETAIL',   hue: 200, blurb: 'Hanging frames, repair stand, espresso bar. Industrial fit-out.',
    variants: [
      'Wall of hanging road bikes by hue, a Park Tool repair stand mid-floor, espresso bar near entry.',
      'Mountain-bike focus: muddy demo-day vibe, tire wall, suspension service bench under task lights.',
      'Boutique e-bike showroom: 6 bikes on plinths with spec cards, charging stations, lounge sofa.',
    ] },
  { id: 'car',       name: 'Car Dealership',      tag: 'RETAIL',   hue: 240, blurb: 'Two showroom vehicles, polished floor, glass desks, brand wall.',
    variants: [
      'Two vehicles on epoxy-resin floor, glass-walled negotiation rooms, brand wall with rotating logo.',
      'Single hero EV under spotlights with charging puck visible, dynamic price-card kiosk, lounge.',
      'Pre-owned premium: three vehicles in a row, detailing bay glimpsed, leather waiting chairs.',
    ] },
  { id: 'fashion',   name: 'Fashion Store',       tag: 'RETAIL',   hue: 340, blurb: 'Minimal racks, mezzanine lounge, tracked spotlights, plinths.',
    variants: [
      'Minimal raw-steel racks, a single curved fitting-room volume in plaster, plinths with folded knits.',
      'Concrete plinths in a grid, suspended garments on stainless cables, mirror wall, oversized bench.',
      'Streetwear / sneaker focus: ladder racks, glass cubes for hero items, neon brand mark, DJ booth corner.',
      'Bridal / formal focus: a single circular dress carousel, plush carpet inset, fitting-room curtain wall.',
    ] },
  { id: 'outdoors',  name: 'Outdoors Shop',       tag: 'RETAIL',   hue: 130, blurb: 'Timber fixtures, kayaks overhead, camping vignette, topo map wall.',
    variants: [
      'Timber fixtures, kayaks overhead from rafters, a pitched tent vignette, topographic map wall.',
      'Climbing-gear focus: rope wall, harness pegboard, a 3m bouldering test-piece in the corner.',
      'Ski / snowboard focus: ski wall by length, boot-fitting bench, après lounge with fireplace.',
    ] },
  { id: 'crossfit',  name: 'CrossFit Box',        tag: 'FITNESS',  hue: 10,  blurb: 'Rubber floor, rigs anchored to columns, rower row, chalk station.',
    variants: [
      'Black rubber floor, a single 6-station rig down the centre, rower row along one wall, chalk station, whiteboard with WOD.',
      'Two-rig setup at right angles, plyo boxes stacked, sled tracks in turf strips, kettlebell ladder.',
      'Open functional-fitness layout: assault bikes in a row, ski-ergs, sandbags, gymnastic rings hung from beams.',
    ] },
  { id: 'gym',       name: 'Training Gym',        tag: 'FITNESS',  hue: 220, blurb: 'Boutique strength + cardio gym. Premium feel — wood floors, mirrors, branded.',
    variants: [
      'Hardwood platforms with bumper plates, a wall of dumbbells (5–50kg), squat racks, mirror wall.',
      'Cardio-forward: treadmills + bikes facing windows, stretching turf zone, juice bar near entry.',
      'Personal-training boutique: 3 private platforms with clients, cable machines, branded wall, reception desk.',
      'Pilates / reformer focus: 8 reformer beds in a row, ballet bar wall, soft sage palette.',
    ] },
  { id: 'cafe',      name: 'Specialty Café',      tag: 'F&B',      hue: 30,  blurb: 'Bar in front, communal table, roastery glimpse, banquette under mezz.',
    variants: [
      'Concrete bar with a 3-group espresso machine, communal oak table seating 12, roastery glimpsed through glass.',
      'Tile-front bar in muted green, marble counters, banquette under mezzanine, pastry case at entry.',
      'Stand-up Italian style: short bar, no tables, copper accents, brass shelving with bottles.',
    ] },
  { id: 'gallery',   name: 'Art Gallery',         tag: 'CULTURE',  hue: 0,   blurb: 'White-walled program, track lighting, plinths, single bench.',
    variants: [
      'White-walled program with 6 large canvases evenly spaced, track lighting, single bench mid-room.',
      'Sculpture-forward: 4 plinths with bronze pieces, dramatic raking light, polished concrete floor.',
      'Photography focus: salon hang of 30 small prints on one wall, oversize works on opposite wall.',
    ] },
  { id: 'coworking', name: 'Coworking',           tag: 'WORK',     hue: 260, blurb: 'Hot desks, phone booths under mezz, kitchen island, lounge.' },
  { id: 'event',     name: 'Event Venue',         tag: 'CULTURE',  hue: 290, blurb: 'Cleared floor, stage end, festoon lighting, bar at side.' },
  { id: 'climbing',  name: 'Bouldering Gym',      tag: 'FITNESS',  hue: 50,  blurb: 'Walls to apex, padded floor, top-out on mezzanine, café corner.' },
  { id: 'showroom',  name: 'Furniture Showroom',  tag: 'RETAIL',   hue: 170, blurb: 'Vignettes, area rugs, mood lighting, mezz as styled bedroom.' },
];

// Default reference image — used until the user uploads their own.
const SG_REF_IMG = 'assets/reference-warehouse-clean.jpg';

// Load the default reference into a Blob on first use so the generation
// pipeline always has a real Blob to work with (real or mock client).
async function sgLoadDefaultRefBlob() {
  const r = await fetch(SG_REF_IMG);
  return await r.blob();
}

function sgVariantSeed(presetId, n) {
  // deterministic pseudo-variant identifier for captions
  let h = 0; const s = presetId + ':' + n;
  for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
  return h.toString(16).slice(0, 6).toUpperCase();
}

// ────────────────────────────────────────────────────────────────
// ThemedSpaceGen — the actual app. Receives a `theme` object with tokens.
// ────────────────────────────────────────────────────────────────
function ThemedSpaceGen({ theme, frameLabel, showPrompt, projectName, projectAddress, mode, occupancy, setOccupancy, onOpenFloorPlan, onOpenTenantFit, floorPlanZones, tenantCatalog }) {
  const t = theme;
  const isResidential = mode === 'residential';
  const [stage, setStage] = React.useState('ready'); // ready | generating | feed
  // Default selection differs by mode. Residential picks 4 of 6 styles.
  const [selectedPresets, setSelectedPresets] = React.useState(
    isResidential
      ? ['res-scandi', 'res-midcentury', 'res-japandi', 'res-coastal']
      : ['creative', 'bike', 'fashion', 'crossfit']
  );
  // User-defined presets — same shape as built-ins. Picked/persisted alongside.
  const [customPresets, setCustomPresets] = React.useState([]);

  // Mode-specific built-in presets.
  const builtInPresets = isResidential
    ? (window.SG_RESIDENTIAL_PRESETS || [])
    : SG_PRESETS;

  // The full set used everywhere downstream: built-ins + the user's own.
  const allPresets = React.useMemo(
    () => [...builtInPresets, ...customPresets],
    [builtInPresets, customPresets]
  );

  // When mode changes, reset preset selection to the new mode's defaults.
  // (Otherwise residential ids linger when you switch to commercial.)
  const lastModeRef = React.useRef(mode);
  React.useEffect(() => {
    if (lastModeRef.current !== mode) {
      setSelectedPresets(
        mode === 'residential'
          ? ['res-scandi', 'res-midcentury', 'res-japandi', 'res-coastal']
          : ['creative', 'bike', 'fashion', 'crossfit']
      );
      setCustomPresets([]);
      lastModeRef.current = mode;
    }
  }, [mode]);
  const findPreset = React.useCallback(
    (id) => allPresets.find((p) => p.id === id),
    [allPresets]
  );

  // Add a custom preset from a free-text prompt. We synthesize a name,
  // assign a random hue (so the mock pipeline visibly differs), and auto-
  // select it so the user sees their addition reflected immediately.
  const addCustomPreset = React.useCallback((rawPrompt) => {
    const text = String(rawPrompt || '').trim();
    if (!text) return;
    // Pull the first noun-y phrase as a name; fall back to first 32 chars.
    const firstClause = text.split(/[,.;\n]/)[0].trim();
    const name = firstClause.length > 0 && firstClause.length <= 40
      ? firstClause.replace(/^a\s+|^an\s+/i, '').replace(/^./, (c) => c.toUpperCase())
      : 'Custom Concept';
    const id = `custom-${Date.now().toString(36)}`;
    const hue = Math.floor(Math.random() * 360);
    const next = { id, name, tag: 'CUSTOM', hue, blurb: text, custom: true };
    setCustomPresets((arr) => [...arr, next]);
    setSelectedPresets((arr) => [...arr, id]);
  }, []);

  const removeCustomPreset = React.useCallback((id) => {
    setCustomPresets((arr) => arr.filter((p) => p.id !== id));
    setSelectedPresets((arr) => arr.filter((x) => x !== id));
  }, []);
  const [variantsPerPreset, setVariantsPerPreset] = React.useState(2);
  const [styleIntensity, setStyleIntensity] = React.useState(60);
  const [timeOfDay, setTimeOfDay] = React.useState('Midday');
  const [activity, setActivity] = React.useState('Quiet');
  const [removals, setRemovals] = React.useState('');
  const [progress, setProgress] = React.useState(0);
  const [pickedForCluster, setPickedForCluster] = React.useState(new Set());
  const [feedItems, setFeedItems] = React.useState([]);
  const [activeIdx, setActiveIdx] = React.useState(0);
  // Real reference: a File/Blob the user uploaded, plus an objectURL for display
  const [refBlob, setRefBlob] = React.useState(null);
  const [refUrl, setRefUrl] = React.useState(SG_REF_IMG);
  const [refMeta, setRefMeta] = React.useState({ name: 'WAREHOUSE-A', size: '1920×1280' });
  const [errors, setErrors] = React.useState([]);
  const feedRef = React.useRef(null);

  // Hydrate the default reference Blob once so the generation pipeline can
  // always use the same code path whether the user uploaded or not.
  React.useEffect(() => {
    if (refBlob) return;
    sgLoadDefaultRefBlob().then(setRefBlob).catch(() => {});
  }, [refBlob]);

  const onUploadRef = (file) => {
    if (!file) return;
    const url = URL.createObjectURL(file);
    setRefBlob(file);
    setRefUrl(url);
    // Read dimensions for the meta caption.
    const img = new Image();
    img.onload = () => setRefMeta({
      name: file.name.replace(/\.[^.]+$/, '').toUpperCase().slice(0, 16),
      size: `${img.naturalWidth}×${img.naturalHeight}`,
    });
    img.src = url;
  };

  // Build the items we'll generate, with status tracking per item.
  const planFeed = React.useCallback(() => {
    const items = [];
    for (const pid of selectedPresets) {
      const p = findPreset(pid);
      if (!p) continue;
      for (let v = 1; v <= variantsPerPreset; v++) {
        items.push({
          key: `${pid}-${v}`,
          preset: p,
          variant: v,
          seed: sgVariantSeed(pid, v),
          intensity: styleIntensity,
          time: timeOfDay,
          activity,
          mode,
          occupancy,
          removals,
          status: 'queued', // queued | running | done | error
          imageUrl: null,
          prompt: null,
        });
      }
    }
    return items;
  }, [selectedPresets, variantsPerPreset, styleIntensity, timeOfDay, activity, mode, occupancy, removals, findPreset]);

  // Real generation pipeline — kicks off when stage becomes 'generating'.
  // Calls NanoBanana.generate() per item with limited concurrency so we
  // don't blast the proxy. Updates per-item status + overall progress.
  React.useEffect(() => {
    if (stage !== 'generating') return;
    let cancelled = false;
    const planned = planFeed();
    setFeedItems(planned);
    setProgress(0);
    setErrors([]);

    const CONCURRENCY = 3;
    let cursor = 0;
    let done = 0;
    const total = planned.length;

    const updateItem = (key, patch) => {
      setFeedItems((items) => items.map((it) => it.key === key ? { ...it, ...patch } : it));
    };

    async function worker() {
      while (!cancelled) {
        const i = cursor++;
        if (i >= total) return;
        const item = planned[i];
        updateItem(item.key, { status: 'running' });
        try {
          const prompt = window.NanoBanana.buildPrompt({
            preset: item.preset,
            seed: item.seed,
            controls: { intensity: item.intensity, time: item.time, activity: item.activity, mode: item.mode, occupancy: item.occupancy, removals: item.removals },
          });
          const resBlob = await window.NanoBanana.generate({
            refImageBlob: refBlob,
            preset: item.preset,
            controls: { intensity: item.intensity, time: item.time, activity: item.activity, mode: item.mode, occupancy: item.occupancy, removals: item.removals },
            seed: item.seed,
          });
          if (cancelled) return;
          const url = URL.createObjectURL(resBlob);
          updateItem(item.key, { status: 'done', imageUrl: url, prompt, blob: resBlob });
        } catch (err) {
          if (cancelled) return;
          updateItem(item.key, { status: 'error', errorMessage: err.message });
          setErrors((e) => [...e, { key: item.key, message: err.message }]);
        } finally {
          done++;
          setProgress((done / total) * 100);
        }
      }
    }

    if (!refBlob) {
      // Should be hydrated by the effect above; bail to ready if not.
      setStage('ready');
      return;
    }

    Promise.all(Array.from({ length: Math.min(CONCURRENCY, total) }, worker)).then(() => {
      if (!cancelled) setTimeout(() => setStage('feed'), 300);
    });

    return () => { cancelled = true; };
  }, [stage, planFeed, refBlob]);

  // active feed item (which preset is currently in view)
  React.useEffect(() => {
    if (stage !== 'feed') return;
    const el = feedRef.current; if (!el) return;
    const onScroll = () => {
      const cards = el.querySelectorAll('[data-feed-card]');
      let best = 0; let bestDist = Infinity;
      const mid = el.scrollTop + el.clientHeight / 2;
      cards.forEach((c, i) => {
        const cMid = c.offsetTop + c.offsetHeight / 2;
        const d = Math.abs(cMid - mid);
        if (d < bestDist) { bestDist = d; best = i; }
      });
      setActiveIdx(best);
    };
    el.addEventListener('scroll', onScroll, { passive: true });
    onScroll();
    return () => el.removeEventListener('scroll', onScroll);
  }, [stage, feedItems]);

  const togglePreset = (id) => {
    setSelectedPresets((s) => s.includes(id) ? s.filter((x) => x !== id) : [...s, id]);
  };
  const togglePick = (key) => {
    setPickedForCluster((s) => {
      const n = new Set(s);
      if (n.has(key)) n.delete(key); else n.add(key);
      return n;
    });
  };
  const reset = () => {
    if (window.sgArchiveItems && feedItems.length) {
      try { window.sgArchiveItems(feedItems); } catch (e) {}
    }
    setStage('ready'); setProgress(0); setFeedItems([]); setPickedForCluster(new Set()); setActiveIdx(0);
  };
  React.useEffect(() => {
    window.sgRestoreItem = (it) => {
      if (!it) return;
      setFeedItems((prev) => prev.find((p) => p.key === it.key) ? prev : [it, ...prev]);
      setStage('feed');
      setActiveIdx(0);
    };
    return () => { delete window.sgRestoreItem; };
  }, []);

  // ── Mobile bottom-sheet state ──────────────────────────────────
  // Below 1024 px the sidebar collapses into a bottom sheet that
  // peeks 72 px above the viewport edge. Tap the grabber to expand;
  // swipe down (or tap the backdrop) to dismiss. Desktop is unaffected.
  const [isMobile, setIsMobile] = React.useState(() =>
    typeof window !== 'undefined' && window.matchMedia('(max-width: 1023.98px)').matches
  );
  React.useEffect(() => {
    if (typeof window === 'undefined') return;
    const mq = window.matchMedia('(max-width: 1023.98px)');
    const onChange = (e) => setIsMobile(e.matches);
    mq.addEventListener ? mq.addEventListener('change', onChange) : mq.addListener(onChange);
    return () => {
      mq.removeEventListener ? mq.removeEventListener('change', onChange) : mq.removeListener(onChange);
    };
  }, []);
  const [sheetOpen, setSheetOpen] = React.useState(false);
  // When the sheet is open, lock body scroll so the underlying stage
  // doesn't rubber-band behind the sheet.
  React.useEffect(() => {
    if (!isMobile) return;
    if (sheetOpen) document.body.classList.add('sg-sheet-open');
    else document.body.classList.remove('sg-sheet-open');
    return () => document.body.classList.remove('sg-sheet-open');
  }, [isMobile, sheetOpen]);
  // Auto-close the sheet when the user transitions stages (generate / reset)
  // so they immediately see the result without a manual dismiss.
  React.useEffect(() => {
    if (isMobile) setSheetOpen(false);
  }, [stage, isMobile]);

  // Expose a refine helper that re-runs NanoBanana with a modifier appended,
  // mutates the matching feed item's image, and returns the new objectURL so
  // the card can update in place.
  React.useEffect(() => {
    window.sgRefineItem = async (item, modifier) => {
      if (!refBlob) return null;
      const controls = {
        intensity: item.intensity, time: item.time, activity: item.activity,
        mode: item.mode, occupancy: item.occupancy, removals: item.removals,
        modifier,
      };
      const prompt = window.NanoBanana.buildPrompt({ preset: item.preset, controls, seed: item.seed });
      const resBlob = await window.NanoBanana.generate({
        refImageBlob: refBlob, preset: item.preset, controls,
        seed: (item.seed || '0') + '-r' + Date.now().toString(36).slice(-3),
      });
      const url = URL.createObjectURL(resBlob);
      setFeedItems((items) => items.map((it) =>
        it.key === item.key ? { ...it, imageUrl: url, prompt, blob: resBlob } : it
      ));
      return url;
    };
    return () => { delete window.sgRefineItem; };
  }, [refBlob]);

  return (
    <div data-screen-label={frameLabel} className="sg-app-shell" style={{
      width: '100%', height: '100%', background: t.bg, color: t.fg,
      fontFamily: t.fontBody, display: 'grid', gridTemplateColumns: '320px 1fr',
      overflow: 'hidden', position: 'relative',
    }}>
      {/* Mobile-only backdrop. Tapping it dismisses the sheet. */}
      <div
        className={`sg-sheet-backdrop${isMobile && sheetOpen ? ' is-visible' : ''}`}
        onClick={() => setSheetOpen(false)}
        aria-hidden="true"
      />
      <SGSidebar
        t={t}
        stage={stage}
        refUrl={refUrl}
        refMeta={refMeta}
        onUploadRef={onUploadRef}
        projectName={projectName}
        projectAddress={projectAddress}
        onOpenFloorPlan={onOpenFloorPlan}
        onOpenTenantFit={onOpenTenantFit}
        floorPlanZones={floorPlanZones}
        tenantCatalog={tenantCatalog}
        selectedPresets={selectedPresets}
        togglePreset={togglePreset}
        variantsPerPreset={variantsPerPreset} setVariantsPerPreset={setVariantsPerPreset}
        styleIntensity={styleIntensity} setStyleIntensity={setStyleIntensity}
        timeOfDay={timeOfDay} setTimeOfDay={setTimeOfDay}
        activity={activity} setActivity={setActivity}
        removals={removals} setRemovals={setRemovals}
        mode={mode}
        occupancy={occupancy}
        setOccupancy={setOccupancy}
        onGenerate={() => { setStage('generating'); setSheetOpen(false); }}
        onReset={reset}
        isMobile={isMobile}
        sheetOpen={sheetOpen}
        setSheetOpen={setSheetOpen}
      />
      <SGStage
        t={t}
        stage={stage}
        progress={progress}
        feedItems={feedItems}
        feedRef={feedRef}
        activeIdx={activeIdx}
        pickedForCluster={pickedForCluster}
        togglePick={togglePick}
        selectedPresets={selectedPresets}
        variantsPerPreset={variantsPerPreset}
        refUrl={refUrl}
        showPrompt={showPrompt}
        allPresets={allPresets}
        customPresets={customPresets}
        addCustomPreset={addCustomPreset}
        removeCustomPreset={removeCustomPreset}
        togglePreset={togglePreset}
        mode={mode}
      />
      {pickedForCluster.size > 0 && stage === 'feed' && (
        <SGClusterDock
          t={t}
          count={pickedForCluster.size}
          pickedItems={feedItems.filter((it) => pickedForCluster.has(it.key))}
          onClear={() => setPickedForCluster(new Set())}
        />
      )}
    </div>
  );
}

// ────────────────────────────────────────────────────────────────
// Sidebar — upload + presets + controls + generate button
// ────────────────────────────────────────────────────────────────
function SGSidebar(props) {
  const [isDragging, setIsDragging] = React.useState(false);
  const { t, stage, refUrl, refMeta, onUploadRef,
    selectedPresets, togglePreset, variantsPerPreset, setVariantsPerPreset,
    styleIntensity, setStyleIntensity, timeOfDay, setTimeOfDay, activity, setActivity, removals, setRemovals,
    mode, occupancy, setOccupancy,
    onGenerate, onReset, projectName, projectAddress,
    onOpenFloorPlan, onOpenTenantFit, floorPlanZones, tenantCatalog,
    isMobile, sheetOpen, setSheetOpen } = props;
  const totalRenders = selectedPresets.length * variantsPerPreset;
  const disabled = stage === 'generating' || selectedPresets.length === 0;
  const fileInputRef = React.useRef(null);

  // ── Bottom-sheet drag mechanics ──────────────────────────────────
  // Touch on the grabber: drag down to dismiss, drag up to expand.
  // We translate the sheet 1:1 with the finger and snap on release.
  const sheetRef = React.useRef(null);
  const dragRef = React.useRef({ active: false, startY: 0, dy: 0, openAtStart: false });
  const [dragging, setDragging] = React.useState(false);

  const onGrabberClick = () => { if (isMobile && setSheetOpen) setSheetOpen(!sheetOpen); };
  const onTouchStart = (e) => {
    if (!isMobile) return;
    const touch = e.touches[0];
    dragRef.current = { active: true, startY: touch.clientY, dy: 0, openAtStart: !!sheetOpen };
    setDragging(true);
  };
  const onTouchMove = (e) => {
    const d = dragRef.current; if (!d.active) return;
    const touch = e.touches[0];
    d.dy = touch.clientY - d.startY;
    // Translate live. From open: only allow downward drag. From closed: only upward.
    const sheet = sheetRef.current; if (!sheet) return;
    if (d.openAtStart) {
      const ty = Math.max(0, d.dy);
      sheet.style.transform = `translateY(${ty}px)`;
    } else {
      const ty = Math.min(0, d.dy);
      // Closed baseline is calc(100% - 72px); add the delta.
      sheet.style.transform = `translateY(calc(100% - 72px + ${ty}px))`;
    }
  };
  const onTouchEnd = () => {
    const d = dragRef.current; if (!d.active) return;
    d.active = false;
    setDragging(false);
    const sheet = sheetRef.current; if (sheet) sheet.style.transform = '';
    const THRESHOLD = 60;
    if (d.openAtStart && d.dy > THRESHOLD) setSheetOpen && setSheetOpen(false);
    else if (!d.openAtStart && d.dy < -THRESHOLD) setSheetOpen && setSheetOpen(true);
  };

  const sheetClassName = [
    'sg-sidebar',
    isMobile && sheetOpen ? 'is-open' : '',
    isMobile && dragging ? 'is-dragging' : '',
  ].filter(Boolean).join(' ');

  return (
    <aside
      ref={sheetRef}
      className={sheetClassName}
      style={{
        borderRight: `1px solid ${t.line}`, background: t.panel, overflowY: 'auto',
        display: 'flex', flexDirection: 'column',
      }}
    >
      {/* Mobile-only grabber. Hidden on desktop via CSS. */}
      <div
        className="sg-sheet-grabber"
        role="button"
        aria-label={sheetOpen ? 'Close controls' : 'Open controls'}
        aria-expanded={!!sheetOpen}
        onClick={onGrabberClick}
        onTouchStart={onTouchStart}
        onTouchMove={onTouchMove}
        onTouchEnd={onTouchEnd}
        onTouchCancel={onTouchEnd}
        style={{
          display: 'none', // CSS shows it at <1024px
          alignItems: 'center', justifyContent: 'space-between', gap: 12,
          padding: '10px 20px 12px',
          borderBottom: `1px solid ${t.line}`,
          background: t.panel,
          position: 'sticky', top: 0, zIndex: 2,
          touchAction: 'none', cursor: 'grab', userSelect: 'none',
          minHeight: 72,
        }}
      >
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 6, flex: 1, minWidth: 0 }}>
          <span style={{
            display: 'block', width: 40, height: 4, borderRadius: 999,
            background: t.line, alignSelf: 'center', marginBottom: 2,
          }} />
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, width: '100%' }}>
            <span style={{ fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.5, color: t.muted }}>
              {sheetOpen ? 'CONTROLS' : 'TAP TO OPEN'}
            </span>
            <span style={{ marginLeft: 'auto', fontFamily: t.fontMono, fontSize: 11, color: t.fg, fontWeight: 600 }}>
              {String(totalRenders).padStart(2, '0')} renders queued
            </span>
          </div>
        </div>
      </div>
      {/* Brand mark */}
      <div style={{ padding: '20px 24px 16px', borderBottom: `1px solid ${t.line}` }}>
        <div style={{ fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.5, color: t.muted }}>
          SUPERSTUDIO · ROOM-TO-RENDER
        </div>
        <img src="assets/spacegen-logo.png" alt="SpaceGen" style={{ height: 28, width: 'auto', display: 'block', marginTop: 8 }} />
        <div style={{ fontFamily: t.fontMono, fontSize: 10, color: t.muted, marginTop: 6 }}>
          v0.4 · {mode === 'residential' ? 'Listing Tool' : 'Pitch Tool'}
        </div>
      </div>

      {/* Project switcher slot — rendered by host */}
      {window.SGProjectSwitcher && <window.SGProjectSwitcher t={t} />}

      {/* Reference image card */}
      <section style={{ padding: 20, borderBottom: `1px solid ${t.line}` }}>
        <SGSectionLabel t={t} num="01" title="Reference" />
        <input
          ref={fileInputRef}
          type="file"
          accept="image/*"
          capture="environment"
          style={{ display: 'none' }}
          onChange={(e) => onUploadRef && onUploadRef(e.target.files && e.target.files[0])}
        />
        <button
          onClick={() => fileInputRef.current && fileInputRef.current.click()}
          onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); if (!isDragging) setIsDragging(true); }}
          onDragEnter={(e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }}
          onDragLeave={(e) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget.contains(e.relatedTarget)) return; setIsDragging(false); }}
          onDrop={(e) => {
            e.preventDefault(); e.stopPropagation(); setIsDragging(false);
            const file = e.dataTransfer.files && e.dataTransfer.files[0];
            if (file && file.type.startsWith('image/') && onUploadRef) onUploadRef(file);
          }}
          style={{
            marginTop: 10, position: 'relative', borderRadius: t.radius, overflow: 'hidden',
            border: `${isDragging ? 2 : 1}px ${isDragging ? 'dashed' : 'solid'} ${isDragging ? t.accent : t.line}`,
            aspectRatio: '4/3', background: '#222',
            padding: 0, cursor: 'pointer', width: '100%', display: 'block',
            transition: 'border-color 120ms ease',
          }}
          title="Click or drop image to replace reference"
        >
          <img src={refUrl} alt="ref" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', pointerEvents: 'none' }} />
          <div style={{
            position: 'absolute', inset: 0, pointerEvents: 'none',
            background: isDragging
              ? `${t.accent}cc`
              : `linear-gradient(180deg, transparent 60%, ${t.bg}99 100%)`,
            transition: 'background 120ms ease',
          }} />
          {isDragging && (
            <div style={{
              position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
              fontFamily: t.fontMono, fontSize: 11, letterSpacing: 2, color: t.fgInv,
              pointerEvents: 'none', textAlign: 'center', padding: 12,
            }}>
              DROP IMAGE TO UPLOAD
            </div>
          )}
          <div style={{
            position: 'absolute', left: 8, bottom: 8, fontFamily: t.fontMono, fontSize: 9,
            letterSpacing: 1, color: t.fgInv, background: t.fg, padding: '3px 6px',
            opacity: isDragging ? 0 : 1, transition: 'opacity 120ms ease',
          }}>
            REF · {(projectName || refMeta.name).toUpperCase()} · {refMeta.size}
          </div>
        </button>
        <div style={{ display: 'flex', gap: 6, marginTop: 8, fontFamily: t.fontMono, fontSize: 10, color: t.muted }}>
          <button
            className="sg-tap-sm"
            onClick={() => fileInputRef.current && fileInputRef.current.click()}
            style={{ background: 'none', border: 'none', color: t.fg, fontFamily: t.fontMono, fontSize: 10, cursor: 'pointer', padding: 0, textDecoration: 'underline', display: 'inline-flex', alignItems: 'center' }}
          >↻ Replace or use camera</button>
          <span style={{ marginLeft: 'auto' }}>or drop image</span>
        </div>
      </section>

      {/* Pro+ tools — floor plan & tenant fit (rendered by host if available) */}
      {window.SGProToolsRow && (
        <window.SGProToolsRow
          t={t}
          floorPlanZones={floorPlanZones}
          tenantCatalog={tenantCatalog}
          onOpenFloorPlan={onOpenFloorPlan}
          onOpenTenantFit={onOpenTenantFit}
        />
      )}

      {/* Occupancy — residential only. Drives the "remove existing furniture" prompt path. */}
      {mode === 'residential' && (
        <section style={{ padding: 20, borderBottom: `1px solid ${t.line}` }}>
          <SGSectionLabel t={t} num="03" title="Existing furniture" />
          <p style={{ fontFamily: t.fontBody, fontSize: 12, color: t.muted, lineHeight: 1.4, margin: '6px 0 12px' }}>
            Is the room already furnished? If so we'll clear it before staging.
          </p>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
            {[
              { key: 'vacant',    label: 'Vacant',    hint: 'Empty room' },
              { key: 'furnished', label: 'Furnished', hint: 'Clear, then stage' },
            ].map((opt) => {
              const isOn = occupancy === opt.key;
              return (
                <button
                  key={opt.key}
                  className="sg-tap"
                  onClick={() => setOccupancy && setOccupancy(opt.key)}
                  style={{
                    padding: '10px 12px', textAlign: 'left',
                    background: isOn ? t.fg : 'transparent',
                    color: isOn ? t.bg : t.fg,
                    border: `1px solid ${isOn ? t.fg : t.line}`,
                    borderRadius: t.radius * 0.5, cursor: 'pointer',
                    fontFamily: t.fontBody,
                  }}
                >
                  <div style={{ fontSize: 13, fontWeight: 600 }}>{opt.label}</div>
                  <div style={{ fontFamily: t.fontMono, fontSize: 9, opacity: 0.65, letterSpacing: 0.8, marginTop: 2 }}>
                    {opt.hint}
                  </div>
                </button>
              );
            })}
          </div>
        </section>
      )}

      {/* Generation controls */}
      <section style={{ padding: 20, borderBottom: `1px solid ${t.line}` }}>
        <SGSectionLabel t={t} num={mode === 'residential' ? '04' : '03'} title="Controls" />
        <SGSlider t={t} label="Style intensity" value={styleIntensity} setValue={setStyleIntensity}
          min={0} max={100} suffix={`${styleIntensity}%`} hint={
            styleIntensity < 30 ? 'subtle' : styleIntensity < 70 ? 'balanced' : 'dramatic'
          } />
        <SGRadio t={t} label="Time of day" value={timeOfDay} setValue={setTimeOfDay}
          options={['Morning', 'Midday', 'Dusk', 'Night']} />
        <SGRadio t={t} label="Activity" value={activity} setValue={setActivity}
          options={['Empty', 'Quiet', 'Busy']} />
        <div style={{ marginTop: 16 }}>
          <div style={{ fontSize: 11, letterSpacing: 1.2, textTransform: 'uppercase', color: t.dim, marginBottom: 6 }}>Remove from scene</div>
          <textarea
            value={removals}
            onChange={(e) => setRemovals(e.target.value)}
            placeholder="e.g. remove the existing kitchen island, the orange forklift, and all signage on the back wall"
            rows={3}
            style={{
              width: '100%', boxSizing: 'border-box', resize: 'vertical',
              padding: 10, fontSize: 13, fontFamily: 'inherit', lineHeight: 1.4,
              background: t.surface || '#fff', color: t.fg, border: `1px solid ${t.line}`, borderRadius: 4, outline: 'none',
            }}
          />
          <div style={{ fontSize: 11, color: t.dim, marginTop: 4 }}>Describe specific objects to delete before staging. Leave blank to keep everything.</div>
        </div>
        <SGStepper t={t} label="Variants per concept" value={variantsPerPreset} setValue={setVariantsPerPreset}
          min={1} max={4} />
      </section>

      {/* Generate — sits on dark panelDeep, so text uses fgInv */}
      <section style={{ padding: 20, marginTop: 'auto', borderTop: `1px solid ${t.line}`, background: t.panelDeep, color: t.fgInv }}>
        <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
          <div style={{ fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.2, color: `${t.fgInv}99` }}>QUEUE</div>
          <div style={{ fontFamily: t.fontMono, fontSize: 10, color: `${t.fgInv}99` }}>~{totalRenders * 14}s · {totalRenders * 0.04}€</div>
        </div>
        <div style={{ fontFamily: t.fontDisplay, fontSize: 36, fontWeight: 700, lineHeight: 1, marginTop: 4, letterSpacing: '-0.02em', color: t.fgInv }}>
          {String(totalRenders).padStart(2, '0')} <span style={{ fontSize: 14, fontWeight: 500, color: `${t.fgInv}88` }}>renders</span>
        </div>
        {stage === 'feed' ? (
          <button onClick={onReset} className="sg-tap" style={{
            width: '100%', marginTop: 14, padding: '14px 18px', border: `1px solid ${t.fgInv}66`,
            background: 'transparent', color: t.fgInv, fontFamily: t.fontMono, fontSize: 11, letterSpacing: 2,
            cursor: 'pointer', borderRadius: t.radius,
          }}>
            ← NEW BRIEF
          </button>
        ) : (
          <button onClick={onGenerate} disabled={disabled} className="sg-tap" style={{
            width: '100%', marginTop: 14, padding: '14px 18px', border: 'none',
            background: disabled ? `${t.fgInv}1a` : t.accent, color: disabled ? `${t.fgInv}55` : t.accentInk,
            fontFamily: t.fontMono, fontSize: 11, letterSpacing: 2, fontWeight: 600,
            cursor: disabled ? 'not-allowed' : 'pointer', borderRadius: t.radius,
            display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          }}>
            <span>{stage === 'generating' ? 'GENERATING…' : 'GENERATE'}</span>
            <span>→</span>
          </button>
        )}
      </section>
    </aside>
  );
}

function SGSectionLabel({ t, num, title }) {
  return (
    <div style={{ display: 'flex', alignItems: 'baseline', gap: 10 }}>
      <span style={{ fontFamily: t.fontMono, fontSize: 10, color: t.muted, letterSpacing: 1.5 }}>{num}</span>
      <span style={{ fontFamily: t.fontDisplay, fontSize: 14, fontWeight: 600, letterSpacing: '-0.01em' }}>{title}</span>
      <span style={{ flex: 1, height: 1, background: t.line, marginLeft: 4 }} />
    </div>
  );
}

function SGSlider({ t, label, value, setValue, min, max, suffix, hint }) {
  return (
    <div style={{ marginTop: 14 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: t.fontMono, fontSize: 10, color: t.muted, letterSpacing: 1 }}>
        <span>{label.toUpperCase()}</span>
        <span style={{ color: t.fg }}>{suffix} <span style={{ color: t.muted }}>· {hint}</span></span>
      </div>
      <input type="range" min={min} max={max} value={value} onChange={(e) => setValue(+e.target.value)}
        style={{ width: '100%', accentColor: t.accent, marginTop: 4 }} />
    </div>
  );
}

function SGRadio({ t, label, value, setValue, options }) {
  return (
    <div style={{ marginTop: 14 }}>
      <div style={{ fontFamily: t.fontMono, fontSize: 10, color: t.muted, letterSpacing: 1 }}>{label.toUpperCase()}</div>
      <div style={{ display: 'grid', gridTemplateColumns: `repeat(${options.length}, 1fr)`, marginTop: 6, border: `1px solid ${t.line}`, borderRadius: t.radius, overflow: 'hidden' }}>
        {options.map((o, i) => {
          const active = o === value;
          return (
            <button key={o} onClick={() => setValue(o)} style={{
              padding: '8px 4px', border: 'none', background: active ? t.fg : 'transparent',
              color: active ? t.fgInv : t.fg, fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.2,
              cursor: 'pointer', borderLeft: i === 0 ? 'none' : `1px solid ${t.line}`,
            }}>{o.toUpperCase()}</button>
          );
        })}
      </div>
    </div>
  );
}

function SGStepper({ t, label, value, setValue, min, max }) {
  return (
    <div style={{ marginTop: 14, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
      <div style={{ fontFamily: t.fontMono, fontSize: 10, color: t.muted, letterSpacing: 1 }}>{label.toUpperCase()}</div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <button className="sg-stepper-btn" onClick={() => setValue(Math.max(min, value - 1))} style={sgStepBtn(t)}>−</button>
        <span style={{ fontFamily: t.fontDisplay, fontSize: 18, fontWeight: 600, minWidth: 20, textAlign: 'center' }}>{value}</span>
        <button className="sg-stepper-btn" onClick={() => setValue(Math.min(max, value + 1))} style={sgStepBtn(t)}>+</button>
      </div>
    </div>
  );
}
function sgStepBtn(t) {
  return {
    width: 24, height: 24, border: `1px solid ${t.line}`, background: 'transparent',
    color: t.fg, fontFamily: t.fontMono, fontSize: 14, cursor: 'pointer', borderRadius: t.radius,
    display: 'flex', alignItems: 'center', justifyContent: 'center',
  };
}
// Apply the sg-stepper-btn class so CSS can grow stepper buttons to 36×36 on mobile.
// (We can't toggle the className inside sgStepBtn since it returns an object, so the
//  caller passes className="sg-stepper-btn" on the <button> elements directly.)

// ────────────────────────────────────────────────────────────────
// Stage — center pane. Shows preset picker, generating, or feed.
// ────────────────────────────────────────────────────────────────
function SGStage(props) {
  if (props.stage === 'generating') {
    return <SGGenerating t={props.t} progress={props.progress} feedItems={props.feedItems} refUrl={props.refUrl} />;
  }
  if (props.stage === 'feed') return <SGFeed t={props.t} feedItems={props.feedItems} feedRef={props.feedRef} activeIdx={props.activeIdx} pickedForCluster={props.pickedForCluster} togglePick={props.togglePick} refUrl={props.refUrl} showPrompt={props.showPrompt} />;
  return <SGPresetPicker
    t={props.t}
    selectedPresets={props.selectedPresets}
    togglePreset={props.togglePreset}
    refUrl={props.refUrl}
    allPresets={props.allPresets}
    customPresets={props.customPresets}
    addCustomPreset={props.addCustomPreset}
    removeCustomPreset={props.removeCustomPreset}
    mode={props.mode}
  />;
}

// Real renders shown in the inspiration strip on the picker stage.
// Same set as the Examples page — proves what's possible from one photo.
const SG_INSPIRATION = [
  { src: 'assets/render-coworking-orange.png',  title: 'Co-working Floor',   tag: 'OFFICE' },
  { src: 'assets/render-photo-studio.png',      title: 'Photo Studio',       tag: 'CULTURE' },
  { src: 'assets/render-climbing-gym.png',      title: 'Climbing Gym',       tag: 'FITNESS' },
  { src: 'assets/render-gallery-minimal.png',   title: 'Art Gallery',        tag: 'CULTURE' },
  { src: 'assets/render-fashion-boutique.png',  title: 'Fashion Boutique',   tag: 'RETAIL' },
  { src: 'assets/render-outdoor-retail.png',    title: 'Outdoors Shop',      tag: 'RETAIL' },
  { src: 'assets/render-paint-store.png',       title: 'Paint & Wallpaper',  tag: 'RETAIL' },
  { src: 'assets/render-office-corporate.png',  title: 'Corporate HQ',       tag: 'OFFICE' },
  { src: 'assets/render-rug-cleaning.png',      title: 'Rug Wash Plant',     tag: 'INDUSTRIAL' },
  { src: 'assets/render-welding-shop.png',      title: 'Welding Shop',       tag: 'INDUSTRIAL' },
];

// Inspiration strip — horizontal rail of real-render thumbnails shown
// at the top of the picker. Read-only: clicking opens nothing, just inspires.
function SGInspirationStrip({ t }) {
  return (
    <div style={{ marginBottom: 32 }}>
      <div style={{
        display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
        marginBottom: 12, gap: 16,
      }}>
        <div>
          <div style={{ fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.8, color: t.accent }}>
            INSPIRATION · REAL RENDERS
          </div>
          <div style={{
            fontFamily: t.fontDisplay, fontSize: 22, fontWeight: 600,
            letterSpacing: '-0.01em', marginTop: 4,
          }}>
            What other agents made{' '}
            <span style={{ fontStyle: t.italicHeadlines === false ? 'normal' : 'italic', fontWeight: 400 }}>
              from one photo.
            </span>
          </div>
        </div>
        <a href="index.html" style={{
          fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.5, color: t.fg,
          textDecoration: 'none', borderBottom: `1px solid ${t.fg}`, paddingBottom: 2,
          flexShrink: 0,
        }}>SEE ALL EXAMPLES →</a>
      </div>
      <div style={{
        display: 'flex', gap: 10, overflowX: 'auto', paddingBottom: 8,
        scrollbarWidth: 'thin',
      }}>
        {SG_INSPIRATION.map((ex) => (
          <div key={ex.src} className="sg-inspiration-card" style={{
            flexShrink: 0, width: 220,
            border: `1px solid ${t.line}`, borderRadius: t.radius, overflow: 'hidden',
            background: t.panel,
          }}>
            <div style={{ position: 'relative', aspectRatio: '4/3' }}>
              <img src={ex.src} alt={ex.title} style={{
                width: '100%', height: '100%', objectFit: 'cover', display: 'block',
              }} />
              <div style={{
                position: 'absolute', top: 8, left: 8, padding: '3px 7px',
                background: 'rgba(255,255,255,0.9)', color: t.fg,
                fontFamily: t.fontMono, fontSize: 9, letterSpacing: 1.4, fontWeight: 600,
                borderRadius: 2,
              }}>{ex.tag}</div>
            </div>
            <div style={{
              padding: '8px 10px', fontFamily: t.fontDisplay, fontSize: 13,
              fontWeight: 500, letterSpacing: '-0.01em',
            }}>
              {ex.title}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ── Preset picker (ready stage) ────────────────────────────────
function SGPresetPicker({ t, selectedPresets, togglePreset, refUrl, allPresets, customPresets, addCustomPreset, removeCustomPreset, mode }) {
  const [filter, setFilter] = React.useState('ALL');
  const [composerOpen, setComposerOpen] = React.useState(false);
  const [draftText, setDraftText] = React.useState('');
  const isResidential = mode === 'residential';
  const tags = isResidential
    ? ['ALL', 'STYLE', 'CUSTOM']
    : ['ALL', 'RETAIL', 'WORK', 'FITNESS', 'F&B', 'CULTURE', 'CUSTOM'];
  const list = allPresets || SG_PRESETS;
  const visible = list.filter((p) => filter === 'ALL' || p.tag === filter);

  const submit = () => {
    if (!draftText.trim()) return;
    addCustomPreset && addCustomPreset(draftText);
    setDraftText('');
    setComposerOpen(false);
  };

  // Mode-aware hero copy.
  const eyebrow = isResidential ? 'STEP 02 / 03 · CHOOSE STYLES' : 'STEP 02 / 03 · CHOOSE CONCEPTS';
  const headlineWord = isResidential ? 'this home' : 'this room';
  const subhead = isResidential
    ? "Pick the styles you want to stage — or describe your own. We'll render each one same camera, same bones, so a buyer can picture their life inside it."
    : "Pick the use-cases you want to pitch — or describe your own. We'll render each one same camera, same bones, so a tenant can see themselves inside the space.";
  const conceptWord = isResidential ? 'STYLES' : 'CONCEPTS';

  return (
    <div style={{ overflowY: 'auto', padding: '32px 48px 80px' }} className="sg-stage-pad">
      {/* Header */}
      <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 24, marginBottom: 32 }}>
        <div>
          <div style={{ fontFamily: t.fontMono, fontSize: 11, letterSpacing: 2, color: t.muted }}>
            {eyebrow}
          </div>
          <h1 className="sg-hero" style={{
            fontFamily: t.fontDisplay, fontSize: 80, lineHeight: 0.9, letterSpacing: '-0.04em',
            margin: '12px 0 0', fontWeight: 700, maxWidth: 880,
          }}>
            What could<br/>
            <span style={{ fontStyle: t.italicHeadlines === false ? 'normal' : 'italic', fontWeight: 400 }}>{headlineWord}</span> become?
          </h1>
          <p style={{ fontFamily: t.fontBody, fontSize: 16, color: t.muted, maxWidth: 540, marginTop: 18, lineHeight: 1.5 }}>
            {subhead}
          </p>
        </div>
        <div style={{ fontFamily: t.fontMono, fontSize: 10, color: t.muted, textAlign: 'right', whiteSpace: 'nowrap' }}>
          <div>{selectedPresets.length} SELECTED</div>
          <div>OF {list.length} {conceptWord}</div>
        </div>
      </div>

      {/* Custom-prompt composer (inline, opens on click) */}
      <div style={{
        marginBottom: 20, padding: composerOpen ? 20 : 0,
        border: composerOpen ? `1.5px solid ${t.accent}` : 'none', borderRadius: t.radius,
        background: composerOpen ? `${t.accent}0a` : 'transparent',
        transition: 'padding .15s',
      }}>
        {!composerOpen ? (
          <button onClick={() => setComposerOpen(true)} style={{
            display: 'flex', alignItems: 'center', gap: 10, padding: '14px 18px',
            border: `1.5px dashed ${t.line}`, background: 'transparent', color: t.fg,
            cursor: 'pointer', borderRadius: t.radius, fontFamily: t.fontBody, fontSize: 14,
            width: '100%', textAlign: 'left',
          }}>
            <span style={{
              width: 28, height: 28, borderRadius: '50%', background: t.accent, color: t.accentInk,
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
              fontFamily: t.fontMono, fontSize: 16, fontWeight: 700, flexShrink: 0,
            }}>+</span>
            <span style={{ fontWeight: 500 }}>Describe your own concept</span>
            <span style={{ color: t.muted, fontFamily: t.fontMono, fontSize: 11 }}>
              — e.g. "a photography studio with a cyclorama and overhead rigging"
            </span>
          </button>
        ) : (
          <div>
            <div style={{ fontFamily: t.fontMono, fontSize: 10, color: t.muted, letterSpacing: 1.5, marginBottom: 8 }}>
              YOUR CONCEPT
            </div>
            <textarea
              autoFocus
              value={draftText}
              onChange={(e) => setDraftText(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) submit();
                if (e.key === 'Escape') { setComposerOpen(false); setDraftText(''); }
              }}
              placeholder="A photography studio with a white cyclorama, modular lighting rig, makeup station, and a small client lounge."
              style={{
                width: '100%', minHeight: 90, padding: 12, fontFamily: t.fontBody, fontSize: 15,
                border: `1px solid ${t.line}`, borderRadius: t.radius, background: t.panel,
                color: t.fg, resize: 'vertical', outline: 'none', lineHeight: 1.5,
              }}
            />
            <div style={{ display: 'flex', gap: 8, marginTop: 10, alignItems: 'center' }}>
              <button onClick={submit} disabled={!draftText.trim()} style={{
                padding: '10px 16px', border: 'none', borderRadius: t.radius,
                background: draftText.trim() ? t.accent : t.line,
                color: draftText.trim() ? t.accentInk : t.muted,
                fontFamily: t.fontMono, fontSize: 11, letterSpacing: 1.5, fontWeight: 600,
                cursor: draftText.trim() ? 'pointer' : 'not-allowed',
              }}>+ ADD CONCEPT</button>
              <button onClick={() => { setComposerOpen(false); setDraftText(''); }} style={{
                padding: '10px 16px', border: `1px solid ${t.line}`, borderRadius: t.radius,
                background: 'transparent', color: t.fg, fontFamily: t.fontMono, fontSize: 11,
                letterSpacing: 1.5, cursor: 'pointer',
              }}>CANCEL</button>
              <span style={{ marginLeft: 'auto', fontFamily: t.fontMono, fontSize: 10, color: t.muted }}>
                ⌘↵ to add · ESC to cancel
              </span>
            </div>
          </div>
        )}
      </div>

      {/* Inspiration strip — real renders, proof of power */}
      <SGInspirationStrip t={t} />

      {/* Filter chips */}
      <div style={{ display: 'flex', gap: 6, marginBottom: 20, flexWrap: 'wrap' }}>
        {tags.map((tag) => {
          // hide CUSTOM chip if no custom presets exist
          if (tag === 'CUSTOM' && (!customPresets || customPresets.length === 0)) return null;
          return (
            <button key={tag} onClick={() => setFilter(tag)} style={{
              padding: '7px 12px', border: `1px solid ${t.line}`,
              background: filter === tag ? t.fg : 'transparent',
              color: filter === tag ? t.fgInv : t.fg,
              fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.2, cursor: 'pointer',
              borderRadius: t.radius,
            }}>{tag}{tag === 'CUSTOM' && customPresets ? ` · ${customPresets.length}` : ''}</button>
          );
        })}
      </div>

      {/* Preset grid */}
      <div className="sg-preset-grid" style={{
        display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 14,
      }}>
        {visible.map((p) => {
          const sel = selectedPresets.includes(p.id);
          const isCustom = p.custom;
          return (
            <div key={p.id} style={{
              position: 'relative',
              border: `1.5px solid ${sel ? t.accent : t.line}`,
              background: t.panel, borderRadius: t.radius, overflow: 'hidden',
              outline: sel ? `2px solid ${t.accent}33` : 'none',
              transition: 'transform .15s, border-color .15s',
              transform: sel ? 'translateY(-2px)' : 'none',
            }}>
              <button onClick={() => togglePreset(p.id)} style={{
                textAlign: 'left', padding: 0, border: 'none', background: 'transparent',
                cursor: 'pointer', width: '100%', fontFamily: 'inherit', color: 'inherit',
                display: 'block',
              }}>
                <SGPresetThumb t={t} preset={p} selected={sel} refUrl={refUrl} />
                <div style={{ padding: '10px 12px 12px' }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
                    <div style={{ fontFamily: t.fontDisplay, fontWeight: 600, fontSize: 16, letterSpacing: '-0.01em' }}>{p.name}</div>
                    <div style={{
                      fontFamily: t.fontMono, fontSize: 9, color: isCustom ? t.accentInk : t.muted,
                      letterSpacing: 1, background: isCustom ? t.accent : 'transparent',
                      padding: isCustom ? '2px 6px' : 0, borderRadius: 2, flexShrink: 0,
                    }}>{p.tag}</div>
                  </div>
                  <div style={{ fontFamily: t.fontBody, fontSize: 12, color: t.muted, marginTop: 4, lineHeight: 1.4, display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
                    {p.blurb}
                  </div>
                </div>
              </button>
              {isCustom && removeCustomPreset && (
                <button
                  onClick={(e) => { e.stopPropagation(); removeCustomPreset(p.id); }}
                  title="Remove this concept"
                  style={{
                    position: 'absolute', top: 8, right: 8, width: 22, height: 22, borderRadius: '50%',
                    border: 'none', background: 'rgba(0,0,0,0.55)', color: '#fff',
                    fontFamily: t.fontMono, fontSize: 11, fontWeight: 700, cursor: 'pointer',
                    display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0,
                    zIndex: 2,
                  }}
                >×</button>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

// Decorative thumb — tinted band over reference.
function SGPresetThumb({ t, preset, selected, refUrl }) {
  return (
    <div style={{
      position: 'relative', aspectRatio: '4/3', background: '#222', overflow: 'hidden',
      borderBottom: `1px solid ${t.line}`,
    }}>
      <img src={refUrl || SG_REF_IMG} alt="" style={{
        width: '100%', height: '100%', objectFit: 'cover', display: 'block',
        filter: `hue-rotate(${preset.hue}deg) saturate(${selected ? 1.3 : 0.9}) ${selected ? '' : 'brightness(0.85)'}`,
      }} />
      <div style={{
        position: 'absolute', inset: 0, mixBlendMode: 'multiply',
        background: `linear-gradient(135deg, hsla(${preset.hue},80%,55%,${selected ? 0.45 : 0.3}), hsla(${preset.hue + 30},70%,40%,${selected ? 0.4 : 0.2}))`,
      }} />
      <div style={{
        position: 'absolute', top: 8, left: 8, fontFamily: t.fontMono, fontSize: 9,
        letterSpacing: 1, color: '#fff', background: 'rgba(0,0,0,.6)', padding: '2px 6px',
      }}>
        {preset.id.toUpperCase()}-{preset.hue.toString().padStart(3, '0')}
      </div>
      {selected && (
        <div style={{
          position: 'absolute', top: 8, right: 8, width: 22, height: 22, borderRadius: '50%',
          background: t.accent, color: t.accentInk, display: 'flex', alignItems: 'center',
          justifyContent: 'center', fontFamily: t.fontMono, fontSize: 12, fontWeight: 700,
        }}>✓</div>
      )}
    </div>
  );
}

// ── Generating state ──────────────────────────────────────────
// Now driven by the real feedItems array — shows a tile per item with
// queued / running / done / error status. The big "scan" hero on top
// gives overall progress.
function SGGenerating({ t, progress, feedItems, refUrl }) {
  const total = feedItems.length;
  const completed = feedItems.filter((i) => i.status === 'done' || i.status === 'error').length;
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '40px 32px 80px', gap: 24, overflowY: 'auto', height: '100%' }}>
      <div style={{ fontFamily: t.fontMono, fontSize: 11, letterSpacing: 3, color: t.muted }}>
        RENDERING {completed} / {total}
      </div>
      <div style={{ position: 'relative', width: '70%', maxWidth: 640, aspectRatio: '4/3', background: '#111', overflow: 'hidden', borderRadius: t.radius }}>
        <img src={refUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', filter: 'grayscale(0.4) brightness(0.8)' }} />
        <div style={{ position: 'absolute', left: 0, right: 0, top: `${progress}%`, height: 2, background: t.accent, boxShadow: `0 0 24px 4px ${t.accent}` }} />
        <div style={{
          position: 'absolute', inset: 0, pointerEvents: 'none',
          backgroundImage: `linear-gradient(${t.accent}33 1px, transparent 1px), linear-gradient(90deg, ${t.accent}33 1px, transparent 1px)`,
          backgroundSize: '40px 40px', opacity: 0.4,
        }} />
        <div style={{ position: 'absolute', right: 16, top: 16, fontFamily: t.fontMono, fontSize: 32, fontWeight: 700, color: '#fff' }}>
          {Math.floor(progress)}<span style={{ fontSize: 14, opacity: 0.6 }}>%</span>
        </div>
        <div style={{ position: 'absolute', left: 16, bottom: 16, fontFamily: t.fontMono, fontSize: 11, color: '#fff', background: 'rgba(0,0,0,.7)', padding: '6px 10px', letterSpacing: 1 }}>
          {window.SG_NB_MOCK !== false ? 'mock pipeline · 2s/render · same camera, same bones' : 'live pipeline · same camera, same bones'}
        </div>
      </div>
      <div style={{ width: '70%', maxWidth: 640, height: 4, background: t.line, borderRadius: 2, overflow: 'hidden' }}>
        <div style={{ width: `${progress}%`, height: '100%', background: t.accent, transition: 'width .2s linear' }} />
      </div>
      {/* Per-item tiles */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 8, width: '90%', maxWidth: 1100 }}>
        {feedItems.map((it) => (
          <div key={it.key} style={{
            border: `1px solid ${it.status === 'done' ? t.accent : t.line}`, borderRadius: t.radius,
            padding: 8, fontFamily: t.fontMono, fontSize: 9, color: t.muted, letterSpacing: 0.5,
            background: it.status === 'running' ? `${t.accent}11` : 'transparent',
            display: 'flex', flexDirection: 'column', gap: 4,
          }}>
            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
              <span style={{ color: t.fg }}>{it.preset.name}</span>
              <span>v{it.variant}</span>
            </div>
            <div style={{ height: 2, background: t.line, borderRadius: 1, overflow: 'hidden' }}>
              <div style={{
                width: it.status === 'done' ? '100%' : it.status === 'running' ? '60%' : it.status === 'error' ? '100%' : '0%',
                height: '100%',
                background: it.status === 'error' ? '#c33' : t.accent,
                transition: 'width .3s linear',
              }} />
            </div>
            <div style={{ color: it.status === 'error' ? '#c33' : t.muted }}>
              {it.status === 'queued' ? 'QUEUED' : it.status === 'running' ? 'RENDERING…' : it.status === 'done' ? '✓ DONE' : '✗ ERROR'}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ── Feed (results) ────────────────────────────────────────────
function SGFeed({ t, feedItems, feedRef, activeIdx, pickedForCluster, togglePick, refUrl, showPrompt }) {
  const active = feedItems[activeIdx];
  return (
    <div style={{ position: 'relative', height: '100%', overflow: 'hidden' }}>
      {/* sticky meta bar */}
      <div style={{
        position: 'absolute', top: 0, left: 0, right: 0, padding: '14px 32px',
        display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        background: `${t.bg}f5`, backdropFilter: 'blur(6px)',
        borderBottom: `1px solid ${t.line}`, zIndex: 5,
      }}>
        <div style={{ fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.5, color: t.muted }}>
          STEP 03 / 03 · GENERATED FEED
        </div>
        <div style={{ fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.5, color: t.fg }}>
          {String(activeIdx + 1).padStart(2, '0')} / {String(feedItems.length).padStart(2, '0')}
          {active && <span style={{ color: t.muted }}> · {active.preset.name.toUpperCase()} · VAR-{active.seed}</span>}
        </div>
        <div style={{ fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.5, color: t.muted }}>
          {pickedForCluster.size} PICKED FOR CLUSTER
        </div>
      </div>

      <div ref={feedRef} style={{
        height: '100%', overflowY: 'auto', padding: '64px 32px 120px',
        scrollSnapType: 'y proximity',
      }}>
        {feedItems.map((it, i) => (
          <SGFeedCard
            key={it.key}
            t={t}
            item={it}
            index={i}
            picked={pickedForCluster.has(it.key)}
            onPick={() => togglePick(it.key)}
            refUrl={refUrl}
            showPrompt={showPrompt}
          />
        ))}
        <div style={{ textAlign: 'center', fontFamily: t.fontMono, fontSize: 10, color: t.muted, padding: '32px 0', letterSpacing: 1.5 }}>
          ── END OF FEED ──
        </div>
      </div>

      {/* side rail with thumbnails */}
      <SGFeedRail t={t} feedItems={feedItems} activeIdx={activeIdx} feedRef={feedRef} pickedForCluster={pickedForCluster} refUrl={refUrl} />
    </div>
  );
}

function SGFeedCard({ t, item, index, picked, onPick, refUrl, showPrompt }) {
  const [hover, setHover] = React.useState(false);
  const [refineOpen, setRefineOpen] = React.useState(false);
  const [refining, setRefining] = React.useState(false);
  // Real image URL when available; fall back to ref + tinted overlay so
  // mock + real pipelines render the same caption frame.
  const imgSrc = item.imageUrl || refUrl;

  // One-tap refinement chips. Each one is a short noun phrase appended to the
  // existing prompt. Selecting any chip kicks the mock pipeline and replaces
  // the card's image in place (no lineage history per user spec).
  const REFINE_CHIPS = [
    { label: 'More like this', mod: 'preserve composition, same camera, same furniture layout, vary materials' },
    { label: 'Warmer', mod: 'warmer color temperature, golden hour, amber accent lighting' },
    { label: 'Cooler', mod: 'cooler color temperature, overcast daylight, blue accent lighting' },
    { label: 'Less furniture', mod: 'sparser, fewer objects, more negative space' },
    { label: 'More people', mod: 'busier, occupied, group of people interacting naturally' },
    { label: 'Add a bar', mod: 'add a bar counter with stools and bottle display' },
    { label: 'Add greenery', mod: 'add large potted plants and hanging vegetation' },
    { label: 'Night mode', mod: 'evening, artificial lighting, exterior darkness through windows' },
  ];

  const handleRefine = async (chip) => {
    if (refining) return;
    setRefining(true);
    try {
      // Re-run the mock pipeline with the modifier appended; replace the card's
      // image in place so the user sees the iterated result on the same card.
      const newUrl = await window.sgRefineItem?.(item, chip.mod);
      if (newUrl) item.imageUrl = newUrl;
    } finally {
      setRefining(false);
      setRefineOpen(false);
    }
  };

  return (
    <article data-feed-card style={{
      maxWidth: 1100, margin: '0 auto 56px', scrollSnapAlign: 'center',
    }}>
      {/* Card header */}
      <header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', gap: 16, marginBottom: 12 }}>
        <div>
          <div style={{ fontFamily: t.fontMono, fontSize: 10, color: t.muted, letterSpacing: 1.5 }}>
            #{String(index + 1).padStart(2, '0')} · {item.preset.tag} · VAR {item.variant} OF · SEED {item.seed}
          </div>
          <h2 style={{
            fontFamily: t.fontDisplay, fontSize: 56, fontWeight: 700, letterSpacing: '-0.03em',
            margin: '6px 0 0', lineHeight: 0.95,
          }}>
            {item.preset.name}
          </h2>
        </div>
        <div style={{ display: 'flex', gap: 6 }}>
          <button onClick={onPick} style={sgPickBtn(t, picked)}>
            {picked ? '✓ PICKED' : '+ PICK'}
          </button>
          <button onClick={() => setRefineOpen((v) => !v)} style={sgIconBtn(t, refineOpen)} title="Refine with prompts">↻</button>
          <button onClick={() => window.sgDownloadItem(item)} style={sgIconBtn(t)} title="Download (watermarked)">↓</button>
        </div>
      </header>

      {/* Refine chip rail */}
      {refineOpen && (
        <div style={{
          display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 12,
          padding: '12px 14px', borderRadius: t.radius,
          background: t.fg, color: t.fgInv,
          alignItems: 'center',
        }}>
          <span style={{ fontFamily: t.fontMono, fontSize: 9, letterSpacing: 1.5, opacity: 0.6, marginRight: 6 }}>
            {refining ? 'REFINING…' : 'REFINE'}
          </span>
          {REFINE_CHIPS.map((chip) => (
            <button
              key={chip.label}
              onClick={() => handleRefine(chip)}
              disabled={refining}
              style={{
                padding: '7px 12px',
                fontFamily: t.fontBody, fontSize: 12, fontWeight: 500,
                background: 'transparent',
                color: t.fgInv,
                border: `1px solid ${t.fgInv}44`,
                borderRadius: 999,
                cursor: refining ? 'wait' : 'pointer',
                opacity: refining ? 0.4 : 1,
                whiteSpace: 'nowrap',
              }}
              onMouseEnter={(e) => { if (!refining) { e.currentTarget.style.background = t.accent; e.currentTarget.style.color = t.accentInk; e.currentTarget.style.borderColor = t.accent; } }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = t.fgInv; e.currentTarget.style.borderColor = `${t.fgInv}44`; }}
            >{chip.label}</button>
          ))}
          <button
            onClick={() => setRefineOpen(false)}
            style={{
              marginLeft: 'auto', background: 'transparent', border: 'none',
              color: `${t.fgInv}88`, cursor: 'pointer', fontSize: 16, padding: '0 4px',
            }}
            title="Close"
          >×</button>
        </div>
      )}

      {/* Image */}
      <div onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
        position: 'relative', aspectRatio: '4/3', background: '#111', overflow: 'hidden',
        borderRadius: t.radius, border: picked ? `3px solid ${t.accent}` : `1px solid ${t.line}`,
        cursor: 'pointer',
      }} onClick={onPick}>
        <img src={imgSrc} alt="" style={{
          width: '100%', height: '100%', objectFit: 'cover',
          // Only apply mock tint if we're showing the fallback ref image
          filter: item.imageUrl ? 'none' : `hue-rotate(${item.preset.hue}deg) saturate(${0.8 + item.intensity / 100 * 0.7}) brightness(${item.time === 'Night' ? 0.5 : item.time === 'Dusk' ? 0.7 : 1})`,
        }} />
        {!item.imageUrl && (
          <div style={{
            position: 'absolute', inset: 0, mixBlendMode: 'multiply', pointerEvents: 'none',
            background: `linear-gradient(${item.variant * 45}deg, hsla(${item.preset.hue},75%,${item.time === 'Night' ? 30 : 50}%,${0.15 + item.intensity / 100 * 0.4}), hsla(${item.preset.hue + 60},65%,${item.time === 'Dusk' ? 35 : 55}%,${0.1 + item.intensity / 100 * 0.3}))`,
          }} />
        )}
        {/* AI-rendered annotation — shows prompt if showPrompt is on */}
        <div style={{
          position: 'absolute', left: 16, bottom: 16, fontFamily: t.fontMono, fontSize: 11,
          color: '#fff', background: 'rgba(0,0,0,.72)', padding: '8px 12px', letterSpacing: 0.5,
          maxWidth: '70%', lineHeight: 1.5,
        }}>
          [{window.SG_NB_MOCK !== false ? 'mock' : 'live'} · {item.preset.id}-{item.seed}]<br/>
          {showPrompt && item.prompt ? item.prompt : item.preset.blurb}
        </div>
        <div style={{
          position: 'absolute', top: 12, right: 12, fontFamily: t.fontMono, fontSize: 9,
          color: '#fff', background: 'rgba(0,0,0,.55)', padding: '4px 8px', letterSpacing: 1,
        }}>
          {item.time.toUpperCase()} · {item.activity.toUpperCase()} · {item.intensity}%
        </div>
        <div style={{
          position: 'absolute', top: 12, left: 12, fontFamily: t.fontMono, fontSize: 9,
          color: t.accentInk, background: t.accent, padding: '4px 8px', letterSpacing: 1.5, fontWeight: 700,
        }} title="AI-generated concept image. Stamped on download.">
          AI RENDER
        </div>
        {hover && (
          <div style={{
            position: 'absolute', inset: 0, background: 'rgba(0,0,0,.25)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <div style={{
              fontFamily: t.fontMono, fontSize: 11, letterSpacing: 2, color: '#fff',
              background: t.accent, padding: '10px 16px', color: t.accentInk, fontWeight: 600,
            }}>
              {picked ? 'CLICK TO UNPICK' : 'CLICK TO PICK FOR CLUSTER'}
            </div>
          </div>
        )}
      </div>

      {/* Caption row */}
      <footer style={{ marginTop: 12, display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 16, fontFamily: t.fontMono, fontSize: 10, color: t.muted }}>
        <SGMeta t={t} k="REFERENCE" v="WAREHOUSE-A" />
        <SGMeta t={t} k="MODEL" v="sg-diff-v0.4" />
        <SGMeta t={t} k="LIGHTING" v={item.time.toUpperCase()} />
        <SGMeta t={t} k="OCCUPANCY" v={item.activity.toUpperCase()} />
      </footer>
    </article>
  );
}

function SGMeta({ t, k, v }) {
  return (
    <div>
      <div style={{ color: t.muted, letterSpacing: 1.5 }}>{k}</div>
      <div style={{ color: t.fg, marginTop: 2, letterSpacing: 0.5 }}>{v}</div>
    </div>
  );
}

function sgPickBtn(t, picked) {
  return {
    padding: '10px 14px', fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.5, fontWeight: 600,
    border: `1.5px solid ${picked ? t.accent : t.line}`, borderRadius: t.radius,
    background: picked ? t.accent : 'transparent', color: picked ? t.accentInk : t.fg,
    cursor: 'pointer',
  };
}
function sgIconBtn(t, active) {
  return {
    width: 38, height: 38, border: `1.5px solid ${active ? t.accent : t.line}`, borderRadius: t.radius,
    background: active ? t.accent : 'transparent',
    color: active ? t.accentInk : t.fg,
    cursor: 'pointer', fontSize: 14,
    display: 'flex', alignItems: 'center', justifyContent: 'center',
  };
}

// Side rail thumbnail strip
function SGFeedRail({ t, feedItems, activeIdx, feedRef, pickedForCluster, refUrl }) {
  const scrollTo = (i) => {
    const cards = feedRef.current?.querySelectorAll('[data-feed-card]');
    if (!cards || !cards[i]) return;
    feedRef.current.scrollTo({ top: cards[i].offsetTop - 64, behavior: 'smooth' });
  };
  return (
    <div style={{
      position: 'absolute', right: 16, top: 80, bottom: 16, width: 64,
      display: 'flex', flexDirection: 'column', gap: 4, overflowY: 'auto',
      padding: 4, scrollbarWidth: 'none',
    }}>
      {feedItems.map((it, i) => {
        const active = i === activeIdx;
        const picked = pickedForCluster.has(it.key);
        return (
          <button key={it.key} onClick={() => scrollTo(i)} style={{
            width: 56, height: 42, padding: 0, position: 'relative', overflow: 'hidden',
            border: active ? `2px solid ${t.fg}` : picked ? `2px solid ${t.accent}` : `1px solid ${t.line}`,
            cursor: 'pointer', background: '#111', borderRadius: 3,
          }}>
            <img src={it.imageUrl || refUrl} alt="" style={{
              width: '100%', height: '100%', objectFit: 'cover',
              filter: it.imageUrl ? 'none' : `hue-rotate(${it.preset.hue}deg) saturate(1.2)`,
            }} />
            {picked && (
              <div style={{
                position: 'absolute', top: 2, right: 2, width: 12, height: 12, borderRadius: '50%',
                background: t.accent, color: t.accentInk, fontSize: 8, fontWeight: 700,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
              }}>✓</div>
            )}
          </button>
        );
      })}
    </div>
  );
}

// ── Cluster dock ──────────────────────────────────────────────
function SGClusterDock({ t, count, pickedItems, onClear }) {
  const [downloading, setDownloading] = React.useState(false);
  const onDownload = async () => {
    if (downloading) return;
    setDownloading(true);
    try {
      await window.sgDownloadCluster(pickedItems || []);
    } finally {
      setDownloading(false);
    }
  };
  return (
    <div style={{
      position: 'absolute', left: 340, right: 16, bottom: 16, padding: '14px 20px',
      background: t.fg, color: t.fgInv, borderRadius: t.radius,
      display: 'flex', alignItems: 'center', gap: 16, zIndex: 20,
      boxShadow: '0 12px 40px rgba(0,0,0,.25)',
      maxWidth: 880, marginLeft: 'auto', marginRight: 'auto',
    }}>
      <div style={{ fontFamily: t.fontMono, fontSize: 10, letterSpacing: 2, opacity: 0.6 }}>CLUSTER</div>
      <div style={{ fontFamily: t.fontDisplay, fontSize: 22, fontWeight: 600 }}>
        {count} render{count === 1 ? '' : 's'} <span style={{ opacity: 0.5, fontWeight: 400 }}>· watermarked on export</span>
      </div>
      <div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
        <button onClick={onClear} style={{
          padding: '10px 14px', fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.5,
          background: 'transparent', color: t.fgInv, border: `1px solid ${t.fgInv}55`,
          cursor: 'pointer', borderRadius: t.radius,
        }}>CLEAR</button>
        <button onClick={onDownload} disabled={downloading} style={{
          padding: '10px 14px', fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.5, fontWeight: 600,
          background: t.accent, color: t.accentInk, border: 'none',
          cursor: downloading ? 'wait' : 'pointer', borderRadius: t.radius, opacity: downloading ? 0.6 : 1,
        }}>{downloading ? 'DOWNLOADING…' : '↓ DOWNLOAD CLUSTER'}</button>
        <button style={{
          padding: '10px 14px', fontFamily: t.fontMono, fontSize: 10, letterSpacing: 1.5, fontWeight: 600,
          background: t.accent, color: t.accentInk, border: 'none',
          cursor: 'pointer', borderRadius: t.radius,
        }}>↗ SHARE BOARD</button>
      </div>
    </div>
  );
}

// ── Watermark + download utilities ─────────────────────────────
// Stamps a non-removable disclosure strip across the bottom of the
// rendered image before download. CRE legal/ethical guardrail: AI
// renders must not be confused with photos of real, leased space.
async function sgStampWatermark(blob) {
  const url = URL.createObjectURL(blob);
  try {
    const img = await new Promise((res, rej) => {
      const i = new Image();
      i.onload = () => res(i);
      i.onerror = rej;
      i.src = url;
    });
    const c = document.createElement('canvas');
    c.width = img.width; c.height = img.height;
    const ctx = c.getContext('2d');
    ctx.drawImage(img, 0, 0);

    // Disclosure strip — bottom 5% of the image, dark band with light type.
    const stripH = Math.max(40, Math.round(img.height * 0.05));
    const padX = Math.round(img.width * 0.03);
    ctx.fillStyle = 'rgba(20, 17, 15, 0.88)';
    ctx.fillRect(0, img.height - stripH, img.width, stripH);

    // Left side: AI RENDER badge
    const badgeFont = Math.max(13, Math.round(stripH * 0.36));
    ctx.fillStyle = '#ff6b2c';
    ctx.font = `700 ${badgeFont}px "JetBrains Mono", monospace`;
    ctx.textBaseline = 'middle';
    ctx.fillText('AI RENDER', padX, img.height - stripH / 2);

    // Right side: disclosure
    const discFont = Math.max(11, Math.round(stripH * 0.30));
    ctx.fillStyle = 'rgba(246, 241, 231, 0.85)';
    ctx.font = `500 ${discFont}px "JetBrains Mono", monospace`;
    ctx.textAlign = 'right';
    ctx.fillText(
      'INSPIRATIONAL CONCEPT  ·  NOT A REPRESENTATION OF THE ACTUAL SPACE',
      img.width - padX,
      img.height - stripH / 2,
    );

    // Tiny SpaceGen mark, centered, small
    ctx.fillStyle = 'rgba(246, 241, 231, 0.5)';
    ctx.textAlign = 'center';
    ctx.font = `500 ${Math.max(9, Math.round(stripH * 0.22))}px "JetBrains Mono", monospace`;
    ctx.fillText('SPACEGEN', img.width / 2, img.height - stripH / 2);

    return await new Promise((res) => c.toBlob(res, 'image/png'));
  } finally {
    URL.revokeObjectURL(url);
  }
}

async function sgDownloadItem(item) {
  if (!item.imageUrl) {
    alert('Image is still rendering — try again in a moment.');
    return;
  }
  try {
    const resp = await fetch(item.imageUrl);
    const raw = await resp.blob();
    const stamped = await sgStampWatermark(raw);
    const url = URL.createObjectURL(stamped);
    const a = document.createElement('a');
    a.href = url;
    const safeName = (item.preset.name || 'render').replace(/[^a-z0-9]+/gi, '-').toLowerCase();
    a.download = `spacegen-${safeName}-seed${item.seed}.png`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  } catch (err) {
    console.error('Download failed:', err);
    alert('Download failed: ' + (err && err.message || err));
  }
}

async function sgDownloadCluster(items) {
  // Sequential to avoid hammering the browser; small batch sizes.
  for (const item of items) {
    if (!item.imageUrl) continue;
    await sgDownloadItem(item);
    await new Promise((r) => setTimeout(r, 250));
  }
}

// Export to window for the host file
Object.assign(window, { ThemedSpaceGen, SG_PRESETS, sgDownloadItem, sgDownloadCluster });
