// app.jsx — Hydrant Flow Test Calculator
// NFPA 291 formula:  Q_R = Q_F · ((H_R / H_F) ^ 0.54)
//                    H_R = (Static − Target residual), H_F = (Static − Test residual)
// Pitot → GPM:       Q = 29.83 · C · d² · √P_pitot

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

// ─────────────────────────────────────────────────────────────────────────────
// Tweak defaults (persisted to disk)
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "target": 20,
  "exponent": 0.54,
  "accent": "#3b82f6",
  "showSteps": true,
  "theme": "dark"
}/*EDITMODE-END*/;

// ─────────────────────────────────────────────────────────────────────────────
// Calculation helpers

// Pumper-outlet coefficient (NFPA 291 Table 4.10.2) — for outlets ≥4″,
// C varies with pitot velocity head instead of the smooth/sharp/projecting
// categories used for smaller hose outlets.
function pumperCoefficient(pitotPsi) {
  const p = Number(pitotPsi);
  if (!isFinite(p) || p <= 0) return null;
  if (p < 2.5) return 0.97;
  if (p < 3.5) return 0.92;
  if (p < 4.5) return 0.89;
  if (p < 5.5) return 0.86;
  if (p < 6.5) return 0.84;
  return 0.83;
}

// Resolve an outlet's pumper correction — only applies to outlets ≥4″.
// Returns null when no correction applies (small outlet or missing pitot reading).
function pumperCorrection(outlet) {
  const d = Number(outlet.diameter);
  if (!(d >= 4)) return null;
  return pumperCoefficient(outlet.pitotPsi);
}

// Pitot → GPM (standard fire-service formula)
//   Q = 29.83 · C_type · d² · √P    (smaller outlets)
//   Q = 29.83 · C_type · d² · √P · C_pumper   (≥4″ pumper outlets)
function pitotGpm(outlet) {
  const p = Number(outlet.pitotPsi);
  const d = Number(outlet.diameter);
  const cType = Number(outlet.coefficient);
  if (!(p > 0) || !(d > 0) || !(cType > 0)) return 0;
  let q = 29.83 * cType * d * d * Math.sqrt(p);
  const cPumper = pumperCorrection(outlet);
  if (cPumper != null) q *= cPumper;
  return q;
}

// Q at any target pressure given a known test point (Qf at Pr) and static Ps.
//   Q_T = Q_F · ((Ps − T) / (Ps − Pr)) ^ exponent
function flowAtPressure({ static: ps, residual: pr, flow: qf, target: t, exponent }) {
  if (!(ps > pr) || !(ps > 0) || !(qf > 0)) return null;
  if (t >= ps) return 0;
  const hr = ps - t;
  const hf = ps - pr;
  if (hf <= 0) return null;
  return qf * Math.pow(hr / hf, exponent);
}

// AWWA M17 classification at 20 psi residual
function classify(gpm) {
  if (gpm == null) return null;
  if (gpm >= 1500) return { code: 'AA', label: 'Class AA', tone: 'aa', desc: 'Excellent supply' };
  if (gpm >= 1000) return { code: 'A',  label: 'Class A',  tone: 'a',  desc: 'Good supply' };
  if (gpm >=  500) return { code: 'B',  label: 'Class B',  tone: 'b',  desc: 'Fair supply' };
  return            { code: 'C',  label: 'Class C',  tone: 'c',  desc: 'Marginal — limited supply' };
}

function fmt(n, d = 0) {
  if (n == null || !isFinite(n)) return '—';
  return Number(n).toLocaleString('en-US', { maximumFractionDigits: d, minimumFractionDigits: d });
}

