/* PLATFORM page — full breakdown of what users can do in the studio.
   Sections: Hero · Workflow (3 steps) · What you can direct (4 inputs) · What comes out · Resolutions · CTA */

function PlatformPage() {
  return (
    <div data-screen-label="02 Platform">
      <PageHero
        eyebrow="Platform"
        breadcrumb="Platform"
        title={<>The studio,<br /><em style={{ fontStyle: 'italic' }}>broken down.</em></>}
        titleStyle={{ fontSize: 'clamp(40px, 7vw, 88px)', letterSpacing: '-0.05em', maxWidth: '13ch', textWrap: 'balance' }}
        subtitle="From the flat-lay you upload to the 4K frame you publish — the same five steps every time."
        figure={{ src: 'assets/hero-platform-leather.jpg', label: 'Studio · 4K', subject: 'editorial', position: 'center 30%', idx: 1 }}
        ctaLabel="Start free trial" />
      

      {/* Section 1 — What you can direct (intro + sticky index + four input sub-sections + Output) */}
      <DirectInputs />

      <BatchUploadSection />

      <CTABand />
    </div>);

}

window.PlatformPage = PlatformPage;

/* Build a non-empty alt description from an asset filename (for SEO / image search). */
function frameAlt(src) {
  if (!src) return 'ViZO \u2014 AI-generated fashion image';
  const base = String(src).split('/').pop().replace(/\.[a-z0-9]+$/i, '');
  const words = base.replace(/[-_]+/g, ' ').replace(/\b\d+\b/g, '').replace(/\s+/g, ' ').trim();
  return words ? `ViZO \u2014 ${words}` : 'ViZO \u2014 AI-generated fashion image';
}

/* ---------- Shared page hero (used by Platform/Solutions/Gallery/Pricing) ---------- */
function PageHero({ eyebrow, title, subtitle, figure, ctaLabel, ctaVariant = 'primary', breadcrumb, titleStyle }) {
  const { isMobile } = useBreakpoint();
  return (
    <section style={{ padding: `clamp(28px, 4vw, 40px) var(--pad-x) clamp(56px, 9vw, 96px)`, background: 'var(--paper)' }}>
      <div style={{ maxWidth: 'var(--container)', margin: '0 auto' }}>
        {breadcrumb && <Breadcrumbs trail={[{ l: breadcrumb }]} />}
        <div style={{ marginTop: breadcrumb ? 24 : 0, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1.1fr 1fr', gap: 'clamp(28px, 5vw, 72px)', alignItems: 'center' }}>
        <div>
          <Eyebrow>{eyebrow}</Eyebrow>
          <h1 style={{
              fontFamily: 'var(--font-display)', fontWeight: 300,
              lineHeight: 0.98, letterSpacing: '-0.04em',
              color: 'var(--ink)', margin: '20px 0 0', fontSize: 'clamp(40px, 6vw, 96px)', ...titleStyle
            }}>{title}</h1>
          {subtitle &&
            <p style={{ marginTop: 'clamp(20px, 3vw, 28px)', fontSize: 'clamp(15px, 1.6vw, 18px)', lineHeight: 1.6, color: 'var(--graphite)', maxWidth: '50ch' }}>{subtitle}</p>
            }
          {ctaLabel &&
            <div style={{ marginTop: 36, display: 'flex', gap: 12, flexWrap: 'wrap' }}>
              <Button variant={ctaVariant} size="large" arrow onClick={() => {window.location.href = 'https://app.vizostudio.ai';}}>{ctaLabel}</Button>
              <Link to="/contact"><Button variant="ghost" size="large">Contact us</Button></Link>
            </div>
            }
        </div>
        {figure &&
          <div style={{ position: 'relative' }}>
            <FashionPlate ratio="4 / 5" {...figure} />
          </div>
          }
      </div>
      </div>
    </section>);

}

window.PageHero = PageHero;

/* ---------- Workflow strip: three numbered steps ---------- */
function WorkflowStrip() {
  const { isMobile } = useBreakpoint();
  const steps = [
  { n: '01', title: 'Inputs', desc: 'Upload a flat-lay, ghost-mannequin shot, or batch of SKUs.' },
  { n: '02', title: 'Generation', desc: 'Direct the model, the set, the light. Iterate fast.' },
  { n: '03', title: 'Output', desc: 'Export 1K / 2K / 4K with SEO copy and tags by SKU.' }];

  return (
    <section style={{ background: 'var(--canvas)', padding: `clamp(56px, 8vw, 96px) var(--pad-x)`, borderTop: '1px solid var(--bone)', borderBottom: '1px solid var(--bone)' }}>
      <div style={{ maxWidth: 'var(--container)', margin: '0 auto' }}>
        <Eyebrow>Process · 01</Eyebrow>
        <h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 400, fontSize: 'clamp(34px, 5.8vw, 56px)', lineHeight: 1.05, letterSpacing: '-0.03em', margin: '20px 0 clamp(36px, 5vw, 56px)', color: 'var(--ink)', maxWidth: '18ch' }}>
          From idea to <em style={{ fontStyle: 'italic' }}>output</em> in minutes.
        </h2>
        <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)', gap: 'clamp(28px, 4vw, 48px)' }}>
          {steps.map((s) =>
          <div key={s.n} style={{ borderTop: '1px solid var(--ink)', paddingTop: 24 }}>
              <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
                <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, letterSpacing: '0.1em', color: 'var(--slate)' }}>STEP / {s.n}</span>
                <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--slate)' }}>—</span>
              </div>
              <h3 style={{ fontFamily: 'var(--font-display)', fontWeight: 400, fontSize: 'clamp(28px, 3.6vw, 36px)', letterSpacing: '-0.03em', margin: '20px 0 12px', color: 'var(--ink)' }}>{s.title}</h3>
              <p style={{ fontSize: 15, lineHeight: 1.6, color: 'var(--graphite)', margin: 0, maxWidth: '32ch' }}>{s.desc}</p>
            </div>
          )}
        </div>
      </div>
    </section>);

}

/* ===========================================================
   Section 3 — What you can direct
   Intro header + four sub-sections (Model, Environment, Products, Accessories)
   Same alternating-image template as the Solutions problem sections,
   but features grid instead of pain/answer bullets.
   =========================================================== */

const DIRECT_INPUTS = [
{
  id: 'model',
  label: 'Model',
  badge: 'Input 01',
  headlineLead: 'Any face.',
  headlineEm: 'Any frame.',
  body: 'Generate a model to spec, upload a digital twin of someone on your roster, or pick from our library. The same person can carry an entire collection.',
  caption: { left: 'Prompt / Digital twin', right: 'digital twin' },
  plate: { src: 'assets/model-portrait-alice.jpg', position: 'center 15%' },
  features: [
  { icon: 'user', title: 'Generate from prompt', desc: 'Any age, build, or look. Direct it like a casting brief.' },
  { icon: 'digitalTwin', title: 'Digital twin upload', desc: 'Build a reusable likeness from one set of references.' },
  { icon: 'grid', title: 'Model library', desc: 'Pre-built models, ready to drop into any shot.' },
  { icon: 'consistency', title: 'Consistent across shots', desc: 'Same face, every frame, every campaign.' }]

},
{
  id: 'environment',
  label: 'Environment',
  badge: 'Input 02',
  headlineLead: 'Any setting.',
  headlineEm: 'Without the travel.',
  body: 'Studio backdrops, real-world locations, editorial interiors. Direct the set the way you would scout one, then lock it for the rest of the campaign.',
  caption: { left: 'Environment · 4K', right: 'location · controlled' },
  plate: { src: 'assets/environment-south-france.jpeg', position: 'center 50%' },
  features: [
  { icon: 'studioLight', title: 'Studio backdrops', desc: 'Clean, controlled, every aspect ratio.' },
  { icon: 'globe', title: 'Real-world locations', desc: 'Streets, interiors, gardens, beaches.' },
  { icon: 'editorialSets', title: 'Editorial sets', desc: 'Built environments with full creative direction.' },
  { icon: 'lock', title: 'Locked across shots', desc: 'One set, applied to every SKU in the batch.' }]

},
{
  id: 'products',
  label: 'Products',
  badge: 'Input 03',
  headlineLead: 'Your product.',
  headlineEm: 'Pixel-true.',
  body: 'Upload flat-lays, ghost mannequins, on-model, or live mannequin shots. The garment stays exact. Everything around it is direction.',
  caption: { left: 'Product · 4K', right: 'pixel-true' },
  plate: { src: 'assets/ghost-mannequin-blue-dress-white.png', fit: 'contain', bg: '#FFFFFF', position: 'center' },
  features: [
  { icon: 'maximize', title: 'Flat-lay input', desc: 'One shot drives a full campaign set.' },
  { icon: 'ghost', title: 'Ghost mannequin', desc: 'Bring shape and drape with no model overhead.' },
  { icon: 'user', title: 'On-model input', desc: 'Refit existing imagery to any new model.' },
  { icon: 'package', title: 'Bulk upload your inventory', desc: 'Prints, textures, and cut stay exact.' }]

},
{
  id: 'accessories',
  label: 'Accessories',
  badge: 'Input 04',
  headlineLead: 'Bags, shoes,',
  headlineEm: 'the full styling.',
  body: 'Layer accessories onto any look. Mix categories, swap pieces, restyle a model in seconds. The set adjusts around them.',
  caption: { left: 'Accessories · 4K', right: 'stacked + styled' },
  plates: [
  { src: 'assets/accessory-wedges-white.png', label: '', subject: '', fit: 'contain', bg: '#FFFFFF', position: 'center', alt: 'Espadrille wedge sandals on a white background' },
  { src: 'assets/accessory-bag-beaded-white.png', label: '', subject: '', fit: 'contain', bg: '#FFFFFF', position: 'center', alt: 'Woven raffia bag with beaded handle on a white background' },
  { src: 'assets/accessory-earrings-gold.png', label: '', subject: '', fit: 'contain', bg: '#FFFFFF', position: 'center', alt: 'Gold huggie hoop earrings on a white background' }],

  features: [
  { icon: 'handbag', title: 'Bags', desc: 'Hand-held, shoulder, crossbody, clutched.' },
  { icon: 'shoe', title: 'Footwear', desc: 'Heels, flats, boots, trainers, on any model.' },
  { icon: 'gem', title: 'Jewellery', desc: 'Fine, costume, layered.' },
  { icon: 'hat', title: 'Eyewear and headwear', desc: 'Sunglasses, hats, scarves, headbands.' }]

}];


window.DIRECT_INPUTS = DIRECT_INPUTS;

