// Direction 3 — Technical Instrument (refined)
// Two variants: 'console' (dark sidebar, denser, more instrument character)
//               'paper' (light sidebar, calmer, fewer grid lines)

const TI_ACCENTS = {
  forge:  { ink: '#1D7A52', soft: '#eef6f1', tint: '#f0f6f3', name: 'Forge green' },
  amber:  { ink: '#a86a14', soft: '#fbf1de', tint: '#fbf3e2', name: 'Survey amber' },
  slate:  { ink: '#27506e', soft: '#e9eff5', tint: '#eef3f8', name: 'Field slate'  },
  rust:   { ink: '#a83f1e', soft: '#fbece5', tint: '#fbeee8', name: 'Iron rust'    },
};

const MONO_STACKS = {
  jetbrains: '"JetBrains Mono", ui-monospace, monospace',
  plex:      '"IBM Plex Mono", ui-monospace, monospace',
  geist:     '"Geist Mono", ui-monospace, monospace',
};

const DENSITY = {
  compact: { row: 6, block: 10, page: 24 },
  regular: { row: 8, block: 14, page: 32 },
  roomy:   { row: 12, block: 18, page: 44 },
};

function tiTheme(opts) {
  const variant = opts.variant || 'console';
  const accent = TI_ACCENTS[opts.accent || 'forge'];
  const mono = MONO_STACKS[opts.mono || 'jetbrains'];
  const grid = opts.grid !== false;
  const d = DENSITY[opts.density || 'regular'];

  const dark = variant === 'console';
  return {
    variant, dark, grid, mono, d,
    accent: accent.ink, accentSoft: accent.soft, accentTint: accent.tint,
    bg: dark ? '#f5f4ee' : '#fbfaf5',
    paper: '#fff',
    ink: '#0d0f0d', ink2: '#5a5e58', ink3: '#9a9e96',
    line: dark ? '#c9c8bf' : '#e1ddcf',
    line2: dark ? '#dfdcd0' : '#ebe6d4',
    side: dark ? '#15181a' : '#f0ecdf',
    sideInk: dark ? '#c8cec3' : '#3d3a30',
    sideInk2: dark ? '#737972' : '#8a8472',
    sideInk3: dark ? '#fff' : '#0d0f0d',
    sideRule: dark ? '#262a28' : '#dcd5be',
    red: '#9a2a1a',
    amberIn: '#a86a14',
  };
}

function TI_Icon({ name, size = 12 }) {
  const p = { width: size, height: size, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' };
  if (name === 'check')    return <svg {...p}><path d="M3 8.5l3 3 7-7"/></svg>;
  if (name === 'download') return <svg {...p}><path d="M8 2v9M4.5 7.5L8 11l3.5-3.5M2.5 13.5h11"/></svg>;
  if (name === 'upload')   return <svg {...p}><path d="M8 11V2M4.5 5.5L8 2l3.5 3.5M2.5 13.5h11"/></svg>;
  if (name === 'arrow')    return <svg {...p}><path d="M3 8h10M9 4l4 4-4 4"/></svg>;
  if (name === 'warn')     return <svg {...p}><path d="M8 2L14 13H2z"/><path d="M8 6.5v3M8 11.5v.01"/></svg>;
  if (name === 'x')        return <svg {...p}><path d="M3.5 3.5l9 9M12.5 3.5l-9 9"/></svg>;
  return null;
}

function TIReadout({ value, label, theme, width = 48, accent }) {
  const [shown, setShown] = React.useState(value);
  React.useEffect(() => {
    let raf, start;
    const from = shown;
    const to = value;
    if (from === to) return;
    const dur = 280;
    const step = (t) => {
      if (!start) start = t;
      const k = Math.min(1, (t - start) / dur);
      setShown(from + (to - from) * (1 - Math.pow(1 - k, 3)));
      if (k < 1) raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [value]);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 2, minWidth: width }}>
      <div style={{ fontSize: 9, letterSpacing: '0.16em', color: theme.ink3, textTransform: 'uppercase' }}>{label}</div>
      <div style={{ fontSize: 14, fontWeight: 600, color: accent || theme.ink, fontVariantNumeric: 'tabular-nums', fontFamily: theme.mono }}>
        {Math.round(shown).toString().padStart(3, '0')}
      </div>
    </div>
  );
}

function TIRibbon({ status, theme }) {
  const map = {
    idle:        { dot: theme.ink3,    label: 'AWAITING DATA' },
    ready:       { dot: theme.accent,  label: 'READY TO GENERATE' },
    generating:  { dot: theme.accent,  label: 'PROCESSING', pulse: true },
    success:     { dot: theme.accent,  label: 'COMPLETE' },
    error:       { dot: theme.red,     label: 'ERROR' },
  };
  const s = map[status] || map.idle;
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
      <div style={{
        width: 7, height: 7, borderRadius: '50%', background: s.dot,
        boxShadow: s.pulse ? `0 0 0 0 ${s.dot}` : 'none',
        animation: s.pulse ? 'ti-pulse 1.4s ease-out infinite' : 'none',
      }} />
      <span style={{ fontSize: 10, letterSpacing: '0.16em', color: theme.ink2 }}>{s.label}</span>
    </div>
  );
}

function useCsvPreview(file) {
  const [data, setData] = React.useState(null);
  React.useEffect(() => {
    if (!file) { setData(null); return; }
    const isXlsx = /\.xlsx?$|\.xlsm$/i.test(file.name);
    if (isXlsx) {
      setData({ kind: 'xlsx', cols: ['(Excel preview not shown)'], rows: [], totalRows: '?' });
      return;
    }
    const reader = new FileReader();
    reader.onload = (e) => {
      const text = String(e.target.result || '');
      const lines = text.split(/\r?\n/).filter(Boolean).slice(0, 80);
      const split = (l) => {
        const out = []; let cur = ''; let q = false;
        for (let i = 0; i < l.length; i++) {
          const c = l[i];
          if (c === '"') q = !q;
          else if (c === ',' && !q) { out.push(cur); cur = ''; }
          else cur += c;
        }
        out.push(cur); return out;
      };
      const cols = (lines[0] ? split(lines[0]) : []).slice(0, 8);
      const rows = lines.slice(1, 9).map(l => split(l).slice(0, 8));
      setData({ kind: 'csv', cols, rows, totalRows: lines.length - 1 });
    };
    reader.readAsText(file.slice(0, 32 * 1024));
  }, [file]);
  return data;
}