// Geocode an address string → {lat, lng} via OpenStreetMap Nominatim
async function geocodeAddress(text) {
  if (!text || text.trim().length < 4) return null;
  try {
    const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(text.trim())}&format=json&limit=1`;
    const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
    const data = await r.json();
    const first = Array.isArray(data) && data[0];
    if (first) return { lat: parseFloat(first.lat), lng: parseFloat(first.lon) };
  } catch (e) { console.warn('geocode error', e); }
  return null;
}

// Elevation lookup disabled for the private rebuild.
async function fetchElevationFt(lat, lng) {
  return null;
}

// ─────────────────────────────────────────────────────────────────────────────
// AddressInput — text input with live Nominatim autocomplete. On select,
// fills label, pins location, and looks up elevation.

function AddressInput({ value, onChange, onResolve, placeholder, inputStyle }) {
  const [suggestions, setSuggestions] = React.useState([]);
  const [open, setOpen] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const wrapRef = React.useRef(null);
  const debounceRef = React.useRef(null);
  const lastQueryRef = React.useRef('');

  // Debounced search
  React.useEffect(() => {
    if (debounceRef.current) clearTimeout(debounceRef.current);
    const q = (value || '').trim();
    if (q.length < 3) { setSuggestions([]); setOpen(false); return; }
    if (q === lastQueryRef.current) return;
    debounceRef.current = setTimeout(async () => {
      lastQueryRef.current = q;
      setBusy(true);
      try {
        const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&limit=5&addressdetails=1`;
        const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
        const data = await r.json();
        setSuggestions(Array.isArray(data) ? data : []);
        setOpen(true);
      } catch (e) { console.warn('autocomplete error', e); }
      setBusy(false);
    }, 400);
  }, [value]);

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

  const choose = async (s) => {
    setOpen(false);
    const lat = parseFloat(s.lat), lng = parseFloat(s.lon);
    if (!isFinite(lat) || !isFinite(lng)) return;
    const label = s.display_name.split(',').slice(0, 2).join(', ').trim();
    lastQueryRef.current = label;
    onResolve({ label, location: { lat, lng } });
    // Fetch elevation in background and pass via onResolve update
    fetchElevationFt(lat, lng).then((elev) => {
      if (elev) onResolve({ elevation: elev });
    });
  };

  return (
    <div ref={wrapRef} style={addrStyles.wrap}>
      <input type="text" value={value} autoComplete="off"
        onChange={(e) => onChange(e.target.value)}
        onFocus={() => suggestions.length > 0 && setOpen(true)}
        placeholder={placeholder}
        style={inputStyle || appStyles.hydrantLabelInput} />
      {busy && <span style={addrStyles.spinner} className="mono">⋯</span>}
      {open && suggestions.length > 0 && (
        <div style={addrStyles.dropdown}>
          {suggestions.map((s, i) => (
            <button key={i} type="button" onClick={() => choose(s)} style={addrStyles.item}>
              <div style={addrStyles.itemMain}>
                {s.display_name.split(',').slice(0, 2).join(', ')}
              </div>
              <div style={addrStyles.itemSub}>
                {s.display_name.split(',').slice(2).join(',').trim()}
              </div>
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

const addrStyles = {
  wrap: { position: 'relative', flex: 1, minWidth: 0 },
  spinner: { position: 'absolute', right: 4, top: 6, fontSize: 14, color: '#7a7a84' },
  dropdown: { position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0,
              background: 'rgba(15,15,18,0.98)', border: '1px solid #26262e',
              borderRadius: 10, overflow: 'hidden', maxHeight: 240, overflowY: 'auto',
              boxShadow: '0 8px 24px rgba(0,0,0,0.55)', zIndex: 30,
              backdropFilter: 'blur(12px)' },
  item: { width: '100%', padding: '9px 12px', background: 'transparent', border: 0,
          borderBottom: '1px solid #1f1f25', color: 'inherit', cursor: 'pointer',
          textAlign: 'left', display: 'flex', flexDirection: 'column', gap: 2,
          fontFamily: 'inherit' },
  itemMain: { fontSize: 12.5, color: '#fafaf7', fontWeight: 500,
              overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
  itemSub: { fontSize: 11, color: '#7a7a84',
             overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
};

// ─────────────────────────────────────────────────────────────────────────────
// Persistent state hooks

// Bump this when we ship a change that should reset every tester's
// in-progress state (e.g. removing seeded test values). On first load
// after a version bump, the per-key live-test state is wiped so users
// land on a clean blank form. Saved history is preserved.
const FLOWTEST_VERSION = '2026-05-27-blank';
(function clearStaleSeedsIfVersionChanged() {
  try {
    if (localStorage.getItem('flowtest.version') === FLOWTEST_VERSION) return;
    // Wipe only the per-key live-test state — not history or auth tokens.
    const keysToWipe = [
      'flowtest.static', 'flowtest.residual', 'flowtest.flow',
      'flowtest.outlets', 'flowtest.flowHydrants',
      'flowtest.residualReadings',
      'flowtest.label', 'flowtest.flowLabel', 'flowtest.staticLabel',
      'flowtest.location', 'flowtest.flowLocation', 'flowtest.staticLocation',
      'flowtest.flowElevation', 'flowtest.staticElevation', 'flowtest.flow.testerName', 'flowtest.testerName',
    ];
    keysToWipe.forEach((k) => localStorage.removeItem(k));
    localStorage.setItem('flowtest.version', FLOWTEST_VERSION);
  } catch {}
})();

function useLocalState(key, initial) {
  const [v, setV] = useState(() => {
    try {
      const raw = localStorage.getItem(key);
      return raw == null ? initial : JSON.parse(raw);
    } catch { return initial; }
  });
  useEffect(() => {
    try { localStorage.setItem(key, JSON.stringify(v)); } catch {}
  }, [key, v]);
  return [v, setV];
}

// ─────────────────────────────────────────────────────────────────────────────
// Big pressure / flow input card

function FieldCard({ label, hint, value, onChange, unit, max = 999, accent }) {
  const id = React.useId();
  const valid = value !== '' && value != null && !isNaN(Number(value));
  return (
    <label htmlFor={id} style={fieldStyles.card}>
      <div style={fieldStyles.top}>
        <span style={fieldStyles.label}>{label}</span>
        {hint && <span style={fieldStyles.hint}>{hint}</span>}
      </div>
      <div style={fieldStyles.row}>
        <input
          id={id}
          inputMode="decimal"
          type="number"
          value={value}
          placeholder="0"
          onChange={(e) => {
            const raw = e.target.value;
            if (raw === '') return onChange('');
            const n = Number(raw);
            if (n < 0) return onChange(0);
            if (n > max) return onChange(max);
            onChange(raw);
          }}
          style={{ ...fieldStyles.input, color: valid ? '#fafaf7' : '#5a5a62' }}
          className="mono"
        />
        <span style={fieldStyles.unit} className="mono">{unit}</span>
      </div>
    </label>
  );
}

const fieldStyles = {
  card: {
    display: 'block',
    background: '#15151a',
    border: '1px solid #26262e',
    borderRadius: 14,
    padding: '14px 16px 12px',
    cursor: 'text',
    minWidth: 0,
  },
  top: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 },
  label: { fontSize: 12, fontWeight: 600, color: '#a8a8b2', letterSpacing: '0.04em', textTransform: 'uppercase' },
  hint: { fontSize: 11, color: '#5a5a62' },
  row: { display: 'flex', alignItems: 'baseline', gap: 8 },
  input: {
    flex: 1, minWidth: 0, width: '100%', background: 'transparent', border: 0, outline: 'none',
    fontSize: 36, fontWeight: 500, padding: 0, letterSpacing: '-0.02em',
    fontFamily: "'IBM Plex Mono', ui-monospace, monospace",
    fontVariantNumeric: 'tabular-nums',
  },
  unit: { fontSize: 14, color: '#6a6a72', fontWeight: 500, paddingBottom: 6 },
};

// ─────────────────────────────────────────────────────────────────────────────
// Mode segmented toggle

function ModeToggle({ value, onChange }) {
  const opts = [
    { v: 'flow',  label: 'Direct flow', sub: 'GPM' },
    { v: 'pitot', label: 'Pitot',       sub: 'psi → GPM' },
  ];
  return (
    <div style={modeStyles.wrap}>
      <div style={{ ...modeStyles.thumb, left: value === 'flow' ? 4 : 'calc(50% + 0px)' }} />
      {opts.map((o) => (
        <button key={o.v} type="button" onClick={() => onChange(o.v)} style={modeStyles.btn}>
          <span style={{ ...modeStyles.btnLabel, color: value === o.v ? '#0a0a0c' : '#a8a8b2' }}>
            {o.label}
          </span>
          <span style={{ ...modeStyles.btnSub, color: value === o.v ? 'rgba(10,10,12,0.6)' : '#5a5a62' }}>
            {o.sub}
          </span>
        </button>
      ))}
    </div>
  );
}
const modeStyles = {
  wrap: { position: 'relative', display: 'flex', padding: 4, borderRadius: 12,
          background: '#15151a', border: '1px solid #26262e' },
  thumb: { position: 'absolute', top: 4, bottom: 4, width: 'calc(50% - 4px)',
           background: 'var(--accent)', borderRadius: 9, transition: 'left .2s cubic-bezier(.3,.7,.4,1)',
           boxShadow: '0 2px 6px rgba(0,0,0,.3)' },
  btn: { position: 'relative', flex: 1, border: 0, background: 'transparent', padding: '8px 4px',
         display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1, cursor: 'pointer', zIndex: 1 },
  btnLabel: { fontSize: 13, fontWeight: 600, transition: 'color .15s' },
  btnSub: { fontSize: 10.5, fontWeight: 500, letterSpacing: '0.04em', textTransform: 'uppercase', transition: 'color .15s' },
};

// ─────────────────────────────────────────────────────────────────────────────
// Pitot outlets — multiple ports flowing simultaneously

const ORIFICE_PRESETS = [
  { v: 2.5,  label: '2½″' },
  { v: 4.5,  label: '4½″' },
  { v: 1.75, label: '1¾″' },
];
const COEFF_PRESETS = [
  { v: 0.90, label: 'Smooth', desc: 'Rounded' },
  { v: 0.80, label: 'Square', desc: 'Sharp edge' },
  { v: 0.70, label: 'Project', desc: 'Projecting' },
];

function PitotOutlet({ outlet, onChange, onRemove, canRemove, index }) {
  const gpm = pitotGpm(outlet);
  return (
    <div style={pitotStyles.row}>
      <div style={pitotStyles.rowHead}>
        <span style={pitotStyles.idx}>Outlet {index + 1}</span>
        <span style={pitotStyles.calcVal} className="mono">
          {gpm > 0 ? `${fmt(gpm, 0)} GPM` : '—'}
        </span>
        {canRemove && (
          <button type="button" onClick={onRemove} style={pitotStyles.remove} aria-label="Remove outlet">
            <svg width="14" height="14" viewBox="0 0 14 14"><path d="M3 7h8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>
          </button>
        )}
      </div>

      <div style={pitotStyles.dualGrid}>
        <MiniField label="Pitot" unit="psi"
          value={outlet.pitotPsi}
          onChange={(v) => onChange({ ...outlet, pitotPsi: v })} />
        <div>
          <MiniField label="Outlet ∅" unit="in" step="0.001"
            value={outlet.diameter}
            onChange={(v) => onChange({ ...outlet, diameter: v })} />
          <div style={pitotStyles.chips}>
            {ORIFICE_PRESETS.map((p) => {
              const on = Number(outlet.diameter) === p.v;
              return (
                <button key={p.v} type="button"
                  onClick={() => onChange({ ...outlet, diameter: p.v })}
                  style={{
                    ...pitotStyles.chip,
                    color: on ? '#0a0a0c' : '#a8a8b2',
                    background: on ? 'var(--accent)' : 'transparent',
                    borderColor: on ? 'var(--accent)' : '#2a2a31',
                  }}
                  className="mono">{p.label}</button>
              );
            })}
          </div>
        </div>
      </div>

      <CoeffPicker value={outlet.coefficient}
        onChange={(v) => onChange({ ...outlet, coefficient: v })}
        outlet={outlet} />
    </div>
  );
}

function CoeffPicker({ value, onChange, outlet }) {
  const numVal = Number(value);
  const isPreset = COEFF_PRESETS.some((p) => p.v === numVal);
  const isCustom = !isPreset && isFinite(numVal) && numVal > 0;
  return (
    <div style={coeffStyles.wrap}>
      <div style={coeffStyles.label}>
        <span>Coefficient (C)</span>
        <span style={coeffStyles.labelHint}>Hydrant outlet type</span>
      </div>
      <div style={coeffStyles.row}>
        {COEFF_PRESETS.map((p) => {
          const on = numVal === p.v;
          return (
            <button key={p.v} type="button" onClick={() => onChange(p.v)}
              style={{
                ...coeffStyles.btn,
                background: on ? 'var(--accent)' : '#101014',
                borderColor: on ? 'var(--accent)' : '#2a2a31',
                color: on ? '#0a0a0c' : '#fafaf7',
              }}>
              <span style={coeffStyles.btnNum} className="mono">{p.v.toFixed(2)}</span>
              <span style={{
                ...coeffStyles.btnLabel,
                color: on ? 'rgba(10,10,12,0.65)' : '#7a7a84',
              }}>{p.desc}</span>
            </button>
          );
        })}
        <button type="button"
          onClick={() => onChange(isCustom ? value : 0.97)}
          title="Custom coefficient — hose monster, flow gauge, etc."
          style={{
            ...coeffStyles.btn,
            background: isCustom ? 'var(--accent)' : '#101014',
            borderColor: isCustom ? 'var(--accent)' : '#2a2a31',
            color: isCustom ? '#0a0a0c' : '#fafaf7',
          }}>
          <span style={coeffStyles.btnNum} className="mono">
            {isCustom ? numVal.toFixed(2) : 'XX'}
          </span>
          <span style={{
            ...coeffStyles.btnLabel,
            color: isCustom ? 'rgba(10,10,12,0.65)' : '#7a7a84',
          }}>Custom</span>
        </button>
      </div>
      {isCustom && (
        <div style={coeffStyles.customRow}>
          <span style={coeffStyles.customLbl}>Value (C)</span>
          <input type="number" step="0.01" min="0" max="1"
            inputMode="decimal"
            value={value}
            onChange={(e) => {
              const v = e.target.value;
              onChange(v === '' ? '' : Number(v));
            }}
            placeholder="0.97"
            style={coeffStyles.customInput} className="mono" />
          <span style={coeffStyles.customHint}>Hose monster, flow gauge, special outlet</span>
        </div>
      )}
      <PumperCorrectionNote outlet={outlet} />
    </div>
  );
}

// Pumper correction — visible only for outlets ≥4″. Computed from pitot
// pressure (NFPA 291 Table 4.10.2) and applied automatically on top of the
// chosen C above.
function PumperCorrectionNote({ outlet }) {
  const d = Number(outlet.diameter);
  if (!(d >= 4)) return null;
  const c = pumperCoefficient(outlet.pitotPsi);
  return (
    <div style={pumperStyles.card}>
      <div style={pumperStyles.left}>
        <span style={pumperStyles.badge}>Pumper outlet</span>
        <span style={pumperStyles.hint}>NFPA 291 Table 4.10.2 — applied automatically</span>
      </div>
      <div style={pumperStyles.right}>
        <span style={pumperStyles.timesLbl}>×</span>
        <span style={pumperStyles.coeff} className="mono">
          {c != null ? c.toFixed(2) : '—'}
        </span>
      </div>
    </div>
  );
}

const pumperStyles = {
  card: { display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          padding: '8px 12px', background: '#101014', border: '1px solid #2a2a31',
          borderLeft: '2px solid var(--accent)', borderRadius: 8, marginTop: 4 },
  left: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 },
  badge: { fontSize: 11, fontWeight: 600, color: 'var(--accent)', letterSpacing: '0.03em' },
  hint: { fontSize: 10.5, color: '#7a7a84' },
  right: { display: 'flex', alignItems: 'baseline', gap: 4 },
  timesLbl: { fontSize: 11, color: '#7a7a84' },
  coeff: { fontSize: 17, fontWeight: 600, color: '#fafaf7', letterSpacing: '-0.01em' },
};

const coeffStyles = {
  wrap: { display: 'flex', flexDirection: 'column', gap: 6 },
  label: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' },
  labelHint: { fontSize: 10.5, color: '#5a5a62' },
  row: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 6 },
  btn: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
         padding: '8px 4px', border: '1px solid', borderRadius: 8, cursor: 'pointer',
         transition: 'background .12s, border-color .12s, color .12s' },
  btnNum: { fontSize: 14, fontWeight: 600, letterSpacing: '-0.01em' },
  btnLabel: { fontSize: 10, fontWeight: 500, letterSpacing: '0.02em', transition: 'color .12s',
              whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '100%' },
  customRow: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 2,
               padding: '8px 12px', background: '#101014', border: '1px solid #2a2a31',
               borderLeft: '2px solid var(--accent)', borderRadius: 8 },
  customLbl: { fontSize: 11, fontWeight: 600, color: '#7a7a84',
               letterSpacing: '0.04em', textTransform: 'uppercase', flexShrink: 0 },
  customInput: { width: 64, background: 'transparent', border: '1px solid #2a2a31',
                 borderRadius: 6, padding: '4px 8px', color: '#fafaf7',
                 fontSize: 14, fontWeight: 600, outline: 'none', textAlign: 'center',
                 fontFamily: "'IBM Plex Mono', monospace" },
  customHint: { fontSize: 10.5, color: '#7a7a84', marginLeft: 'auto',
                overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
};

function MiniField({ label, unit, value, onChange, step }) {
  const id = React.useId();
  return (
    <label htmlFor={id} style={miniStyles.wrap}>
      <span style={miniStyles.label}>{label}</span>
      <span style={miniStyles.inputRow}>
        <input id={id} type="number" inputMode="decimal" value={value} placeholder="0"
               step={step}
               onChange={(e) => onChange(e.target.value)} style={miniStyles.input} className="mono" />
        {unit && <span style={miniStyles.unit} className="mono">{unit}</span>}
      </span>
    </label>
  );
}

function MiniSelect({ label, value, options, onChange }) {
  const id = React.useId();
  return (
    <label htmlFor={id} style={miniStyles.wrap}>
      <span style={miniStyles.label}>{label}</span>
      <span style={miniStyles.inputRow}>
        <select id={id} value={value} onChange={(e) => onChange(e.target.value)} style={miniStyles.select} className="mono">
          {options.map((o) => (
            <option key={o.v} value={o.v}>{o.label} ({o.v})</option>
          ))}
        </select>
      </span>
    </label>
  );
}

const miniStyles = {
  wrap: { display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 },
  label: { fontSize: 10.5, fontWeight: 600, color: '#7a7a84', letterSpacing: '0.04em', textTransform: 'uppercase' },
  inputRow: { display: 'flex', alignItems: 'center', background: '#101014', border: '1px solid #2a2a31',
              borderRadius: 9, padding: '6px 9px', gap: 6, minWidth: 0 },
  input: { width: '100%', minWidth: 0, background: 'transparent', border: 0, outline: 'none',
           color: '#fafaf7', fontSize: 16, fontWeight: 500, padding: 0, fontFamily: "'IBM Plex Mono', monospace" },
  select: { width: '100%', minWidth: 0, background: 'transparent', border: 0, outline: 'none',
            color: '#fafaf7', fontSize: 13, fontWeight: 500, fontFamily: "'IBM Plex Mono', monospace",
            appearance: 'none', WebkitAppearance: 'none', paddingRight: 14,
            backgroundImage: "url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'><path fill='%237a7a84' d='M0 0h8L4 5z'/></svg>\")",
            backgroundRepeat: 'no-repeat', backgroundPosition: 'right center' },
  unit: { fontSize: 11, color: '#6a6a72' },
};

const pitotStyles = {
  row: { background: '#15151a', border: '1px solid #26262e', borderRadius: 14, padding: '12px 14px',
         display: 'flex', flexDirection: 'column', gap: 10 },
  rowHead: { display: 'flex', alignItems: 'center', gap: 10 },
  idx: { fontSize: 11.5, fontWeight: 600, color: '#a8a8b2', letterSpacing: '0.05em', textTransform: 'uppercase', flex: 1 },
  calcVal: { fontSize: 13, color: 'var(--accent)', fontWeight: 600 },
  remove: { width: 24, height: 24, borderRadius: 6, border: '1px solid #2a2a31', background: '#101014',
            color: '#a8a8b2', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0 },
  grid: { display: 'grid', gridTemplateColumns: 'minmax(0,1fr) minmax(0,1fr) minmax(0,1fr)', gap: 8 },
  dualGrid: { display: 'grid', gridTemplateColumns: 'minmax(0,1fr) minmax(0,1.4fr)', gap: 10, alignItems: 'start' },
  chips: { display: 'flex', gap: 4, marginTop: 5 },
  chip: { flex: 1, minWidth: 0, padding: '4px 2px', borderRadius: 6, border: '1px solid',
          fontSize: 11, fontWeight: 500, cursor: 'pointer', transition: 'all .12s' },
};

// ─────────────────────────────────────────────────────────────────────────────
// Result card

const TONE = {
  aa: { bg: 'linear-gradient(135deg, #1b3a5a 0%, #0f2238 100%)', stripe: '#5dadec', text: '#cfe6fb' },
  a:  { bg: 'linear-gradient(135deg, #1a3a2a 0%, #0e2218 100%)', stripe: '#4ade80', text: '#bdf5cb' },
  b:  { bg: 'linear-gradient(135deg, #3d2e10 0%, #221806 100%)', stripe: '#f5a524', text: '#fbe2a6' },
  c:  { bg: 'linear-gradient(135deg, #3a1818 0%, #220c0c 100%)', stripe: '#f87171', text: '#fbc8c8' },
  err:{ bg: 'linear-gradient(135deg, #2a1416 0%, #170808 100%)', stripe: '#f87171', text: '#fbc8c8' },
  idle:{ bg: '#15151a',                                          stripe: '#3a3a44', text: '#7a7a84' },
};

const PAGE_INFO = {
  hydraulic: {
    title: 'Hydraulic Design Test — how to use',
    items: [
      'Use this tab for hydraulic design work when you need more than one test on the same site.',
      'Use Add test to duplicate the current test so you can compare a second scenario without rebuilding the hydrants from scratch.',
      'Each test keeps one residual hydrant and its flowing hydrants together; pin locations with the map before exporting.',
      'If you are only doing a single hydrant flow check, stay on the Hydrant Flow Test tab instead.',
    ],
  },
  flow: {
    title: 'Hydrant Flow Test — how to use',
    items: [
      'Use this tab for single-hydrant annual flow tests. Each card is one hydrant and keeps the static, residual, and outlet data together.',
      'Enter the hydrant ID/address, then pin its location on the map and record the static and residual pressures from that same hydrant.',
      'Add one or more outlets when the hydrant is flowed, then export the PDF to generate one report page for that hydrant.',
    ],
  },
};

function InfoModal({ open, onClose, title, items }) {
  if (!open) return null;
  return (
    <div style={modalStyles.overlay} onClick={onClose}>
      <div style={modalStyles.panel} onClick={(e) => e.stopPropagation()}>
        <div style={modalStyles.head}>
          <div style={modalStyles.title}>{title}</div>
          <button onClick={onClose} style={modalStyles.close} aria-label="Close info">
            <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
              <path d="M2 2l8 8M10 2 2 10" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
            </svg>
          </button>
        </div>
        <div style={modalStyles.body}>
          <div style={modalStyles.kicker}>How to use this page</div>
          <ul style={modalStyles.list}>
            {items.map((item) => <li key={item} style={modalStyles.li}>{item}</li>)}
          </ul>
        </div>
      </div>
    </div>
  );
}

function ResultCard({ available, classInfo, target, error }) {
  const tone = error ? TONE.err : classInfo ? TONE[classInfo.tone] : TONE.idle;
  return (
    <div style={{ ...resultStyles.card, background: tone.bg }}>
      <div style={{ ...resultStyles.stripe, background: tone.stripe }} />
      <div style={resultStyles.label}>
        Available at <span className="mono" style={{ color: tone.text }}>{target} psi</span> residual
      </div>
      {error ? (
        <>
          <div style={resultStyles.errBig}>—</div>
          <div style={{ ...resultStyles.err, color: tone.text }}>{error}</div>
        </>
      ) : available == null ? (
        <>
          <div style={resultStyles.bigIdle} className="mono">—</div>
          <div style={resultStyles.helper}>Enter static, residual, and flow to calculate</div>
        </>
      ) : (
        <>
          <div style={resultStyles.bigWrap}>
            <span style={{ ...resultStyles.big, color: tone.text }} className="mono">
              {fmt(available, 0)}
            </span>
            <span style={resultStyles.bigUnit}>GPM</span>
          </div>
          {classInfo && (
            <div style={resultStyles.classRow}>
              <span style={{ ...resultStyles.classBadge, color: tone.stripe, borderColor: tone.stripe }} className="mono">
                {classInfo.label}
              </span>
              <span style={{ ...resultStyles.classDesc, color: tone.text }}>
                {classInfo.desc}
              </span>
            </div>
          )}
        </>
      )}
    </div>
  );
}

const modalStyles = {
  overlay: { position: 'fixed', inset: 0, background: 'rgba(5, 5, 8, 0.58)', zIndex: 60, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 },
  panel: { width: 'min(92vw, 520px)', borderRadius: 18, border: '1px solid #2a2a31', background: 'linear-gradient(180deg, #15151a 0%, #101014 100%)', boxShadow: '0 20px 60px rgba(0,0,0,0.45)', overflow: 'hidden' },
  head: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, padding: '16px 18px 12px', borderBottom: '1px solid #22222a' },
  title: { color: '#fafaf7', fontSize: 15.5, fontWeight: 700, letterSpacing: '-0.01em' },
  close: { width: 28, height: 28, borderRadius: 8, border: '1px solid #2a2a31', background: '#0f0f13', color: '#a8a8b2', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', padding: 0 },
  body: { padding: '14px 18px 18px' },
  kicker: { fontSize: 11, color: '#a8a8b2', letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: 10, fontWeight: 700 },
  list: { margin: 0, paddingLeft: 18, color: '#e4e4e7', fontSize: 13.5, lineHeight: 1.5, display: 'grid', gap: 8 },
  li: { paddingLeft: 2 },
};

const resultStyles = {
  card: { position: 'relative', borderRadius: 18, padding: '18px 18px 20px', overflow: 'hidden',
          border: '1px solid #26262e' },
  stripe: { position: 'absolute', top: 0, left: 0, right: 0, height: 3 },
  label: { fontSize: 11.5, fontWeight: 600, color: '#a8a8b2', letterSpacing: '0.06em',
           textTransform: 'uppercase', marginBottom: 10 },
  bigWrap: { display: 'flex', alignItems: 'baseline', gap: 10, lineHeight: 1 },
  big: { fontSize: 64, fontWeight: 500, letterSpacing: '-0.04em' },
  bigIdle: { fontSize: 64, fontWeight: 500, letterSpacing: '-0.04em', color: '#3a3a44', lineHeight: 1 },
  bigUnit: { fontSize: 16, color: '#a8a8b2', fontWeight: 500 },
  classRow: { marginTop: 12, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' },
  classBadge: { padding: '3px 8px', borderRadius: 5, border: '1px solid', fontSize: 11.5, fontWeight: 600, letterSpacing: '0.05em' },
  classDesc: { fontSize: 12.5, fontWeight: 500 },
  helper: { marginTop: 8, fontSize: 12, color: '#6a6a72' },
  err: { marginTop: 6, fontSize: 13, fontWeight: 500 },
  errBig: { fontSize: 48, fontWeight: 500, color: '#7a3030', lineHeight: 1, fontFamily: "'IBM Plex Mono', monospace" },
};

// ─────────────────────────────────────────────────────────────────────────────
// Flow curve chart

function FlowCurve({ staticPsi, residualPsi, testGpm, target, exponent, accent }) {
  const W = 600, H = 220, PAD = { l: 40, r: 16, t: 16, b: 32 };
  const innerW = W - PAD.l - PAD.r, innerH = H - PAD.t - PAD.b;

  // Compute available at target
  const qAtTarget = flowAtPressure({ static: staticPsi, residual: residualPsi, flow: testGpm, target, exponent });
  const qAtZero   = flowAtPressure({ static: staticPsi, residual: residualPsi, flow: testGpm, target: 0, exponent });

  const xMax = Math.max(qAtZero || 0, (qAtTarget || 0) * 1.15, (testGpm || 0) * 1.3, 100);
  const yMax = Math.max(staticPsi || 0, 100);

  const xScale = (q) => PAD.l + (q / xMax) * innerW;
  const yScale = (p) => PAD.t + (1 - p / yMax) * innerH;

  // Build the curve path
  const path = useMemo(() => {
    if (!(staticPsi > 0 && residualPsi >= 0 && testGpm > 0 && staticPsi > residualPsi)) return null;
    const hf = staticPsi - residualPsi;
    const pts = [];
    const N = 60;
    for (let i = 0; i <= N; i++) {
      const q = (i / N) * xMax;
      // P = Ps − hf · (q/Qf)^(1/0.54)
      const drop = hf * Math.pow(q / testGpm, 1 / exponent);
      const p = Math.max(0, staticPsi - drop);
      pts.push([xScale(q), yScale(p)]);
    }
    return pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ');
  }, [staticPsi, residualPsi, testGpm, exponent, xMax, yMax]);

  // y-axis ticks
  const yTicks = useMemo(() => {
    const step = yMax <= 60 ? 10 : yMax <= 120 ? 20 : 25;
    const ticks = [];
    for (let v = 0; v <= yMax; v += step) ticks.push(v);
    return ticks;
  }, [yMax]);

  // x-axis ticks (nice rounded)
  const xTicks = useMemo(() => {
    const step = xMax <= 500 ? 100 : xMax <= 1500 ? 250 : xMax <= 3000 ? 500 : 1000;
    const ticks = [];
    for (let v = 0; v <= xMax; v += step) ticks.push(v);
    return ticks;
  }, [xMax]);

  const hasCurve = !!path;
  const targetY = yScale(target);

  return (
    <div style={curveStyles.card}>
      <div style={curveStyles.header}>
        <span style={curveStyles.title}>Flow curve</span>
        <span style={curveStyles.legend}>
          <i style={{ ...curveStyles.dot, background: accent }} /> Available @ {target} psi
        </span>
      </div>
      <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: 'auto', display: 'block' }}>
        {/* Grid + axes */}
        {yTicks.map((v) => (
          <g key={`y${v}`}>
            <line x1={PAD.l} x2={W - PAD.r} y1={yScale(v)} y2={yScale(v)}
                  stroke="#23232a" strokeWidth="1" />
            <text x={PAD.l - 6} y={yScale(v) + 3.5} textAnchor="end"
                  fill="#5a5a62" fontSize="10" className="mono">{v}</text>
          </g>
        ))}
        {xTicks.map((v) => (
          <g key={`x${v}`}>
            <line x1={xScale(v)} x2={xScale(v)} y1={PAD.t} y2={H - PAD.b}
                  stroke="#1a1a20" strokeWidth="1" />
            <text x={xScale(v)} y={H - PAD.b + 14} textAnchor="middle"
                  fill="#5a5a62" fontSize="10" className="mono">{v}</text>
          </g>
        ))}

        {/* Axis labels */}
        <text x={PAD.l - 30} y={PAD.t + 8} fill="#7a7a84" fontSize="9.5"
              textAnchor="start" letterSpacing="0.5">PSI</text>
        <text x={W - PAD.r} y={H - 4} fill="#7a7a84" fontSize="9.5"
              textAnchor="end" letterSpacing="0.5">GPM</text>

        {/* Target line at residual */}
        <line x1={PAD.l} x2={W - PAD.r} y1={targetY} y2={targetY}
              stroke={accent} strokeWidth="1" strokeDasharray="3 3" opacity="0.5" />
        <text x={W - PAD.r - 4} y={targetY - 4} textAnchor="end"
              fill={accent} fontSize="10" fontWeight="600" className="mono">{target} psi</text>

        {/* Shade below target */}
        {hasCurve && (
          <rect x={PAD.l} y={targetY} width={innerW} height={H - PAD.b - targetY}
                fill={accent} opacity="0.04" />
        )}

        {/* Curve */}
        {hasCurve && (
          <>
            <path d={path} fill="none" stroke="#6a6a72" strokeWidth="1.5" strokeLinecap="round" />
            {/* Test point (Qf, Pr) */}
            <circle cx={xScale(testGpm)} cy={yScale(residualPsi)} r="4"
                    fill="#15151a" stroke="#fafaf7" strokeWidth="1.5" />
            {/* Static point (0, Ps) */}
            <circle cx={xScale(0)} cy={yScale(staticPsi)} r="4"
                    fill="#15151a" stroke="#fafaf7" strokeWidth="1.5" />
            {/* Available point (Q@target, target) */}
            {qAtTarget > 0 && (
              <>
                <line x1={xScale(qAtTarget)} x2={xScale(qAtTarget)} y1={targetY} y2={H - PAD.b}
                      stroke={accent} strokeWidth="1" strokeDasharray="2 2" opacity="0.6" />
                <circle cx={xScale(qAtTarget)} cy={targetY} r="6"
                        fill={accent} stroke="#0a0a0c" strokeWidth="2" />
                <text x={xScale(qAtTarget)} y={H - PAD.b - 6} textAnchor="middle"
                      fill={accent} fontSize="11" fontWeight="600" className="mono">
                  {fmt(qAtTarget, 0)}
                </text>
              </>
            )}
          </>
        )}
      </svg>
    </div>
  );
}

const curveStyles = {
  card: { background: '#15151a', border: '1px solid #26262e', borderRadius: 14, padding: '14px 14px 8px' },
  header: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 6, padding: '0 4px' },
  title: { fontSize: 11.5, fontWeight: 600, color: '#a8a8b2', letterSpacing: '0.06em', textTransform: 'uppercase' },
  legend: { fontSize: 11, color: '#a8a8b2', display: 'flex', alignItems: 'center', gap: 6 },
  dot: { width: 8, height: 8, borderRadius: '50%', display: 'inline-block' },
};

// ─────────────────────────────────────────────────────────────────────────────
// Math breakdown — show the work

function CalcBreakdown({ staticPsi, residualPsi, testGpm, target, exponent, pitotOutlets, mode, available, flowHydrantCount = 1 }) {
  const hr = staticPsi - target;
  const hf = staticPsi - residualPsi;
  const ratio = hf > 0 ? hr / hf : null;

  return (
    <div style={breakdownStyles.card}>
      <div style={breakdownStyles.title}>How this is calculated</div>

      {mode === 'pitot' && pitotOutlets.length > 0 && (
        <div style={breakdownStyles.section}>
          <div style={breakdownStyles.sectionLabel}>1 · Convert pitot pressure → GPM</div>
          <div style={breakdownStyles.formula} className="mono">
            Q = 29.83 × C × d² × √P{' '}
            {pitotOutlets.some((o) => pumperCorrection(o) != null) && (
              <span style={breakdownStyles.muted}>× C_pumper&nbsp;(≥4″)</span>
            )}
          </div>
          {pitotOutlets.map((o, i) => {
            const q = pitotGpm(o);
            const c = Number(o.coefficient);
            const cPumper = pumperCorrection(o);
            return (
              <div key={i} style={breakdownStyles.calcLine} className="mono">
                <span style={breakdownStyles.muted}>
                  {flowHydrantCount > 1 ? `${o._hydrantLabel} · Outlet ${i + 1}:` : `Outlet ${i + 1}:`}
                </span>{' '}
                29.83 × {c.toFixed(2)} × {fmt(o.diameter, 3)}² × √{o.pitotPsi || 0}
                {cPumper != null && (
                  <>{' × '}<span style={breakdownStyles.muted}>{cPumper.toFixed(2)} (pumper)</span></>
                )}
                {' '}<span style={breakdownStyles.eq}>=</span>{' '}
                <span style={breakdownStyles.val}>{fmt(q, 0)} GPM</span>
              </div>
            );
          })}
          {pitotOutlets.length > 1 && (
            <div style={breakdownStyles.calcLine} className="mono">
              <span style={breakdownStyles.muted}>Total flow Q_F:</span>{' '}
              <span style={breakdownStyles.val}>
                {fmt(pitotOutlets.reduce((s, o) => s + pitotGpm(o), 0), 0)} GPM
              </span>
            </div>
          )}
        </div>
      )}

      <div style={breakdownStyles.section}>
        <div style={breakdownStyles.sectionLabel}>
          {mode === 'pitot' ? '2' : '1'} · Pressure drop ratio
        </div>
        <div style={breakdownStyles.calcLine} className="mono">
          <span style={breakdownStyles.muted}>H_R</span> = Static − target ={' '}
          {fmt(staticPsi, 0)} − {target} = <span style={breakdownStyles.val}>{fmt(hr, 0)} psi</span>
        </div>
        <div style={breakdownStyles.calcLine} className="mono">
          <span style={breakdownStyles.muted}>H_F</span> = Static − residual ={' '}
          {fmt(staticPsi, 0)} − {fmt(residualPsi, 0)} = <span style={breakdownStyles.val}>{fmt(hf, 0)} psi</span>
        </div>
      </div>

      <div style={breakdownStyles.section}>
        <div style={breakdownStyles.sectionLabel}>
          {mode === 'pitot' ? '3' : '2'} · Available at {target} psi (NFPA 291)
        </div>
        <div style={breakdownStyles.formula} className="mono">
          Q_R = Q_F × (H_R / H_F)^{exponent}
        </div>
        <div style={breakdownStyles.calcLine} className="mono">
          = {fmt(testGpm, 0)} × ({fmt(hr, 0)} / {fmt(hf, 0)})^{exponent}{' '}
          {ratio != null && (
            <>
              <span style={breakdownStyles.eq}>=</span>{' '}
              {fmt(testGpm, 0)} × {fmt(Math.pow(ratio, exponent), 3)}
            </>
          )}
        </div>
        <div style={breakdownStyles.resultLine} className="mono">
          ≈ <span style={breakdownStyles.resultVal}>{fmt(available, 0)} GPM</span>
        </div>
      </div>
    </div>
  );
}

const breakdownStyles = {
  card: { background: '#101014', border: '1px solid #1f1f25', borderRadius: 14, padding: '14px 16px',
          display: 'flex', flexDirection: 'column', gap: 14 },
  title: { fontSize: 11.5, fontWeight: 600, color: '#a8a8b2', letterSpacing: '0.06em', textTransform: 'uppercase' },
  section: { display: 'flex', flexDirection: 'column', gap: 6 },
  sectionLabel: { fontSize: 11.5, fontWeight: 600, color: '#8a8a92', marginBottom: 2 },
  formula: { fontSize: 13, color: '#fafaf7', background: '#1a1a20', padding: '7px 10px', borderRadius: 7,
             border: '1px solid #26262e', fontWeight: 500, display: 'inline-block', width: 'fit-content' },
  calcLine: { fontSize: 12.5, color: '#c8c8d2', lineHeight: 1.6 },
  muted: { color: '#7a7a84' },
  eq: { color: '#5a5a62' },
  val: { color: 'var(--accent)', fontWeight: 600 },
  resultLine: { fontSize: 14, color: '#fafaf7', paddingTop: 4, borderTop: '1px dashed #26262e', marginTop: 2 },
  resultVal: { color: 'var(--accent)', fontWeight: 600, fontSize: 16 },
};

// ─────────────────────────────────────────────────────────────────────────────
// History drawer

function HistoryPanel({ history, onClose, onLoad, onRetest, onClear, onDelete, onExport }) {
  // File import handler — read JSON and merge unique entries by id
  const fileRef = React.useRef(null);
  return (
    <>
      <div style={historyStyles.scrim} onClick={onClose} />
      <div style={historyStyles.panel}>
        <div style={historyStyles.head}>
          <div style={historyStyles.headTitle}>Test history</div>
          <button onClick={onClose} style={historyStyles.close} aria-label="Close">
            <svg width="14" height="14" viewBox="0 0 14 14">
              <path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
            </svg>
          </button>
        </div>
        {history.length === 0 ? (
          <div style={historyStyles.empty}>
            <div style={{ fontSize: 38, marginBottom: 8, color: '#3a3a44' }}>○</div>
            <div style={{ fontSize: 13, color: '#7a7a84' }}>No saved tests yet</div>
            <div style={{ fontSize: 12, color: '#5a5a62', marginTop: 4 }}>Save a calculation to add it here</div>
            <button onClick={() => fileRef.current?.click()} style={historyStyles.importBtn}>
              Import from JSON
            </button>
            <input ref={fileRef} type="file" accept=".json,application/json"
              style={{ display: 'none' }}
              onChange={(e) => {
                const f = e.target.files?.[0]; if (!f) return;
                const reader = new FileReader();
                reader.onload = () => {
                  try { onExport.import(JSON.parse(reader.result)); }
                  catch { alert('Invalid file — expected exported JSON'); }
                };
                reader.readAsText(f);
                e.target.value = '';
              }} />
          </div>
        ) : (
          <div style={historyStyles.list}>
            {history.map((h) => {
              // New format counts residuals from residualReadings; legacy entries
              // have a single residualPsi field.
              const residualCount = h.residualReadings ? h.residualReadings.length : 1;
              const residualSummary = h.residualReadings
                ? `${residualCount} residual${residualCount === 1 ? '' : 's'}`
                : `R ${fmt(h.residualPsi, 0)}`;
              return (
                <div key={h.id} style={historyStyles.item}>
                  <button onClick={() => onLoad(h)} style={historyStyles.itemBody}>
                    <div style={historyStyles.itemTop}>
                      <span style={historyStyles.itemHydrant}>{h.label || 'Test'}</span>
                      <span style={historyStyles.itemDate} className="mono">{
                        new Date(h.ts).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })
                      }</span>
                    </div>
                    <div style={historyStyles.itemRow}>
                      <span style={{ ...historyStyles.itemVal, color: 'var(--accent)' }} className="mono">{fmt(h.available, 0)} GPM</span>
                      <span style={historyStyles.itemSep}>@ {h.target} psi</span>
                    </div>
                    <div style={historyStyles.itemMeta} className="mono">
                      S {fmt(h.staticPsi, 0)} · {residualSummary} · F {fmt(h.testGpm, 0)}
                    </div>
                  </button>
                  <div style={historyStyles.itemActions}>
                    <button onClick={() => onRetest(h)} style={historyStyles.itemAct} aria-label="Re-test" title="Re-test (pre-fill locations, blank pressures)">
                      <svg width="12" height="12" viewBox="0 0 14 14" fill="none">
                        <path d="M3 7a4 4 0 1 0 1.2-2.8M3 3v2.5h2.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
                      </svg>
                    </button>
                    <button onClick={() => window.open(`report.html#tab=hydraulic&id=${encodeURIComponent(h.id)}`, '_blank')}
                            style={historyStyles.itemAct} aria-label="Print report" title="Print report (PDF)">
                      <svg width="12" height="12" viewBox="0 0 14 14" fill="none">
                        <path d="M4 5V2h6v3M4 10H2.5v-4h9v4H10M4 8h6v4H4z"
                              stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round"/>
                      </svg>
                    </button>
                    <button onClick={() => onDelete(h.id)} style={historyStyles.itemAct} aria-label="Delete" title="Delete">
                      <svg width="12" height="12" viewBox="0 0 12 12"><path d="M2 6h8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>
                    </button>
                  </div>
                </div>
              );
            })}
            <div style={historyStyles.footer}>
              <div style={historyStyles.footerRow}>
                <button onClick={onExport.json} style={historyStyles.footerBtn}>
                  <svg width="12" height="12" viewBox="0 0 14 14" fill="none" style={{ marginRight: 5 }}>
                    <path d="M7 1v8M4 6l3 3 3-3M2 12h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
                  </svg>
                  JSON
                </button>
                <button onClick={onExport.csv} style={historyStyles.footerBtn}>
                  <svg width="12" height="12" viewBox="0 0 14 14" fill="none" style={{ marginRight: 5 }}>
                    <path d="M7 1v8M4 6l3 3 3-3M2 12h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
                  </svg>
                  CSV
                </button>
                <button onClick={() => fileRef.current?.click()} style={historyStyles.footerBtn}>
                  <svg width="12" height="12" viewBox="0 0 14 14" fill="none" style={{ marginRight: 5 }}>
                    <path d="M7 13V5M4 8l3-3 3 3M2 2h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
                  </svg>
                  Import
                </button>
              </div>
              <input ref={fileRef} type="file" accept=".json,application/json"
                style={{ display: 'none' }}
                onChange={(e) => {
                  const f = e.target.files?.[0]; if (!f) return;
                  const reader = new FileReader();
                  reader.onload = () => {
                    try { onExport.import(JSON.parse(reader.result)); }
                    catch { alert('Invalid file — expected exported JSON'); }
                  };
                  reader.readAsText(f);
                  e.target.value = '';
                }} />
              <button onClick={onClear} style={historyStyles.clearAll}>Clear all</button>
            </div>
          </div>
        )}
      </div>
    </>
  );
}