function DirectInputs() {
  return (
    <PlatformIndexProvider>
      <DirectInputsIntro />
      <PlatformIndexStrip />
      {DIRECT_INPUTS.map((input, i) =>
      <DirectInputSection key={input.id} input={input} index={i} />
      )}
      {(() => {
        const outputCol = (window.PLATFORM_COLUMNS || []).find((c) => c.heading === 'Output');
        return outputCol ? <PlatformSection col={outputCol} idx={4} sectionLabel="05" imageRight={false} bg="var(--paper)" /> : null;
      })()}
      <VideoStep />
    </PlatformIndexProvider>);

}

/* Five tab ids that the sticky strip and scroll-spy track. */
const PLATFORM_INDEX_TABS = [
{ id: 'model', label: 'Model' },
{ id: 'environment', label: 'Environment' },
{ id: 'products', label: 'Products' },
{ id: 'accessories', label: 'Accessories' },
{ id: 'output', label: 'Output' }];


function PlatformIndexProvider({ children }) {
  /* Scroll spy. Each tab corresponds to an element with id `input-{tab.id}`. */
  const [active, setActive] = React.useState(PLATFORM_INDEX_TABS[0].id);
  React.useEffect(() => {
    const onScroll = () => {
      const offset = 220; // nav (72) + strip (~64) + a little breathing room
      let current = PLATFORM_INDEX_TABS[0].id;
      for (const t of PLATFORM_INDEX_TABS) {
        const el = document.getElementById(`input-${t.id}`);
        if (!el) continue;
        const top = el.getBoundingClientRect().top;
        if (top - offset <= 0) current = t.id;
      }
      setActive(current);
    };
    window.addEventListener('scroll', onScroll, { passive: true });
    onScroll();
    return () => window.removeEventListener('scroll', onScroll);
  }, []);
  return (
    <PlatformIndexCtx.Provider value={{ active }}>
      {children}
    </PlatformIndexCtx.Provider>);

}

const PlatformIndexCtx = React.createContext({ active: PLATFORM_INDEX_TABS[0].id });
window.PlatformIndexCtx = PlatformIndexCtx;

function PlatformIndexStrip() {
  const { isMobile } = useBreakpoint();
  const { active } = React.useContext(PlatformIndexCtx);
  const jump = (id) => (e) => {
    e.preventDefault();
    const el = document.getElementById(`input-${id}`);
    if (!el) return;
    const stripH = isMobile ? 56 : 64;
    const navH = isMobile ? 64 : 72;
    const y = el.getBoundingClientRect().top + window.scrollY - (navH + stripH) + 1;
    window.scrollTo({ top: y, behavior: 'smooth' });
  };
  return (
    <nav aria-label="Platform steps" style={{
      position: 'sticky',
      top: isMobile ? 64 : 72,
      zIndex: 30,
      background: 'var(--canvas)',
      borderTop: '1px solid var(--bone)',
      borderBottom: '1px solid var(--bone)'
    }}>
      <div style={{ maxWidth: 'var(--container)', margin: '0 auto', padding: `0 var(--pad-x)` }}>
        <ul style={{
          display: 'flex', alignItems: 'stretch',
          gap: 'clamp(16px, 3vw, 40px)',
          listStyle: 'none', margin: 0, padding: 0,
          overflowX: 'auto', scrollbarWidth: 'none', msOverflowStyle: 'none'
        }} className="vizo-no-scrollbar">
          {PLATFORM_INDEX_TABS.map((t, i) => {
            const isActive = t.id === active;
            return (
              <li key={t.id} style={{ flex: '0 0 auto' }}>
                <a
                  href={`#input-${t.id}`}
                  onClick={jump(t.id)}
                  style={{
                    position: 'relative',
                    display: 'inline-flex', alignItems: 'center', gap: 8,
                    padding: `${isMobile ? 16 : 20}px 0`,
                    fontFamily: 'var(--font-mono)',
                    fontSize: isMobile ? 11 : 12,
                    letterSpacing: '0.1em',
                    textTransform: 'uppercase',
                    color: isActive ? 'var(--ink)' : 'var(--slate)',
                    textDecoration: 'none',
                    whiteSpace: 'nowrap',
                    transition: 'color 200ms var(--ease-standard)'
                  }}>
                  <span style={{ opacity: isActive ? 1 : 0.7 }}>{String(i + 1).padStart(2, '0')}</span>
                  <span aria-hidden="true" style={{ opacity: 0.5 }}>·</span>
                  <span style={{ fontWeight: isActive ? 600 : 500 }}>{t.label}</span>
                  <span style={{
                    position: 'absolute', left: 0, right: 0, bottom: -1, height: 2,
                    background: 'var(--ink)',
                    transform: isActive ? 'scaleX(1)' : 'scaleX(0)',
                    transformOrigin: 'left center',
                    transition: 'transform 250ms var(--ease-standard)'
                  }} />
                </a>
              </li>);

          })}
        </ul>
      </div>
      <style>{`.vizo-no-scrollbar::-webkit-scrollbar { display: none; }`}</style>
    </nav>);

}

window.PlatformIndexProvider = PlatformIndexProvider;
window.PlatformIndexStrip = PlatformIndexStrip;

window.DirectInputs = DirectInputs;

/* ---------- Section 3 intro header ---------- */
function DirectInputsIntro() {
  return (
    <section style={{
      background: 'var(--canvas)',
      padding: `clamp(64px, 8vw, 96px) var(--pad-x) clamp(40px, 5vw, 64px)`,
      borderTop: '1px solid var(--bone)'
    }}>
      <div style={{ maxWidth: 'var(--container)', margin: '0 auto' }}>
        <Eyebrow></Eyebrow>
        <h2 style={{
          fontFamily: 'var(--font-display)', fontWeight: 300,
          fontSize: 'clamp(40px, 6.4vw, 80px)', lineHeight: 0.98, letterSpacing: '-0.04em',
          color: 'var(--ink)', margin: '24px 0 0', maxWidth: '14ch', textWrap: 'pretty'
        }}>
          The shoot, under <em style={{ fontStyle: 'italic', fontWeight: 700 }}>your control.</em>
        </h2>
        <p style={{
          marginTop: 'clamp(20px, 2.5vw, 28px)',
          fontFamily: 'var(--font-sans)', fontSize: 'clamp(16px, 1.2vw, 19px)',
          lineHeight: 1.55, color: 'var(--graphite)',
          maxWidth: 600
        }}>
          Compose every frame the way you would on a real set — model, environment, products, and accessories all directed from one brief.
        </p>
      </div>
    </section>);

}

/* ---------- One input sub-section ---------- */
function DirectInputSection({ input, index }) {
  const { isMobile, isTablet } = useBreakpoint();
  // 01 image right, 02 image left, 03 image right, 04 image left
  const imageRight = index % 2 === 0;
  const bg = index % 2 === 0 ? 'var(--paper)' : 'var(--canvas)';
  const subN = String(index + 1);

  const text =
  <div>
      <Eyebrow>Step {subN} · {input.label}</Eyebrow>
      <h3 style={{
      fontFamily: 'var(--font-display)', fontWeight: 300,
      fontSize: 'clamp(34px, 5.2vw, 60px)',
      lineHeight: 1.0, letterSpacing: '-0.035em',
      color: 'var(--ink)',
      margin: '20px 0 0',
      maxWidth: '14ch',
      textWrap: 'pretty'
    }}>
        {input.headlineLead}<br /><em style={{ fontStyle: 'italic' }}>{input.headlineEm}</em>
      </h3>
      <p style={{
      marginTop: 'clamp(20px, 2.5vw, 28px)',
      fontSize: 'clamp(15.5px, 1.45vw, 17.5px)',
      lineHeight: 1.6, color: 'var(--graphite)',
      maxWidth: '52ch'
    }}>
        {input.body}
      </p>

      {/* 2×2 feature grid — no card backgrounds, icon + title + desc */}
      <div style={{
      marginTop: 'clamp(32px, 4vw, 48px)',
      paddingTop: 24, borderTop: '1px solid var(--bone)',
      display: 'grid',
      gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr',
      rowGap: 32, columnGap: 32
    }}>
        {input.features.map((f, i) => {
        const IconComp = Icon[f.icon] || Icon.sparkle;
        return (
          <div key={i} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
              <IconComp width="24" height="24" style={{ color: 'var(--ink)' }} />
              <div style={{
              fontFamily: 'var(--font-sans)', fontSize: 16, fontWeight: 600,
              color: 'var(--ink)', letterSpacing: '-0.005em'
            }}>{f.title}</div>
              <div style={{
              fontFamily: 'var(--font-sans)', fontSize: 14,
              lineHeight: 1.55, color: 'var(--slate)',
              maxWidth: '34ch'
            }}>{f.desc}</div>
            </div>);

      })}
      </div>
    </div>;


  const figure = input.plates ?
  <div style={{ position: 'relative' }}>
      <div style={{
      display: 'grid',
      gridTemplateColumns: '1fr 1fr',
      gridTemplateRows: 'auto auto',
      gap: 12
    }}>
        {/* Top: bag spanning full width (4:3) */}
        <div style={{ gridColumn: '1 / -1', position: 'relative' }}>
          <FashionPlate
          ratio="4 / 3"
          src={input.plates[0].src}
          position={input.plates[0].position}
          fit={input.plates[0].fit}
          bg={input.plates[0].bg}
          label={input.plates[0].label}
          subject={input.plates[0].subject}
          alt={input.plates[0].alt}
          idx={index} />
        
        </div>
        {/* Bottom row: sandals + earrings (1:1 each) */}
        {input.plates.slice(1).map((p, i) =>
      <FashionPlate
        key={i}
        ratio="1 / 1"
        src={p.src}
        position={p.position}
        fit={p.fit}
        bg={p.bg}
        label={p.label}
        subject={p.subject}
        alt={p.alt}
        idx={index + 1 + i} />

      )}
      </div>
      {/* Top-left input badge sits over the bag tile */}
      <div style={{
      position: 'absolute', top: 16, left: 16,
      background: 'var(--canvas)', color: 'var(--ink)',
      fontFamily: 'var(--font-mono)', fontSize: 11, letterSpacing: '0.1em',
      padding: '6px 10px', textTransform: 'uppercase', fontWeight: 600
    }}>
        {input.badge}
      </div>
    </div> :

  <div style={{ position: 'relative' }}>
      <FashionPlate
      ratio="4 / 5"
      src={input.plate.src}
      position={input.plate.position}
      fit={input.plate.fit}
      bg={input.plate.bg}
      label={input.caption.left}
      subject={input.caption.right}
      idx={index} />
    
      {/* Top-left input badge */}
      <div style={{
      position: 'absolute', top: 16, left: 16,
      background: 'var(--canvas)', color: 'var(--ink)',
      fontFamily: 'var(--font-mono)', fontSize: 11, letterSpacing: '0.1em',
      padding: '6px 10px', textTransform: 'uppercase', fontWeight: 600
    }}>
        {input.badge}
      </div>
    </div>;


  return (
    <section
      id={`input-${input.id}`}
      data-screen-label={`02 Platform › 03.${subN} ${input.label}`}
      style={{
        background: bg,
        padding: `clamp(64px, 8vw, 120px) var(--pad-x)`,
        borderTop: '1px solid var(--bone)'
      }}>
      
      <div style={{
        maxWidth: 'var(--container)', margin: '0 auto', width: '100%',
        display: 'grid',
        gridTemplateColumns: isMobile ? '1fr' : isTablet ? '1fr 1fr' : '1.05fr 1fr',
        gap: 'clamp(28px, 5vw, 72px)',
        alignItems: 'center'
      }}>
        {isMobile ?
        <>{figure}{text}</> :
        imageRight ?
        <>{text}{figure}</> :

        <>{figure}{text}</>
        }
      </div>
    </section>);

}