function TechInstrument({ initialStatus, opts }) {
  const theme = React.useMemo(() => tiTheme(opts || {}), [opts]);
  const meta = useSurveyForgeMeta();
  const { state, set, setFile, reset, generate, simulate } = useSurveyForgeForm({ initialStatus });
  const [drag, setDrag] = React.useState(false);
  const fileRef = React.useRef(null);
  const preview = useCsvPreview(state.file);

  React.useEffect(() => {
    if (initialStatus && initialStatus !== 'idle') simulate(initialStatus);
  }, [initialStatus]);

  React.useEffect(() => {
    if (meta.surveyTypes && !state.surveyType) set({ surveyType: meta.surveyTypes[0]?.slug || '' });
    if (meta.templates && !state.templateSlug) set({ templateSlug: meta.templates.default || '' });
  }, [meta.surveyTypes, meta.templates]);

  const [now, setNow] = React.useState(() => new Date());
  React.useEffect(() => { const id = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(id); }, []);
  const ts = now.toISOString().replace('T', ' ').slice(0, 19) + ' UTC';

  const onDrop = (e) => {
    e.preventDefault(); setDrag(false);
    const f = e.dataTransfer.files?.[0];
    if (f) setFile(f);
  };

  const surveyTypes = meta.surveyTypes || SF_FALLBACK_SURVEY_TYPES;
  const templates = (meta.templates || SF_FALLBACK_TEMPLATES).templates;

  const styles = makeTIStyles(theme);

  const expectedCols = React.useMemo(() => {
    const map = {
      reptile: ['site', 'date', 'observer', 'visit', 'species', 'count', 'lifestage', 'weather'],
      dormouse: ['site', 'date', 'box_id', 'occupied', 'species', 'count', 'observer'],
      badger: ['site', 'date', 'feature', 'easting', 'northing', 'activity', 'observer'],
      gcn: ['site', 'date', 'pond_id', 'method', 'count', 'sex', 'observer'],
    };
    return map[state.surveyType] || ['site', 'date', 'observer', 'count'];
  }, [state.surveyType]);

  const colsOk = preview?.kind === 'csv' && expectedCols.every(c => preview.cols.some(pc => pc.toLowerCase().includes(c) || c.includes(pc.toLowerCase())));

  return (
    <div style={styles.root}>
      <aside style={styles.side}>
        <div style={styles.brand}>
          <div style={styles.brandMark}>SF</div>
          <div>
            <div style={styles.brandText}>SurveyForge</div>
            <div style={styles.brandKicker}>v0.4 · BUILD 207</div>
          </div>
        </div>

        <div style={styles.sideHd}>Tasks</div>
        <NavRow theme={theme} code="01" label="New report" active />
        <NavRow theme={theme} code="02" label="Past reports" disabled badge="SOON" />
        <NavRow theme={theme} code="03" label="Templates" disabled badge="SOON" />
        <NavRow theme={theme} code="04" label="Billing" disabled badge="SOON" />

        <div style={{ ...styles.sideHd, marginTop: 22 }}>System</div>
        <NavRow theme={theme} code="91" label="Help & docs" />
        <NavRow theme={theme} code="92" label="Settings" />

        <div style={{ flex: 1 }} />

        <div style={styles.readoutsWrap}>
          <div style={styles.sideHd}>Telemetry</div>
          <div style={styles.readouts}>
            <TIReadout
              theme={{ ink: theme.sideInk3, ink2: theme.sideInk, ink3: theme.sideInk2, mono: theme.mono }}
              accent={theme.accent}
              label="QUEUE" value={0} />
            <TIReadout
              theme={{ ink: theme.sideInk3, ink2: theme.sideInk, ink3: theme.sideInk2, mono: theme.mono }}
              accent={state.status === 'generating' ? theme.accent : theme.sideInk3}
              label="JOBS" value={state.status === 'generating' ? 1 : 0} />
            <TIReadout
              theme={{ ink: theme.sideInk3, ink2: theme.sideInk, ink3: theme.sideInk2, mono: theme.mono }}
              label="LAT MS" value={Math.floor(120 + (now.getSeconds() % 7) * 6)} />
          </div>
        </div>

        <div style={styles.sideMeta}>
          <div style={{ display: 'flex', justifyContent: 'space-between' }}>
            <span>OPERATOR</span><span style={{ color: theme.sideInk3 }}>WT</span>
          </div>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
            <span>API</span>
            <span style={{ color: theme.accent, display: 'flex', alignItems: 'center', gap: 6 }}>
              <span style={{ width: 6, height: 6, borderRadius: '50%', background: theme.accent, animation: 'ti-pulse 2s ease-out infinite' }} />
              ONLINE
            </span>
          </div>
          <div style={{ display: 'flex', justifyContent: 'space-between' }}>
            <span>UTC</span><span style={{ fontVariantNumeric: 'tabular-nums', color: theme.sideInk }}>{ts.slice(11, 19)}</span>
          </div>
        </div>
      </aside>

      <main style={styles.main}>
        <div style={styles.topbar}>
          <div style={styles.topL}>
            <div style={styles.formId}>SF-01 / Rev 04 / Generate from data</div>
            <TIRibbon status={state.status} theme={theme} />
          </div>
          <div style={styles.topR}>
            <span style={{ fontFamily: theme.mono, fontSize: 11, color: theme.ink2, fontVariantNumeric: 'tabular-nums' }}>{ts}</span>
            <div style={styles.avatar}>WT</div>
          </div>
        </div>

        <div style={styles.page}>
          <RegisterMarks theme={theme} />

          <div style={styles.hd}>
            <div>
              <div style={styles.kicker}>NEW REPORT · FORM SF-01</div>
              <h1 style={styles.h1}>Generate a report from your survey data</h1>
              <p style={styles.lede}>
                Pick a survey type, attach your CSV or Excel file, and we'll return a CIEEM-compliant Word document. Most jobs complete in five to ten seconds.
              </p>
            </div>
          </div>

          <Block theme={theme} title="§A Survey type" meta="REQUIRED">
            <Row theme={theme} label="Type">
              <select style={styles.selectBare} value={state.surveyType} onChange={(e) => set({ surveyType: e.target.value })}>
                {surveyTypes.map(t => (
                  <option key={t.slug} value={t.slug}>{t.label}</option>
                ))}
              </select>
            </Row>
            <Row theme={theme} label="Code" last>
              <div style={{ ...styles.inputBare, color: theme.ink2 }}>
                {(state.surveyType || '—').toUpperCase()}
              </div>
            </Row>
          </Block>

          <Block theme={theme} title="§B Cover page" meta="OPTIONAL">
            <Row theme={theme} label="Site">
              <input style={styles.inputBare} placeholder="—" value={state.siteName} onChange={(e) => set({ siteName: e.target.value })} />
            </Row>
            <Row theme={theme} label="Client">
              <input style={styles.inputBare} placeholder="—" value={state.clientName} onChange={(e) => set({ clientName: e.target.value })} />
            </Row>
            <Row theme={theme} label="Surveyor">
              <input style={styles.inputBare} placeholder="—" value={state.surveyorName} onChange={(e) => set({ surveyorName: e.target.value })} />
            </Row>
            <Row theme={theme} label="Project no.">
              <input style={styles.inputBare} placeholder="—" value={state.projectNumber} onChange={(e) => set({ projectNumber: e.target.value })} />
            </Row>
            <Row theme={theme} label="Report title">
              <input style={styles.inputBare} placeholder="—" value={state.reportTitle} onChange={(e) => set({ reportTitle: e.target.value })} />
            </Row>
            <Row theme={theme} label="Status">
              <select style={styles.selectBare} value={state.reportStatus} onChange={(e) => set({ reportStatus: e.target.value })}>
                <option value="">—</option>
                <option value="Draft">Draft</option>
                <option value="For Review">For Review</option>
                <option value="For Issue">For Issue</option>
                <option value="Final">Final</option>
                <option value="Issued">Issued</option>
              </select>
            </Row>
            <Row theme={theme} label="Report date" last>
              <input style={styles.inputBare} placeholder="e.g. 28 July 2025" value={state.reportDate} onChange={(e) => set({ reportDate: e.target.value })} />
            </Row>
          </Block>

          <Block theme={theme} title="§C Document template" meta={state.customTemplateFile ? 'CUSTOM UPLOADED' : `SELECT 1 OF ${templates.length}`}>
            <div style={styles.tplGrid}>
              {templates.map((t, i) => {
                const active = !state.customTemplateFile && state.templateSlug === t.slug;
                return (
                  <div key={t.slug}
                       style={{ ...styles.tplCell(active), borderRight: `1px solid ${theme.line2}` }}
                       onClick={() => set({ templateSlug: t.slug, customTemplateFile: null })}>
                    <div style={styles.tplCheck(active)}>{active && <TI_Icon name="check" size={9} />}</div>
                    <div style={styles.tplName}>{t.name}</div>
                    <div style={styles.tplTag}>{t.tagline}</div>
                    <div style={styles.tplIdx}>{String(i + 1).padStart(2, '0')}</div>
                  </div>
                );
              })}
            </div>
            <div style={{
              marginTop: 10, padding: '10px 12px',
              border: `1px dashed ${state.customTemplateFile ? theme.accent : theme.line}`,
              background: state.customTemplateFile ? theme.accentTint : (theme.dark ? '#fafaf6' : '#fdfbf3'),
              display: 'flex', alignItems: 'center', gap: 12,
            }}>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase', color: theme.ink, marginBottom: 2 }}>
                  Or upload your own .docx
                </div>
                <div style={{ fontSize: 10.5, color: theme.ink2 }}>
                  {state.customTemplateFile
                    ? state.customTemplateFile.name
                    : "Use your consultancy's letterhead, fonts, and styles"}
                </div>
              </div>
              {state.customTemplateFile ? (
                <button style={styles.ghost} onClick={() => set({ customTemplateFile: null })}>Remove</button>
              ) : (
                <label style={{ ...styles.ghost, display: 'inline-flex', alignItems: 'center', cursor: 'pointer' }}>
                  Browse…
                  <input type="file" accept=".docx" hidden
                    onChange={(e) => {
                      const f = e.target.files?.[0];
                      if (f) set({ customTemplateFile: f, templateSlug: '' });
                    }} />
                </label>
              )}
            </div>
          </Block>

          <Block theme={theme} title="§E Photos & figures" meta={state.photos.length ? `${state.photos.length} ATTACHED` : 'OPTIONAL'}>
            <PhotoBlock state={state} set={set} theme={theme} styles={styles} />
          </Block>

          <Block theme={theme} title="§D Data attachment" meta="REQUIRED · CSV / XLSX">
            {!state.file ? (
              <div
                style={styles.drop(drag, false)}
                onClick={() => fileRef.current?.click()}
                onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
                onDragLeave={() => setDrag(false)}
                onDrop={onDrop}
              >
                <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10, color: theme.ink2 }}>
                  <div style={{ color: theme.accent }}>
                    <svg width="22" height="22" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
                      <path d="M8 11V2M4.5 5.5L8 2l3.5 3.5M2.5 13.5h11"/>
                    </svg>
                  </div>
                  <div style={{ fontSize: 12, color: theme.ink, letterSpacing: '0.04em' }}>
                    DROP FILE HERE — OR <span style={{ color: theme.accent, textDecoration: 'underline' }}>BROWSE</span>
                  </div>
                  <div style={{ fontSize: 10, color: theme.ink3, letterSpacing: '0.16em' }}>
                    .CSV  .XLSX  .XLSM  .XLS · MAX 10MB
                  </div>
                </div>
              </div>
            ) : (
              <div>
                <div style={styles.fileRow}>
                  <div style={styles.fileBadge}>
                    {(state.file.name.split('.').pop() || 'CSV').toUpperCase().slice(0, 4)}
                  </div>
                  <div style={{ minWidth: 0, flex: 1 }}>
                    <div style={styles.fileName}>{state.file.name}</div>
                    <div style={styles.fileMeta}>
                      SIZE {fileSize(state.file).toUpperCase()}
                      {preview?.kind === 'csv' && <> · {preview.cols.length} COLS · {preview.totalRows} ROWS</>}
                      {' · '}
                      <span style={{ color: colsOk ? theme.accent : theme.amberIn }}>
                        {preview?.kind === 'csv' ? (colsOk ? 'SCHEMA OK' : 'SCHEMA WARN') : 'PREVIEW N/A'}
                      </span>
                    </div>
                  </div>
                  <button style={styles.ghost} onClick={() => setFile(null)}>Replace</button>
                </div>

                {preview?.kind === 'csv' && preview.cols.length > 0 && (
                  <div style={styles.previewWrap}>
                    <div style={styles.previewHd}>
                      <span style={styles.previewLabel}>PREVIEW · FIRST {preview.rows.length} ROWS</span>
                      <span style={styles.previewLabel}>SCROLL →</span>
                    </div>
                    <div style={styles.previewScroll}>
                      <table style={styles.previewTable}>
                        <thead>
                          <tr>
                            {preview.cols.map((c, i) => {
                              const matched = expectedCols.some(ec => c.toLowerCase().includes(ec) || ec.includes(c.toLowerCase()));
                              return (
                                <th key={i} style={{ ...styles.previewTh, color: matched ? theme.accent : theme.amberIn }}>
                                  {c || `col_${i+1}`}
                                  {!matched && <span style={{ marginLeft: 4, opacity: 0.7 }}>?</span>}
                                </th>
                              );
                            })}
                          </tr>
                        </thead>
                        <tbody>
                          {preview.rows.map((r, ri) => (
                            <tr key={ri}>
                              {preview.cols.map((_, ci) => (
                                <td key={ci} style={styles.previewTd}>{r[ci] ?? ''}</td>
                              ))}
                            </tr>
                          ))}
                        </tbody>
                      </table>
                    </div>
                    {!colsOk && (
                      <div style={styles.previewWarn}>
                        Some headers don't match the expected schema for <b>{(state.surveyType || '').toUpperCase()}</b>. Generation may still work — the API will tell you for sure.
                      </div>
                    )}
                  </div>
                )}
              </div>
            )}
            <input ref={fileRef} type="file" hidden accept={SF_ACCEPT_FILES}
                   onChange={(e) => e.target.files?.[0] && setFile(e.target.files[0])} />
          </Block>

          {state.status === 'generating' && <TIGenerating state={state} theme={theme} styles={styles} />}
          {state.status === 'success' && <TISuccess state={state} theme={theme} styles={styles} onReset={reset} />}
          {state.status === 'error' && <TIError state={state} theme={theme} styles={styles} onRetry={generate} onReset={reset} />}

          {state.status !== 'success' && (
            <div style={styles.actionPanel}>
              <div style={styles.actionLeft}>
                <div style={styles.actionLabel}>READY CHECK</div>
                <div style={styles.actionList}>
                  <CheckItem ok={!!state.surveyType} theme={theme}>Survey type</CheckItem>
                  <CheckItem ok={!!state.templateSlug} theme={theme}>Template</CheckItem>
                  <CheckItem ok={!!state.file} theme={theme}>Data file</CheckItem>
                </div>
              </div>
              <div style={styles.actionRight}>
                <div style={{ fontSize: 10, letterSpacing: '0.16em', color: theme.ink3, marginBottom: 6 }}>
                  EST. 5–10 SEC
                </div>
                <button
                  style={styles.primary(!state.file || !state.surveyType || state.status === 'generating')}
                  disabled={!state.file || !state.surveyType || state.status === 'generating'}
                  onClick={generate}
                >
                  {state.status === 'generating' ? 'PROCESSING…' : 'GENERATE REPORT'} <TI_Icon name="arrow" size={11} />
                </button>
              </div>
            </div>
          )}
        </div>
      </main>
    </div>
  );
}