const historyStyles = {
  scrim: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', zIndex: 50,
           backdropFilter: 'blur(4px)', WebkitBackdropFilter: 'blur(4px)' },
  panel: { position: 'fixed', top: 0, right: 0, bottom: 0, width: 'min(420px, 100vw)',
           background: '#0d0d10', borderLeft: '1px solid #26262e', zIndex: 51,
           display: 'flex', flexDirection: 'column' },
  head: { padding: '18px 20px 14px', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          borderBottom: '1px solid #1f1f25' },
  headTitle: { fontSize: 15, fontWeight: 600, color: '#fafaf7' },
  close: { width: 28, height: 28, borderRadius: 7, border: '1px solid #2a2a31', background: '#15151a',
           color: '#a8a8b2', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0 },
  empty: { padding: '60px 20px', textAlign: 'center', flex: 1, display: 'flex',
           flexDirection: 'column', justifyContent: 'center', alignItems: 'center' },
  list: { overflowY: 'auto', padding: '12px 12px 20px', display: 'flex', flexDirection: 'column', gap: 8 },
  item: { position: 'relative', display: 'flex', alignItems: 'stretch' },
  itemBody: { flex: 1, textAlign: 'left', background: '#15151a', border: '1px solid #26262e',
              borderRadius: 12, padding: '12px 14px', cursor: 'pointer', display: 'flex',
              flexDirection: 'column', gap: 5, color: 'inherit', fontFamily: 'inherit' },
  itemTop: { display: 'flex', justifyContent: 'space-between', gap: 12 },
  itemHydrant: { fontSize: 13, fontWeight: 600, color: '#fafaf7' },
  itemDate: { fontSize: 11, color: '#7a7a84' },
  itemRow: { display: 'flex', alignItems: 'baseline', gap: 8 },
  itemVal: { fontSize: 22, fontWeight: 500, letterSpacing: '-0.02em' },
  itemSep: { fontSize: 11.5, color: '#7a7a84' },
  itemMeta: { fontSize: 11, color: '#6a6a72', letterSpacing: '0.02em' },
  itemActions: { position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4 },
  itemAct: { width: 24, height: 24, borderRadius: 6,
             border: '1px solid #2a2a31', background: '#101014', color: '#7a7a84',
             cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0 },
  itemDel: { position: 'absolute', top: 8, right: 8, width: 24, height: 24, borderRadius: 6,
             border: '1px solid #2a2a31', background: '#101014', color: '#7a7a84',
             cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0 },
  footer: { marginTop: 12, display: 'flex', flexDirection: 'column', gap: 8 },
  footerRow: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 },
  footerBtn: { padding: '7px', background: '#15151a', border: '1px solid #26262e',
               borderRadius: 7, color: '#fafaf7', fontSize: 11.5, fontWeight: 600,
               cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' },
  importBtn: { marginTop: 16, padding: '8px 14px', background: '#15151a', border: '1px solid #26262e',
               borderRadius: 8, color: '#a8a8b2', fontSize: 12, fontWeight: 500, cursor: 'pointer' },
  clearAll: { marginTop: 12, padding: '8px', background: 'transparent', border: '1px solid #26262e',
              borderRadius: 8, color: '#8a8a92', fontSize: 12, fontWeight: 500, cursor: 'pointer' },
};