window.DirectInputSection = DirectInputSection;

/* ---------- Each Platform column rendered as a section ---------- */
function PlatformSection({ col, idx, sectionLabel, imageRight: imageRightProp, bg: bgProp }) {
  const { isMobile, isTablet } = useBreakpoint();
  const sectionN = sectionLabel || String(idx + 1).padStart(2, '0');
  const isDark = false;
  const imgs = {
    Inputs: { src: 'assets/ghost-mannequin-blue-dress.png', fit: 'contain', bg: 'var(--bone)', label: 'Input · ghost', subject: 'flat-lay', position: 'center' },
    Generation: { src: 'assets/hero-model-garden.png', position: 'center 22%', label: 'Generation · model', subject: 'directed pose' },
    Output: { src: 'assets/output-flyer-dress-courtyard.png', position: 'center 30%', label: 'Output · 4K', subject: 'export-ready' }
  };
  const img = imgs[col.heading] || imgs.Output;
  // imageRight prop overrides the default alternation if provided; same for bg.
  const flip = imageRightProp !== undefined ? !imageRightProp : idx % 2 === 1;
  const sectionBg = bgProp !== undefined ? bgProp : idx % 2 === 0 ? 'var(--paper)' : 'var(--canvas)';

  return (
    <section
      id={col.heading === 'Output' ? 'input-output' : undefined}
      data-screen-label={col.heading === 'Output' ? '02 Platform › 05 Output' : undefined}
      style={{
        background: isDark ? 'var(--ink)' : sectionBg,
        color: isDark ? 'var(--canvas)' : 'var(--ink)',
        padding: 'var(--pad-y) var(--pad-x)',
        borderTop: '1px solid var(--bone)',
        scrollMarginTop: 136
      }}>
      <div style={{ maxWidth: 'var(--container)', margin: '0 auto' }}>
        <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : flip ? '1.1fr 1fr' : '1fr 1.1fr', gap: 'clamp(28px, 5vw, 72px)', alignItems: 'center' }}>
          {!flip && !isMobile &&
          <div style={{ position: 'relative' }}>
              <FashionPlate ratio="4 / 5" {...img} dark={isDark} idx={idx} />
            </div>
          }
          <div>
            <Eyebrow color={isDark ? 'var(--ash)' : 'var(--graphite)'}>Step 5 · Output</Eyebrow>
            <h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 400, fontSize: 'clamp(34px, 5.8vw, 56px)', lineHeight: 1.05, letterSpacing: '-0.03em', margin: '20px 0 0', maxWidth: '14ch', color: isDark ? 'var(--canvas)' : 'var(--ink)' }}>
              {col.heading === 'Inputs' && <>What goes <em style={{ fontStyle: 'italic' }}>in.</em></>}
              {col.heading === 'Generation' && <>How it <em style={{ fontStyle: 'italic' }}>renders.</em></>}
              {col.heading === 'Output' && <>Campaign-ready, <em style={{ fontStyle: 'italic' }}>on day one.</em></>}
            </h2>
            {col.heading === 'Output' &&
            <p style={{
              marginTop: 'clamp(20px, 2.5vw, 28px)',
              fontFamily: 'var(--font-sans)', fontSize: 'clamp(15.5px, 1.45vw, 17.5px)',
              lineHeight: 1.6, color: isDark ? 'var(--mist)' : 'var(--graphite)',
              maxWidth: '52ch'
            }}>Print-ready frames, optimised copy, and the final say. Every output leaves the studio ready to publish.

            </p>
            }

            <ul style={{ listStyle: 'none', padding: 0, margin: 'clamp(28px, 4vw, 40px) 0 0', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 'clamp(20px, 2.5vw, 24px)' }}>
              {col.items.map((it) => {
                const IconComp = Icon[it.icon] || Icon.sparkle;
                return (
                  <li key={it.title} style={{ paddingTop: 20, borderTop: '1px solid ' + (isDark ? 'var(--ink-soft)' : 'var(--bone)') }}>
                    <IconComp width="22" height="22" style={{ color: isDark ? 'var(--canvas)' : 'var(--ink)' }} />
                    <div style={{ marginTop: 16, fontFamily: 'var(--font-sans)', fontSize: 16, fontWeight: 600, letterSpacing: '-0.01em' }}>{it.title}</div>
                    <div style={{ marginTop: 6, fontSize: 14, lineHeight: 1.55, color: isDark ? 'var(--mist)' : 'var(--graphite)' }}>{it.desc}</div>
                  </li>);

              })}
            </ul>
          </div>
          {(flip || isMobile) &&
          <div style={{ position: 'relative' }}>
              <FashionPlate ratio="4 / 5" {...img} dark={isDark} idx={idx} />
            </div>
          }
        </div>
      </div>
    </section>);

}

/* ---------- Step 6 · Video — finished frames become motion ----------
   Matches the Step sequence: same section padding, border, eyebrow + display
   heading, body line and a 4/5 media frame. Alternates from Step 5 (Output):
   media on the LEFT, white background. The clip autoplays muted+looped inline,
   lazily, only once scrolled into view; a poster image is the fallback. */
function VideoStep() {
  const { isMobile } = useBreakpoint();
  return (
    <section
      id="input-video"
      data-screen-label="02 Platform › 06 Video"
      style={{
        background: 'var(--canvas)',
        color: 'var(--ink)',
        padding: 'var(--pad-y) var(--pad-x)',
        borderTop: '1px solid var(--bone)',
        scrollMarginTop: 136
      }}>
      <div style={{ maxWidth: 'var(--container)', margin: '0 auto' }}>
        <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1.1fr', gap: 'clamp(28px, 5vw, 72px)', alignItems: 'center' }}>
          {!isMobile &&
          <div style={{ position: 'relative' }}>
              <VideoPlate />
            </div>
          }
          <div>
            <Eyebrow color="var(--graphite)">Step 6 · Video</Eyebrow>
            <h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 400, fontSize: 'clamp(34px, 5.8vw, 56px)', lineHeight: 1.05, letterSpacing: '-0.03em', margin: '20px 0 0', maxWidth: '14ch', color: 'var(--ink)' }}>
              Turn any frame <em style={{ fontStyle: 'italic' }}>into motion.</em>
            </h2>
            <p style={{
              marginTop: 'clamp(20px, 2.5vw, 28px)',
              fontFamily: 'var(--font-sans)', fontSize: 'clamp(15.5px, 1.45vw, 17.5px)',
              lineHeight: 1.6, color: 'var(--graphite)',
              maxWidth: '52ch'
            }}>
              Any image you generate can become a studio-quality motion clip — a slow cinematic pan, a gentle zoom, fabric catching the light. Set the movement and duration; the look stays exactly as you directed it.
            </p>

            <ul style={{ listStyle: 'none', padding: 0, margin: 'clamp(28px, 4vw, 40px) 0 0', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 'clamp(20px, 2.5vw, 24px)' }}>
              {[
              { icon: 'film', title: 'Cinematic motion', desc: 'Pans, push-ins and reveals from a single still.' },
              { icon: 'clock', title: 'Configurable length', desc: 'Set duration for social, ads or product pages.' },
              { icon: 'sparkles', title: '4K resolution', desc: 'Export up to ultra-HD — crisp enough for hero banners and the big screen.' },
              { icon: 'imagePlay', title: 'From a single still', desc: 'No reshoot, no studio — animate the product shot you already generated.' }].
              map((it) => {
                const IconComp = Icon[it.icon] || Icon.sparkle;
                return (
                  <li key={it.title} style={{ paddingTop: 20, borderTop: '1px solid var(--bone)' }}>
                    <IconComp width="22" height="22" style={{ color: 'var(--ink)' }} />
                    <div style={{ marginTop: 16, fontFamily: 'var(--font-sans)', fontSize: 16, fontWeight: 600, letterSpacing: '-0.01em' }}>{it.title}</div>
                    <div style={{ marginTop: 6, fontSize: 14, lineHeight: 1.55, color: 'var(--graphite)' }}>{it.desc}</div>
                  </li>);

              })}
            </ul>
          </div>
          {isMobile &&
          <div style={{ position: 'relative' }}>
              <VideoPlate />
            </div>
          }
        </div>
      </div>
    </section>);

}

/* 4/5 media frame holding the autoplay clip — mirrors FashionPlate's caption. */
function VideoPlate() {
  const videoRef = React.useRef(null);
  const loadedRef = React.useRef(false);
  React.useEffect(() => {
    const el = videoRef.current;
    if (!el) return;
    let cancelled = false;
    // Preview server lacks range support, so a direct <video src> shows a black
    // frame — fetch the file as a Blob and play from an object URL, loaded lazily
    // the first time the frame scrolls into view.
    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach(async (e) => {
          if (e.isIntersecting) {
            if (!loadedRef.current) {
              loadedRef.current = true;
              try {
                const res = await fetch('assets/platform-video-demo.mp4');
                const blob = await res.blob();
                if (cancelled) return;
                el.src = URL.createObjectURL(blob);
              } catch (err) {
                loadedRef.current = false;
                return;
              }
            }
            const p = el.play();
            if (p && p.catch) p.catch(() => {});
          } else {
            el.pause();
          }
        });
      },
      { threshold: 0.2 });

    io.observe(el);
    return () => { cancelled = true; io.disconnect(); };
  }, []);
  return (
    <div style={{ position: 'relative', aspectRatio: '4 / 5', overflow: 'hidden', background: 'var(--ink)' }}>
      <video
        ref={videoRef}
        muted
        loop
        playsInline
        preload="none"
        poster="assets/platform-video-poster.jpg"
        aria-label="ViZO — AI-generated fashion video, garment in motion"
        style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%', display: 'block' }} />

      <div style={{
        position: 'absolute', bottom: 16, left: 16, right: 16,
        display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end',
        color: 'rgba(255,255,255,1)',
        textShadow: '0 1px 3px rgba(0,0,0,0.55), 0 0 1px rgba(0,0,0,0.4)',
        fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.12em', textTransform: 'uppercase', fontWeight: 600,
        pointerEvents: 'none'
      }}>
        <span>Motion · loop</span>
        <span>Video · 4K</span>
      </div>
    </div>);

}