function PhotoBlock({ state, set, theme, styles }) {
  const fileRef = React.useRef(null);
  const [drag, setDrag] = React.useState(false);

  const addFiles = (files) => {
    if (!files || !files.length) return;
    const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff'];
    const next = [];
    for (const f of files) {
      const ok = allowed.includes(f.type) || /\.(jpe?g|png|gif|bmp|tiff?)$/i.test(f.name);
      if (!ok) continue;
      next.push({ file: f, caption: '' });
    }
    if (next.length) set({ photos: [...state.photos, ...next] });
  };

  const updateCaption = (idx, caption) => {
    const next = state.photos.slice();
    next[idx] = { ...next[idx], caption };
    set({ photos: next });
  };

  const removePhoto = (idx) => {
    const next = state.photos.filter((_, i) => i !== idx);
    set({ photos: next });
  };

  const onDrop = (e) => {
    e.preventDefault(); setDrag(false);
    addFiles(e.dataTransfer.files);
  };

  return (
    <div>
      <div
        style={{
          border: `1px dashed ${drag ? theme.accent : theme.line}`,
          padding: state.photos.length ? '12px' : '24px 14px',
          background: drag ? theme.accentTint : (theme.dark ? '#fafaf6' : '#fdfbf3'),
          textAlign: 'center', cursor: 'pointer',
        }}
        onClick={() => fileRef.current?.click()}
        onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
        onDragLeave={() => setDrag(false)}
        onDrop={onDrop}
      >
        <div style={{ color: theme.ink2, fontSize: 11, letterSpacing: '0.04em' }}>
          DROP PHOTOS HERE — OR{' '}
          <span style={{ color: theme.accent, textDecoration: 'underline' }}>BROWSE</span>
        </div>
        <div style={{ color: theme.ink3, fontSize: 10, letterSpacing: '0.16em', marginTop: 4 }}>
          .JPG  .PNG  .GIF  .BMP  .TIFF · MULTIPLE OK
        </div>
      </div>
      <input ref={fileRef} type="file" hidden multiple
             accept=".jpg,.jpeg,.png,.gif,.bmp,.tif,.tiff,image/*"
             onChange={(e) => addFiles(e.target.files)} />

      {state.photos.length > 0 && (
        <div style={{ marginTop: 12, border: `1px solid ${theme.line2}`, background: '#fff' }}>
          {state.photos.map((p, i) => (
            <div key={i} style={{
              display: 'grid',
              gridTemplateColumns: 'auto 80px 1fr auto',
              alignItems: 'center', gap: 12,
              padding: '8px 12px',
              borderBottom: i === state.photos.length - 1 ? 'none' : `1px solid ${theme.line2}`,
            }}>
              <div style={{
                fontFamily: theme.mono, fontSize: 10, fontWeight: 700,
                color: theme.accent, letterSpacing: '0.08em', minWidth: 56,
              }}>
                FIGURE {i + 1}
              </div>
              <div style={{
                fontSize: 10.5, color: theme.ink2, fontFamily: theme.mono,
                whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
              }}>
                {p.file.name}
              </div>
              <input
                style={{
                  ...styles.inputBare, padding: '4px 8px',
                  border: `1px solid ${theme.line2}`, fontSize: 12,
                }}
                placeholder="Caption (optional) — e.g. South elevation, roost feature 1"
                value={p.caption}
                onChange={(e) => updateCaption(i, e.target.value)}
              />
              <button
                style={{ ...styles.ghost, height: 28, padding: '0 10px', fontSize: 9 }}
                onClick={() => removePhoto(i)}
              >
                REMOVE
              </button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function NavRow({ theme, code, label, active, disabled, badge }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 10, padding: '6px 0',
      color: disabled ? (theme.dark ? '#4a4d49' : '#a8a08a')
              : (active ? theme.sideInk3 : theme.sideInk),
      cursor: disabled ? 'default' : 'pointer',
      fontSize: 12, fontFamily: theme.mono,
      position: 'relative',
    }}>
      <span style={{ fontSize: 10, color: theme.sideInk2, width: 28, letterSpacing: '0.04em' }}>{code}</span>
      <span style={{ flex: 1, fontWeight: active ? 600 : 400 }}>{label}</span>
      {badge && <span style={{
        fontSize: 9, padding: '1px 5px',
        background: theme.dark ? '#262a28' : '#dcd5be',
        color: theme.sideInk2, letterSpacing: '0.06em',
      }}>{badge}</span>}
      {active && <span style={{
        position: 'absolute', left: -18, top: 6, bottom: 6, width: 2, background: theme.accent,
      }} />}
    </div>
  );
}

function Block({ theme, title, meta, children, accentBorder }) {
  const styles = makeTIStyles(theme);
  return (
    <div style={{ ...styles.block, ...(accentBorder ? { borderColor: accentBorder, borderWidth: 1.5 } : {}) }}>
      <div style={{ ...styles.blockHd, ...(accentBorder ? { borderBottomColor: accentBorder } : {}) }}>
        <div style={styles.blockTitle}>{title}</div>
        <div style={styles.blockMeta}>{meta}</div>
      </div>
      <div style={styles.blockBody}>{children}</div>
    </div>
  );
}

function Row({ theme, label, children, last }) {
  const styles = makeTIStyles(theme);
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '110px 1fr', alignItems: 'center' }}>
      <div style={{ ...styles.rowLabel, ...(last ? { borderBottom: 'none' } : {}) }}>{label}</div>
      <div style={{ ...styles.rowField, ...(last ? { borderBottom: 'none' } : {}) }}>{children}</div>
    </div>
  );
}

function CheckItem({ ok, theme, children }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11, color: ok ? theme.ink : theme.ink3, fontFamily: theme.mono }}>
      <div style={{
        width: 12, height: 12,
        background: ok ? theme.accent : 'transparent',
        border: ok ? 'none' : `1px solid ${theme.line}`,
        display: 'grid', placeItems: 'center', color: '#fff', flexShrink: 0,
      }}>
        {ok && <TI_Icon name="check" size={9} />}
      </div>
      <span style={{ letterSpacing: '0.04em' }}>{children}</span>
    </div>
  );
}

