// 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',
    ];
    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.25"
            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' },
};

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 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)} × {o.diameter || 0}² × √{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#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', 'test'); // 'test' | 'map'
  const [mode, setMode] = useLocalState('flowtest.mode', 'flow');
  const [label, setLabel] = useLocalState('flowtest.label', '');

  // 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);
      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', label: '', staticPsi: s, residualPsi: r, location: loc, elevation: '' }];
    } catch {
      return [{ id: 'r1', 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);
    } 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: 'f1',
      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);
  // Picker target: null | 'flow' | 'static' | <residual id>
  const [pickerFor, setPickerFor] = useState(null);

  useEffect(() => {
    if (view === 'trends') setView('test');
  }, [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-hydrant test flow (Qf contribution) — direct entry or sum of pitot outlets
  const flowHydrantGpm = useCallback((f) => {
    if (mode === 'flow') return Number(f.flowGpm) || 0;
    return (f.outlets || []).reduce((s, o) => s + pitotGpm(o), 0);
  }, [mode]);

  // Total test flow (Qf) — sum across every flow hydrant
  const testGpm = useMemo(() => {
    return flowHydrants.reduce((s, f) => s + flowHydrantGpm(f), 0);
  }, [flowHydrants, flowHydrantGpm]);

  // Migrate legacy data: copy the shared staticPsi onto each residualReading
  // entry that doesn't have its own. Each gauge hydrant now stores both its
  // own static AND residual psi (since they're read from the same gauge).
  useEffect(() => {
    if (staticPsi && residualReadings.some((r) => !r.staticPsi)) {
      setResidualReadings(residualReadings.map((r) => ({
        ...r,
        staticPsi: r.staticPsi || staticPsi,
        location: r.location || staticLocation || null,
        elevation: r.elevation || staticElevation || '',
        label: r.label || staticLabel || '',
      })));
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Per-gauge Available @ target. Each gauge has its own staticPsi (with
  // fallback to legacy shared value).
  const perResidualResults = useMemo(() => {
    return residualReadings.map((r) => {
      const psVal = Number(r.staticPsi || staticPsi) || 0;
      const prVal = Number(r.residualPsi) || 0;
      let err = null;
      if ((r.staticPsi || staticPsi) !== '' && r.residualPsi !== '' && testGpm > 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: testGpm, target, exponent });
      return { reading: r, available: avail, error: err, ps: psVal, pr: prVal,
               classInfo: avail != null ? classify(avail) : null };
    });
  }, [residualReadings, staticPsi, testGpm, target, exponent]);

  const validResults = perResidualResults.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 gauge when multiple are entered.
  const available = limiting ? limiting.available : null;
  const classInfo = limiting ? limiting.classInfo : null;
  const ps = limiting ? limiting.ps : (Number(residualReadings[0]?.staticPsi || staticPsi) || 0);
  const pr = limiting ? limiting.pr : (Number(residualReadings[0]?.residualPsi) || 0);
  const firstError = perResidualResults.find((r) => r.error)?.error || null;
  const error = available == null ? firstError : null;

  // Reset
  const reset = useCallback(() => {
    setLabel('');
    setStaticLabel('');
    setStaticPsi('');
    setStaticLocation(null);
    setStaticElevation('');
    setResidualReadings([{ id: 'r1', label: '', residualPsi: '', location: null, elevation: '' }]);
    setFlowHydrants([{ id: 'f1', 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(() => {
    // First hydrant carries the legacy mirror fields so older history viewers
    // / cloud sync code still see something sensible.
    const primary = flowHydrants[0] || {};
    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, testGpm, target, available,
      staticLabel, staticPsi: ps, staticLocation: staticLocation || null,
      staticElevation: staticElevation || null,
      residualReadings: residualReadings.map((r) => ({ ...r })),
      // New multi-flow-hydrant shape
      flowHydrants: flowHydrants.map((f) => ({
        ...f,
        // store this hydrant's computed contribution for downstream consumers
        contribGpm: flowHydrantGpm(f),
      })),
      // Legacy mirror fields (first hydrant only) — keep until all consumers
      // are updated to read flowHydrants.
      outlets: mode === 'pitot' ? (primary.outlets || null) : null,
      flowGpm: mode === 'flow' ? (Number(primary.flowGpm) || null) : null,
      flowLabel: primary.label || null,
      flowLocation: primary.location || null,
      flowElevation: primary.elevation || null,
      results: perResidualResults.map((r) => ({
        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,
      })),
    };
    setHistory([entry, ...history].slice(0, 100));
    // Mirror to cloud if signed in
    const profile = window.flowSync && window.__flowProfile;
    if (profile?.organization_id) {
      window.flowSync.uploadTest(entry, profile).catch((e) => console.warn('upload', e));
    }
  }, [label, mode, testGpm, target, available, staticLabel, ps, staticLocation, staticElevation,
      residualReadings, flowHydrants, flowHydrantGpm,
      perResidualResults, history, setHistory]);

  // 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) : '');
    if (h.residualReadings && h.residualReadings.length > 0) {
      setResidualReadings(h.residualReadings);
    } else if (h.residualPsi != null) {
      // Legacy single-hydrant history entry
      setResidualReadings([{ id: 'r1', label: '',
        residualPsi: String(h.residualPsi),
        location: h.location || null }]);
    }
    // Flow hydrants — new multi-hydrant array, or legacy single-hydrant fields
    if (Array.isArray(h.flowHydrants) && h.flowHydrants.length > 0) {
      setFlowHydrants(h.flowHydrants.map((f) => ({
        id: f.id || ('f' + Math.random().toString(36).slice(2, 7)),
        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 {
      setFlowHydrants([{
        id: 'f1',
        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('test');
  }, [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 {
        setResidualReadings([
          ...residualReadings,
          { id: 'r' + Date.now().toString(36), label: '', residualPsi: '', location: latlng },
        ]);
      }
    }
    setView('test');
  }, [staticLocation, setStaticLocation, residualReadings, setResidualReadings, setView]);

  // 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);
    if (h.residualReadings && h.residualReadings.length > 0) {
      setResidualReadings(h.residualReadings.map((r) => ({
        ...r, residualPsi: '',
      })));
    } else if (h.residualPsi != null) {
      setResidualReadings([{ id: 'r1', label: '', residualPsi: '',
        location: h.location || null }]);
    }
    // Flow hydrants — blank pressures, keep hardware + location
    if (Array.isArray(h.flowHydrants) && h.flowHydrants.length > 0) {
      setFlowHydrants(h.flowHydrants.map((f) => ({
        id: f.id || ('f' + Math.random().toString(36).slice(2, 7)),
        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 {
      setFlowHydrants([{
        id: 'f1',
        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('test');
  }, [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 = () => setResidualReadings((prev) => [
    ...prev,
    { id: 'r' + Date.now().toString(36), label: '',
      staticPsi: '', residualPsi: '', location: null, elevation: '' },
  ]);
  const updateResidualReading = (id, patch) => setResidualReadings((prev) =>
    prev.map((r) => (r.id === id ? { ...r, ...patch } : r)));
  const removeResidualReading = (id) => 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') setStaticLocation(coord);
    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' ? staticLocation
    : (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 = () => setFlowHydrants((prev) => [
    ...prev,
    { id: 'f' + Date.now().toString(36), 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));

  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 === 'test' && (
            <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>
          )}
          {/* sync/history removed for local-only operation */}
        </div>
      </header>

      {/* ─────── Tab strip ─────── */}
      <div style={appStyles.tabs}>
        {[
          { id: 'test', label: '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,
              // legacy mirror — first flow hydrant only
              flowLocation: flowHydrants[0]?.location || null,
            }} />
        </div>
      ) : (
      <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}
        />

        {/* ── 01 Gauge hydrant(s) ── */}
        <section style={appStyles.section}>
          <SectionLabel n="01"
            title={`Gauge hydrant${residualReadings.length > 1 ? `s · ${residualReadings.length}` : ''}`} />
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {residualReadings.map((r, i) => {
              const result = perResidualResults.find((x) => x.reading.id === r.id);
              const isLimiting = limiting && limiting.reading.id === r.id && validResults.length > 1;
              return (
                <ResidualReadingCard key={r.id} reading={r} index={i}
                  result={result} target={target} isLimiting={isLimiting}
                  onChange={(patch) => updateResidualReading(r.id, patch)}
                  onPickLocation={() => setPickerFor(r.id)}
                  onClearLocation={() => updateResidualReading(r.id, { location: null })}
                  onRemove={() => removeResidualReading(r.id)}
                  canRemove={residualReadings.length > 1} />
              );
            })}
            <button onClick={addResidualReading} style={appStyles.addBtn}>
              <span style={{ fontSize: 14, lineHeight: 1 }}>+</span> Add gauge hydrant
            </button>
          </div>
        </section>

        {/* ── 02 Flow hydrant(s) ── */}
        <section style={appStyles.section}>
          <SectionLabel n="02"
            title={`Flow hydrant${flowHydrants.length > 1 ? `s · ${flowHydrants.length}` : ''}`} />
          <ModeToggle value={mode} onChange={setMode} />
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            {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={flowHydrants.length > 1} />
            ))}
            <button onClick={addFlowHydrant} style={appStyles.addBtn}>
              <span style={{ fontSize: 14, lineHeight: 1 }}>+</span> Add flow hydrant
            </button>
            {flowHydrants.length > 1 && (
              <div style={appStyles.totalLine}>
                <span style={{ color: '#7a7a84' }}>Total test flow Q_F</span>
                <span style={{ color: 'var(--accent)', fontWeight: 600 }} className="mono">
                  {fmt(testGpm, 0)} GPM
                </span>
              </div>
            )}
          </div>

          {/* Available @ target — result lives on the flow hydrant 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={testGpm} 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={testGpm}
              target={target} exponent={exponent}
              pitotOutlets={mode === 'pitot'
                ? flowHydrants.flatMap((f, fi) =>
                    (f.outlets || []).map((o) => ({
                      ...o,
                      _hydrantIdx: fi,
                      _hydrantLabel: f.label || `Hydrant ${fi + 1}`,
                    })))
                : []}
              flowHydrantCount={flowHydrants.length}
              mode={mode}
              available={available}
            />
          </section>
        )}

        <div style={{ height: 24 }} />
        <div style={appStyles.actionRow}>
          <button onClick={() => window.open('report.html', '_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>
      </section>
      </main>
      )}

      <div style={appStyles.footerDisclaimer}>
        Poels Protection 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,
}) {
  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 or address (type to search)"
          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 ResidualReadingCard({ reading, index, result, target, isLimiting, onChange,
                              onPickLocation, onClearLocation, onRemove, canRemove }) {
  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 or address (type to search)"
          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>

      <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={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' },
  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 />);