/* ---------- Resolution / pricing-at-glance card row ---------- */
function PlatformResolutions() {
  const { isMobile } = useBreakpoint();
  const rows = [
  { res: '1K', credits: '1 credit', use: 'thumbnails · social tiles · proofs' },
  { res: '2K', credits: '2 credits', use: 'e-commerce hero · marketplace listings' },
  { res: '4K', credits: '4 credits', use: 'campaigns · lookbooks · OOH' }];

  return (
    <section style={{ background: 'var(--paper)', padding: 'var(--pad-y) var(--pad-x)', borderTop: '1px solid var(--bone)' }}>
      <div style={{ maxWidth: 'var(--container)', margin: '0 auto' }}>
        <Eyebrow>What you pay for</Eyebrow>
        <h2 style={{ fontFamily: 'var(--font-display)', fontWeight: 400, fontSize: 'clamp(34px, 5.8vw, 56px)', lineHeight: 1.05, letterSpacing: '-0.03em', margin: '20px 0 0', maxWidth: '18ch', color: 'var(--ink)' }}>
          Pay only for the <em style={{ fontStyle: 'italic' }}>resolution</em> you export.
        </h2>
        <div style={{ marginTop: 'clamp(32px, 5vw, 56px)', borderTop: '1px solid var(--ink)' }}>
          {rows.map((r, i) =>
          <div key={r.res} style={{
            display: 'grid',
            gridTemplateColumns: isMobile ? '70px 1fr' : 'minmax(100px, 120px) minmax(140px, 200px) 1fr 60px',
            gap: 16,
            padding: 'clamp(18px, 3vw, 28px) 0',
            borderBottom: '1px solid var(--bone)',
            alignItems: 'baseline'
          }}>
              <span style={{ fontFamily: 'var(--font-display)', fontWeight: 400, fontSize: 'clamp(30px, 4vw, 48px)', letterSpacing: '-0.03em', color: 'var(--ink)' }}>{r.res}</span>
              {!isMobile && <span style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--ink)' }}>{r.credits}</span>}
              <span style={{ fontSize: 'clamp(14px, 1.4vw, 16px)', color: 'var(--graphite)' }}>
                {isMobile && <span style={{ display: 'block', fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--ink)', marginBottom: 4 }}>{r.credits}</span>}
                {r.use}
              </span>
              {!isMobile && <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--slate)', textAlign: 'right' }}>0{i + 1}</span>}
            </div>
          )}
        </div>
      </div>
    </section>);

}

Object.assign(window, { WorkflowStrip, PlatformSection, VideoStep, VideoPlate, PlatformResolutions, DirectInputsIntro, BatchUploadSection });

/* ===========================================================
   Section 5 — Batch upload (replaces the old "What you pay for" strip)
   Anchor #batch-upload so the homepage teaser link resolves.
   Four steps + a stats strip + closing links.
   =========================================================== */

const BATCH_STEPS = [
{
  id: 'tag',
  badge: 'Step 01',
  headLead: 'Tag every',
  headEm: 'upload.',
  body: 'Each garment image gets a SKU reference and a view label: front, back, side, or detail. ViZO uses these tags to apply the correct shot type to each frame and organise outputs on return.',
  caption: 'Tagging interface · preview',
  mock: 'tag'
},
{
  id: 'upload',
  badge: 'Step 02',
  headLead: 'Upload your',
  headEm: 'batch.',
  body: 'Drop your tagged files into the upload queue. CSV uploads accept up to 1000 SKUs per job. ViZO validates tags, flags missing references, and queues the batch for processing.',
  caption: 'Batch upload · preview',
  mock: 'upload'
},
{
  id: 'direct',
  badge: 'Step 03',
  headLead: 'Direct',
  headEm: 'the shoot.',
  body: 'Write your brief once. Choose models, environments, lighting, and styling. The brief applies across every SKU in the batch, ensuring consistent output without you reviewing each frame.',
  caption: 'Brief interface · preview',
  mock: 'brief'
},
{
  id: 'export',
  badge: 'Step 04',
  headLead: 'Export',
  headEm: 'tagged.',
  body: 'Outputs return organised by SKU and view, ready to drop straight into your PIM or e-commerce platform. Download as a bulk ZIP, integrate via API, or sync to your storage.',
  caption: 'Export interface · preview',
  mock: 'export'
}];


function BatchUploadSection() {
  return (
    <>
      <AtScaleSection />
      <BatchSummary />
    </>);

}

window.BatchUploadSection = BatchUploadSection;

/* ===========================================================
   AT SCALE — one editorial section that replaces the four-step
   "Tag / Upload / Direct / Export" walkthrough.

   Animation:
     - Tiles fade in one-by-one with 65ms stagger, 900ms ease-out each
     - Status pill flips from "Processing" to "Complete" when counter lands
     - prefers-reduced-motion: skip everything, show the final state
     - Fires once per page visit via IntersectionObserver
   =========================================================== */

const AT_SCALE_TILES = 12;
const AT_SCALE_COLS = 4; // tiles per row on desktop
const AT_SCALE_STAGGER = 200; // ms between tiles
const AT_SCALE_FADE = 1200; // ms per-tile fade
const AT_SCALE_FADEOUT = 200; // ms cross-fade when switching batches

/* Each batch is its own 3-row visual story: row 0 = SKU 247, row 1 = 248, row 2 = 249.
   The "vibe" between batches is intentionally distinct (studio / garden / urban / interior)
   so flipping the dropdown delivers a real visual diff. */
const AT_SCALE_BATCHES = [
{
  id: 'b01',
  label: 'Batch 01 • SS26 Outdoor • Jade • Mediterranean',
  total: 240,
  rows: [
  ['assets/batch-floral-front.jpg', 'assets/batch-floral-back.jpg', 'assets/batch-floral-detail.jpg', 'assets/batch-floral-34.jpg'],
  ['assets/batch-zigzag-front.jpg', 'assets/batch-zigzag-back.jpg', 'assets/batch-zigzag-detail.jpg', 'assets/batch-zigzag-34.jpg'],
  ['assets/batch-yellow-front.jpg', 'assets/batch-yellow-back.jpg', 'assets/batch-yellow-detail.jpg', 'assets/batch-yellow-34.jpg']]

},
{
  id: 'b02',
  label: 'Batch 02 · SS26 Edit · Tish · Studio',
  total: 847,
  rows: [
  ['assets/batch-polka-front.jpg', 'assets/batch-polka-back.jpg', 'assets/batch-polka-detail.jpg', 'assets/batch-polka-34.jpg'],
  ['assets/batch-blazer-front.jpg', 'assets/batch-blazer-back.jpg', 'assets/batch-blazer-detail.jpg', 'assets/batch-blazer-side.jpg'],
  ['assets/batch-khaki-front.jpg', 'assets/batch-khaki-back.jpg', 'assets/batch-khaki-detail.jpg', 'assets/batch-khaki-34.jpg']]

}];

const AT_SCALE_DEFAULT_BATCH = 'b02';
const AT_SCALE_SOURCES = AT_SCALE_BATCHES.find((b) => b.id === AT_SCALE_DEFAULT_BATCH).rows.flat();

function AtScaleSection() {
  const { isMobile } = useBreakpoint();
  const ref = React.useRef(null);
  const triggerRef = React.useRef(null); // observed element — the grid header strip
  const [revealed, setRevealed] = React.useState(0); // number of tiles faded in
  const [counter, setCounter] = React.useState(0);
  const [status, setStatus] = React.useState('processing');
  const [batchId, setBatchId] = React.useState(AT_SCALE_DEFAULT_BATCH);
  const [fadingOut, setFadingOut] = React.useState(false);
  const startedRef = React.useRef(false);
  const timeoutsRef = React.useRef([]);
  const rafRef = React.useRef(null);
  const reduceMotionRef = React.useRef(false);
  // Keep current batch synchronously available to the animation loop — avoids stale closures.
  const batchRef = React.useRef(null);

  const currentBatch = AT_SCALE_BATCHES.find((b) => b.id === batchId) || AT_SCALE_BATCHES[0];
  batchRef.current = currentBatch;

  const cancelAnimation = React.useCallback(() => {
    timeoutsRef.current.forEach((id) => clearTimeout(id));
    timeoutsRef.current = [];
    if (rafRef.current != null) {
      cancelAnimationFrame(rafRef.current);
      rafRef.current = null;
    }
  }, []);

  const runAnimation = React.useCallback(() => {
    cancelAnimation();
    setRevealed(0);
    setCounter(0);
    setStatus('processing');
    setFadingOut(false);

    const target = AT_SCALE_TILES;
    if (reduceMotionRef.current) {
      setRevealed(AT_SCALE_TILES);
      setCounter(target);
      setStatus('complete');
      return;
    }

    // Tile stagger — 200ms between tiles, 1200ms ease-out each.
    for (let i = 0; i < AT_SCALE_TILES; i++) {
      const id = setTimeout(() => {
        setRevealed((c) => Math.max(c, i + 1));
      }, i * AT_SCALE_STAGGER);
      timeoutsRef.current.push(id);
    }

    // Counter: ease-out cubic from 0 → 15, lands as the last tile finishes fading in.
    const duration = AT_SCALE_TILES * AT_SCALE_STAGGER + AT_SCALE_FADE;
    const t0 = performance.now();
    const easeOut = (t) => 1 - Math.pow(1 - t, 3);
    let landed = false;
    const tick = (now) => {
      const t = Math.min(1, (now - t0) / duration);
      const value = Math.round(easeOut(t) * target);
      setCounter(value);
      // Flip the status pill the moment the displayed counter actually hits the target,
      // not at the very end of the easing curve (rounding lands ~30% before t=1).
      if (!landed && value >= target) {
        landed = true;
        setStatus('complete');
      }
      if (t < 1) {
        rafRef.current = requestAnimationFrame(tick);
      } else {
        rafRef.current = null;
        if (!landed) setStatus('complete');
      }
    };
    rafRef.current = requestAnimationFrame(tick);
  }, [cancelAnimation]);

  const replay = React.useCallback(() => {
    runAnimation();
  }, [runAnimation]);

  /* Batch switch: 200ms cross-fade out, swap images, re-run stagger. */
  const changeBatch = React.useCallback((nextId) => {
    if (nextId === batchRef.current.id) return;
    cancelAnimation();

    const nextBatch = AT_SCALE_BATCHES.find((b) => b.id === nextId);
    if (!nextBatch) return;

    if (reduceMotionRef.current) {
      batchRef.current = nextBatch;
      setBatchId(nextId);
      setRevealed(AT_SCALE_TILES);
      setCounter(AT_SCALE_TILES);
      setStatus('complete');
      setFadingOut(false);
      return;
    }

    setFadingOut(true);
    const id = setTimeout(() => {
      batchRef.current = nextBatch;
      setBatchId(nextId);
      runAnimation();
    }, AT_SCALE_FADEOUT);
    timeoutsRef.current.push(id);
  }, [cancelAnimation, runAnimation]);

  React.useEffect(() => {
    /* Observe the grid header strip (not the whole section) so the staggered
       reveal only fires once the user has scrolled past the value-props row
       above and the grid is actually about to come into focus. */
    const el = triggerRef.current;
    if (!el) return;
    reduceMotionRef.current = typeof window.matchMedia === 'function' &&
    window.matchMedia('(prefers-reduced-motion: reduce)').matches;

    const begin = () => {
      if (startedRef.current) return;
      startedRef.current = true;
      runAnimation();
    };

    const io = new IntersectionObserver((entries) => {
      for (const entry of entries) {
        if (entry.isIntersecting) {
          begin();
          io.disconnect();
          break;
        }
      }
    }, { threshold: 0.2 });
    io.observe(el);
    return () => {
      io.disconnect();
      cancelAnimation();
    };
  }, [runAnimation, cancelAnimation]);

  const headingId = 'at-scale-heading';

  return (
    <section
      id="batch-upload"
      ref={ref}
      aria-labelledby={headingId}
      data-screen-label="02 Platform › 06 At scale"
      style={{
        background: 'var(--canvas)',
        padding: 0,
        borderTop: '1px solid var(--bone)'
      }}>
      {/* Intro band — full-bleed paper behind the headline + lead paragraph. */}
      <div style={{ background: 'var(--paper)', padding: `clamp(80px, 12vw, 120px) var(--pad-x) clamp(48px, 7vw, 80px)` }}>
        <div style={{ maxWidth: 'var(--container)', margin: '0 auto' }}>
          <AtScaleBanner isMobile={isMobile} headingId={headingId} part="intro" />
        </div>
      </div>
      {/* Body — white, from the definition block down through the grid. */}
      <div style={{ padding: `clamp(40px, 6vw, 64px) var(--pad-x) clamp(40px, 5vw, 56px)` }}>
      <div style={{ maxWidth: 'var(--container)', margin: '0 auto' }}>
        <AtScaleBanner isMobile={isMobile} headingId={headingId} part="rest" />
        {/* Generous spacer before the grid so the section reads as: definition → reasoning → evidence. */}
        <div style={{ height: 'clamp(56px, 7vw, 80px)' }} />
        <AtScaleGrid
          revealed={revealed}
          counter={counter}
          status={status}
          isMobile={isMobile}
          fadingOut={fadingOut}
          batches={AT_SCALE_BATCHES}
          batch={currentBatch}
          onSelectBatch={changeBatch}
          onReplay={replay}
          triggerRef={triggerRef} />

      </div>
      </div>

      {/* Local keyframes for the status dot pulse. */}
      <style>{`
        @keyframes atscale-pulse {
          0%, 100% { opacity: 1; transform: scale(1); }
          50% { opacity: 0.4; transform: scale(0.85); }
        }
        @media (prefers-reduced-motion: reduce) {
          .atscale-tile { opacity: 1 !important; transform: none !important; }
          .atscale-dot { animation: none !important; }
        }
      `}</style>
    </section>);

}

window.AtScaleSection = AtScaleSection;

function AtScaleBanner({ isMobile, headingId, part }) {
  if (part === 'rest') {
    return (
      <div>
        {/* Definition block — a pull-quote-style frame that names what a batch is. */}
        <div style={{
          borderTop: '1px solid var(--ink)',
          borderBottom: '1px solid var(--ink)',
          padding: 'clamp(28px, 4vw, 40px) 0'
        }}>
          <p style={{
            margin: 0, maxWidth: '60ch',
            fontFamily: 'var(--font-display)',
            fontSize: isMobile ? 'clamp(18px, 4.6vw, 22px)' : 'clamp(20px, 1.8vw, 24px)',
            lineHeight: 1.4, letterSpacing: '-0.01em',
            color: 'var(--graphite)'
          }}>
            <span style={{ color: 'var(--ink)', fontWeight: 500 }}>A batch is one campaign, run as one job.</span>{' '}
            Lock your inventory, model, location, and brief. ViZO processes overnight at half the credits, and delivers the morning you log back in.
          </p>
        </div>

        {/* Three value props — text-only, scannable. No icons, no dividers. */}
        <ValuePropsRow isMobile={isMobile}>
          <ValueProp
            title="Better output"
            claim="Higher fidelity on every frame."
            detail="Tighter detail, cleaner edges, fewer regenerations." />
          
          <ValueProp
            title="Lower cost"
            claim="50% off credits on every batch."
            detail="Bulk economics without the bulk overhead." />
          
          <ValueProp
            title="Faster ship"
            claim="~24 hours from upload to delivery."
            detail="Catalogue-ready frames before your team logs in." />
          
        </ValuePropsRow>
      </div>);

  }

  return (
    <div>
      <Eyebrow>At scale</Eyebrow>
      <h2 id={headingId} style={{ ...{
          fontFamily: 'var(--font-display)', fontWeight: 300,
          fontSize: isMobile ? 'clamp(44px, 11vw, 56px)' : 'clamp(64px, 9vw, 96px)',
          lineHeight: 0.98, letterSpacing: '-0.04em',
          color: 'var(--ink)', margin: '20px 0 0', textWrap: 'pretty'
        } }}>
        One brief.<br />One season.<br />
        <em style={{ fontStyle: 'italic' }}>One thousand frames.</em>
      </h2>
      <p style={{
        marginTop: 'clamp(28px, 3.5vw, 40px)', maxWidth: '72ch',
        fontFamily: 'var(--font-sans)', fontSize: 'clamp(16px, 1.2vw, 19px)',
        lineHeight: 1.6, color: 'var(--graphite)'
      }}>
        Pick your inventory. Cast the face of your season. Accessorise once. Write one creative brief. ViZO runs the whole catalogue overnight — every garment lit the same, framed the same, shipping the morning you log back in.
      </p>
    </div>);

}

function ValueProp({ title, claim, detail }) {
  return (
    <div className="value-prop" style={{ maxWidth: '28ch' }}>
      <div className="value-prop__el value-prop__title" style={{
        fontFamily: 'var(--font-mono)', fontSize: 13,
        letterSpacing: '0.15em', textTransform: 'uppercase', fontWeight: 600,
        color: 'var(--ink)'
      }}>{title}</div>
      <div className="value-prop__el value-prop__claim" style={{
        marginTop: 14,
        fontFamily: 'var(--font-sans)', fontSize: 16,
        fontWeight: 600, lineHeight: 1.4, letterSpacing: '-0.005em',
        color: 'var(--ink)'
      }}>{claim}</div>
      <div className="value-prop__el value-prop__detail" style={{
        marginTop: 8,
        fontFamily: 'var(--font-sans)', fontSize: 13,
        lineHeight: 1.55,
        color: 'var(--slate)'
      }}>{detail}</div>
    </div>);

}

/* ValuePropsRow ----------------------------------------------------------
   Wraps the three ValueProp blocks and triggers a once-per-page entrance
   animation when the row enters the viewport (threshold: 0.25).

   The animation is driven by a single class swap (`.animated`) so CSS
   transitions — not JS — do all the timing. transition-delay is set per
   element via nth-child + element-class selectors below; total run is ~1.5s.

   prefers-reduced-motion: skips the animation entirely. */
function ValuePropsRow({ children, isMobile }) {
  const ref = React.useRef(null);
  const [animated, setAnimated] = React.useState(false);

  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;

    const reduceMotion = typeof window.matchMedia === 'function' &&
    window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduceMotion) {
      setAnimated(true);
      return;
    }

    const io = new IntersectionObserver((entries) => {
      for (const entry of entries) {
        if (entry.isIntersecting) {
          setAnimated(true);
          io.disconnect();
          break;
        }
      }
    }, { threshold: 0.25 });
    io.observe(el);
    return () => io.disconnect();
  }, []);

  return (
    <>
      <div
        ref={ref}
        className={`value-props-row${animated ? ' animated' : ''}`}
        style={{
          marginTop: 'clamp(56px, 7vw, 80px)',
          display: 'grid',
          gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)',
          columnGap: 'clamp(32px, 4vw, 48px)',
          rowGap: 'clamp(28px, 4vw, 40px)',
          alignItems: 'start'
        }}>
        {children}
      </div>
      <style>{`
        .value-props-row .value-prop__el {
          opacity: 0;
          transform: translateY(16px);
          transition:
            opacity 900ms cubic-bezier(0.16, 1, 0.3, 1),
            transform 900ms cubic-bezier(0.16, 1, 0.3, 1);
          will-change: opacity, transform;
        }
        .value-props-row.animated .value-prop__el {
          opacity: 1;
          transform: translateY(0);
        }
        /* Stagger between blocks: 0ms, 220ms, 440ms.
           Within each block: title +0, claim +80, detail +160. */
        .value-props-row .value-prop:nth-child(1) .value-prop__title  { transition-delay:   0ms; }
        .value-props-row .value-prop:nth-child(1) .value-prop__claim  { transition-delay:  80ms; }
        .value-props-row .value-prop:nth-child(1) .value-prop__detail { transition-delay: 160ms; }
        .value-props-row .value-prop:nth-child(2) .value-prop__title  { transition-delay: 220ms; }
        .value-props-row .value-prop:nth-child(2) .value-prop__claim  { transition-delay: 300ms; }
        .value-props-row .value-prop:nth-child(2) .value-prop__detail { transition-delay: 380ms; }
        .value-props-row .value-prop:nth-child(3) .value-prop__title  { transition-delay: 440ms; }
        .value-props-row .value-prop:nth-child(3) .value-prop__claim  { transition-delay: 520ms; }
        .value-props-row .value-prop:nth-child(3) .value-prop__detail { transition-delay: 600ms; }
        @media (prefers-reduced-motion: reduce) {
          .value-props-row .value-prop__el {
            opacity: 1 !important;
            transform: none !important;
            transition: none !important;
          }
        }
      `}</style>
    </>);

}