// ─────────────────────────────────────────────────────────────────────────────
// Main App

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const accent = t.accent;
  const target = Number(t.target) || 20;
  const exponent = Number(t.exponent) || 0.54;

  // Cloud sync state
  const cloudProfile = window.flowSync ? window.flowSync.useProfile() : null;
  // Keep a global reference so non-React callbacks (save / delete) can reach it
  React.useEffect(() => { window.__flowProfile = cloudProfile; }, [cloudProfile]);

  // CSS variable for accent
  useEffect(() => {
    document.documentElement.style.setProperty('--accent', accent);
  }, [accent]);

  // State
  const [view, setView] = useLocalState('flowtest.view', 'hydraulic'); // 'hydraulic' | 'flow' | 'map'
  const [mode, setMode] = useLocalState('flowtest.mode', 'flow');
  const [label, setLabel] = useLocalState('flowtest.label', '');
  const [infoOpen, setInfoOpen] = useState(false);

  // ONE static reading (system supply pressure at the gauge)
  const [staticLabel, setStaticLabel] = useLocalState('flowtest.staticLabel', '');
  const [staticPsi, setStaticPsi] = useLocalState('flowtest.static', '');
  const [staticLocation, setStaticLocation] = useLocalState('flowtest.staticLocation', null);
  const [staticElevation, setStaticElevation] = useLocalState('flowtest.staticElevation', '');

  // MULTIPLE residual readings — each is a gauge location with its own residual
  // pressure during the flow. Available @ target is computed per residual.
  // Migrates from the legacy single-hydrant keys on first load.
  const [residualReadings, setResidualReadings] = useState(() => {
    try {
      const stored = localStorage.getItem('flowtest.residualReadings');
      if (stored) return JSON.parse(stored).map((r, i) => ({
        ...r,
        id: r.id || `r${i + 1}`,
        testId: r.testId || 't1',
      }));
      const s = JSON.parse(localStorage.getItem('flowtest.static') || '""');
      const r = JSON.parse(localStorage.getItem('flowtest.residual') || '""');
      const loc = JSON.parse(localStorage.getItem('flowtest.location') || 'null');
      return [{ id: 'r1', testId: 't1', label: '', staticPsi: s, residualPsi: r, location: loc, elevation: '' }];
    } catch {
      return [{ id: 'r1', testId: 't1', label: '', staticPsi: '', residualPsi: '', location: null, elevation: '' }];
    }
  });
  useEffect(() => {
    try { localStorage.setItem('flowtest.residualReadings', JSON.stringify(residualReadings)); } catch {}
  }, [residualReadings]);

  // Flow hydrants — N hydrants flowing simultaneously, each with its own
  // location, elevation, and either a measured GPM (mode='flow') or one or
  // more pitot outlets (mode='pitot'). The total test flow Q_F is the sum of
  // contributions from every hydrant. Migrates from the legacy
  // single-hydrant keys on first load.
  const [flowHydrants, setFlowHydrants] = useState(() => {
    try {
      const stored = localStorage.getItem('flowtest.flowHydrants');
      if (stored) return JSON.parse(stored).map((f, i) => ({
        ...f,
        id: f.id || `f${i + 1}`,
        testId: f.testId || 't1',
      }));
    } catch {}
    // Migration from legacy keys
    const getJSON = (k, fallback) => {
      try { const raw = localStorage.getItem(k); return raw == null ? fallback : JSON.parse(raw); }
      catch { return fallback; }
    };
    const legacyLabel    = getJSON('flowtest.flowLabel', '');
    const legacyLocation = getJSON('flowtest.flowLocation', null);
    const legacyElev     = getJSON('flowtest.flowElevation', '');
    const legacyGpm      = getJSON('flowtest.flow', '');
    const legacyOutlets  = getJSON('flowtest.outlets',
      [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }]);
    return [{
      id: 't1',
      testId: 't1',
      label: legacyLabel,
      location: legacyLocation,
      elevation: legacyElev,
      flowGpm: legacyGpm,
      outlets: legacyOutlets,
    }];
  });
  useEffect(() => {
    try { localStorage.setItem('flowtest.flowHydrants', JSON.stringify(flowHydrants)); } catch {}
  }, [flowHydrants]);
  const [history, setHistory] = useLocalState('flowtest.history', []);
  const [historyOpen, setHistoryOpen] = useState(false);

  // Separate persisted state for the Hydrant Flow tab so it doesn't bleed into
  // the Hydraulic Design tab.
  const [flowLabel, setFlowLabel] = useLocalState('flowtest.flow.label', '');
  const [testerName, setTesterName] = useLocalState('flowtest.testerName', '');
  const [flowStaticLabel, setFlowStaticLabel] = useLocalState('flowtest.flow.staticLabel', '');
  const [flowStaticPsi, setFlowStaticPsi] = useLocalState('flowtest.flow.static', '');
  const [flowStaticLocation, setFlowStaticLocation] = useLocalState('flowtest.flow.staticLocation', null);
  const [flowStaticElevation, setFlowStaticElevation] = useLocalState('flowtest.flow.staticElevation', '');
  const [flowResidualReadings, setFlowResidualReadings] = useState(() => {
    try {
      const stored = localStorage.getItem('flowtest.flow.residualReadings');
      if (stored) return JSON.parse(stored).map((r, i) => ({
        ...r,
        id: r.id || `r${i + 1}`,
        testId: r.testId || 't1',
      }));
    } catch {}
    try {
      const stored = localStorage.getItem('flowtest.residualReadings');
      if (stored) return JSON.parse(stored).map((r, i) => ({
        ...r,
        id: r.id || `r${i + 1}`,
        testId: r.testId || 't1',
      }));
    } catch {}
    return [{ id: 'r1', testId: 't1', label: '', staticPsi: '', residualPsi: '', location: null, elevation: '' }];
  });
  const [flowHydrantsFlow, setFlowHydrantsFlow] = useState(() => {
    try {
      const stored = localStorage.getItem('flowtest.flow.flowHydrants');
      if (stored) return JSON.parse(stored).map((f, i) => ({
        ...f,
        id: f.id || `f${i + 1}`,
        testId: f.testId || 't1',
      }));
    } catch {}
    try {
      const stored = localStorage.getItem('flowtest.flowHydrants');
      if (stored) return JSON.parse(stored).map((f, i) => ({
        ...f,
        id: f.id || `f${i + 1}`,
        testId: f.testId || 't1',
      }));
    } catch {}
    return [{
      id: 'f1',
      testId: 't1',
      label: '',
      location: null,
      elevation: '',
      outlets: [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }],
    }];
  });
  useEffect(() => {
    try { localStorage.setItem('flowtest.flow.residualReadings', JSON.stringify(flowResidualReadings)); } catch {}
  }, [flowResidualReadings]);
  useEffect(() => {
    try { localStorage.setItem('flowtest.flow.flowHydrants', JSON.stringify(flowHydrantsFlow)); } catch {}
  }, [flowHydrantsFlow]);

  // Picker target: null | 'flow' | 'static' | <residual id> | flow-panel:<testId>
  const [pickerFor, setPickerFor] = useState(null);
  const activeMode = view === 'flow' ? 'pitot' : mode;

  useEffect(() => {
    if (view === 'trends' || view === 'test') setView('hydraulic');
  }, [view, setView]);

  // Migrate any legacy 'auto' coefficient values from a previous build of
  // this app — the pumper correction is now applied automatically, so the
  // stored value should be a concrete number again.
  useEffect(() => {
    if (flowHydrants.some((f) => (f.outlets || []).some((o) => o.coefficient === 'auto'))) {
      setFlowHydrants(flowHydrants.map((f) => ({
        ...f,
        outlets: (f.outlets || []).map((o) =>
          o.coefficient === 'auto' ? { ...o, coefficient: 0.90 } : o),
      })));
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Per-test pairings — each residual reading is matched with the corresponding
  // flow-hydrant test slot at the same index.
  const flowHydrantGpm = useCallback((f) => {
    if (activeMode === 'flow') return Number(f.flowGpm) || 0;
    return (f.outlets || []).reduce((s, o) => s + pitotGpm(o), 0);
  }, [activeMode]);

  const tests = useMemo(() => {
    const ids = [];
    const seen = new Set();
    [...residualReadings, ...flowHydrants].forEach((item) => {
      const testId = item.testId || 't1';
      if (!seen.has(testId)) {
        seen.add(testId);
        ids.push(testId);
      }
    });
    return ids.map((testId) => ({
      testId,
      residual: residualReadings.find((r) => (r.testId || 't1') === testId) || null,
      flowHydrants: flowHydrants.filter((f) => (f.testId || 't1') === testId),
    }));
  }, [residualReadings, flowHydrants]);

  const testGpmById = useMemo(() => {
    const out = {};
    tests.forEach((t) => {
      out[t.testId] = (t.flowHydrants || []).reduce((s, f) => s + flowHydrantGpm(f), 0);
    });
    return out;
  }, [tests, flowHydrantGpm]);

  const perTestResults = useMemo(() => {
    return tests.map((t) => {
      const r = t.residual || {};
      const psVal = Number(r.staticPsi || staticPsi) || 0;
      const prVal = Number(r.residualPsi) || 0;
      const flowValue = testGpmById[t.testId] || 0;
      let err = null;
      if ((r.staticPsi || staticPsi) !== '' && r.residualPsi !== '' && flowValue > 0) {
        if (psVal <= prVal) err = 'Residual must be less than static';
        else if (psVal <= target) err = `Static must exceed ${target} psi`;
      }
      const avail = err ? null
        : flowAtPressure({ static: psVal, residual: prVal, flow: flowValue, target, exponent });
      return { testId: t.testId, reading: r, flowHydrants: t.flowHydrants, flowGpm: flowValue, available: avail, error: err, ps: psVal, pr: prVal,
               classInfo: avail != null ? classify(avail) : null };
    });
  }, [tests, staticPsi, target, exponent, testGpmById]);

  const validResults = perTestResults.filter((r) => r.available != null && r.available > 0);
  const limiting = validResults.length > 0
    ? validResults.reduce((m, r) => (r.available < m.available ? r : m))
    : null;
  // Top-level numbers driving the Result card / chart / breakdown — use the most
  // limiting test when multiple are entered.
  const available = limiting ? limiting.available : null;
  const classInfo = limiting ? limiting.classInfo : null;
  const activeTest = limiting || tests[0] || null;
  const ps = limiting ? limiting.ps : (Number(residualReadings[0]?.staticPsi || staticPsi) || 0);
  const pr = limiting ? limiting.pr : (Number(residualReadings[0]?.residualPsi) || 0);
  const pitotOutlets = useMemo(() => (
    activeTest && mode === 'pitot'
      ? (activeTest.flowHydrants || []).flatMap((f, i) => (f.outlets || []).map((o) => ({
          ...o,
          _hydrantIdx: i,
          _hydrantLabel: f.label || `Flowing Hydrant ${i + 1}`,
        })))
      : []
  ), [activeTest, mode]);
  const firstError = perTestResults.find((r) => r.error)?.error || null;
  const error = available == null ? firstError : null;
  const pageInfo = PAGE_INFO[view === 'flow' ? 'flow' : 'hydraulic'];

  // Reset
  const reset = useCallback(() => {
    setLabel('');
    setStaticLabel('');
    setStaticPsi('');
    setStaticLocation(null);
    setStaticElevation('');
    setTesterName('');
    setResidualReadings([{ id: 'r1', testId: 't1', label: '', staticPsi: '', residualPsi: '', location: null, elevation: '' }]);
    setFlowHydrants([{ id: 'f1', testId: 't1', label: '', location: null, elevation: '',
      flowGpm: '', outlets: [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }] }]);
  }, [setLabel, setStaticLabel, setStaticPsi, setStaticLocation, setStaticElevation,
      setResidualReadings, setFlowHydrants]);

  // Save
  const canSave = available != null && available > 0;
  const save = useCallback(() => {
    const primary = activeTest?.flowHydrants?.[0] || {};
    const savedTests = tests.map((t) => ({
      testId: t.testId,
      residual: t.residual ? { ...t.residual } : null,
      flowHydrants: (t.flowHydrants || []).map((f) => ({
        ...f,
        contribGpm: flowHydrantGpm(f),
      })),
    }));
    const entry = {
      id: (typeof crypto !== 'undefined' && crypto.randomUUID)
        ? crypto.randomUUID()
        : Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
      ts: Date.now(), label: label || `Test ${history.length + 1}`,
      mode: activeMode, target, available,
      staticLabel, staticPsi: ps, staticLocation: staticLocation || null,
      staticElevation: staticElevation || null,
      tests: savedTests,
      residualReadings: residualReadings.map((r) => ({ ...r })),
      flowHydrants: flowHydrants.map((f) => ({
        ...f,
        contribGpm: flowHydrantGpm(f),
      })),
      testGpm: limiting?.flowGpm || perTestResults[0]?.flowGpm || null,
      // Legacy mirror fields (first hydrant only) — keep until all consumers
      // are updated to read tests.flowHydrants.
      outlets: activeMode === 'pitot' ? (primary.outlets || null) : null,
      flowGpm: activeMode === 'flow' ? (Number(primary.flowGpm) || null) : null,
      flowLabel: primary.label || null,
      testerName: testerName || null,
      flowLocation: primary.location || null,
      flowElevation: primary.elevation || null,
      results: perTestResults.map((r) => ({
        testId: r.testId,
        residualId: r.reading.id, label: r.reading.label || '',
        staticPsi: r.ps, residualPsi: r.pr, location: r.reading.location || null,
        available: r.available, classCode: r.classInfo?.code || null,
        flowGpm: r.flowGpm,
      })),
    };
    setHistory([entry, ...history].slice(0, 100));
    const profile = window.flowSync && window.__flowProfile;
    if (profile?.organization_id) {
      window.flowSync.uploadTest(entry, profile).catch((e) => console.warn('upload', e));
    }
  }, [label, mode, target, available, staticLabel, ps, staticLocation, staticElevation,
      residualReadings, flowHydrants, flowHydrantGpm,
      perTestResults, history, setHistory, tests, activeTest]);

  // Load from history (supports new multi-residual format and legacy single-hydrant)
  const loadEntry = useCallback((h) => {
    setLabel(h.label || '');
    setMode(h.mode);
    setStaticLabel(h.staticLabel || '');
    setStaticPsi(String(h.staticPsi ?? ''));
    setStaticLocation(h.staticLocation || null);
    setStaticElevation(h.staticElevation ? String(h.staticElevation) : '');
    setTesterName(h.testerName || '');
    if (Array.isArray(h.tests) && h.tests.length > 0) {
      setResidualReadings(h.tests.map((t, i) => ({
        id: t.residual?.id || (`r${i + 1}`),
        testId: t.testId || (`t${i + 1}`),
        label: t.residual?.label || '',
        staticPsi: t.residual?.staticPsi ?? '',
        residualPsi: t.residual?.residualPsi ?? '',
        location: t.residual?.location || null,
        elevation: t.residual?.elevation ? String(t.residual.elevation) : '',
      })));
      setFlowHydrants(h.tests.flatMap((t, i) => (t.flowHydrants || []).map((f, j) => ({
        id: f.id || (`f${i + 1}-${j + 1}`),
        testId: t.testId || (`t${i + 1}`),
        label: f.label || '',
        location: f.location || null,
        elevation: f.elevation ? String(f.elevation) : '',
        flowGpm: f.flowGpm != null ? String(f.flowGpm) : '',
        outlets: Array.isArray(f.outlets) && f.outlets.length > 0
          ? f.outlets
          : [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }],
      }))));
    } else if (h.residualReadings && h.residualReadings.length > 0) {
      setResidualReadings(h.residualReadings.map((r, i) => ({
        id: r.id || (`r${i + 1}`),
        testId: r.testId || 't1',
        label: r.label || '',
        staticPsi: r.staticPsi ?? '',
        residualPsi: r.residualPsi ?? '',
        location: r.location || null,
        elevation: r.elevation ? String(r.elevation) : '',
      })));
      setFlowHydrants((h.flowHydrants || []).map((f, i) => ({
        id: f.id || (`f${i + 1}`),
        testId: f.testId || 't1',
        label: f.label || '',
        location: f.location || null,
        elevation: f.elevation ? String(f.elevation) : '',
        flowGpm: f.flowGpm != null ? String(f.flowGpm) : '',
        outlets: Array.isArray(f.outlets) && f.outlets.length > 0
          ? f.outlets
          : [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }],
      })));
    } else if (h.residualPsi != null) {
      setResidualReadings([{ id: 'r1', testId: 't1', label: '',
        residualPsi: String(h.residualPsi),
        location: h.location || null }]);
      setFlowHydrants([{
        id: 'f1',
        testId: 't1',
        label: h.flowLabel || '',
        location: h.flowLocation || null,
        elevation: h.flowElevation ? String(h.flowElevation) : '',
        flowGpm: h.mode === 'flow'
          ? String(h.flowGpm ?? h.testGpm ?? '')
          : '',
        outlets: Array.isArray(h.outlets) && h.outlets.length > 0
          ? h.outlets
          : [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }],
      }]);
    }
    setHistoryOpen(false);
    setView('hydraulic');
  }, [setLabel, setMode, setStaticLabel, setStaticPsi, setStaticLocation,
      setStaticElevation, setResidualReadings, setFlowHydrants, setView]);

  // Add hydrant from map click — if the static location is empty, set it;
  // otherwise drop on the first empty residual, else append a new residual.
  const addHydrantFromMap = useCallback((latlng) => {
    if (!staticLocation) {
      setStaticLocation(latlng);
    } else {
      const idx = residualReadings.findIndex((r) => !r.location);
      if (idx >= 0) {
        setResidualReadings(residualReadings.map((r, i) =>
          i === idx ? { ...r, location: latlng } : r));
      } else {
        const testId = tests[tests.length - 1]?.testId || 't1';
        setResidualReadings([
          ...residualReadings,
          { id: 'r' + Date.now().toString(36), testId, label: '', staticPsi: '', residualPsi: '', location: latlng, elevation: '' },
        ]);
      }
    }
    setView('hydraulic');
  }, [staticLocation, setStaticLocation, residualReadings, setResidualReadings, setView, tests]);

  // Re-test a saved hydrant — pre-fill locations and outlet hardware, but
  // blank every pressure / flow value so a fresh reading can be entered.
  const retestEntry = useCallback((h) => {
    setLabel(h.label || '');
    setMode(h.mode);
    setStaticPsi('');
    setStaticLocation(h.staticLocation || null);
    setTesterName(h.testerName || '');
    if (Array.isArray(h.tests) && h.tests.length > 0) {
      setResidualReadings(h.tests.map((t, i) => ({
        id: t.residual?.id || (`r${i + 1}`),
        testId: t.testId || (`t${i + 1}`),
        label: t.residual?.label || '',
        staticPsi: '',
        residualPsi: '',
        location: t.residual?.location || null,
        elevation: t.residual?.elevation ? String(t.residual.elevation) : '',
      })));
      setFlowHydrants(h.tests.flatMap((t, i) => (t.flowHydrants || []).map((f, j) => ({
        id: f.id || (`f${i + 1}-${j + 1}`),
        testId: t.testId || (`t${i + 1}`),
        label: f.label || '',
        location: f.location || null,
        elevation: f.elevation ? String(f.elevation) : '',
        flowGpm: '',
        outlets: Array.isArray(f.outlets) && f.outlets.length > 0
          ? f.outlets.map((o) => ({ ...o, pitotPsi: '' }))
          : [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }],
      }))));
    } else if (h.residualReadings && h.residualReadings.length > 0) {
      setResidualReadings(h.residualReadings.map((r, i) => ({
        id: r.id || (`r${i + 1}`),
        testId: r.testId || 't1',
        label: r.label || '',
        staticPsi: '',
        residualPsi: '',
        location: r.location || null,
        elevation: r.elevation ? String(r.elevation) : '',
      })));
      setFlowHydrants((h.flowHydrants || []).map((f, i) => ({
        id: f.id || (`f${i + 1}`),
        testId: f.testId || 't1',
        label: f.label || '',
        location: f.location || null,
        elevation: f.elevation ? String(f.elevation) : '',
        flowGpm: '',
        outlets: Array.isArray(f.outlets) && f.outlets.length > 0
          ? f.outlets.map((o) => ({ ...o, pitotPsi: '' }))
          : [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }],
      })));
    } else if (h.residualPsi != null) {
      setResidualReadings([{ id: 'r1', testId: 't1', label: '', residualPsi: '', location: h.location || null }]);
      setFlowHydrants([{
        id: 'f1',
        testId: 't1',
        label: h.flowLabel || '',
        location: h.flowLocation || null,
        elevation: h.flowElevation ? String(h.flowElevation) : '',
        flowGpm: '',
        outlets: Array.isArray(h.outlets) && h.outlets.length > 0
          ? h.outlets.map((o) => ({ ...o, pitotPsi: '' }))
          : [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }],
      }]);
    }
    setHistoryOpen(false);
    setView('hydraulic');
  }, [setLabel, setMode, setStaticPsi, setStaticLocation, setResidualReadings,
      setFlowHydrants, setView]);

  // Download a Blob as a file
  const downloadBlob = (blob, filename) => {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = filename;
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  };

  // Export entire history as JSON
  const exportJson = () => {
    const blob = new Blob([JSON.stringify({
      exportedAt: new Date().toISOString(),
      version: 2,
      history,
    }, null, 2)], { type: 'application/json' });
    downloadBlob(blob, `flowtest-history-${new Date().toISOString().slice(0, 10)}.json`);
  };

  // Export history as CSV — one row per residual reading per test
  const exportCsv = () => {
    const rows = [[
      'test_id','timestamp','test_label','target_psi','static_psi',
      'static_lat','static_lng',
      'mode','test_flow_gpm','flow_lat','flow_lng',
      'residual_index','residual_label','residual_psi',
      'residual_lat','residual_lng',
      'available_at_target_gpm','class','is_limiting_for_test',
    ]];
    history.forEach((h) => {
      const results = h.results || [];
      const minId = results.length > 0
        ? results.reduce((m, r) => (r.available != null && (m == null || r.available < m.available) ? r : m), null)?.residualId
        : null;
      const residuals = h.residualReadings || (h.residualPsi != null
        ? [{ id: 'r1', label: '', residualPsi: h.residualPsi, location: h.location || null }]
        : []);
      const ts = new Date(h.ts).toISOString();
      if (residuals.length === 0) {
        rows.push([h.id, ts, h.label || '', h.target, h.staticPsi ?? '',
          h.staticLocation?.lat ?? '', h.staticLocation?.lng ?? '',
          h.mode || '', h.testGpm ?? '',
          h.flowLocation?.lat ?? '', h.flowLocation?.lng ?? '',
          '', '', '', '', '', '', '', '']);
      } else {
        residuals.forEach((r, i) => {
          const result = results.find((x) => x.residualId === r.id);
          rows.push([h.id, ts, h.label || '', h.target, h.staticPsi ?? '',
            h.staticLocation?.lat ?? '', h.staticLocation?.lng ?? '',
            h.mode || '', h.testGpm ?? '',
            h.flowLocation?.lat ?? '', h.flowLocation?.lng ?? '',
            i + 1, r.label || '', r.residualPsi ?? '',
            r.location?.lat ?? '', r.location?.lng ?? '',
            result?.available ?? '', result?.classCode ?? '',
            r.id === minId ? 'yes' : 'no',
          ]);
        });
      }
    });
    // CSV escaping: wrap fields with comma/quote/newline in quotes, double internal quotes
    const csv = rows.map((row) => row.map((cell) => {
      const s = String(cell ?? '');
      return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
    }).join(',')).join('\n');
    const blob = new Blob([csv], { type: 'text/csv' });
    downloadBlob(blob, `flowtest-history-${new Date().toISOString().slice(0, 10)}.csv`);
  };

  // Import history JSON — merge into existing, dedup by id (keep newer)
  const importJson = (payload) => {
    if (!payload) return;
    const incoming = Array.isArray(payload) ? payload
      : Array.isArray(payload.history) ? payload.history
      : null;
    if (!incoming) { alert('Invalid file — no history array found'); return; }
    const byId = new Map(history.map((h) => [h.id, h]));
    incoming.forEach((h) => {
      if (!h?.id) return;
      const existing = byId.get(h.id);
      if (!existing || (h.ts || 0) >= (existing.ts || 0)) byId.set(h.id, h);
    });
    const merged = [...byId.values()].sort((a, b) => (b.ts || 0) - (a.ts || 0)).slice(0, 500);
    setHistory(merged);
    alert(`Imported. ${merged.length} test${merged.length === 1 ? '' : 's'} in history (${incoming.length} new/updated).`);
  };

  // Residual reading helpers (functional updates — safe against async races)
  const addResidualReading = () => {
    const source = tests[tests.length - 1] || null;
    const newTestId = `t${tests.length + 1}`;
    const newResidual = source?.residual
      ? {
          ...source.residual,
          id: `r${Date.now().toString(36)}`,
          testId: newTestId,
          staticPsi: '',
          residualPsi: '',
        }
      : {
          id: `r${Date.now().toString(36)}`,
          testId: newTestId,
          label: '',
          staticPsi: '',
          residualPsi: '',
          location: null,
          elevation: '',
        };
    const sourceFlows = source?.flowHydrants?.length
      ? source.flowHydrants
      : flowHydrants.filter((f) => (f.testId || 't1') === (source?.testId || 't1'));
    const clonedFlows = (sourceFlows.length > 0 ? sourceFlows : [{
      id: `f${Date.now().toString(36)}`,
      testId: newTestId,
      label: '',
      location: null,
      elevation: '',
      flowGpm: '',
      outlets: [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }],
    }]).map((f, i) => ({
      ...f,
      id: `f${Date.now().toString(36)}${i}`,
      testId: newTestId,
      flowGpm: '',
      outlets: (f.outlets || [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }]).map((o) => ({
        ...o,
        pitotPsi: '',
      })),
    }));
    setResidualReadings((prev) => [...prev, newResidual]);
    setFlowHydrants((prev) => [...prev, ...clonedFlows]);
  };
  const updateResidualReading = (id, patch) => {
    setResidualReadings((prev) => prev.map((r) => (r.id === id ? { ...r, ...patch } : r)));
  };
  const removeResidualReading = (id) => {
    const target = residualReadings.find((r) => r.id === id);
    if (target) {
      const testId = target.testId || 't1';
      setFlowHydrants((prev) => prev.filter((f) => (f.testId || 't1') !== testId));
    }
    setResidualReadings((prev) => prev.filter((r) => r.id !== id));
  };

  // Confirm picked location for whichever target opened the picker.
  // Elevation is intentionally not calculated in the private rebuild.
  const confirmPickerLocation = async (coord) => {
    const targetFor = pickerFor;
    if (targetFor === 'static') {
      if (view === 'flow') setFlowStaticLocation(coord);
      else setStaticLocation(coord);
    } else if (typeof targetFor === 'string' && targetFor.startsWith('flow-panel:')) {
      const testId = targetFor.slice('flow-panel:'.length);
      setFlowResidualReadings((prev) => prev.map((r) => (r.testId || 't1') === testId ? { ...r, location: coord } : r));
      setFlowHydrantsFlow((prev) => prev.map((f) => (f.testId || 't1') === testId ? { ...f, location: coord } : f));
    } else if (typeof targetFor === 'string' && targetFor.startsWith('flow:')) {
      const fid = targetFor.slice(5);
      setFlowHydrants((prev) => prev.map((f) => (f.id === fid ? { ...f, location: coord } : f)));
    } else if (targetFor) updateResidualReading(targetFor, { location: coord });
    setPickerFor(null);
  };
  const pickerInitial =
      pickerFor === 'static' ? (view === 'flow' ? flowStaticLocation : staticLocation)
    : (typeof pickerFor === 'string' && pickerFor.startsWith('flow-panel:'))
        ? (flowHydrantsFlow.find((f) => (f.testId || 't1') === pickerFor.slice('flow-panel:'.length))?.location
            || flowResidualReadings.find((r) => (r.testId || 't1') === pickerFor.slice('flow-panel:'.length))?.location || null)
    : (typeof pickerFor === 'string' && pickerFor.startsWith('flow:'))
        ? (flowHydrants.find((f) => f.id === pickerFor.slice(5))?.location || null)
    : residualReadings.find((r) => r.id === pickerFor)?.location || null;

  // Flow hydrant helpers (functional updates — safe against async races
  // from the address-autocomplete + elevation-lookup pipeline)
  const addFlowHydrant = (testId = tests[tests.length - 1]?.testId || 't1') => {
    setFlowHydrants((prev) => [
      ...prev,
      { id: `f${Date.now().toString(36)}`, testId, label: '', location: null, elevation: '',
        flowGpm: '', outlets: [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }] },
    ]);
  };
  const updateFlowHydrant = (id, patch) => {
    setFlowHydrants((prev) => prev.map((f) => (f.id === id ? { ...f, ...patch } : f)));
  };
  const removeFlowHydrant = (id) => {
    setFlowHydrants((prev) => prev.filter((f) => f.id !== id));
  };

  // Outlet helpers (scoped to a single flow hydrant) — also functional so the
  // pitot-pressure typing doesn't race with parent state.
  const addOutlet = (fid) => setFlowHydrants((prev) =>
    prev.map((f) => f.id === fid
      ? { ...f, outlets: [...(f.outlets || []), { pitotPsi: '', diameter: 2.5, coefficient: 0.9 }] }
      : f));
  const updateOutlet = (fid, i, o) => setFlowHydrants((prev) =>
    prev.map((f) => f.id === fid
      ? { ...f, outlets: (f.outlets || []).map((x, j) => (j === i ? o : x)) }
      : f));
  const removeOutlet = (fid, i) => setFlowHydrants((prev) =>
    prev.map((f) => f.id === fid
      ? { ...f, outlets: (f.outlets || []).filter((_, j) => j !== i) }
      : f));

  const makePanelId = (prefix) => `${prefix}${Date.now().toString(36)}${Math.random().toString(36).slice(2, 5)}`;
  const addFlowPanel = () => {
    const testId = makePanelId('t');
    setResidualReadings((prev) => [
      ...prev,
      { id: makePanelId('r'), testId, label: '', staticPsi: '', residualPsi: '', location: null, elevation: '' },
    ]);
    setFlowHydrants((prev) => [
      ...prev,
      {
        id: makePanelId('f'),
        testId,
        label: '',
        location: null,
        elevation: '',
        flowGpm: '',
        outlets: [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }],
      },
    ]);
  };
  const removeFlowPanel = (testId) => {
    setResidualReadings((prev) => prev.filter((r) => (r.testId || 't1') !== testId));
    setFlowHydrants((prev) => prev.filter((f) => (f.testId || 't1') !== testId));
  };

  // Flow-tab only helpers/calculations. These intentionally use the separate
  // flow-specific state so the Hydrant Flow tab cannot mutate the Hydraulic
  // Design tab's draft data.
  const flowPanelGpm = useCallback((f) => (f.outlets || []).reduce((s, o) => s + pitotGpm(o), 0), []);

  const flowTests = useMemo(() => {
    const ids = [];
    const seen = new Set();
    [...flowResidualReadings, ...flowHydrantsFlow].forEach((item) => {
      const testId = item.testId || 't1';
      if (!seen.has(testId)) {
        seen.add(testId);
        ids.push(testId);
      }
    });
    return ids.map((testId) => ({
      testId,
      residual: flowResidualReadings.find((r) => (r.testId || 't1') === testId) || null,
      flowHydrants: flowHydrantsFlow.filter((f) => (f.testId || 't1') === testId),
    }));
  }, [flowResidualReadings, flowHydrantsFlow]);

  const flowTestGpmById = useMemo(() => {
    const out = {};
    flowTests.forEach((t) => {
      out[t.testId] = (t.flowHydrants || []).reduce((s, f) => s + flowPanelGpm(f), 0);
    });
    return out;
  }, [flowTests, flowPanelGpm]);

  const flowPerTestResults = useMemo(() => {
    return flowTests.map((t) => {
      const r = t.residual || {};
      const psVal = Number(r.staticPsi || flowStaticPsi) || 0;
      const prVal = Number(r.residualPsi) || 0;
      const flowValue = flowTestGpmById[t.testId] || 0;
      let err = null;
      if ((r.staticPsi || flowStaticPsi) !== '' && r.residualPsi !== '' && flowValue > 0) {
        if (psVal <= prVal) err = 'Residual must be less than static';
        else if (psVal <= target) err = `Static must exceed ${target} psi`;
      }
      const avail = err ? null
        : flowAtPressure({ static: psVal, residual: prVal, flow: flowValue, target, exponent });
      return { testId: t.testId, reading: r, flowHydrants: t.flowHydrants, flowGpm: flowValue, available: avail, error: err, ps: psVal, pr: prVal,
               classInfo: avail != null ? classify(avail) : null };
    });
  }, [flowTests, flowStaticPsi, target, exponent, flowTestGpmById]);

  const flowValidResults = flowPerTestResults.filter((r) => r.available != null && r.available > 0);
  const flowLimiting = flowValidResults.length > 0
    ? flowValidResults.reduce((m, r) => (r.available < m.available ? r : m))
    : null;
  const flowAvailable = flowLimiting ? flowLimiting.available : null;
  const flowClassInfo = flowLimiting ? flowLimiting.classInfo : null;
  const flowActiveTest = flowLimiting || flowTests[0] || null;
  const flowPs = flowLimiting ? flowLimiting.ps : (Number(flowResidualReadings[0]?.staticPsi || flowStaticPsi) || 0);
  const flowPr = flowLimiting ? flowLimiting.pr : (Number(flowResidualReadings[0]?.residualPsi) || 0);
  const flowError = flowAvailable == null ? (flowPerTestResults.find((r) => r.error)?.error || null) : null;

  const flowReset = useCallback(() => {
    setFlowLabel('');
    setTesterName('');
    setFlowStaticLabel('');
    setFlowStaticPsi('');
    setFlowStaticLocation(null);
    setFlowStaticElevation('');
    setFlowResidualReadings([{ id: 'r1', testId: 't1', label: '', staticPsi: '', residualPsi: '', location: null, elevation: '' }]);
    setFlowHydrantsFlow([{ id: 'f1', testId: 't1', label: '', location: null, elevation: '', outlets: [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }] }]);
    setPickerFor(null);
  }, [setFlowLabel, setTesterName, setFlowStaticLabel, setFlowStaticPsi, setFlowStaticLocation, setFlowStaticElevation, setFlowResidualReadings, setFlowHydrantsFlow]);

  const flowMakePanelId = (prefix) => `${prefix}${Date.now().toString(36)}${Math.random().toString(36).slice(2, 5)}`;
  const flowAddPanel = () => {
    const testId = flowTests[flowTests.length - 1]?.testId || 't1';
    const nextTestId = flowTests.length > 0 ? flowMakePanelId('t') : 't1';
    const useTestId = flowTests.length > 0 ? nextTestId : testId;
    setFlowResidualReadings((prev) => [
      ...prev,
      { id: flowMakePanelId('r'), testId: useTestId, label: '', staticPsi: '', residualPsi: '', location: null, elevation: '' },
    ]);
    setFlowHydrantsFlow((prev) => [
      ...prev,
      {
        id: flowMakePanelId('f'),
        testId: useTestId,
        label: '',
        location: null,
        elevation: '',
        outlets: [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }],
      },
    ]);
  };
  const flowUpdateResidualReading = (id, patch) => {
    setFlowResidualReadings((prev) => prev.map((r) => (r.id === id ? { ...r, ...patch } : r)));
  };
  const flowRemoveResidualReading = (id) => {
    const target = flowResidualReadings.find((r) => r.id === id);
    if (target) {
      const testId = target.testId || 't1';
      setFlowHydrantsFlow((prev) => prev.filter((f) => (f.testId || 't1') !== testId));
    }
    setFlowResidualReadings((prev) => prev.filter((r) => r.id !== id));
  };
  const flowUpdateFlowHydrant = (id, patch) => {
    setFlowHydrantsFlow((prev) => prev.map((f) => (f.id === id ? { ...f, ...patch } : f)));
  };
  const flowRemoveFlowHydrant = (id) => {
    setFlowHydrantsFlow((prev) => prev.filter((f) => f.id !== id));
  };
  const flowAddOutlet = (fid) => setFlowHydrantsFlow((prev) =>
    prev.map((f) => f.id === fid
      ? { ...f, outlets: [...(f.outlets || []), { pitotPsi: '', diameter: 2.5, coefficient: 0.9 }] }
      : f));
  const flowUpdateOutlet = (fid, i, o) => setFlowHydrantsFlow((prev) =>
    prev.map((f) => f.id === fid
      ? { ...f, outlets: (f.outlets || []).map((x, j) => (j === i ? o : x)) }
      : f));
  const flowRemoveOutlet = (fid, i) => setFlowHydrantsFlow((prev) =>
    prev.map((f) => f.id === fid
      ? { ...f, outlets: (f.outlets || []).filter((_, j) => j !== i) }
      : f));

  return (
    <div style={appStyles.shell}>
      {/* ─────── Header ─────── */}
      <header style={appStyles.header}>
        <div style={appStyles.headLeft}>
          <div style={appStyles.brandIcon}>
            <svg width="22" height="22" viewBox="0 0 22 22" fill="none">
              <path d="M11 2c-3 4-5 6.5-5 10a5 5 0 0 0 10 0c0-3.5-2-6-5-10z"
                    fill={accent} opacity="0.85" />
              <path d="M11 7c-1.5 2-2.5 3.5-2.5 5.5a2.5 2.5 0 0 0 5 0c0-2-1-3.5-2.5-5.5z"
                    fill="#0a0a0c" opacity="0.4" />
            </svg>
          </div>
          <div>
            <div style={appStyles.brandTitle}>Hydrant Flow Tests</div>
            <div style={appStyles.brandSub}>NFPA 291 · @ {target} psi</div>
          </div>
        </div>
        <div style={appStyles.headRight}>
          {(view === 'hydraulic' || view === 'flow') && (
            <button style={appStyles.iconBtn} onClick={() => setInfoOpen(true)} aria-label="Information" title="Information">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                <path d="M8 2.75a5.25 5.25 0 1 0 0 10.5a5.25 5.25 0 0 0 0-10.5Zm0 2.1a.72.72 0 1 1 0 1.44a.72.72 0 0 1 0-1.44Zm1 6.15H7.3v-.92h.56V7.4H7.2v-.93h1.83v3.61H9v.92Z" fill="currentColor"/>
              </svg>
            </button>
          )}
          {view === 'flow' && (
            <button style={appStyles.iconBtn} onClick={flowReset} aria-label="Reset" title="Reset">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                <path d="M3 8a5 5 0 1 0 1.5-3.5M3 3v3h3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
            </button>
          )}
          {view === 'hydraulic' && (
            <button style={appStyles.iconBtn} onClick={reset} aria-label="Reset" title="Reset">
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
                <path d="M3 8a5 5 0 1 0 1.5-3.5M3 3v3h3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
            </button>
          )}

        </div>
      </header>

      {/* ─────── Tab strip ─────── */}
      <div style={appStyles.tabs}>
        {[
          { id: 'flow', label: 'Hydrant Flow Test' },
          { id: 'hydraulic', label: 'Hydraulic Design Test' },
          { id: 'map', label: 'Map' },
        ].map((tab) => {
          const on = view === tab.id;
          return (
            <button key={tab.id} onClick={() => setView(tab.id)}
              style={{ ...appStyles.tab, color: on ? '#fafaf7' : '#7a7a84',
                       borderBottomColor: on ? 'var(--accent)' : 'transparent' }}>
              {tab.label}

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

      {view === 'map' ? (
        <div style={appStyles.mapWrap}>
          <MapView history={history} accent={accent}
            onLoadEntry={loadEntry}
            onDeleteEntry={(id) => setHistory(history.filter((h) => h.id !== id))}
            onAddHydrant={addHydrantFromMap}
            inProgress={{
              staticLabel,
              staticLocation,
              staticPsi,
              staticElevation,
              residualReadings,
              flowHydrants,
              tests,
              // legacy mirror — first flow hydrant only
              flowLocation: flowHydrants[0]?.location || null,
            }} />
        </div>
      ) : view === 'flow' ? (
        <main style={appStyles.main}>
          <input
            type="text"
            placeholder="Test name or job site (optional)"
            value={flowLabel}
            onChange={(e) => setFlowLabel(e.target.value)}
            style={appStyles.labelInput}
          />
          <input
            type="text"
            placeholder="Tester name or licence # (optional)"
            value={testerName}
            onChange={(e) => setTesterName(e.target.value)}
            style={appStyles.labelInput}
          />

          <section style={appStyles.section}>
            <SectionLabel n="01" title={`Hydrants${flowTests.length > 1 ? ` · ${flowTests.length}` : ''}`} />
            <div style={appStyles.testBlockStack}>
              {flowTests.map((t, ti) => {
                const result = flowPerTestResults.find((x) => x.testId === t.testId);
                const reading = t.residual || {
                  id: `r${ti + 1}`,
                  testId: t.testId,
                  label: '',
                  staticPsi: '',
                  residualPsi: '',
                  location: null,
                  elevation: '',
                };
                const hydrant = (t.flowHydrants || [])[0] || {
                  id: `f${ti + 1}`,
                  testId: t.testId,
                  label: '',
                  location: null,
                  elevation: '',
                  outlets: [{ pitotPsi: '', diameter: 2.5, coefficient: 0.9 }],
                };
                const isLimiting = flowLimiting && flowLimiting.testId === t.testId && flowValidResults.length > 1;
                return (
                  <HydrantFlowPanelCard
                    key={t.testId}
                    testIndex={ti}
                    testId={t.testId}
                    reading={reading}
                    hydrant={hydrant}
                    result={result}
                    target={target}
                    isLimiting={isLimiting}
                    onChangeReading={(patch) => flowUpdateResidualReading(reading.id, patch)}
                    onChangeHydrant={(patch) => flowUpdateFlowHydrant(hydrant.id, patch)}
                    onPickLocation={() => setPickerFor(`flow-panel:${t.testId}`)}
                    onClearLocation={() => {
                      flowUpdateResidualReading(reading.id, { location: null });
                      flowUpdateFlowHydrant(hydrant.id, { location: null });
                    }}
                    onAddOutlet={() => flowAddOutlet(hydrant.id)}
                    onUpdateOutlet={(i2, o) => flowUpdateOutlet(hydrant.id, i2, o)}
                    onRemoveOutlet={(i2) => flowRemoveOutlet(hydrant.id, i2)}
                    onRemove={() => flowRemoveResidualReading(reading.id)}
                    canRemove={flowTests.length > 1}
                  />
                );
              })}
              <button onClick={flowAddPanel} style={appStyles.addBtn}>
                <span style={{ fontSize: 14, lineHeight: 1 }}>+</span> Add hydrant
              </button>
            </div>
          </section>

          <div style={{ height: 24 }} />
          <div style={appStyles.actionRow}>
            <button onClick={() => window.open('report.html#tab=flow', '_blank')}
              style={appStyles.exportBtn}>
              <svg width="14" height="14" viewBox="0 0 14 14" fill="none" style={{ marginRight: 6 }}>
                <path d="M7 1v8M4 6l3 3 3-3M2 11h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
              Export PDF
            </button>
          </div>
        </main>
      ) : (
      <main style={appStyles.main}>
        {/* Optional label */}
        <input
          type="text"
          placeholder="Test name or job site (optional)"
          value={label}
          onChange={(e) => setLabel(e.target.value)}
          style={appStyles.labelInput}
        />
        <input
          type="text"
          placeholder="Testers name or licence # (optional)"
          value={testerName}
          onChange={(e) => setTesterName(e.target.value)}
          style={appStyles.labelInput}
        />

        <ModeToggle value={mode} onChange={setMode} />

        {/* ── 01 Test blocks ── */}
        <section style={appStyles.section}>
          <SectionLabel n="01"
            title={`Tests${tests.length > 1 ? ` · ${tests.length}` : ''}`} />
          <div style={appStyles.testBlockStack}>
            {tests.map((t, ti) => {
              const result = perTestResults.find((x) => x.testId === t.testId);
              const isLimiting = limiting && limiting.testId === t.testId && validResults.length > 1;
              return (
                <div key={t.testId} style={appStyles.testBlock}>
                  <div style={appStyles.testBlockHeader}>
                    <div style={appStyles.testBlockTitle}>Test {ti + 1}</div>
                    <div className="mono" style={appStyles.testBlockFlow}>
                      {fmt(testGpmById[t.testId] || 0, 0)} GPM total flow
                    </div>
                  </div>

                  <div style={appStyles.testSubheading}>Residual Hydrants</div>
                  {t.residual && (
                    <ResidualReadingCard
                      key={t.residual.id}
                      reading={t.residual}
                      index={0}
                      result={result}
                      target={target}
                      isLimiting={isLimiting}
                      onChange={(patch) => updateResidualReading(t.residual.id, patch)}
                      onPickLocation={() => setPickerFor(t.residual.id)}
                      onClearLocation={() => updateResidualReading(t.residual.id, { location: null })}
                      onRemove={() => removeResidualReading(t.residual.id)}
                      canRemove={tests.length > 1}
                      showLocation
                    />
                  )}

                  <div style={appStyles.testSubsection}>
                    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                      <div style={appStyles.testSubheading}>
                        Flowing hydrants
                      </div>
                      <button onClick={() => addFlowHydrant(t.testId)} style={appStyles.addBtn}>
                        <span style={{ fontSize: 14, lineHeight: 1 }}>+</span> Add flowing hydrant
                      </button>
                    </div>
                    {(t.flowHydrants || []).map((f, i) => (
                      <FlowHydrantCard
                        key={f.id}
                        hydrant={f}
                        index={i}
                        mode={mode}
                        contribGpm={flowHydrantGpm(f)}
                        onChange={(patch) => updateFlowHydrant(f.id, patch)}
                        onPickLocation={() => setPickerFor(`flow:${f.id}`)}
                        onClearLocation={() => updateFlowHydrant(f.id, { location: null })}
                        onAddOutlet={() => addOutlet(f.id)}
                        onUpdateOutlet={(i2, o) => updateOutlet(f.id, i2, o)}
                        onRemoveOutlet={(i2) => removeOutlet(f.id, i2)}
                        onRemove={() => removeFlowHydrant(f.id)}
                        canRemove={(t.flowHydrants || []).length > 1}
                        showLocation
                      />
                    ))}
                  </div>
                </div>
              );
            })}
            <button onClick={addResidualReading} style={appStyles.addBtn}>
              <span style={{ fontSize: 14, lineHeight: 1 }}>+</span> Add test
            </button>
          </div>
        </section>

        {/* Available @ target — result lives on the limiting test card now */}
        <div style={{ marginTop: 6 }}>
          <ResultCard available={available} classInfo={classInfo} target={target} error={error} />
        </div>
        {validResults.length > 1 && limiting && (
          <div style={appStyles.limitingNote}>
            Limited by{' '}
            <span style={{ color: '#fafaf7', fontWeight: 600 }}>
              {limiting.reading.label || `Gauge ${residualReadings.findIndex((x) => x.id === limiting.reading.id) + 1}`}
            </span>
            {' · '}
            <span className="mono">S {fmt(limiting.ps, 0)} / R {fmt(limiting.pr, 0)} psi</span>
          </div>
        )}

        {/* Classification */}
        <section style={{ ...appStyles.section, opacity: 0.55 }}>
          <div style={appStyles.refCard}>
            <div style={appStyles.refTitle}>Classification at 20 psi (AWWA M17)</div>
            <div style={appStyles.refGrid}>
              {[
                ['AA', '≥ 1500 GPM', '#5dadec'],
                ['A',  '1000–1499',  '#4ade80'],
                ['B',  '500–999',    '#f5a524'],
                ['C',  '< 500',      '#f87171'],
              ].map(([c, r, col]) => (
                <div key={c} style={appStyles.refRow}>
                  <span style={{ ...appStyles.refCode, color: col, borderColor: col }} className="mono">{c}</span>
                  <span style={appStyles.refRange} className="mono">{r}</span>
                </div>
              ))}
            </div>
          </div>
        </section>

      {/* Chart */}
      {available != null && (
        <section style={appStyles.section}>
          <SectionLabel n="03" title="Flow curve" />
          <FlowCurve staticPsi={ps} residualPsi={pr} testGpm={limiting ? limiting.flowGpm : 0} target={target} exponent={exponent} accent={accent} />
        </section>
      )}

      {/* Calc breakdown */}
      {t.showSteps && available != null && (
        <section style={appStyles.section}>
          <SectionLabel n="04" title="Math" />
          <CalcBreakdown
            staticPsi={ps} residualPsi={pr} testGpm={limiting ? limiting.flowGpm : 0}
            target={target} exponent={exponent}
            pitotOutlets={mode === 'pitot' ? pitotOutlets : []}
            flowHydrantCount={activeTest?.flowHydrants?.length || 1}
            mode={mode}
            available={available}
          />
        </section>
      )}

        <div style={{ height: 24 }} />
        <div style={appStyles.actionRow}>
          <button onClick={() => window.open('report.html#tab=hydraulic', '_blank')}
            disabled={available == null}
            style={{
              ...appStyles.exportBtn,
              opacity: available != null ? 1 : 0.4,
              cursor: available != null ? 'pointer' : 'not-allowed',
            }}>
            <svg width="14" height="14" viewBox="0 0 14 14" fill="none" style={{ marginRight: 6 }}>
              <path d="M7 1v8M4 6l3 3 3-3M2 11h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
            Export PDF
          </button>
        </div>
      </main>
      )}

      <InfoModal open={infoOpen} onClose={() => setInfoOpen(false)} title={pageInfo.title} items={pageInfo.items} />

      <div style={appStyles.footerDisclaimer}>
        PoelsTools provides this tool as a field reference only. Verify all readings, calculations,
        and final reports independently before acting on them.
      </div>

      {pickerFor && (
        <LocationPicker
          initial={pickerInitial}
          onCancel={() => setPickerFor(null)}
          onConfirm={confirmPickerLocation} />
      )}

      {historyOpen && (
        <HistoryPanel
          history={history}
          onClose={() => setHistoryOpen(false)}
          onLoad={loadEntry}
          onRetest={retestEntry}
          onDelete={(id) => {
            setHistory(history.filter((h) => h.id !== id));
            if (cloudProfile?.organization_id) {
              window.flowSync.deleteTest(id).catch((e) => console.warn('cloud delete', e));
            }
          }}
          onClear={() => { setHistory([]); setHistoryOpen(false); }}
          onExport={{ json: exportJson, csv: exportCsv, import: importJson }}
        />
      )}

      {/* Tweaks */}
      <TweaksPanel title="Tweaks">
        <TweakSection label="Calculation" />
        <TweakSlider label="Target residual" value={t.target} min={10} max={40} step={1} unit=" psi"
          onChange={(v) => setTweak('target', v)} />
        <TweakRadio label="Formula exponent" value={t.exponent}
          options={[{ value: 0.54, label: 'NFPA 291' }, { value: 0.5, label: 'Simplified' }]}
          onChange={(v) => setTweak('exponent', v)} />
        <TweakToggle label="Show math breakdown" value={t.showSteps}
          onChange={(v) => setTweak('showSteps', v)} />
        <TweakSection label="Appearance" />
        <TweakColor label="Accent" value={t.accent}
          options={['#3b82f6', '#f5a524', '#ef4444', '#22c55e']}
          onChange={(v) => setTweak('accent', v)} />
      </TweaksPanel>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// FlowHydrantCard — one card per flow hydrant. Holds label + location +
// elevation, and either a measured-GPM input (mode='flow') or one or more
// PitotOutlet rows (mode='pitot'). Sums its outlets into a per-hydrant
// contribution that's displayed in the card head.

function FlowHydrantCard({
  hydrant, index, mode, contribGpm,
  onChange, onPickLocation, onClearLocation,
  onAddOutlet, onUpdateOutlet, onRemoveOutlet,
  onRemove, canRemove, showLocation = true,
}) {
  const outlets = hydrant.outlets || [];
  return (
    <div style={flowCardStyles.card}>
      <div style={flowCardStyles.head}>
        <span style={flowCardStyles.idx}>F{index + 1}</span>
        <AddressInput
          value={hydrant.label || ''}
          onChange={(v) => onChange({ label: v })}
          onResolve={(patch) => {
            const update = {};
            if (patch.label != null) update.label = patch.label;
            if (patch.location && !hydrant.location) update.location = patch.location;
            if (patch.elevation && !hydrant.elevation) update.elevation = patch.elevation;
            onChange(update);
          }}
          placeholder="Hydrant ID/Address"
          inputStyle={flowCardStyles.labelInput}
        />
        {contribGpm > 0 && (
          <span style={flowCardStyles.contrib} className="mono">
            {fmt(contribGpm, 0)} GPM
          </span>
        )}
        {canRemove && (
          <button type="button" onClick={onRemove} style={flowCardStyles.remove}
                  aria-label="Remove flow hydrant">
            <svg width="14" height="14" viewBox="0 0 14 14">
              <path d="M3 7h8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
            </svg>
          </button>
        )}
      </div>

      <LocationRow location={hydrant.location}
        placeholder="Pin flow hydrant location"
        onPick={onPickLocation}
        onClear={onClearLocation} />

      <ElevationRow value={hydrant.elevation || ''}
        onChange={(v) => onChange({ elevation: v })}
        hasLocation={!!hydrant.location}
        compact />

      {mode === 'flow' ? (
        <FieldCard label="Measured flow" hint="From flowmeter or chart"
          value={hydrant.flowGpm || ''}
          onChange={(v) => onChange({ flowGpm: v })}
          unit="GPM" max={99999} />
      ) : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {outlets.map((o, i) => (
            <PitotOutlet key={i} index={i} outlet={o}
              onChange={(x) => onUpdateOutlet(i, x)}
              onRemove={() => onRemoveOutlet(i)}
              canRemove={outlets.length > 1} />
          ))}
          <button type="button" onClick={onAddOutlet} style={flowCardStyles.outletAddBtn}>
            <span style={{ fontSize: 13, lineHeight: 1 }}>+</span> Add outlet
          </button>
          {outlets.length > 1 && (
            <div style={flowCardStyles.subTotal}>
              <span style={{ color: '#7a7a84' }}>Hydrant subtotal</span>
              <span style={{ color: 'var(--accent)', fontWeight: 600 }} className="mono">
                {fmt(contribGpm, 0)} GPM
              </span>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

const flowCardStyles = {
  card: { background: '#15151a', border: '1px solid #26262e', borderRadius: 14,
          padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 10 },
  head: { display: 'flex', alignItems: 'center', gap: 8 },
  idx: { fontSize: 11, fontWeight: 600, color: '#a8a8b2', letterSpacing: '0.05em',
         padding: '3px 6px', background: '#101014', borderRadius: 5,
         border: '1px solid #2a2a31', fontFamily: "'IBM Plex Mono', monospace" },
  labelInput: { flex: 1, minWidth: 0, background: 'transparent', border: 0, outline: 'none',
                color: '#fafaf7', fontSize: 13.5, fontWeight: 500, padding: '4px 0',
                fontFamily: 'inherit' },
  contrib: { fontSize: 12, color: 'var(--accent)', fontWeight: 600,
             padding: '3px 7px', background: 'rgba(59,130,246,0.08)',
             border: '1px solid rgba(59,130,246,0.25)', borderRadius: 5 },
  remove: { width: 24, height: 24, borderRadius: 6, border: '1px solid #2a2a31',
            background: '#101014', color: '#a8a8b2', cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0 },
  outletAddBtn: { padding: '8px', background: '#101014', border: '1px dashed #2a2a31',
                  borderRadius: 8, color: '#a8a8b2', fontSize: 12, fontWeight: 600,
                  cursor: 'pointer', display: 'flex', alignItems: 'center',
                  justifyContent: 'center', gap: 6, fontFamily: 'inherit' },
  subTotal: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
              padding: '6px 10px', background: '#101014', borderRadius: 8,
              border: '1px solid #1f1f25', fontSize: 12 },
};

function HydrantFlowPanelCard({
  testIndex, testId, reading, hydrant, result, target, isLimiting,
  onChangeReading, onChangeHydrant,
  onPickLocation, onClearLocation,
  onAddOutlet, onUpdateOutlet, onRemoveOutlet,
  onRemove, canRemove,
}) {
  const outlets = hydrant.outlets || [];
  const totalFlow = result?.flowGpm || 0;
  const cls = result?.classInfo;
  const tone = cls ? TONE[cls.tone] : null;
  const panelName = reading.label || hydrant.label || `Hydrant ${testIndex + 1}`;
  return (
    <div style={{
      ...flowPanelStyles.card,
      borderColor: isLimiting ? 'var(--accent)' : '#26262e',
      boxShadow: isLimiting ? '0 0 0 1px var(--accent), 0 4px 14px rgba(59,130,246,0.12)' : 'none',
    }}>
      <div style={flowPanelStyles.head}>
        <span style={flowPanelStyles.idx}>H{testIndex + 1}</span>
        <div style={flowPanelStyles.titleWrap}>
          <div style={flowPanelStyles.title}>{panelName}</div>
          <div style={flowPanelStyles.subtitle}>Single hydrant · static / residual / outlets</div>
        </div>
        <span style={flowPanelStyles.total} className="mono">{fmt(totalFlow, 0)} GPM total</span>
        {canRemove && (
          <button type="button" onClick={onRemove} style={flowPanelStyles.remove} aria-label="Remove hydrant">
            <svg width="14" height="14" viewBox="0 0 14 14"><path d="M3 7h8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>
          </button>
        )}
      </div>

      <AddressInput
        value={hydrant.label || ''}
        onChange={(v) => {
          onChangeHydrant({ label: v });
          onChangeReading({ label: v });
        }}
        onResolve={(patch) => {
          const updateHydrant = {};
          const updateReading = {};
          if (patch.label != null) {
            updateHydrant.label = patch.label;
            updateReading.label = patch.label;
          }
          if (patch.location && !hydrant.location) {
            updateHydrant.location = patch.location;
            updateReading.location = patch.location;
          }
          if (patch.elevation && !hydrant.elevation) {
            updateHydrant.elevation = patch.elevation;
            updateReading.elevation = patch.elevation;
          }
          if (Object.keys(updateHydrant).length) onChangeHydrant(updateHydrant);
          if (Object.keys(updateReading).length) onChangeReading(updateReading);
        }}
        placeholder="Hydrant ID/Address"
        inputStyle={flowCardStyles.labelInput}
      />

      <LocationRow
        location={hydrant.location}
        placeholder="Pin hydrant location"
        onPick={onPickLocation}
        onClear={onClearLocation}
      />

      <div style={resReadStyles.pressureGrid}>
        <div style={resReadStyles.psiField}>
          <span style={resReadStyles.psiLbl}>Static</span>
          <span style={resReadStyles.psiInputRow}>
            <input type="number" inputMode="decimal" value={reading.staticPsi || ''}
              placeholder="0" onChange={(e) => onChangeReading({ staticPsi: e.target.value })}
              style={resReadStyles.psiInput} className="mono" />
            <span style={resReadStyles.psiUnit} className="mono">psi</span>
          </span>
        </div>
        <div style={resReadStyles.psiField}>
          <span style={resReadStyles.psiLbl}>Residual</span>
          <span style={resReadStyles.psiInputRow}>
            <input type="number" inputMode="decimal" value={reading.residualPsi || ''}
              placeholder="0" onChange={(e) => onChangeReading({ residualPsi: e.target.value })}
              style={resReadStyles.psiInput} className="mono" />
            <span style={resReadStyles.psiUnit} className="mono">psi</span>
          </span>
        </div>
      </div>

      <div style={flowPanelStyles.outletHead}>
        <div style={flowPanelStyles.sectionLabel}>Flowing outlets</div>
        <button type="button" onClick={onAddOutlet} style={flowCardStyles.outletAddBtn}>
          <span style={{ fontSize: 13, lineHeight: 1 }}>+</span> Add outlet
        </button>
      </div>
      {outlets.map((o, i) => (
        <PitotOutlet key={i} index={i} outlet={o}
          onChange={(x) => onUpdateOutlet(i, x)}
          onRemove={() => onRemoveOutlet(i)}
          canRemove={outlets.length > 1} />
      ))}

      <div style={resReadStyles.miniResult}>
        {result?.error ? (
          <span style={resReadStyles.miniErr}>{result.error}</span>
        ) : result?.available != null ? (
          <>
            <span style={resReadStyles.miniLbl}>Available @ {target}</span>
            <div style={resReadStyles.miniValRow}>
              <span style={{ ...resReadStyles.miniVal, color: tone?.stripe || 'var(--accent)' }} className="mono">
                {fmt(result.available, 0)}
              </span>
              <span style={resReadStyles.miniValUnit}>GPM</span>
              {cls && (
                <span style={{ ...resReadStyles.miniBadge, color: tone.stripe, borderColor: tone.stripe }} className="mono">
                  {cls.code}
                </span>
              )}
            </div>
          </>
        ) : (
          <span style={resReadStyles.miniIdle}>Enter static and residual psi to calculate</span>
        )}
      </div>
    </div>
  );
}

const flowPanelStyles = {
  card: {
    background: '#15151a', border: '1px solid #26262e', borderRadius: 14,
    padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 10,
  },
  head: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' },
  idx: { fontSize: 11, fontWeight: 600, color: '#a8a8b2', letterSpacing: '0.05em',
         padding: '3px 6px', background: '#101014', borderRadius: 5,
         border: '1px solid #2a2a31', fontFamily: "'IBM Plex Mono', monospace" },
  titleWrap: { display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0, flex: 1 },
  title: { fontSize: 13.5, fontWeight: 600, color: '#fafaf7', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
  subtitle: { fontSize: 11, color: '#7a7a84' },
  total: { fontSize: 12, color: 'var(--accent)', fontWeight: 600,
           padding: '3px 7px', background: 'rgba(59,130,246,0.08)',
           border: '1px solid rgba(59,130,246,0.25)', borderRadius: 5 },
  remove: { width: 24, height: 24, borderRadius: 6, border: '1px solid #2a2a31',
            background: '#101014', color: '#a8a8b2', cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0 },
  outletHead: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 },
  sectionLabel: { fontSize: 12, fontWeight: 600, color: '#a8a8b2', letterSpacing: '0.04em', textTransform: 'uppercase' },
};

function ResidualReadingCard({ reading, index, result, target, isLimiting, onChange,
                              onPickLocation, onClearLocation, onRemove, canRemove, showLocation = true }) {
  const cls = result?.classInfo;
  const tone = cls ? TONE[cls.tone] : null;
  return (
    <div style={{ ...resReadStyles.card,
                  borderColor: isLimiting ? 'var(--accent)' : '#26262e',
                  boxShadow: isLimiting ? '0 0 0 1px var(--accent), 0 4px 14px rgba(59,130,246,0.12)' : 'none' }}>
      <div style={resReadStyles.head}>
        <span style={resReadStyles.idx}>R{index + 1}</span>
        <AddressInput
          value={reading.label || ''}
          onChange={(v) => onChange({ label: v })}
          onResolve={(patch) => {
            const update = {};
            if (patch.label != null) update.label = patch.label;
            if (patch.location && !reading.location) update.location = patch.location;
            if (patch.elevation && !reading.elevation) update.elevation = patch.elevation;
            onChange(update);
          }}
          placeholder="Hydrant ID/Address"
          inputStyle={resReadStyles.labelInput}
        />
        {isLimiting && (
          <span style={resReadStyles.limitBadge} className="mono">LIMIT</span>
        )}
        {canRemove && (
          <button type="button" onClick={onRemove} style={resReadStyles.remove} aria-label="Remove residual">
            <svg width="14" height="14" viewBox="0 0 14 14"><path d="M3 7h8" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>
          </button>
        )}
      </div>

      {showLocation ? (
        <>
          <LocationRow location={reading.location}
            placeholder="Pin gauge location"
            onPick={onPickLocation}
            onClear={onClearLocation} />

          <ElevationRow value={reading.elevation || ''}
            onChange={(v) => onChange({ elevation: v })}
            hasLocation={!!reading.location}
            compact />
        </>
      ) : (
        <div style={{ fontSize: 11.5, color: '#7a7a84' }}>
          Uses the same location as the first residual test.
        </div>
      )}

      <div style={resReadStyles.pressureGrid}>
        <div style={resReadStyles.psiField}>
          <span style={resReadStyles.psiLbl}>Static</span>
          <span style={resReadStyles.psiInputRow}>
            <input type="number" inputMode="decimal" value={reading.staticPsi || ''}
              placeholder="0"
              onChange={(e) => onChange({ staticPsi: e.target.value })}
              style={resReadStyles.psiInput} className="mono" />
            <span style={resReadStyles.psiUnit} className="mono">psi</span>
          </span>
        </div>
        <div style={resReadStyles.psiField}>
          <span style={resReadStyles.psiLbl}>Residual</span>
          <span style={resReadStyles.psiInputRow}>
            <input type="number" inputMode="decimal" value={reading.residualPsi}
              placeholder="0"
              onChange={(e) => onChange({ residualPsi: e.target.value })}
              style={resReadStyles.psiInput} className="mono" />
            <span style={resReadStyles.psiUnit} className="mono">psi</span>
          </span>
        </div>
      </div>

      <div style={resReadStyles.miniResult}>
        {result?.error ? (
          <span style={resReadStyles.miniErr}>{result.error}</span>
        ) : result?.available != null ? (
          <>
            <span style={resReadStyles.miniLbl}>Available @ {target}</span>
            <div style={resReadStyles.miniValRow}>
              <span style={{ ...resReadStyles.miniVal, color: tone?.stripe || 'var(--accent)' }} className="mono">
                {fmt(result.available, 0)}
              </span>
              <span style={resReadStyles.miniValUnit}>GPM</span>
              {cls && (
                <span style={{ ...resReadStyles.miniBadge, color: tone.stripe, borderColor: tone.stripe }} className="mono">
                  {cls.code}
                </span>
              )}
            </div>
          </>
        ) : (
          <span style={resReadStyles.miniIdle}>Enter static and residual psi to calculate</span>
        )}
      </div>
    </div>
  );
}

const resReadStyles = {
  card: { background: '#15151a', border: '1px solid', borderRadius: 14,
          padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 10,
          transition: 'border-color .15s, box-shadow .15s' },
  head: { display: 'flex', alignItems: 'center', gap: 8 },
  idx: { fontSize: 11, fontWeight: 600, color: '#a8a8b2', letterSpacing: '0.05em',
         padding: '3px 6px', background: '#101014', borderRadius: 5,
         border: '1px solid #2a2a31', fontFamily: "'IBM Plex Mono', monospace" },
  labelInput: { flex: 1, minWidth: 0, background: 'transparent', border: 0, outline: 'none',
                color: '#fafaf7', fontSize: 13.5, fontWeight: 500, padding: '4px 0',
                fontFamily: 'inherit' },
  limitBadge: { fontSize: 10, fontWeight: 700, color: 'var(--accent)',
                border: '1px solid var(--accent)', padding: '2px 6px', borderRadius: 4,
                letterSpacing: '0.08em' },
  remove: { width: 24, height: 24, borderRadius: 6, border: '1px solid #2a2a31',
            background: '#101014', color: '#a8a8b2', cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0 },
  row: { display: 'grid', gridTemplateColumns: 'minmax(0, 0.85fr) minmax(0, 1.15fr)', gap: 10, alignItems: 'stretch' },
  psiField: { display: 'flex', flexDirection: 'column', gap: 5,
              background: '#0c0c10', border: '1px solid #26262e', borderRadius: 10,
              padding: '8px 12px 6px' },
  pressureGrid: { display: 'grid', gridTemplateColumns: 'minmax(0,1fr) minmax(0,1fr)', gap: 8 },
  psiLbl: { fontSize: 10.5, fontWeight: 600, color: '#7a7a84', letterSpacing: '0.04em', textTransform: 'uppercase' },
  psiInputRow: { display: 'flex', alignItems: 'baseline', gap: 4 },
  psiInput: { flex: 1, minWidth: 0, width: '100%', background: 'transparent', border: 0, outline: 'none',
              color: '#fafaf7', fontSize: 26, fontWeight: 500, padding: 0,
              letterSpacing: '-0.02em', fontFamily: "'IBM Plex Mono', monospace" },
  psiUnit: { fontSize: 12, color: '#6a6a72' },
  miniResult: { display: 'flex', flexDirection: 'column', gap: 4,
                background: '#101014', border: '1px solid #1f1f25', borderRadius: 10,
                padding: '8px 12px', justifyContent: 'center' },
  miniLbl: { fontSize: 10, fontWeight: 600, color: '#7a7a84', letterSpacing: '0.04em', textTransform: 'uppercase' },
  miniValRow: { display: 'flex', alignItems: 'baseline', gap: 6, flexWrap: 'wrap' },
  miniVal: { fontSize: 22, fontWeight: 500, letterSpacing: '-0.02em' },
  miniValUnit: { fontSize: 11, color: '#7a7a84', fontWeight: 500 },
  miniBadge: { fontSize: 10, fontWeight: 600, padding: '1px 5px', borderRadius: 3,
               border: '1px solid', marginLeft: 'auto', letterSpacing: '0.05em' },
  miniIdle: { fontSize: 11.5, color: '#5a5a62' },
  miniErr: { fontSize: 11.5, color: '#f87171', lineHeight: 1.3 },
};

function ElevationRow({ value, onChange, hasLocation, compact = false }) {
  return null;
}

const elevStyles = {
  row: { display: 'flex', alignItems: 'center', gap: 8,
         background: '#101014', border: '1px solid #1f1f25', borderRadius: 10 },
  label: { fontSize: 11, fontWeight: 600, color: '#7a7a84', letterSpacing: '0.04em',
           textTransform: 'uppercase', flexShrink: 0 },
  input: { flex: 1, minWidth: 0, background: 'transparent', border: 0, outline: 'none',
           color: '#fafaf7', fontSize: 13, fontWeight: 500, padding: 0, textAlign: 'right',
           fontFamily: "'IBM Plex Mono', monospace" },
  unit: { fontSize: 11, color: '#6a6a72', flexShrink: 0 },
};

function LocationRow({ location, onPick, onClear, placeholder = 'Pin location on map' }) {
  if (!location) {
    return (
      <button onClick={onPick} style={locRowStyles.empty}>
        <svg width="14" height="14" viewBox="0 0 14 14" fill="none" style={{ flexShrink: 0 }}>
          <path d="M7 1C4.5 1 2.5 3 2.5 5.5 2.5 9 7 13 7 13s4.5-4 4.5-7.5C11.5 3 9.5 1 7 1z"
            stroke="currentColor" strokeWidth="1.4"/>
          <circle cx="7" cy="5.5" r="1.5" fill="currentColor"/>
        </svg>
        <span>{placeholder}</span>
        <span style={locRowStyles.emptyHint}>optional</span>
      </button>
    );
  }
  return (
    <div style={locRowStyles.set}>
      <div style={locRowStyles.dot}>
        <svg width="12" height="12" viewBox="0 0 14 14" fill="none">
          <path d="M7 1C4.5 1 2.5 3 2.5 5.5 2.5 9 7 13 7 13s4.5-4 4.5-7.5C11.5 3 9.5 1 7 1z"
            fill="currentColor"/>
        </svg>
      </div>
      <div style={locRowStyles.coords}>
        <span style={locRowStyles.coordsLbl}>Location set</span>
        <span style={locRowStyles.coordsVal} className="mono">
          {location.lat.toFixed(5)}, {location.lng.toFixed(5)}
        </span>
      </div>
      <button onClick={onPick} style={locRowStyles.act}>Edit</button>
      <button onClick={onClear} style={locRowStyles.act} aria-label="Clear location">×</button>
    </div>
  );
}

const locRowStyles = {
  empty: { display: 'flex', alignItems: 'center', gap: 8, padding: '9px 12px',
           background: '#101014', border: '1px dashed #2a2a31', borderRadius: 10,
           color: '#8a8a92', fontSize: 12.5, fontWeight: 500, cursor: 'pointer',
           width: '100%', fontFamily: 'inherit' },
  emptyHint: { marginLeft: 'auto', fontSize: 10.5, color: '#5a5a62', letterSpacing: '0.05em',
               textTransform: 'uppercase' },
  set: { display: 'flex', alignItems: 'center', gap: 10, padding: '8px 10px 8px 12px',
         background: '#15151a', border: '1px solid #26262e', borderRadius: 10 },
  dot: { width: 26, height: 26, borderRadius: '50%', background: 'rgba(59,130,246,0.15)',
         color: 'var(--accent)', display: 'flex', alignItems: 'center', justifyContent: 'center' },
  coords: { display: 'flex', flexDirection: 'column', gap: 0, flex: 1, minWidth: 0 },
  coordsLbl: { fontSize: 10.5, color: '#7a7a84', letterSpacing: '0.05em', textTransform: 'uppercase', fontWeight: 600 },
  coordsVal: { fontSize: 12, color: '#fafaf7', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
  act: { padding: '4px 8px', background: 'transparent', border: '1px solid #2a2a31',
         borderRadius: 6, color: '#a8a8b2', fontSize: 11.5, fontWeight: 500, cursor: 'pointer' },
};

function SectionLabel({ n, title }) {
  return (
    <div style={appStyles.sectionLabel}>
      <span style={appStyles.sectionNum} className="mono">{n}</span>
      <span style={appStyles.sectionTitle}>{title}</span>
    </div>
  );
}

const appStyles = {
  shell: { maxWidth: 560, margin: '0 auto', minHeight: '100vh', background: '#0a0a0c', position: 'relative' },
  tabs: { display: 'flex', gap: 0, padding: '0 18px',
          borderBottom: '1px solid #1f1f25', position: 'sticky',
          top: 75, background: 'rgba(10,10,12,0.85)', zIndex: 9,
          backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)' },
  tab: { padding: '11px 14px', background: 'transparent', border: 0,
         borderBottom: '2px solid transparent', fontSize: 13, fontWeight: 600,
         cursor: 'pointer', letterSpacing: '0.01em', display: 'flex',
         alignItems: 'center', gap: 6, marginBottom: -1, transition: 'color .15s, border-color .15s' },
  tabCount: { padding: '0 5px', minWidth: 18, height: 16, borderRadius: 8,
              background: '#26262e', color: '#a8a8b2', fontSize: 10, fontWeight: 600,
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center' },
  mapWrap: { position: 'relative', width: '100%', height: 'calc(100vh - 110px)' },
  header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center',
            padding: '18px 18px 8px', position: 'sticky', top: 0, background: 'rgba(10,10,12,0.85)',
            backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)', zIndex: 10,
            borderBottom: '1px solid rgba(38,38,46,0.5)' },
  headLeft: { display: 'flex', alignItems: 'center', gap: 12 },
  brandIcon: { width: 38, height: 38, borderRadius: 10, background: '#15151a',
               border: '1px solid #26262e', display: 'flex', alignItems: 'center', justifyContent: 'center' },
  brandTitle: { fontSize: 16, fontWeight: 700, letterSpacing: '-0.01em', color: '#fafaf7' },
  brandSub: { fontSize: 11, color: '#7a7a84', fontFamily: "'IBM Plex Mono', monospace", marginTop: 1 },
  headRight: { display: 'flex', gap: 8 },
  iconBtn: { position: 'relative', width: 36, height: 36, borderRadius: 9, border: '1px solid #26262e',
             background: '#15151a', color: '#a8a8b2', cursor: 'pointer', display: 'flex',
             alignItems: 'center', justifyContent: 'center', padding: 0 },
  iconBadge: { position: 'absolute', top: -4, right: -4, minWidth: 16, height: 16, borderRadius: 8,
               background: 'var(--accent)', color: '#0a0a0c', fontSize: 10, fontWeight: 700,
               display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 4px' },
  main: { padding: '16px 18px 32px', display: 'flex', flexDirection: 'column', gap: 22 },
  labelInput: { width: '100%', background: 'transparent', border: 0, borderBottom: '1px solid #26262e',
                padding: '6px 2px 10px', color: '#fafaf7', fontSize: 15, fontWeight: 500, outline: 'none',
                fontFamily: 'inherit' },
  section: { display: 'flex', flexDirection: 'column', gap: 10 },
  sectionLabel: { display: 'flex', alignItems: 'baseline', gap: 10, padding: '0 2px' },
  sectionNum: { fontSize: 10.5, color: '#5a5a62', letterSpacing: '0.06em' },
  sectionTitle: { fontSize: 11.5, fontWeight: 600, color: '#a8a8b2', letterSpacing: '0.06em', textTransform: 'uppercase' },
  testBlockStack: { display: 'flex', flexDirection: 'column', gap: 16 },
  testBlock: { background: '#101014', border: '1px solid #343846', borderRadius: 18,
               padding: 12, boxShadow: '0 0 0 1px rgba(59,130,246,0.08), 0 14px 28px rgba(0,0,0,0.18)' },
  testBlockHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 12,
                     margin: '-12px -12px 12px', padding: '11px 14px', borderRadius: '17px 17px 0 0',
                     borderBottom: '1px solid rgba(59,130,246,0.28)',
                     background: 'linear-gradient(90deg, rgba(59,130,246,0.24), rgba(59,130,246,0.07))' },
  testBlockTitle: { fontSize: 16, fontWeight: 800, color: '#fafaf7', letterSpacing: '0.04em', textTransform: 'uppercase' },
  testBlockFlow: { fontSize: 12, color: 'var(--accent)', fontWeight: 700, whiteSpace: 'nowrap' },
  testSubheading: { fontSize: 12, fontWeight: 700, color: '#8b8b96', textTransform: 'uppercase', letterSpacing: '0.06em' },
  testSubsection: { display: 'flex', flexDirection: 'column', gap: 10, marginTop: 14,
                    paddingTop: 12, borderTop: '1px solid #24242c' },
  dualGrid: { display: 'grid', gridTemplateColumns: 'minmax(0,1fr) minmax(0,1fr)', gap: 10 },
  addBtn: { padding: '11px', background: '#15151a', border: '1px solid #2a2a31',
            borderRadius: 10, color: 'var(--accent)', fontSize: 13, fontWeight: 600,
            cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
            gap: 8, transition: 'background .12s, border-color .12s' },
  totalLine: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
               padding: '8px 14px', background: '#101014', borderRadius: 10,
               border: '1px solid #1f1f25', fontSize: 13 },
  limitingNote: { fontSize: 12, color: '#a8a8b2', padding: '8px 12px',
                  background: '#101014', border: '1px solid #1f1f25', borderRadius: 10 },
  hydrantLabelInput: { width: '100%', background: 'transparent', border: 0,
                       borderBottom: '1px solid #26262e',
                       padding: '4px 2px 8px', color: '#fafaf7',
                       fontSize: 14, fontWeight: 500, outline: 'none', fontFamily: 'inherit' },
  saveBtn: { display: 'flex', alignItems: 'center', justifyContent: 'center',
             padding: '11px', background: '#15151a', border: '1px solid #26262e',
             borderRadius: 10, color: '#fafaf7', fontSize: 13, fontWeight: 600,
             transition: 'opacity .15s', flex: 1 },
  exportBtn: { display: 'flex', alignItems: 'center', justifyContent: 'center',
               padding: '11px', background: 'var(--accent)', border: 0,
               borderRadius: 10, color: '#0a0a0c', fontSize: 13, fontWeight: 600,
               transition: 'opacity .15s', flex: 1 },
  actionRow: { display: 'flex', gap: 8 },
  refCard: { background: 'transparent', border: '1px dashed #1f1f25', borderRadius: 12, padding: '12px 14px' },
  refTitle: { fontSize: 11, color: '#7a7a84', marginBottom: 8, letterSpacing: '0.03em' },
  refGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 },
  refRow: { display: 'flex', alignItems: 'center', gap: 8 },
  refCode: { padding: '2px 6px', borderRadius: 4, border: '1px solid', fontSize: 10.5, fontWeight: 600 },
  refRange: { fontSize: 11.5, color: '#a8a8b2' },
  footerDisclaimer: { padding: '8px 18px 18px', color: '#5a5a62', fontSize: 10.5, lineHeight: 1.45,
                      textAlign: 'center', maxWidth: 560, margin: '0 auto' },
};

// Mount
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