function RegisterMarks({ theme }) {
  if (!theme.grid) return null;
  const c = theme.line;
  const M = ({ style }) => (
    <div style={{ position: 'absolute', width: 14, height: 14, ...style, pointerEvents: 'none' }}>
      <svg viewBox="0 0 14 14" width="14" height="14"><circle cx="7" cy="7" r="3.5" fill="none" stroke={c} strokeWidth="0.7"/><line x1="0" y1="7" x2="14" y2="7" stroke={c} strokeWidth="0.7"/><line x1="7" y1="0" x2="7" y2="14" stroke={c} strokeWidth="0.7"/></svg>
    </div>
  );
  return (
    <>
      <M style={{ top: 12, left: 12 }} />
      <M style={{ top: 12, right: 12 }} />
      <M style={{ bottom: 12, left: 12 }} />
      <M style={{ bottom: 12, right: 12 }} />
    </>
  );
}

function TIGenerating({ state, theme, styles }) {
  const stepIdx = state.progress < 30 ? 0 : state.progress < 60 ? 1 : state.progress < 90 ? 2 : 3;
  const stepLabels = [
    'READING DATA FILE',
    `APPLYING TEMPLATE: ${(state.templateSlug || 'DEFAULT').toUpperCase()}`,
    'ASSEMBLING DOCUMENT',
    'FINALISING OUTPUT',
  ];
  return (
    <div style={{ ...styles.block, marginBottom: 16 }}>
      <div style={{ ...styles.blockHd, background: theme.accentTint, borderBottomColor: theme.accent }}>
        <div style={{ ...styles.blockTitle, color: theme.accent }}>
          <span style={{ display: 'inline-block', width: 7, height: 7, borderRadius: '50%', background: theme.accent, marginRight: 8, animation: 'ti-pulse 1.4s ease-out infinite' }} />
          PROCESSING
        </div>
        <div style={{ ...styles.blockMeta, fontVariantNumeric: 'tabular-nums', color: theme.accent, fontWeight: 600 }}>
          {Math.round(state.progress).toString().padStart(3, '0')}%
        </div>
      </div>
      <div style={{ padding: 16 }}>
        <div style={{ height: 6, background: theme.line2, marginBottom: 16, position: 'relative', overflow: 'hidden' }}>
          <div style={{ height: '100%', width: `${state.progress}%`, background: theme.accent, transition: 'width .4s ease' }} />
          <div style={{ position: 'absolute', inset: 0, background: `linear-gradient(90deg, transparent, ${theme.accent}40, transparent)`, animation: 'ti-shimmer 1.6s linear infinite', transform: `translateX(${state.progress - 100}%)` }} />
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          {stepLabels.map((label, i) => (
            <div key={i} style={{
              display: 'flex', alignItems: 'center', gap: 10, fontSize: 11,
              fontFamily: theme.mono, letterSpacing: '0.04em',
              color: i < stepIdx ? theme.ink2 : (i === stepIdx ? theme.ink : theme.ink3),
            }}>
              <span style={{ width: 12, color: i < stepIdx ? theme.accent : theme.ink3 }}>
                {i < stepIdx ? '✓' : i === stepIdx ? '▸' : '·'}
              </span>
              <span>{label}{i === stepIdx && <span style={{ animation: 'ti-blink 1s steps(1) infinite' }}>…</span>}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function TISuccess({ state, theme, styles, onReset }) {
  return (
    <div style={{ ...styles.block, borderColor: theme.accent, borderWidth: 1.5, marginBottom: 16, position: 'relative', overflow: 'hidden' }}>
      <div style={{ ...styles.blockHd, background: theme.accentTint, borderBottomColor: theme.accent }}>
        <div style={{ ...styles.blockTitle, color: theme.accent, display: 'flex', alignItems: 'center', gap: 8 }}>
          <span style={{ width: 14, height: 14, borderRadius: '50%', background: theme.accent, color: '#fff', display: 'inline-grid', placeItems: 'center' }}>
            <TI_Icon name="check" size={9} />
          </span>
          COMPLETE
        </div>
        <div style={styles.blockMeta}>EXIT 0 · 100%</div>
      </div>
      <div style={{ padding: 22, display: 'flex', alignItems: 'center', gap: 22 }}>
        <div style={{
          width: 60, height: 78, background: '#fff',
          border: `1.5px solid ${theme.accent}`, position: 'relative', flexShrink: 0,
          boxShadow: `4px 4px 0 ${theme.accentSoft}`,
        }}>
          <div style={{ position: 'absolute', top: 0, right: 0, width: 14, height: 14, background: theme.bg, borderLeft: `1.5px solid ${theme.accent}`, borderBottom: `1.5px solid ${theme.accent}` }} />
          <div style={{ position: 'absolute', top: 18, left: 8, right: 8, height: 1.5, background: theme.line2 }} />
          <div style={{ position: 'absolute', top: 24, left: 8, right: 8, height: 1.5, background: theme.line2 }} />
          <div style={{ position: 'absolute', top: 30, left: 8, right: 14, height: 1.5, background: theme.line2 }} />
          <div style={{
            position: 'absolute', bottom: 8, right: 6, transform: 'rotate(-12deg)',
            border: `1.5px solid ${theme.accent}`, color: theme.accent,
            fontSize: 7, padding: '2px 4px', letterSpacing: '0.1em', fontWeight: 700,
            fontFamily: theme.mono,
          }}>OK</div>
        </div>

        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 10, letterSpacing: '0.18em', color: theme.ink2, marginBottom: 4 }}>OUTPUT FILE</div>
          <div style={{ fontSize: 16, fontWeight: 600, color: theme.ink, marginBottom: 4, wordBreak: 'break-all', fontFamily: theme.mono }}>
            {state.downloadName}
          </div>
          <div style={{ fontSize: 11, color: theme.ink2, fontFamily: theme.mono, letterSpacing: '0.04em' }}>
            FORMAT .DOCX · CIEEM-COMPLIANT · GENERATED {new Date().toISOString().slice(0, 19).replace('T', ' ')}
          </div>
          <div style={{ display: 'flex', gap: 10, marginTop: 16 }}>
            <a href={state.downloadUrl || '#'} download={state.downloadName} style={styles.primaryAccent}>
              <TI_Icon name="download" /> DOWNLOAD .DOCX
            </a>
            <button style={styles.ghost} onClick={onReset}>NEW REPORT</button>
          </div>
        </div>
      </div>
    </div>
  );
}

function TIError({ state, theme, styles, onRetry, onReset }) {
  const code = state.errorKind === 'columns' ? 'ERR_COLUMN_MISMATCH' : state.errorKind === 'server' ? 'ERR_SERVER' : state.errorKind === 'network' ? 'ERR_NETWORK' : 'ERR_REQUEST';
  const exit = state.errorKind === 'server' ? 500 : state.errorKind === 'network' ? -1 : 400;
  return (
    <div style={{ ...styles.block, borderColor: theme.red, borderWidth: 1.5, marginBottom: 16 }}>
      <div style={{ ...styles.blockHd, background: '#fbeeea', borderBottomColor: theme.red }}>
        <div style={{ ...styles.blockTitle, color: theme.red }}>
          <span style={{ marginRight: 6 }}>✕</span>{code}
        </div>
        <div style={styles.blockMeta}>EXIT {exit}</div>
      </div>
      <div style={{ padding: 16 }}>
        <div style={{ fontSize: 13, color: theme.ink, marginBottom: 12, lineHeight: 1.55 }}>{state.error}</div>
        {state.errorKind === 'columns' && (
          <div style={{ fontSize: 11, color: theme.ink2, marginBottom: 14, padding: 12, background: theme.bg, borderLeft: `3px solid ${theme.amberIn}`, fontFamily: theme.mono, letterSpacing: '0.03em', lineHeight: 1.6 }}>
            HINT — Verify CSV headers match the schema for survey type<br />
            <b>{(state.surveyType || '').toUpperCase()}</b>. Expected columns are listed in the API docs.
          </div>
        )}
        <div style={{ display: 'flex', gap: 10 }}>
          <button style={styles.ghost} onClick={onRetry}>RETRY</button>
          <button style={styles.ghost} onClick={onReset}>REPLACE FILE</button>
        </div>
      </div>
    </div>
  );
}

function makeTIStyles(theme) {
  return {
    root: {
      width: '100%', height: '100%', display: 'flex',
      background: theme.bg, color: theme.ink,
      fontFamily: theme.mono, fontSize: 13, lineHeight: 1.55,
    },
    side: {
      width: 240, padding: '20px 18px', background: theme.side, color: theme.sideInk,
      display: 'flex', flexDirection: 'column', flexShrink: 0, fontSize: 12,
      borderRight: theme.dark ? 'none' : `1px solid ${theme.sideRule}`,
    },
    sideHd: {
      fontSize: 9, letterSpacing: '0.2em', color: theme.sideInk2,
      textTransform: 'uppercase', marginBottom: 12, fontWeight: 500,
    },
    brand: {
      display: 'flex', alignItems: 'center', gap: 12, paddingBottom: 18,
      borderBottom: `1px solid ${theme.sideRule}`, marginBottom: 18,
    },
    brandMark: {
      width: 28, height: 28, background: theme.accent, color: '#fff',
      display: 'grid', placeItems: 'center', fontSize: 11, fontWeight: 700,
      letterSpacing: '-0.02em', borderRadius: 2,
    },
    brandText: { color: theme.sideInk3, fontSize: 14, letterSpacing: '0.01em', fontWeight: 600 },
    brandKicker: { fontSize: 9, color: theme.sideInk2, letterSpacing: '0.16em', marginTop: 2 },

    readoutsWrap: { paddingTop: 18, borderTop: `1px solid ${theme.sideRule}`, marginTop: 18 },
    readouts: { display: 'flex', gap: 14, marginBottom: 18 },
    sideMeta: {
      borderTop: `1px solid ${theme.sideRule}`, paddingTop: 12,
      fontSize: 10, color: theme.sideInk2, lineHeight: 1.7, letterSpacing: '0.04em',
    },

    main: { flex: 1, overflow: 'auto', display: 'flex', flexDirection: 'column' },
    topbar: {
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      padding: '14px 32px', borderBottom: `1px solid ${theme.line}`, background: theme.paper,
    },
    topL: { display: 'flex', alignItems: 'center', gap: 20 },
    topR: { display: 'flex', alignItems: 'center', gap: 14 },
    formId: { fontSize: 10, letterSpacing: '0.18em', color: theme.ink2, textTransform: 'uppercase' },
    avatar: { width: 26, height: 26, background: theme.line2, color: theme.ink, display: 'grid', placeItems: 'center', fontSize: 10, fontWeight: 600, letterSpacing: '0.04em' },

    page: { padding: `${theme.d.page}px 40px 80px`, maxWidth: 920, width: '100%', position: 'relative' },
    hd: { marginBottom: 24, paddingBottom: 16, borderBottom: `2px solid ${theme.ink}` },
    kicker: { fontSize: 10, letterSpacing: '0.2em', color: theme.ink2, textTransform: 'uppercase', marginBottom: 10 },
    h1: { fontSize: 26, fontWeight: 600, letterSpacing: '-0.015em', margin: '0 0 10px', color: theme.ink, fontFamily: theme.mono },
    lede: { fontSize: 13, color: theme.ink2, margin: 0, maxWidth: '64ch', lineHeight: 1.65 },

    block: {
      border: `1px solid ${theme.line}`, background: theme.paper,
      marginBottom: theme.d.block,
    },
    blockHd: {
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      padding: '10px 16px', borderBottom: `1px solid ${theme.line2}`,
      background: theme.dark ? '#fafaf6' : '#fdfbf3',
    },
    blockTitle: { fontSize: 11, fontWeight: 600, letterSpacing: '0.08em', textTransform: 'uppercase' },
    blockMeta: { fontSize: 10, color: theme.ink3, letterSpacing: '0.08em' },
    blockBody: { padding: '12px 16px' },

    rowLabel: {
      fontSize: 11, color: theme.ink2, padding: `${theme.d.row + 2}px 12px ${theme.d.row + 2}px 0`,
      borderBottom: `1px solid ${theme.line2}`, letterSpacing: '0.06em',
    },
    rowField: { padding: '4px 0', borderBottom: `1px solid ${theme.line2}` },
    inputBare: {
      background: 'transparent', border: 'none',
      padding: '6px 8px', fontSize: 13,
      fontFamily: theme.mono, color: theme.ink, outline: 'none', width: '100%',
    },
    selectBare: {
      background: 'transparent', border: 'none',
      padding: '6px 24px 6px 8px', fontSize: 13,
      fontFamily: theme.mono, color: theme.ink, outline: 'none', width: '100%',
      appearance: 'none',
      backgroundImage: `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' stroke='%235a5e58' stroke-width='1.5' fill='none' stroke-linecap='round'/></svg>")`,
      backgroundRepeat: 'no-repeat', backgroundPosition: 'right 8px center',
    },

    tplGrid: { display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', border: `1px solid ${theme.line2}` },
    tplCell: (active) => ({
      padding: '12px 12px',
      borderRight: `1px solid ${theme.line2}`,
      background: active ? theme.accentTint : theme.paper,
      cursor: 'pointer', position: 'relative', minHeight: 92,
      transition: 'background .12s',
    }),
    tplCheck: (active) => ({
      width: 12, height: 12,
      border: `1.5px solid ${active ? theme.accent : theme.line}`,
      background: active ? theme.accent : '#fff',
      display: 'grid', placeItems: 'center', color: '#fff', marginBottom: 8,
    }),
    tplName: { fontSize: 11, fontWeight: 700, letterSpacing: '0.04em', marginBottom: 4, textTransform: 'uppercase' },
    tplTag: { fontSize: 10.5, color: theme.ink2, lineHeight: 1.45 },
    tplIdx: { position: 'absolute', top: 10, right: 12, fontSize: 9, color: theme.ink3, letterSpacing: '0.1em' },

    drop: (active, has) => ({
      border: `1px dashed ${active ? theme.accent : theme.line}`,
      padding: has ? '14px' : '36px 14px',
      background: has ? theme.accentTint : (active ? theme.accentTint : (theme.dark ? '#fafaf6' : '#fdfbf3')),
      cursor: has ? 'default' : 'pointer',
      textAlign: has ? 'left' : 'center',
      transition: 'background .12s, border-color .12s',
    }),

    fileRow: { display: 'flex', alignItems: 'center', gap: 14 },
    fileBadge: {
      width: 38, height: 38, background: '#fff',
      border: `1px solid ${theme.line}`,
      display: 'grid', placeItems: 'center',
      fontSize: 9, fontWeight: 700, letterSpacing: '0.06em', color: theme.accent,
    },
    fileName: { fontSize: 13, fontWeight: 600, color: theme.ink, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
    fileMeta: { fontSize: 10, color: theme.ink2, letterSpacing: '0.06em', marginTop: 4 },

    previewWrap: { marginTop: 12, border: `1px solid ${theme.line2}`, background: '#fff' },
    previewHd: {
      display: 'flex', justifyContent: 'space-between',
      padding: '6px 10px', borderBottom: `1px solid ${theme.line2}`,
      background: theme.dark ? '#fafaf6' : '#fdfbf3',
    },
    previewLabel: { fontSize: 9, letterSpacing: '0.16em', color: theme.ink3 },
    previewScroll: { overflowX: 'auto' },
    previewTable: { borderCollapse: 'collapse', fontSize: 11, fontFamily: theme.mono, width: '100%', minWidth: 600 },
    previewTh: {
      textAlign: 'left', padding: '6px 10px',
      borderBottom: `1px solid ${theme.line2}`, borderRight: `1px solid ${theme.line2}`,
      fontSize: 10, letterSpacing: '0.06em', textTransform: 'lowercase',
      whiteSpace: 'nowrap', background: theme.dark ? '#fafaf6' : '#fdfbf3',
    },
    previewTd: {
      padding: '5px 10px', borderBottom: `1px solid ${theme.line2}`, borderRight: `1px solid ${theme.line2}`,
      color: theme.ink2, whiteSpace: 'nowrap', maxWidth: 160, overflow: 'hidden', textOverflow: 'ellipsis',
    },
    previewWarn: { padding: '8px 10px', fontSize: 11, color: theme.amberIn, borderTop: `1px solid ${theme.line2}`, background: '#fdf6e7', letterSpacing: '0.02em' },

    actionPanel: {
      display: 'grid', gridTemplateColumns: '1fr auto', gap: 24, alignItems: 'center',
      padding: '18px 18px',
      background: theme.paper,
      border: `1.5px solid ${theme.ink}`,
      marginTop: 8,
    },
    actionLeft: { display: 'flex', flexDirection: 'column', gap: 8 },
    actionLabel: { fontSize: 9, letterSpacing: '0.2em', color: theme.ink3, textTransform: 'uppercase' },
    actionList: { display: 'flex', gap: 18, flexWrap: 'wrap' },
    actionRight: { textAlign: 'right' },

    primary: (disabled) => ({
      height: 44, padding: '0 22px',
      background: disabled ? theme.line : theme.ink,
      color: '#fff', border: 'none',
      fontFamily: theme.mono, fontSize: 12, fontWeight: 700, letterSpacing: '0.12em',
      textTransform: 'uppercase', cursor: disabled ? 'default' : 'pointer',
      display: 'inline-flex', alignItems: 'center', gap: 10,
      transition: 'background .12s',
    }),
    primaryAccent: {
      height: 40, padding: '0 18px',
      background: theme.accent, color: '#fff', border: 'none',
      fontFamily: theme.mono, fontSize: 11, fontWeight: 700, letterSpacing: '0.12em',
      textTransform: 'uppercase', cursor: 'pointer',
      display: 'inline-flex', alignItems: 'center', gap: 10, textDecoration: 'none',
    },
    ghost: {
      height: 36, padding: '0 14px', background: theme.paper, color: theme.ink,
      border: `1px solid ${theme.line}`,
      fontFamily: theme.mono, fontSize: 10, fontWeight: 600, letterSpacing: '0.1em',
      textTransform: 'uppercase', cursor: 'pointer',
    },
  };
}

Object.assign(window, { TechInstrument, tiTheme });