window.ValuePropsRow = ValuePropsRow;

window.ValueProp = ValueProp;

window.AtScaleBanner = AtScaleBanner;

function Stat({ value, label }) {
  return (
    <div>
      <div style={{
        fontFamily: 'var(--font-display)', fontWeight: 500,
        fontSize: 'clamp(28px, 3vw, 40px)', lineHeight: 1, letterSpacing: '-0.02em',
        fontVariantNumeric: 'tabular-nums',
        color: 'var(--ink)'
      }}>{value}</div>
      <div style={{
        marginTop: 8, fontFamily: 'var(--font-sans)', fontSize: 11,
        letterSpacing: '0.06em', textTransform: 'uppercase',
        color: 'var(--slate)'
      }}>{label}</div>
    </div>);

}

function AtScaleGrid({ revealed, counter, status, isMobile, fadingOut, batches, batch, onSelectBatch, onReplay, triggerRef }) {
  const cols = isMobile ? 2 : AT_SCALE_COLS;
  const skuStart = 247;
  const sources = batch.rows.flat();
  const [open, setOpen] = React.useState(false);
  const [lightbox, setLightbox] = React.useState(null);
  const dropdownRef = React.useRef(null);

  // Close lightbox on Escape.
  React.useEffect(() => {
    if (!lightbox) return;
    const onKey = (e) => {if (e.key === 'Escape') setLightbox(null);};
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [lightbox]);

  // Close on outside click or Escape.
  React.useEffect(() => {
    if (!open) return;
    const onDown = (e) => {
      if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
        setOpen(false);
      }
    };
    const onKey = (e) => {if (e.key === 'Escape') setOpen(false);};
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDown);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);

  return (
    <div>
      {/* Header strip — also the animation trigger (observed via triggerRef so the
                          tile stagger only fires once this element scrolls into view). */}
      <div ref={triggerRef} style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        flexWrap: 'wrap', gap: 12,
        fontFamily: 'var(--font-mono)', fontSize: 11, letterSpacing: '0.08em',
        color: 'var(--slate)', textTransform: 'uppercase'
      }}>
        {/* Batch label — clickable trigger with chevron, opens dropdown */}
        <div ref={dropdownRef} style={{ position: 'relative' }}>
          <button
            type="button"
            onClick={() => setOpen((o) => !o)}
            aria-expanded={open}
            aria-haspopup="listbox"
            style={{
              appearance: 'none', cursor: 'pointer',
              background: 'transparent', border: 'none', padding: 0,
              display: 'inline-flex', alignItems: 'center', gap: 8,
              fontFamily: 'var(--font-mono)', fontSize: 11, letterSpacing: '0.08em',
              color: 'var(--ink)', textTransform: 'uppercase', fontWeight: 600,
              transition: 'opacity 200ms var(--ease-standard)'
            }}
            onMouseEnter={(e) => {e.currentTarget.style.opacity = '0.75';}}
            onMouseLeave={(e) => {e.currentTarget.style.opacity = '1';}}>
            <span style={{ whiteSpace: 'nowrap' }}>{batch.label}</span>
            <span aria-hidden="true" style={{
              display: 'inline-block',
              transform: open ? 'rotate(180deg)' : 'rotate(0)',
              transition: 'transform 200ms var(--ease-standard)'
            }}>▾</span>
          </button>
          {open &&
          <div
            role="listbox"
            aria-label="Select batch"
            style={{
              position: 'absolute', top: 'calc(100% + 8px)', left: 0, zIndex: 20,
              background: 'var(--canvas)',
              border: '1px solid var(--mist)',
              boxShadow: '0 12px 32px rgba(10,10,10,0.12)',
              padding: 4, minWidth: 'min(420px, 92vw)',
              animation: 'atscale-drop 180ms var(--ease-standard)'
            }}>
              {batches.map((b) => {
              const isCurrent = b.id === batch.id;
              return (
                <button
                  key={b.id}
                  type="button"
                  role="option"
                  aria-selected={isCurrent}
                  onClick={() => {setOpen(false);onSelectBatch(b.id);}}
                  style={{
                    appearance: 'none', cursor: 'pointer',
                    width: '100%', textAlign: 'left',
                    background: 'transparent', border: 'none',
                    padding: '10px 12px',
                    display: 'flex', alignItems: 'center', gap: 10,
                    fontFamily: 'var(--font-mono)', fontSize: 11, letterSpacing: '0.08em',
                    color: isCurrent ? 'var(--ink)' : 'var(--slate)',
                    textTransform: 'uppercase',
                    fontWeight: isCurrent ? 600 : 500,
                    transition: 'background 150ms var(--ease-standard), color 150ms var(--ease-standard)'
                  }}
                  onMouseEnter={(e) => {e.currentTarget.style.background = 'var(--paper)';e.currentTarget.style.color = 'var(--ink)';}}
                  onMouseLeave={(e) => {e.currentTarget.style.background = 'transparent';e.currentTarget.style.color = isCurrent ? 'var(--ink)' : 'var(--slate)';}}>
                  <span aria-hidden="true" style={{
                    width: 6, height: 6, borderRadius: 999,
                    flex: '0 0 auto',
                    background: isCurrent ? 'var(--ink)' : 'transparent',
                    border: isCurrent ? 'none' : '1px solid var(--mist)'
                  }} />
                  <span>{b.label}</span>
                </button>);

            })}
              <style>{`
                @keyframes atscale-drop {
                  from { opacity: 0; transform: translateY(-4px); }
                  to { opacity: 1; transform: translateY(0); }
                }
              `}</style>
            </div>
          }
        </div>
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
          <span
            className="atscale-dot"
            aria-hidden="true"
            style={{
              width: 8, height: 8, borderRadius: 999,
              background: '#3FB76C',
              animation: `atscale-pulse ${status === 'complete' ? '4s' : '2s'} ease-in-out infinite`
            }} />

          <span style={{ color: status === 'complete' ? 'var(--ink)' : 'var(--slate)' }}>
            {status === 'complete' ? 'Complete' : 'Processing'}
          </span>
        </span>
      </div>

      <div style={{ height: 1, background: 'var(--bone)', margin: '12px 0' }} />

      {/* Grid */}
      <div style={{
        display: 'grid',
        gridTemplateColumns: `repeat(${cols}, 1fr)`,
        gap: 4
      }}>
        {Array.from({ length: AT_SCALE_TILES }, (_, i) => {
          const src = sources[i] || sources[i % sources.length];
          const isVisible = !fadingOut && i < revealed;
          // On desktop: 4 cols, so SKU label sits on every 4th tile (start of each row).
          // On mobile: 2 cols, so we still tag the first tile of every SKU group (every 4th in source order).
          const isRowStart = i % AT_SCALE_COLS === 0;
          const skuNum = String(skuStart + Math.floor(i / AT_SCALE_COLS)).padStart(4, '0');
          return (
            <div
              key={i}
              className="atscale-tile"
              onClick={() => isVisible && setLightbox(src)}
              role="button"
              tabIndex={isVisible ? 0 : -1}
              aria-label="Open larger preview"
              onKeyDown={(e) => {if (isVisible && (e.key === 'Enter' || e.key === ' ')) {e.preventDefault();setLightbox(src);}}}
              style={{
                position: 'relative', aspectRatio: '4 / 3',
                overflow: 'hidden', background: 'var(--bone)',
                cursor: isVisible ? 'zoom-in' : 'default',
                opacity: isVisible ? 1 : 0,
                transform: isVisible ? 'translateY(0) scale(1)' : 'translateY(6px) scale(0.98)',
                transition: fadingOut ?
                `opacity ${AT_SCALE_FADEOUT}ms ease-in-out, transform ${AT_SCALE_FADEOUT}ms ease-in-out` :
                `opacity ${AT_SCALE_FADE}ms cubic-bezier(0.16, 1, 0.3, 1), transform ${AT_SCALE_FADE}ms cubic-bezier(0.16, 1, 0.3, 1)`
              }}>
              <img
                src={src}
                alt={frameAlt(src)}
                loading="lazy"
                style={{
                  position: 'absolute', inset: 0,
                  width: '100%', height: '100%',
                  objectFit: 'cover', objectPosition: 'center',
                  display: 'block',
                  transition: 'transform 600ms ease'
                }}
                onMouseEnter={(e) => {e.currentTarget.style.transform = 'scale(1.05)';}}
                onMouseLeave={(e) => {e.currentTarget.style.transform = 'scale(1)';}} />

              {isRowStart &&
              <span style={{
                position: 'absolute', top: 8, left: 8,
                padding: '4px 8px',
                fontFamily: 'var(--font-mono)', fontSize: 10,
                letterSpacing: '0.08em', textTransform: 'uppercase',
                color: 'rgba(255,255,255,0.95)',
                background: 'rgba(10,10,10,0.6)',
                pointerEvents: 'none'
              }}>
                  SKU · {skuNum}
                </span>
              }
            </div>);

        })}
      </div>

      <div style={{ height: 1, background: 'var(--bone)', margin: '12px 0' }} />

      {/* Footer strip */}
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        flexWrap: 'wrap', gap: 12,
        fontFamily: 'var(--font-mono)', fontSize: 11, letterSpacing: '0.08em',
        color: 'var(--slate)', textTransform: 'uppercase'
      }}>
        <span style={{ fontVariantNumeric: 'tabular-nums', color: 'var(--ink)' }}>
          {counter.toLocaleString()} / {AT_SCALE_TILES} frames shown
        </span>
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 12 }}>
          <span>Failed frames automatically reprocessed · No credits charged on errors</span>
          {onReplay &&
          <button
            type="button"
            onClick={onReplay}
            aria-label="Replay overnight render animation"
            disabled={status !== 'complete'}
            style={{
              appearance: 'none', cursor: status === 'complete' ? 'pointer' : 'default',
              background: 'transparent',
              color: status === 'complete' ? 'var(--ink)' : 'var(--slate)',
              opacity: status === 'complete' ? 1 : 0.4,
              border: '1px solid var(--mist)',
              borderColor: status === 'complete' ? 'var(--ink)' : 'var(--mist)',
              padding: '5px 10px',
              fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600,
              letterSpacing: '0.08em', textTransform: 'uppercase',
              display: 'inline-flex', alignItems: 'center', gap: 6,
              transition: 'all 200ms var(--ease-standard)'
            }}>
              <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7" /><path d="M3 4v5h5" /></svg>
              Replay
            </button>
          }
        </span>
      </div>

      {/* Lightbox — larger preview of a clicked tile */}
      {lightbox &&
      <div
        onClick={() => setLightbox(null)}
        role="dialog"
        aria-modal="true"
        aria-label="Image preview"
        style={{
          position: 'fixed', inset: 0, zIndex: 1000,
          background: 'rgba(10,10,10,0.86)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          padding: 'clamp(24px, 5vw, 64px)',
          animation: 'atscale-lightbox-in 200ms var(--ease-standard)',
          cursor: 'zoom-out'
        }}>
        <img
          src={lightbox}
          alt={frameAlt(lightbox)}
          onClick={(e) => e.stopPropagation()}
          style={{
            maxWidth: 'min(880px, 100%)', maxHeight: '100%',
            objectFit: 'contain', display: 'block',
            boxShadow: '0 24px 80px rgba(0,0,0,0.5)', cursor: 'default'
          }} />
        <button
          type="button"
          onClick={() => setLightbox(null)}
          aria-label="Close preview"
          style={{
            position: 'absolute', top: 'clamp(16px, 3vw, 28px)', right: 'clamp(16px, 3vw, 28px)',
            appearance: 'none', cursor: 'pointer',
            width: 44, height: 44, borderRadius: 999,
            background: 'rgba(255,255,255,0.12)', border: '1px solid rgba(255,255,255,0.3)',
            color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            backdropFilter: 'blur(4px)'
          }}>
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M18 6 6 18" /><path d="M6 6l12 12" /></svg>
        </button>
        <style>{`
          @keyframes atscale-lightbox-in {
            from { opacity: 0; }
            to { opacity: 1; }
          }
        `}</style>
      </div>
      }
    </div>);

}
window.AtScaleGrid = AtScaleGrid;

function BatchHeader() {
  return (
    <section
      id="batch-upload"
      data-screen-label="02 Platform › 06 Batch upload"
      style={{
        background: 'var(--canvas)',
        padding: `clamp(64px, 8vw, 96px) var(--pad-x) clamp(40px, 5vw, 64px)`,
        borderTop: '1px solid var(--bone)',
        scrollMarginTop: 88
      }}>
      <div style={{ maxWidth: 'var(--container)', margin: '0 auto' }}>
        <Eyebrow>Batch upload</Eyebrow>
        <h2 style={{
          fontFamily: 'var(--font-display)', fontWeight: 300,
          fontSize: 'clamp(40px, 6.4vw, 80px)', lineHeight: 0.98, letterSpacing: '-0.04em',
          color: 'var(--ink)', margin: '24px 0 0', maxWidth: '14ch', textWrap: 'pretty'
        }}>
          From one product. To one <em style={{ fontStyle: 'italic' }}>thousand.</em>
        </h2>
        <p style={{
          marginTop: 'clamp(20px, 2.5vw, 28px)',
          fontFamily: 'var(--font-sans)', fontSize: 'clamp(16px, 1.2vw, 19px)',
          lineHeight: 1.55, color: 'var(--graphite)',
          maxWidth: 600
        }}>Batch upload turns a 1000-SKU shoot list into a single overnight job. Here&rsquo;s how it works, from upload to organised export.

        </p>
      </div>
    </section>);

}

function BatchStep({ step, index }) {
  const { isMobile, isTablet } = useBreakpoint();
  const imageRight = index % 2 === 0; // 01,03 image right; 02,04 image left
  const num = String(index + 1).padStart(2, '0');
  const bg = index % 2 === 0 ? 'var(--paper)' : 'var(--canvas)';

  const text =
  <div style={{
    width: '100%',
    maxWidth: imageRight ? 'none' : '52ch',
    marginLeft: imageRight ? 0 : 'auto'
  }}>
      <Eyebrow>{step.badge}</Eyebrow>
      <h3 style={{
      fontFamily: 'var(--font-display)', fontWeight: 300,
      fontSize: 'clamp(34px, 5.2vw, 60px)',
      lineHeight: 1.0, letterSpacing: '-0.035em',
      color: 'var(--ink)', margin: '20px 0 0',
      maxWidth: '14ch', textWrap: 'pretty'
    }}>
        {step.headLead}<br /><em style={{ fontStyle: 'italic' }}>{step.headEm}</em>
      </h3>
      <p style={{
      marginTop: 'clamp(20px, 2.5vw, 28px)',
      fontFamily: 'var(--font-sans)', fontSize: 'clamp(15.5px, 1.45vw, 17.5px)',
      lineHeight: 1.6, color: 'var(--graphite)',
      maxWidth: '52ch'
    }}>
        {step.body}
      </p>
    </div>;


  const figure =
  <div style={{ position: 'relative' }}>
      <BatchMock kind={step.mock} num={num} />
      {/* Top-left badge — vertically aligned with the titlebar's right-side "ViZO · 01" label */}
      <div style={{
      position: 'absolute', top: 7, left: 16,
      background: 'var(--canvas)', color: 'var(--ink)',
      fontFamily: 'var(--font-mono)', fontSize: 11, letterSpacing: '0.1em',
      padding: '6px 10px', textTransform: 'uppercase', fontWeight: 600,
      zIndex: 2
    }}>
        {step.badge}
      </div>
    </div>;


  return (
    <section
      data-screen-label={`02 Platform › 06.${num} ${step.headEm.replace('.', '')}`}
      style={{
        background: bg,
        padding: `clamp(64px, 8vw, 120px) var(--pad-x)`,
        borderTop: '1px solid var(--bone)'
      }}>
      <div style={{
        maxWidth: 'var(--container)', margin: '0 auto', width: '100%',
        display: 'grid',
        gridTemplateColumns: isMobile ? '1fr' : isTablet ? '1fr 1fr' : '1.05fr 1fr',
        gap: 'clamp(28px, 5vw, 72px)',
        alignItems: 'center'
      }}>
        {isMobile ? <>{figure}{text}</> : imageRight ? <>{text}{figure}</> : <>{figure}{text}</>}
      </div>
    </section>);

}

/* Final summary — stats strip + closing links, in its own section to mirror page rhythm. */
function BatchSummary() {
  return (
    <section
      style={{
        background: 'var(--canvas)',
        padding: `0 var(--pad-x) clamp(56px, 8vw, 96px)`
      }}>
      <div style={{ maxWidth: 'var(--container)', margin: '0 auto' }}>
        <BatchSpecs />
        <BatchClosingLinks />
      </div>
    </section>);

}

window.BatchSummary = BatchSummary;

/* ---------- Inline UI mocks ----------
   Editorial, monochrome, hint-of-brand. Built with CSS so they scale and read cleanly. */
function BatchMock({ kind, num }) {
  const shell = {
    background: 'var(--paper)',
    border: '1px solid var(--mist)',
    borderRadius: 10,
    overflow: 'hidden',
    position: 'relative',
    boxShadow: '0 12px 32px rgba(0,0,0,0.05)'
  };
  const titlebar = (label) =>
  <div style={{
    display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
    padding: '12px 16px', borderBottom: '1px solid var(--bone)', background: 'var(--canvas)'
  }}>
      <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <span style={{ width: 8, height: 8, borderRadius: 999, background: 'var(--accent-amber)' }} />
        <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, letterSpacing: '0.08em', color: 'var(--ink)', textTransform: 'uppercase', fontWeight: 600 }}>{label}</span>
      </span>
      <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.08em', color: 'var(--slate)' }}>ViZO · {num}</span>
    </div>;


  if (kind === 'tag') {
    const thumbs = [
    'assets/sku-ivory-front.png',
    'assets/sku-ivory-detail.png',
    'assets/sku-ivory-back.png',
    'assets/sku-pink-front.png',
    'assets/sku-pink-detail.png',
    'assets/sku-pink-back.png'];

    return (
      <div style={shell}>
        {titlebar('Tag')}
        <div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 16, padding: 16 }}>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 }}>
            {thumbs.map((src, i) =>
            <div key={i} style={{
              aspectRatio: '1 / 1', overflow: 'hidden',
              outline: i === 0 ? '2px solid var(--ink)' : '1px solid var(--bone)',
              outlineOffset: i === 0 ? '-2px' : 0
            }}>
                <img src={src} alt={frameAlt(src)} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
              </div>
            )}
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.08em', color: 'var(--slate)', textTransform: 'uppercase' }}>Selected</span>
            <Field label="SKU - 2260" />
            <Field label="View · Front" dropdown />
          </div>
        </div>
      </div>);

  }

  if (kind === 'upload') {
    return (
      <div style={shell}>
        {titlebar('')}
        <div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 16, padding: 16 }}>
          <div style={{
            border: '2px dashed var(--mist)', borderRadius: 8,
            padding: 24, display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', gap: 10,
            minHeight: 200,
            background: 'repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,0.015) 6px 12px)'
          }}>
            <Icon.download width="22" height="22" style={{ color: 'var(--ink)' }} />
            <div style={{ fontFamily: 'var(--font-sans)', fontSize: 14, fontWeight: 600, color: 'var(--ink)' }}>Drop CSV + image set</div>
            <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--slate)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>up to 5,000 SKUs</div>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
            <SummaryRow label="Validated" value="4,847" tone="ok" />
            <SummaryRow label="Flagged" value="12" tone="warn" />
            <SummaryRow label="Duplicate" value="6" tone="warn" />
            <SummaryRow label="Total" value="4,865" />
            <div style={{
              marginTop: 'auto', borderTop: '1px solid var(--bone)', paddingTop: 10,
              fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--ink)',
              letterSpacing: '0.06em', textTransform: 'uppercase'
            }}>Queue ready</div>
          </div>
        </div>
      </div>);

  }

  if (kind === 'brief') {
    const models = ['assets/model-portrait-tshirt.png', 'assets/hero-model-chair.jpg', 'assets/hero-model-garden.png'];
    const envs = ['assets/environment-south-france.jpeg', 'assets/editorial-pink-curtains.jpg', 'assets/output-flyer-dress-courtyard.png'];
    return (
      <div style={shell}>
        {titlebar('Brief')}
        <div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 16, padding: 16 }}>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10.5, letterSpacing: '0.08em', color: 'var(--slate)', textTransform: 'uppercase' }}>Brief · SS26 drop 02</span>
            <div style={{
              background: 'var(--canvas)', border: '1px solid var(--bone)', borderRadius: 8,
              padding: 14, minHeight: 180,
              fontFamily: 'var(--font-sans)', fontSize: 13.5, lineHeight: 1.55, color: 'var(--graphite)'
            }}>
              Mediterranean garden, late afternoon, warm low light. Cast model two: relaxed pose, leaning, half-smile. Apply to every front-view SKU in the batch.<span style={{ display: 'inline-block', width: 1, height: 14, marginLeft: 2, background: 'var(--ink)', verticalAlign: 'middle', animation: 'briefBlink 1.1s steps(2) infinite' }} />
            </div>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
            <ThumbRow label="Model" srcs={models} activeIdx={0} />
            <ThumbRow label="Environment" srcs={envs} activeIdx={0} />
          </div>
        </div>
        <style>{`@keyframes briefBlink { to { opacity: 0; } }`}</style>
      </div>);

  }

  if (kind === 'export') {
    const tree = [
    { sku: 'SS26-024', expanded: true, views: ['front', 'back', 'side', 'detail'] },
    { sku: 'SS26-025', expanded: false },
    { sku: 'SS26-026', expanded: false },
    { sku: 'SS26-027', expanded: false }];

    const previews = [
    'assets/hero-model-chair.jpg',
    'assets/editorial-black-dress-red.jpg',
    'assets/hero-model-garden.png',
    'assets/output-flyer-dress-courtyard.png',
    'assets/beauty-blue-shadow.jpg',
    'assets/editorial-pink-curtains.jpg'];

    return (
      <div style={shell}>
        {titlebar('Export')}
        <div style={{ display: 'grid', gridTemplateColumns: '0.9fr 1.4fr', gap: 16, padding: 16 }}>
          <div style={{ background: 'var(--canvas)', border: '1px solid var(--bone)', borderRadius: 6, padding: 12, fontFamily: 'var(--font-mono)', fontSize: 12 }}>
            {tree.map((row, i) =>
            <div key={i} style={{ marginBottom: 6 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'var(--ink)' }}>
                  <span style={{ width: 8, color: 'var(--slate)' }}>{row.expanded ? '▾' : '▸'}</span>
                  <span style={{ fontWeight: 600 }}>{row.sku}/</span>
                </div>
                {row.expanded && row.views &&
              <div style={{ paddingLeft: 18, marginTop: 4 }}>
                    {row.views.map((v) =>
                <div key={v} style={{ color: 'var(--graphite)', padding: '2px 0' }}>{v}.jpg</div>
                )}
                  </div>
              }
              </div>
            )}
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }}>
            {previews.map((src, i) =>
            <div key={i} style={{ aspectRatio: '1 / 1', overflow: 'hidden', background: 'var(--bone)' }}>
                <img src={src} alt={frameAlt(src)} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
              </div>
            )}
          </div>
        </div>
      </div>);

  }
  return null;
}

function Field({ label, value, dropdown }) {
  return (
    <div>
      {value &&
      <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.08em', color: 'var(--slate)', textTransform: 'uppercase', marginBottom: 4 }}>{label}</div>
      }
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        padding: '10px 12px', borderRadius: 6,
        background: 'var(--canvas)', border: '1px solid var(--bone)',
        fontFamily: 'var(--font-mono)', fontSize: 11.5, color: 'var(--ink)',
        letterSpacing: '0.06em', textTransform: 'uppercase', fontWeight: 600
      }}>
        <span>{value || label}</span>
        {dropdown && <span style={{ color: 'var(--slate)' }}>▾</span>}
      </div>
    </div>);

}

function SummaryRow({ label, value, tone }) {
  const dot = tone === 'ok' ? 'var(--accent-amber)' : tone === 'warn' ? '#C77E2E' : 'var(--slate)';
  return (
    <div style={{
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      padding: '8px 0', borderBottom: '1px solid var(--bone)'
    }}>
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontFamily: 'var(--font-mono)', fontSize: 11, letterSpacing: '0.06em', color: 'var(--slate)', textTransform: 'uppercase' }}>
        <span style={{ width: 6, height: 6, borderRadius: 999, background: dot }} />
        {label}
      </span>
      <span style={{ fontFamily: 'var(--font-display)', fontSize: 18, fontWeight: 400, letterSpacing: '-0.01em', color: 'var(--ink)', fontVariantNumeric: 'tabular-nums' }}>{value}</span>
    </div>);

}

function ThumbRow({ label, srcs, activeIdx = 0 }) {
  return (
    <div>
      <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.08em', color: 'var(--slate)', textTransform: 'uppercase', marginBottom: 6 }}>{label}</div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 }}>
        {srcs.map((src, i) =>
        <div key={i} style={{
          aspectRatio: '1 / 1', overflow: 'hidden',
          outline: i === activeIdx ? '2px solid var(--ink)' : '1px solid var(--bone)',
          outlineOffset: i === activeIdx ? '-2px' : 0
        }}>
            <img src={src} alt={frameAlt(src)} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
          </div>
        )}
      </div>
    </div>);

}

function BatchSpecs() {
  const { isMobile } = useBreakpoint();
  const ref = React.useRef(null);
  const playedRef = React.useRef(false);
  const [counter, setCounter] = React.useState(0);
  const [show50, setShow50] = React.useState(false);
  const [show24, setShow24] = React.useState(false);
  const [pulseOn, setPulseOn] = React.useState(false);
  const [showLabel0, setShowLabel0] = React.useState(false);
  const [showLabel1, setShowLabel1] = React.useState(false);
  const [showLabel2, setShowLabel2] = React.useState(false);
  const timeoutsRef = React.useRef([]);
  const rafRef = React.useRef(null);

  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;

    const reduceMotion = typeof window.matchMedia === 'function' &&
    window.matchMedia('(prefers-reduced-motion: reduce)').matches;

    const startAnimation = () => {
      if (reduceMotion) {
        setCounter(1000);
        setShow50(true);
        setShow24(true);
        setPulseOn(false);
        setShowLabel0(true);
        setShowLabel1(true);
        setShowLabel2(true);
        return;
      }

      const t0 = performance.now();
      const duration = 1200;
      const easeOut = (t) => 1 - Math.pow(1 - t, 3);
      const tick = (now) => {
        const t = Math.min(1, (now - t0) / duration);
        const value = Math.round(easeOut(t) * 1000);
        setCounter(value);
        if (t < 1) {
          rafRef.current = requestAnimationFrame(tick);
        } else {
          rafRef.current = null;
        }
      };
      rafRef.current = requestAnimationFrame(tick);

      timeoutsRef.current.push(setTimeout(() => setShow50(true), 200));
      timeoutsRef.current.push(setTimeout(() => setShow24(true), 400));
      timeoutsRef.current.push(setTimeout(() => setShowLabel1(true), 800));
      timeoutsRef.current.push(setTimeout(() => setShowLabel2(true), 1000));
      timeoutsRef.current.push(setTimeout(() => setShowLabel0(true), 1200));
      timeoutsRef.current.push(setTimeout(() => setPulseOn(true), 1000));
    };

    const io = new IntersectionObserver((entries) => {
      for (const entry of entries) {
        if (entry.isIntersecting && !playedRef.current) {
          playedRef.current = true;
          startAnimation();
          io.disconnect();
          break;
        }
      }
    }, { threshold: 0.3 });
    io.observe(el);

    return () => {
      io.disconnect();
      // Don't kill in-flight timeouts / rAF on cleanup — only the parent unmount should stop them.
    };
  }, []);

  // Separate cleanup that only fires on true unmount.
  React.useEffect(() => () => {
    timeoutsRef.current.forEach((id) => clearTimeout(id));
    timeoutsRef.current = [];
    if (rafRef.current != null) {
      cancelAnimationFrame(rafRef.current);
      rafRef.current = null;
    }
  }, []);

  /* Shared number style — tabular figures + nowrap so "~24 hrs" never breaks.
     Font-size clamp upper bound is tuned so the longest stat fits one line
     at the narrowest desktop column width. */
  const numberStyle = {
    fontFamily: 'var(--font-display)', fontWeight: 400,
    fontSize: 'clamp(34px, 3.8vw, 44px)', letterSpacing: '-0.02em', lineHeight: 1,
    color: 'var(--ink)',
    whiteSpace: 'nowrap',
    fontFeatureSettings: '"tnum" 1',
    fontVariantNumeric: 'tabular-nums'
  };
  const labelStyle = (visible) => ({
    marginTop: 10, fontFamily: 'var(--font-sans)', fontSize: 14, color: 'var(--slate)',
    opacity: visible ? 1 : 0,
    transition: 'opacity 400ms ease-out'
  });

  return (
    <div
      ref={ref}
      style={{
        paddingTop: 'clamp(28px, 4vw, 40px)',
        borderTop: '1px solid var(--ink)',
        display: 'grid',
        gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)',
        gap: 'clamp(24px, 4vw, 48px)'
      }}>
      {/* 1 — 1000 counts up */}
      <div style={{ paddingTop: 0 }}>
        <div style={numberStyle}>{counter.toLocaleString()}</div>
        <div style={labelStyle(showLabel0)}>SKUs per overnight batch</div>
      </div>

      {/* 2 — 50% scales and fades in */}
      <div style={{ paddingTop: isMobile ? 20 : 0, borderTop: isMobile ? '1px solid var(--bone)' : 'none' }}>
        <div style={{
          ...numberStyle,
          opacity: show50 ? 1 : 0,
          transform: show50 ? 'scale(1)' : 'scale(0.9)',
          transformOrigin: 'left center',
          transition: 'opacity 600ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1)'
        }}>95%</div>
        <div style={labelStyle(showLabel1)}>frames accepted on first delivery</div>
      </div>

      {/* 3 — ~24 hrs fades up, tilde pulses perpetually after entrance */}
      <div style={{ paddingTop: isMobile ? 20 : 0, borderTop: isMobile ? '1px solid var(--bone)' : 'none' }}>
        <div style={{
          ...numberStyle,
          opacity: show24 ? 1 : 0,
          transform: show24 ? 'translateY(0)' : 'translateY(12px)',
          transition: 'opacity 600ms cubic-bezier(0.16, 1, 0.3, 1), transform 600ms cubic-bezier(0.16, 1, 0.3, 1)'
        }}>
          <span>5</span>
          <span
            className={pulseOn ? 'batchspecs-tilde-pulse' : ''}
            style={{ display: 'inline-block', transformOrigin: 'center' }}>+</span>
        </div>
        <div style={labelStyle(showLabel2)}>camera angles per SKU</div>
      </div>

      <style>{`
        @keyframes batchspecs-tilde {
          0%, 100% { transform: scale(1); opacity: 1; }
          50% { transform: scale(1.08); opacity: 0.7; }
        }
        .batchspecs-tilde-pulse {
          animation: batchspecs-tilde 2.2s ease-in-out infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .batchspecs-tilde-pulse { animation: none !important; }
        }
      `}</style>
    </div>);

}

function BatchClosingLinks() {
  const { isMobile } = useBreakpoint();
  const link = (to, label) =>
  <BatchClosingLink to={to} label={label} />;

  return (
    <div style={{
      marginTop: 'clamp(40px, 6vw, 64px)',
      display: 'flex', flexDirection: isMobile ? 'column' : 'row',
      gap: isMobile ? 16 : 48
    }}>
      {link('/solutions#scale', 'See who batch is built for')}
      {link('/pricing', 'See pricing')}
    </div>);

}

function BatchClosingLink({ to, label }) {
  const [hover, setHover] = React.useState(false);
  return (
    <Link
      to={to}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      style={{
        display: 'inline-flex', alignItems: 'center', gap: 8,
        fontFamily: 'var(--font-sans)', fontSize: 15, fontWeight: 600,
        color: 'var(--ink)', textDecoration: 'none',
        borderBottom: '1px solid var(--ink)',
        paddingBottom: 2
      }}>
      {label}
      <span aria-hidden="true" style={{
        display: 'inline-block',
        transform: hover ? 'translateX(4px)' : 'translateX(0)',
        transition: 'transform 200ms var(--ease-standard)'
      }}>→</span>
    </Link>);

}

Object.assign(window, {
  BatchHeader, BatchStep, BatchMock, BatchSpecs, BatchClosingLinks, BatchClosingLink,
  Field, SummaryRow, ThumbRow
});