// Loaded as a plain <script type="text/babel"> (no bundler), so this relies on the
// React/ReactDOM UMD globals from CDN rather than ES module imports.
const { useState, useEffect, useRef, useCallback } = React;

/* ---------- Design tokens (from Claude Design's visual direction) ---------- */
const THEME = {
  bg: "oklch(0.955 0.018 75)",
  surface: "oklch(0.985 0.01 75)",
  surface2: "oklch(0.995 0.006 75)",
  ink: "oklch(0.24 0.02 75)",
  ink2: "oklch(0.46 0.02 75)",
  ink3: "oklch(0.62 0.018 75)",
  line: "oklch(0.85 0.02 75)",
  lineStrong: "oklch(0.7 0.03 75)",
  accent: "oklch(0.5 0.16 35)",
  accentStrong: "oklch(0.4 0.15 35)",
  accentTint: "oklch(0.94 0.045 35)",
  onAccent: "oklch(0.99 0.004 75)",
};
const FONT = "'IBM Plex Sans', system-ui, sans-serif";
const MONO = "'IBM Plex Mono', monospace";

/* ---------- Icons (matching Design's stroke-based SVG set) ---------- */
function Icon({ name, size = 18, color = "currentColor", strokeWidth = 1.7 }) {
  const common = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth, strokeLinecap: "round", strokeLinejoin: "round" };
  switch (name) {
    case "back": return <svg {...common}><path d="M19 12H5M11 18l-6-6 6-6" /></svg>;
    case "plus": return <svg {...common}><path d="M12 5v14M5 12h14" /></svg>;
    case "camera": return <svg {...common}><path d="M4 8h3l1.5-2h7L17 8h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z" /><circle cx="12" cy="14" r="3.5" /></svg>;
    case "upload": return <svg {...common}><path d="M12 20V9M7 13l5-5 5 5" /><path d="M5 19h14" /></svg>;
    case "download": return <svg {...common}><path d="M12 4v11M7 11l5 5 5-5" /><path d="M5 19h14" /></svg>;
    case "filecheck": return <svg {...common}><path d="M7 3h7l4 4v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z" /><path d="M9 13l2 2 4-4" /></svg>;
    case "inbox": return <svg {...common}><path d="M3 12h5l2 3h4l2-3h5" /><path d="M5.5 5h13L21 12v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6L5.5 5z" /></svg>;
    case "lock": return <svg {...common}><rect x="6" y="10" width="12" height="9" rx="1.5" /><path d="M8.5 10V7a3.5 3.5 0 1 1 7 0v3" /></svg>;
    case "mappin": return <svg {...common}><path d="M12 21s7-6.5 7-12a7 7 0 1 0-14 0c0 5.5 7 12 7 12z" /><circle cx="12" cy="9" r="2.25" /></svg>;
    case "play": return <svg {...common}><path d="M6 4l14 8-14 8V4z" /></svg>;
    case "stop": return <svg {...common}><rect x="6" y="6" width="12" height="12" rx="1.5" /></svg>;
    case "receipt": return <svg {...common}><path d="M7 3h10v17l-2-1.3L13 20l-2-1.3L9 20l-2-1.3z" /><path d="M9 8h6M9 11h6M9 14h4" /></svg>;
    case "settings": return <svg {...common}><path d="M4 7h10M17 7h3M4 17h3M10 17h10" /><circle cx="14" cy="7" r="2" /><circle cx="7" cy="17" r="2" /></svg>;
    case "trash": return <svg {...common}><path d="M4 7h16" /><path d="M9 7V4h6v3" /><path d="M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13" /></svg>;
    case "x": return <svg {...common}><path d="M6 6l12 12M18 6L6 18" /></svg>;
    case "scan": return <svg {...common}><path d="M4 8V5a1 1 0 0 1 1-1h3M20 8V5a1 1 0 0 0-1-1h-3M4 16v3a1 1 0 0 0 1 1h3M20 16v3a1 1 0 0 1-1 1h-3" /></svg>;
    default: return null;
  }
}

/* ---------- Data ---------- */
const REP_NAMES = ["Aaron Lyons","Alex Intihar","Alexis Berkey","Alida Sweat","Alison Klopp","Alyssa Curtis","Alyssa Sweeden","Amanda Zientek","Amber Elder","Amber Nerad","Amey Graybeal","Amie Siville","Ana Sifuentes","Andrea Avery","Andrew Bell","Angele Rivera","Anne Peytavin","Anthony Byrnes","Ashley Cowherd","Ashley Cunningham","Barbara Baird","Beth Johnson","Beth Olson","Blake Ahrens","Bonnie Blackman","Brad Hudnall","Brandie Morgan","Brenda Deuschle","Brett Baird","Brian Ierardi","Brian Stubbs","Carlos Trejo","Casey Hinde","Cassi Balke","Cathy Glenn","Chris Cline","Chris Creswell","Chris Joy","Chris Mahon","Chris Nail","Chris Spelman","Christiana Hackett","Christie Hume","Christopher Coleman","Christopher Holland","Chuck Hudson","Cindy Vera","Clint Sanford","Colleen Arneson","Corey J Laroche","Cory Leek","Craig Orness","Craig Rosenberger","Crystal Meyer","Crystel Labrosse","Dan Bussis","Dana Weiss","Danielle Derosa","Danielle Desrosiers","Darla Berkel","David Belland","David Patrick","Debbie Capogreco-Nicoll","Dixie Mcgrath","Donald Boyd","Doryan Williams","Dustin Cox","Dustin Keirsey","Enmanuel Esteban","Eric Glennon","Ericka Metevier","Ernest Kerr","Etta Koehne","Frank Hutsell","Frederick Reeping","Graham Rucker","Greg Volz","Grey Cotham","Hannah Funderburg","Helena Sampson","Isabella Myszak","Jack Murray","Jackson Peach","Jaclyn Paffrath","Jacob Wright","Jacqui Koplik","James Anderson","James Mitro","James Oszlak","Janey Wilding","Jason Baranowski","Jason Castleton","Jay Hall","Jay Hollis Iii","Jayme Bogner","Jeffrey Carns","Jeffrey Williams","Jenebeth Ortiz","Jennifer Massey","Jenny Howe","Jim Hawkinson","Joanna Staszeczka","John Brannigan","John Bumgarner","Joseph Elmore","Joshua Riedy","Jr., Bernard Shaughnessy","Julie A Maddaleni","Kande Chee","Kandis L Bernal","Kari Iannaci","Kari Moore","Karissa Tumbusch","Katherine Dowdy","Katie Zablah","Kaylyn Mccoy","Kelli Rath","Kelly Barker","Kelly Salassi","Kelsey Link","Kendall Spencer","Keri Lange","Kerri Barton","Kerri Dolena","Kevin Myjak","Kim Trezona","Kyla Evans","Larry Paul","Laura Tortarella","Lauren Nicholson","Lee Holcomb","Leslie Hull","Lindsay Shepherd","Lisa Schwartz","Loren Collins","Luda Monroe","Luke Samples","Lyndsay Kelley","Lyndsey Bland-Gunn","Mandi Wiley","Mary Marrin-Cross","Matthew Sheets","Matthew Stucchio","Mckinley Witbrodt","Megan Howard","Megan Taylor","Megan Turner","Melissa Hodge","Melissa Jones","Melvin Mccants","Mica Carpenter","Michael Berschback","Michael Lenfest","Michael Page","Michelle Fugarino","Michelle Weber","Mike Cannon","Mike Conlon","Mitchell Anzovino","Molly Shaheen","Nicholas Crafts","Nicole Horvat","Pamela Pearson","Patrick Roberts","Paula Shelton","Payton Zeigler","Philip Page","Rachel Anderson","Rachel Nelson","Raphael Pond","Raymond Woodbury","Rebecca Radchenko","Regina Murray","Rianne Price","Rob Rathbun","Robert Carper","Robert Metivier","Rod Evans","Sarah Dockery","Scott Hicks","Sean Mccarthy","Shannon Neuhofer","Sharnell Mullen","Sheri Siebert","Sherri Loney","Sherry Clontz","Stacy Harrington","Stacy Janes","Stacy Mehrens","Stephen Sweet","Stephen Sweet Jr","Stephen Wagner","Suzanne Ross","Tara Anderson","Taylor Crowley","Terry Cleveland","Terry Mcgovern","Terry Tomek","Thomas Babcock","Thomas Shields","Tim Mcgovern","Todd Featherston","Todd Schoenherr","Tracey Barrick","Trent Schauster","Tyler Malins","Tyler Melton","Victor Santiago","Whitney Baker","Whitney Raffo","Will Zeigler","Gary Huffman"];

const CATEGORIES = [
  { key: "miles", label: "Personal car miles", unit: "mi" },
  { key: "airfare", label: "Airfare", unit: "$" },
  { key: "luggage", label: "Checked luggage fee", unit: "$" },
  { key: "rental", label: "Rental car", unit: "$" },
  { key: "gas", label: "Gas", unit: "$" },
  { key: "parking", label: "Parking", unit: "$" },
  { key: "taxi", label: "Taxi/rideshare", unit: "$" },
  { key: "tolls", label: "Tolls", unit: "$" },
  { key: "lodging", label: "Lodging", unit: "$" },
  { key: "meals", label: "Meals", unit: "$" },
];

const CONCUR_TYPE_MAP = [
  { match: /meals.*multiple attendees|multiple attendees.*meals/i, key: "meals" },
  { match: /meals.*dining alone|dining alone/i, key: "meals" },
  { match: /hotel tax/i, key: "lodging" },
  { match: /lodging/i, key: "lodging" },
  { match: /fuel/i, key: "gas" },
  { match: /personal car mileage/i, key: "miles" },
  { match: /parking/i, key: "parking" },
  { match: /taxi|car service/i, key: "taxi" },
  { match: /tolls?/i, key: "tolls" },
  { match: /car rental/i, key: "rental" },
  { match: /airline fee/i, key: "luggage" },
  { match: /agency booking fee|airfare/i, key: "airfare" },
];
function concurTypeToCategory(text) {
  for (const m of CONCUR_TYPE_MAP) if (m.match.test(text)) return m.key;
  return null;
}
function catByKey(k) { return CATEGORIES.find((c) => c.key === k); }

/* ---------- Storage / hooks ---------- */
const uid = () => Math.random().toString(36).slice(2) + Date.now().toString(36);
const todayStr = () => new Date().toISOString().slice(0, 10);
const fmt = (n) => (Math.round((Number(n || 0) + Number.EPSILON) * 100) / 100).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const fmtMi = (n) => (Math.round((Number(n || 0) + Number.EPSILON) * 10) / 10).toLocaleString();

async function loadStore(key, fallback) {
  try {
    const raw = window.localStorage.getItem("repexp_" + key);
    return raw != null ? JSON.parse(raw) : fallback;
  } catch (e) { return fallback; }
}
async function saveStore(key, value) {
  try { window.localStorage.setItem("repexp_" + key, JSON.stringify(value)); } catch (e) { console.error("storage set failed", key, e); }
}

function useTrips() {
  const [trips, setTrips] = useState(null);
  useEffect(() => { loadStore("trips", []).then(setTrips); }, []);
  const persist = useCallback((next) => { setTrips(next); saveStore("trips", next); }, []);
  return [trips, persist];
}
function useReceipts() {
  const [receipts, setReceipts] = useState(null);
  useEffect(() => { loadStore("receipts", []).then(setReceipts); }, []);
  const persist = useCallback((next) => { setReceipts(next); saveStore("receipts", next); }, []);
  return [receipts, persist];
}
function useSettings() {
  const [settings, setSettings] = useState(null);
  useEffect(() => { loadStore("settings", { designerName: "" }).then(setSettings); }, []);
  const persist = useCallback((next) => { setSettings(next); saveStore("settings", next); }, []);
  return [settings, persist];
}
function useTimeEntries() {
  const [entries, setEntries] = useState(null);
  useEffect(() => { loadStore("timeEntries", []).then(setEntries); }, []);
  const persist = useCallback((next) => { setEntries(next); saveStore("timeEntries", next); }, []);
  return [entries, persist];
}
function useViewMode() {
  const [override, setOverride] = useState(null);
  const [width, setWidth] = useState(typeof window !== "undefined" ? window.innerWidth : 900);
  useEffect(() => { loadStore("viewModeOverride", null).then(setOverride); }, []);
  useEffect(() => {
    function onResize() { setWidth(window.innerWidth); }
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);
  const auto = width >= 820 ? "review" : "field";
  const mode = override || auto;
  function setModeOverride(next) { setOverride(next); saveStore("viewModeOverride", next); }
  return [mode, override, setModeOverride];
}

function elapsedLabel(ms) {
  const totalMin = Math.floor(ms / 60000);
  const h = Math.floor(totalMin / 60);
  const m = totalMin % 60;
  return h > 0 ? `${h}h ${m}m` : `${m}m`;
}
function amountForTrip(receipt, tripId) {
  if (receipt.split && receipt.splits) {
    const s = receipt.splits.find((sp) => sp.tripId === tripId);
    return s ? Number(s.amount) || 0 : 0;
  }
  return receipt.tripId === tripId ? Number(receipt.amount) || 0 : 0;
}
function receiptTouchesTrip(receipt, tripId) {
  if (receipt.split && receipt.splits) return receipt.splits.some((sp) => sp.tripId === tripId);
  return receipt.tripId === tripId;
}

/* ---------- Shared bits ---------- */
function Badge({ children, tone = "muted" }) {
  const bg = tone === "accent" ? THEME.accentTint : THEME.surface;
  const fg = tone === "accent" ? THEME.accentStrong : THEME.ink2;
  return (
    <span style={{ background: bg, color: fg, fontSize: 10.5, fontWeight: 600, letterSpacing: "0.03em", textTransform: "uppercase", padding: "2px 7px", borderRadius: 999, whiteSpace: "nowrap", display: "inline-block" }}>
      {children}
    </span>
  );
}

function EmptyState({ icon, title, body, action }) {
  return (
    <div style={{ textAlign: "center", padding: "64px 16px" }}>
      <div style={{ color: THEME.ink3, marginBottom: 10, display: "flex", justifyContent: "center" }}>
        <Icon name={icon} size={30} strokeWidth={1.6} />
      </div>
      <p style={{ fontSize: 15, fontWeight: 500, color: THEME.ink, margin: "0 0 4px" }}>{title}</p>
      <p style={{ fontSize: 14, color: THEME.ink2, margin: "0 0 18px" }}>{body}</p>
      {action}
    </div>
  );
}

const inputStyle = { width: "100%", fontSize: 15, color: THEME.ink, border: "none", borderBottom: `1.5px solid ${THEME.lineStrong}`, background: "transparent", padding: "6px 2px 8px", boxSizing: "border-box", outline: "none", fontFamily: FONT };
const labelStyle = { fontSize: 13, color: THEME.ink2, display: "block", marginBottom: 5 };
const primaryBtn = { flex: 1, padding: "12px", borderRadius: 5, border: `1.5px solid ${THEME.accent}`, background: "transparent", color: THEME.accentStrong, fontWeight: 500, fontSize: 14, fontFamily: FONT, cursor: "pointer" };
const secondaryBtn = { flex: 1, padding: "12px", borderRadius: 5, border: `1px solid ${THEME.lineStrong}`, background: THEME.surface, color: THEME.ink, fontSize: 14, fontFamily: FONT, cursor: "pointer" };
const plainBtn = { padding: "10px 14px", borderRadius: 5, border: `1px solid ${THEME.line}`, background: THEME.surface, color: THEME.ink, fontSize: 14, fontFamily: FONT, cursor: "pointer" };

function RepPicker({ value, onChange, placeholder = "Search reps..." }) {
  const [query, setQuery] = useState(value || "");
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => setQuery(value || ""), [value]);
  useEffect(() => {
    function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);

  const parts = query.split(",");
  const lastFragment = parts[parts.length - 1].trim();
  const priorNames = parts.slice(0, -1).map((p) => p.trim()).filter(Boolean);
  const filtered = lastFragment
    ? REP_NAMES.filter((n) => n.toLowerCase().includes(lastFragment.toLowerCase()) && !priorNames.includes(n)).slice(0, 8)
    : REP_NAMES.filter((n) => !priorNames.includes(n)).slice(0, 8);

  function pick(n) {
    const next = priorNames.length ? `${priorNames.join(", ")}, ${n}` : n;
    setQuery(next);
    onChange(next);
    setOpen(true);
  }

  return (
    <div ref={ref} style={{ position: "relative" }}>
      <input type="text" value={query} placeholder={placeholder} onFocus={() => setOpen(true)}
        onChange={(e) => { setQuery(e.target.value); setOpen(true); onChange(e.target.value); }}
        style={inputStyle} />
      {open && filtered.length > 0 && (
        <div style={{ position: "absolute", top: "100%", left: 0, right: 0, background: THEME.surface2, border: `1px solid ${THEME.line}`, borderRadius: 5, zIndex: 20, maxHeight: 190, overflowY: "auto", boxShadow: "0 6px 16px rgba(40,20,10,0.12)", marginTop: 3 }}>
          {filtered.map((n) => (
            <div key={n} onClick={() => pick(n)} onMouseDown={(e) => e.preventDefault()}
              style={{ padding: "9px 12px", fontSize: 14, cursor: "pointer", borderBottom: `1px solid ${THEME.line}` }}>
              {n}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

/* ---------- Modals ---------- */
const overlayStyle = { position: "fixed", inset: 0, background: "rgba(35,26,18,0.4)", display: "flex", alignItems: "flex-end", justifyContent: "center", zIndex: 50 };
const dialogPanelStyle = { background: THEME.surface, borderRadius: 10, padding: "1.25rem", width: "100%", maxWidth: 380, border: `1px solid ${THEME.line}`, boxSizing: "border-box", margin: "auto" };
const sheetPanelStyle = { background: THEME.surface, borderRadius: "14px 14px 0 0", padding: "1.25rem", width: "100%", maxWidth: 420, maxHeight: "88vh", overflowY: "auto", border: `1px solid ${THEME.line}`, boxSizing: "border-box" };

function RepMultiSelect({ value, onChange, extraNames = [] }) {
  // value is a comma-separated string (kept for compatibility with the rest of the app,
  // which stores rep as free text on the trip). Internally this works as a set of chips.
  // extraNames lets the app surface reps that were manually added on other trips but
  // aren't in the base roster yet, so they don't get typed twice.
  const [query, setQuery] = useState("");
  const [open, setOpen] = useState(false);
  const ref = useRef(null);

  const selected = (value || "").split(",").map((s) => s.trim()).filter(Boolean);
  const allNames = [...new Set([...REP_NAMES, ...extraNames])];

  useEffect(() => {
    function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);

  const filtered = (query ? allNames.filter((n) => n.toLowerCase().includes(query.toLowerCase())) : allNames).slice(0, 40);
  const exactMatch = allNames.some((n) => n.toLowerCase() === query.trim().toLowerCase()) || selected.some((n) => n.toLowerCase() === query.trim().toLowerCase());

  function toggle(name) {
    const next = selected.includes(name) ? selected.filter((n) => n !== name) : [...selected, name];
    onChange(next.join(", "));
  }
  function remove(name) {
    onChange(selected.filter((n) => n !== name).join(", "));
  }

  return (
    <div ref={ref} style={{ position: "relative" }}>
      {selected.length > 0 && (
        <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 8 }}>
          {selected.map((n) => (
            <span key={n} style={{ display: "flex", alignItems: "center", gap: 5, background: THEME.accentTint, color: THEME.accentStrong, fontSize: 12.5, fontWeight: 500, padding: "4px 6px 4px 10px", borderRadius: 999 }}>
              {n}
              <span onClick={() => remove(n)} style={{ cursor: "pointer", display: "flex", padding: 2 }}><Icon name="x" size={11} strokeWidth={2.2} /></span>
            </span>
          ))}
        </div>
      )}
      <input
        type="text"
        value={query}
        placeholder="Search reps to add..."
        onFocus={() => setOpen(true)}
        onChange={(e) => { setQuery(e.target.value); setOpen(true); }}
        style={inputStyle}
      />
      {open && (
        <div style={{ position: "absolute", top: "100%", left: 0, right: 0, background: THEME.surface2, border: `1px solid ${THEME.line}`, borderRadius: 5, zIndex: 20, maxHeight: 240, overflowY: "auto", boxShadow: "0 6px 16px rgba(40,20,10,0.12)", marginTop: 3 }}>
          {filtered.length === 0 && !query.trim() && (
            <div style={{ padding: "12px", fontSize: 13, color: THEME.ink3 }}>No matches</div>
          )}
          {filtered.map((n) => {
            const checked = selected.includes(n);
            return (
              <label key={n} onMouseDown={(e) => e.preventDefault()} onClick={() => toggle(n)}
                style={{ display: "flex", alignItems: "center", gap: 10, padding: "9px 12px", fontSize: 14, cursor: "pointer", borderBottom: `1px solid ${THEME.line}`, background: checked ? THEME.accentTint : "transparent" }}>
                <input type="checkbox" checked={checked} readOnly style={{ pointerEvents: "none" }} />
                <span style={{ color: checked ? THEME.accentStrong : THEME.ink }}>{n}</span>
              </label>
            );
          })}
          {query.trim() && !exactMatch && (
            <div onMouseDown={(e) => e.preventDefault()} onClick={() => { toggle(query.trim()); setQuery(""); }}
              style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 12px", fontSize: 14, cursor: "pointer", color: THEME.accentStrong }}>
              <Icon name="plus" size={14} strokeWidth={2.1} />
              Add "{query.trim()}" as a new rep
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function NewTripModal({ onClose, onCreate, extraNames }) {
  const [name, setName] = useState("");
  const [designer, setDesigner] = useState("");
  const [rep, setRep] = useState("");
  const [error, setError] = useState("");

  function submit() {
    if (!name.trim()) { setError("Enter a trip name."); return; }
    onCreate({ id: uid(), name: name.trim(), designer: designer.trim(), rep: rep.trim(), createdAt: Date.now() });
  }

  return (
    <div style={{ ...overlayStyle, alignItems: "center" }} onMouseDown={onClose}>
      <div style={dialogPanelStyle} onMouseDown={(e) => e.stopPropagation()}>
        <h3 style={{ margin: "0 0 18px", fontSize: 17, fontWeight: 600, color: THEME.ink }}>New trip</h3>
        <div style={{ marginBottom: 14 }}>
          <label style={labelStyle}>Trip name</label>
          <input type="text" placeholder="e.g. Auburn workshop" value={name} onChange={(e) => { setName(e.target.value); setError(""); }} style={inputStyle} />
          {error && <p style={{ fontSize: 12, color: THEME.accentStrong, margin: "6px 0 0" }}>{error}</p>}
        </div>
        <div style={{ marginBottom: 14 }}>
          <label style={labelStyle}>Designer name</label>
          <input type="text" placeholder="Your name" value={designer} onChange={(e) => setDesigner(e.target.value)} style={inputStyle} />
        </div>
        <div style={{ marginBottom: 4, position: "relative" }}>
          <label style={labelStyle}>Rep(s)</label>
          <RepMultiSelect value={rep} onChange={setRep} extraNames={extraNames} />
        </div>
        <div style={{ display: "flex", gap: 10, marginTop: 24 }}>
          <button onClick={onClose} style={secondaryBtn}>Cancel</button>
          <button onClick={submit} style={primaryBtn}>Create trip</button>
        </div>
      </div>
    </div>
  );
}

function CaptureChoiceModal({ onTakePhoto, onUpload, onClose }) {
  return (
    <div style={overlayStyle} onMouseDown={onClose}>
      <div style={{ background: THEME.surface, borderRadius: "14px 14px 0 0", padding: 10, width: "100%", maxWidth: 420, border: `1px solid ${THEME.line}`, boxSizing: "border-box" }} onMouseDown={(e) => e.stopPropagation()}>
        <button onClick={onTakePhoto} style={{ width: "100%", display: "flex", alignItems: "center", gap: 12, padding: "14px 10px", background: "transparent", border: "none", borderBottom: `1px solid ${THEME.line}`, fontSize: 15, color: THEME.ink, cursor: "pointer", fontFamily: FONT, textAlign: "left" }}>
          <Icon name="camera" size={20} strokeWidth={1.7} /> Take photo
        </button>
        <button onClick={onUpload} style={{ width: "100%", display: "flex", alignItems: "center", gap: 12, padding: "14px 10px", background: "transparent", border: "none", fontSize: 15, color: THEME.ink, cursor: "pointer", fontFamily: FONT, textAlign: "left" }}>
          <Icon name="upload" size={20} strokeWidth={1.8} /> Upload photo
        </button>
        <button onClick={onClose} style={{ width: "100%", marginTop: 8, padding: 13, borderRadius: 8, border: `1px solid ${THEME.line}`, background: "transparent", color: THEME.ink2, fontSize: 14, fontFamily: FONT, cursor: "pointer" }}>Cancel</button>
      </div>
    </div>
  );
}

function CameraCapture({ onCapture, onClose }) {
  const videoRef = useRef(null);
  const canvasRef = useRef(null);
  const streamRef = useRef(null);
  const [error, setError] = useState("");
  const [ready, setReady] = useState(false);

  useEffect(() => {
    let cancelled = false;
    navigator.mediaDevices?.getUserMedia({ video: { facingMode: "environment" } })
      .then((stream) => {
        if (cancelled) { stream.getTracks().forEach((t) => t.stop()); return; }
        streamRef.current = stream;
        if (videoRef.current) { videoRef.current.srcObject = stream; videoRef.current.play(); setReady(true); }
      })
      .catch(() => setError("Camera unavailable. Check browser permissions, or upload a photo instead."));
    return () => { cancelled = true; streamRef.current?.getTracks().forEach((t) => t.stop()); };
  }, []);

  function snap() {
    const video = videoRef.current, canvas = canvasRef.current;
    if (!video || !canvas) return;
    canvas.width = video.videoWidth; canvas.height = video.videoHeight;
    canvas.getContext("2d").drawImage(video, 0, 0);
    const dataUrl = canvas.toDataURL("image/jpeg", 0.85);
    streamRef.current?.getTracks().forEach((t) => t.stop());
    onCapture(dataUrl);
  }

  return (
    <div style={{ position: "fixed", inset: 0, background: "rgba(20,16,12,0.95)", zIndex: 60, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: 20, boxSizing: "border-box" }}>
      {error ? (
        <div style={{ background: THEME.surface, borderRadius: 8, padding: 22, maxWidth: 320, textAlign: "center" }}>
          <p style={{ fontSize: 14, color: THEME.ink, margin: "0 0 16px" }}>{error}</p>
          <button onClick={onClose} style={{ width: "100%", padding: 10, borderRadius: 5, border: `1px solid ${THEME.lineStrong}`, background: "transparent", fontFamily: FONT, cursor: "pointer" }}>Close</button>
        </div>
      ) : (
        <>
          <video ref={videoRef} playsInline muted style={{ width: "100%", maxWidth: 420, borderRadius: 8, background: "#000" }} />
          <canvas ref={canvasRef} style={{ display: "none" }} />
          <div style={{ display: "flex", gap: 14, marginTop: 20 }}>
            <button onClick={onClose} style={{ padding: "13px 20px", borderRadius: 5, border: "1px solid rgba(255,255,255,0.3)", background: "transparent", color: "#fff", fontFamily: FONT, cursor: "pointer", fontSize: 14 }}>Cancel</button>
            <button onClick={snap} disabled={!ready} style={{ padding: "13px 24px", borderRadius: 5, border: "none", background: THEME.accent, color: THEME.onAccent, fontWeight: 500, fontFamily: FONT, cursor: "pointer", fontSize: 14, display: "flex", alignItems: "center", gap: 8 }}>
              <Icon name="camera" size={16} strokeWidth={1.8} color={THEME.onAccent} /> Capture
            </button>
          </div>
        </>
      )}
    </div>
  );
}

function SplitEditor({ splits, totalAmount, trips, onChange }) {
  const amt = Number(totalAmount) || 0;

  function addSplit() {
    const remaining = amt - splits.reduce((s, sp) => s + (Number(sp.amount) || 0), 0);
    onChange([...splits, { id: uid(), tripId: "", amount: remaining > 0 ? remaining.toFixed(2) : "" }]);
  }
  function updateSplit(id, field, val) { onChange(splits.map((sp) => (sp.id === id ? { ...sp, [field]: val } : sp))); }
  function removeSplit(id) { onChange(splits.filter((sp) => sp.id !== id)); }
  function splitEvenly() {
    if (splits.length === 0) return;
    const each = amt / splits.length;
    onChange(splits.map((sp, i) => ({ ...sp, amount: i === splits.length - 1 ? (amt - each * (splits.length - 1)).toFixed(2) : each.toFixed(2) })));
  }

  const allocated = splits.reduce((s, sp) => s + (Number(sp.amount) || 0), 0);
  const diff = Math.round((amt - allocated) * 100) / 100;
  const isTwoWay = splits.length === 2;
  const firstPct = isTwoWay && amt > 0 ? Math.round(((Number(splits[0].amount) || 0) / amt) * 100) : 50;

  function setPct(pct) {
    if (!isTwoWay) return;
    const clamped = Math.max(0, Math.min(100, pct));
    const firstAmt = Math.round(amt * clamped) / 100;
    const secondAmt = Math.round((amt - firstAmt) * 100) / 100;
    onChange([{ ...splits[0], amount: firstAmt.toFixed(2) }, { ...splits[1], amount: secondAmt.toFixed(2) }]);
  }

  const smallSelect = { flex: 1, fontSize: 13, padding: "8px 6px", borderRadius: 5, border: `1px solid ${THEME.line}`, background: THEME.surface, fontFamily: FONT };
  const smallInput = { width: 76, fontFamily: MONO, fontSize: 13, padding: "8px 6px", borderRadius: 5, border: `1px solid ${THEME.line}`, background: THEME.surface, boxSizing: "border-box" };

  return (
    <div style={{ border: `1px solid ${THEME.line}`, borderRadius: 5, padding: 10 }}>
      {splits.map((sp) => (
        <div key={sp.id} style={{ display: "flex", gap: 6, marginBottom: 8, alignItems: "center" }}>
          <select value={sp.tripId} onChange={(e) => updateSplit(sp.id, "tripId", e.target.value)} style={smallSelect}>
            <option value="">Select trip</option>
            {trips.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}
          </select>
          <input type="number" step="0.01" value={sp.amount} onChange={(e) => updateSplit(sp.id, "amount", e.target.value)} style={smallInput} placeholder="$" />
          <button onClick={() => removeSplit(sp.id)} aria-label="Remove split" style={{ width: 32, height: 32, flexShrink: 0, borderRadius: 5, border: `1px solid ${THEME.line}`, background: "transparent", color: THEME.ink2, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", padding: 0 }}>
            <Icon name="x" size={13} strokeWidth={2} />
          </button>
        </div>
      ))}

      {isTwoWay && amt > 0 && (
        <div style={{ margin: "4px 0 12px" }}>
          <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12, color: THEME.ink2, marginBottom: 4 }}>
            <span>{splits[0].tripId ? trips.find((t) => t.id === splits[0].tripId)?.name : "Trip A"}: {firstPct}%</span>
            <span>{splits[1].tripId ? trips.find((t) => t.id === splits[1].tripId)?.name : "Trip B"}: {100 - firstPct}%</span>
          </div>
          <input type="range" min={0} max={100} step={5} value={firstPct} onChange={(e) => setPct(Number(e.target.value))} style={{ width: "100%", accentColor: THEME.accent }} />
          <div style={{ display: "flex", justifyContent: "space-between", gap: 6, marginTop: 4 }}>
            {[25, 50, 75].map((p) => (
              <button key={p} onClick={() => setPct(p)} style={{ flex: 1, fontSize: 12, padding: "6px 0", borderRadius: 5, border: `1px solid ${THEME.line}`, background: "transparent", color: THEME.ink, cursor: "pointer", fontFamily: FONT }}>{p}/{100 - p}</button>
            ))}
          </div>
        </div>
      )}

      <div style={{ display: "flex", gap: 8 }}>
        <button onClick={addSplit} style={{ flex: 1, fontSize: 12.5, padding: 8, borderRadius: 5, border: `1px solid ${THEME.lineStrong}`, background: "transparent", color: THEME.ink, cursor: "pointer", fontFamily: FONT }}>+ Add trip</button>
        {splits.length > 1 && <button onClick={splitEvenly} style={{ flex: 1, fontSize: 12.5, padding: 8, borderRadius: 5, border: `1px solid ${THEME.lineStrong}`, background: "transparent", color: THEME.ink, cursor: "pointer", fontFamily: FONT }}>Split evenly</button>}
      </div>
      {Math.abs(diff) > 0.01 && (
        <p style={{ fontSize: 12, color: THEME.accentStrong, margin: "8px 0 0" }}>{diff > 0 ? `$${fmt(diff)} unallocated` : `$${fmt(-diff)} over the total`}</p>
      )}
    </div>
  );
}

function ReceiptEditor({ receipt, trips, onSave, onDelete, onClose }) {
  const [r, setR] = useState({ ...receipt });
  const [ocrBusy, setOcrBusy] = useState(false);
  const [ocrError, setOcrError] = useState("");

  function update(field, val) { setR((prev) => ({ ...prev, [field]: val })); }

  async function runOcr() {
    if (!r.image) return;
    setOcrBusy(true); setOcrError("");
    try {
      const base64 = r.image.split(",")[1];
      const resp = await fetch("/api/ocr-receipt", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ imageBase64: base64 }),
      });
      if (!resp.ok) throw new Error("OCR request failed");
      const parsed = await resp.json();
      setR((prev) => ({
        ...prev,
        vendor: parsed.vendor || prev.vendor,
        date: parsed.date || prev.date,
        amount: parsed.amount != null ? String(parsed.amount) : prev.amount,
        category: CATEGORIES.some((c) => c.key === parsed.category) ? parsed.category : prev.category,
        notes: prev.notes,
      }));
    } catch (e) {
      setOcrError("Couldn't read the receipt automatically. Fill in the details manually.");
    } finally { setOcrBusy(false); }
  }

  return (
    <div style={overlayStyle} onMouseDown={onClose}>
      <div style={sheetPanelStyle} onMouseDown={(e) => e.stopPropagation()}>
        {r.image && <img src={r.image} alt="Receipt" style={{ width: "100%", maxHeight: 220, objectFit: "cover", borderRadius: 6, marginBottom: 14 }} />}

        {r.image && (
          <button onClick={runOcr} disabled={ocrBusy} style={{ width: "100%", display: "flex", alignItems: "center", justifyContent: "center", gap: 8, padding: 11, marginBottom: 14, borderRadius: 5, border: `1px dashed ${THEME.lineStrong}`, background: "transparent", color: THEME.ink2, fontSize: 14, fontFamily: FONT, cursor: "pointer" }}>
            <Icon name="scan" size={15} strokeWidth={1.8} />
            {ocrBusy ? "Reading receipt…" : "Auto-fill from photo"}
          </button>
        )}
        {ocrError && <p style={{ fontSize: 13, color: THEME.accentStrong, margin: "0 0 12px" }}>{ocrError}</p>}

        <div style={{ marginBottom: 12 }}>
          <label style={labelStyle}>Vendor</label>
          <input type="text" value={r.vendor || ""} onChange={(e) => update("vendor", e.target.value)} placeholder="e.g. Enterprise" style={inputStyle} />
        </div>
        <div style={{ display: "flex", gap: 12, marginBottom: 12 }}>
          <div style={{ flex: 1 }}>
            <label style={labelStyle}>Date</label>
            <input type="date" value={r.date || ""} onChange={(e) => update("date", e.target.value)} style={inputStyle} />
          </div>
          <div style={{ flex: 1 }}>
            <label style={labelStyle}>Amount ($)</label>
            <input type="number" step="0.01" value={r.amount || ""} onChange={(e) => update("amount", e.target.value)} placeholder="0.00" style={{ ...inputStyle, fontFamily: MONO }} />
          </div>
        </div>
        <div style={{ marginBottom: 12 }}>
          <label style={labelStyle}>Category</label>
          <select value={r.category || ""} onChange={(e) => update("category", e.target.value)} style={{ ...inputStyle, padding: "7px 2px" }}>
            <option value="">Select category</option>
            {CATEGORIES.filter((c) => c.key !== "miles").map((c) => <option key={c.key} value={c.key}>{c.label}</option>)}
            <option value="other">Other / uncategorized</option>
          </select>
        </div>

        <div style={{ marginBottom: 12 }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 6 }}>
            <label style={{ fontSize: 13, color: THEME.ink2 }}>Assign to trip</label>
            <label style={{ fontSize: 12, color: THEME.ink2, display: "flex", alignItems: "center", gap: 5, cursor: "pointer" }}>
              <input type="checkbox" checked={!!r.split} onChange={(e) => {
                if (e.target.checked) { update("split", true); update("splits", r.tripId ? [{ id: uid(), tripId: r.tripId, amount: r.amount || "" }] : []); }
                else { update("split", false); update("tripId", (r.splits && r.splits[0]?.tripId) || ""); }
              }} />
              Split across trips
            </label>
          </div>
          {r.split ? (
            <SplitEditor splits={r.splits || []} totalAmount={r.amount} trips={trips} onChange={(s) => update("splits", s)} />
          ) : (
            <select value={r.tripId || ""} onChange={(e) => update("tripId", e.target.value)} style={{ ...inputStyle, padding: "7px 2px" }}>
              <option value="">Unassigned (save for later)</option>
              {trips.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}
            </select>
          )}
        </div>

        <div style={{ marginBottom: 6 }}>
          <label style={labelStyle}>Notes</label>
          <input type="text" value={r.notes || ""} onChange={(e) => update("notes", e.target.value)} placeholder="e.g. dined with Kelly" style={inputStyle} />
        </div>

        <div style={{ display: "flex", gap: 8, marginTop: 18 }}>
          <button onClick={() => onDelete(r.id)} aria-label="Delete receipt" style={{ width: 44, flexShrink: 0, borderRadius: 5, border: `1px solid ${THEME.lineStrong}`, background: "transparent", color: THEME.ink2, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}>
            <Icon name="trash" size={16} strokeWidth={1.6} />
          </button>
          <button onClick={onClose} style={secondaryBtn}>Close</button>
          <button onClick={() => onSave(r)} style={primaryBtn}>Save</button>
        </div>
      </div>
    </div>
  );
}

function ReceiptRow({ receipt, onClick }) {
  const cat = catByKey(receipt.category);
  return (
    <div onClick={onClick} style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 0", borderBottom: `1px solid ${THEME.line}`, cursor: "pointer" }}>
      {receipt.image ? (
        <img src={receipt.image} alt="" style={{ width: 46, height: 46, borderRadius: 5, objectFit: "cover", flexShrink: 0, border: `1px solid ${THEME.line}` }} />
      ) : (
        <div style={{ width: 46, height: 46, borderRadius: 5, flexShrink: 0, background: THEME.surface, border: `1px solid ${THEME.line}`, display: "flex", alignItems: "center", justifyContent: "center", color: THEME.ink3 }}>
          <Icon name="receipt" size={18} strokeWidth={1.6} />
        </div>
      )}
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 14, fontWeight: 500, color: THEME.ink, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{receipt.vendor || "Untitled receipt"}</div>
        <div style={{ fontSize: 12, color: THEME.ink2, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
          {receipt.date || "No date"}{cat ? ` · ${cat.label}` : ""}
        </div>
      </div>
      <div style={{ textAlign: "right", flexShrink: 0 }}>
        <div style={{ fontFamily: MONO, fontSize: 14, color: THEME.ink }}>{receipt.amount ? `$${fmt(Number(receipt.amount))}` : "—"}</div>
        {receipt.split && <span style={{ display: "inline-block", marginTop: 2 }}><Badge tone="accent">Split</Badge></span>}
      </div>
    </div>
  );
}

function ReconcileModal({ trip, tripReceipts, onClose, onApply }) {
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");
  const [result, setResult] = useState(null);
  const fileRef = useRef(null);

  async function handleFile(e) {
    const file = e.target.files?.[0];
    if (!file) return;
    setBusy(true); setError("");
    try {
      const buf = await file.arrayBuffer();
      const base64 = btoa(new Uint8Array(buf).reduce((s, b) => s + String.fromCharCode(b), ""));
      const resp = await fetch("/api/reconcile-report", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ pdfBase64: base64 }),
      });
      if (!resp.ok) throw new Error("Reconcile request failed");
      const { items } = await resp.json();

      const tripDates = tripReceipts.map((r) => r.date).filter(Boolean).sort();
      const relevant = items.filter((it) => {
        if (!it.date) return false;
        if (tripDates.length === 0) return true;
        return it.date >= tripDates[0] && it.date <= tripDates[tripDates.length - 1];
      });

      const mileageItems = relevant.filter((it) => concurTypeToCategory(it.expenseType) === "miles");
      const mileageReimbursement = mileageItems.length ? mileageItems.reduce((s, it) => s + (Number(it.amount) || 0), 0) : null;

      const diningNotes = relevant
        .filter((it) => /multiple attendees/i.test(it.expenseType) && it.attendees && it.attendees.length > 1)
        .map((it) => ({ date: it.date, amount: it.amount, attendees: it.attendees.filter((a) => !/NEIL BRUBAKER/i.test(a)) }));

      const matched = [];
      const unmatchedReceipts = [...tripReceipts];
      const unmatchedItems = [];
      relevant.forEach((it) => {
        if (concurTypeToCategory(it.expenseType) === "miles") return;
        const idx = unmatchedReceipts.findIndex((r) => r.date === it.date && Math.abs((Number(r.amount) || 0) - (Number(it.amount) || 0)) < 0.01);
        if (idx >= 0) { matched.push({ receipt: unmatchedReceipts[idx], item: it }); unmatchedReceipts.splice(idx, 1); }
        else unmatchedItems.push(it);
      });

      setResult({ mileageReimbursement, diningNotes, matched, unmatchedReceipts, unmatchedItems, allItems: relevant });
    } catch (e) {
      setError("Couldn't read that report. Make sure it's the PDF export from Concur.");
    } finally { setBusy(false); if (fileRef.current) fileRef.current.value = ""; }
  }

  return (
    <div style={overlayStyle} onMouseDown={onClose}>
      <div style={sheetPanelStyle} onMouseDown={(e) => e.stopPropagation()}>
        <h3 style={{ margin: "0 0 8px", fontSize: 17, fontWeight: 600, color: THEME.ink }}>Reconcile against report</h3>
        <p style={{ fontSize: 13, color: THEME.ink2, margin: "0 0 18px", lineHeight: 1.5 }}>
          Upload the Concur PDF for this trip's dates. This fills in the real mileage reimbursement and checks captured receipts against what actually got filed. Nothing here touches Concur — it only reads the file.
        </p>

        {!result && (
          <>
            <input ref={fileRef} type="file" accept="application/pdf" onChange={handleFile} disabled={busy} style={{ width: "100%", fontSize: 14, fontFamily: FONT }} />
            {busy && <p style={{ fontSize: 13, color: THEME.ink2, marginTop: 8 }}>Reading report...</p>}
            {error && <p style={{ fontSize: 13, color: THEME.accentStrong, marginTop: 8 }}>{error}</p>}
          </>
        )}

        {result && (
          <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
            {result.mileageReimbursement != null && (
              <div style={{ background: THEME.accentTint, borderRadius: 5, padding: "0.75rem" }}>
                <p style={{ fontSize: 13, color: THEME.accentStrong, margin: 0, fontWeight: 500 }}>Mileage reimbursement found: ${fmt(result.mileageReimbursement)}</p>
              </div>
            )}
            {result.diningNotes.length > 0 && (
              <div>
                <p style={{ fontSize: 13, fontWeight: 500, margin: "0 0 6px", color: THEME.ink }}>Attendee notes found</p>
                {result.diningNotes.map((d, i) => (
                  <p key={i} style={{ fontSize: 13, color: THEME.ink2, margin: "0 0 4px" }}>{d.date} · ${fmt(d.amount)} · Dined with {d.attendees.join(", ")}</p>
                ))}
              </div>
            )}
            <div>
              <p style={{ fontSize: 13, fontWeight: 500, margin: "0 0 6px", color: THEME.ink }}>Matched receipts ({result.matched.length})</p>
              {result.matched.length === 0 && <p style={{ fontSize: 13, color: THEME.ink3, margin: 0 }}>None matched exactly by date and amount.</p>}
            </div>
            {result.unmatchedReceipts.length > 0 && (
              <div>
                <p style={{ fontSize: 13, fontWeight: 500, margin: "0 0 6px", color: THEME.accentStrong }}>Captured but not found on the report ({result.unmatchedReceipts.length})</p>
                {result.unmatchedReceipts.map((r) => (
                  <p key={r.id} style={{ fontSize: 13, color: THEME.ink2, margin: "0 0 4px" }}>{r.date} · {r.vendor || "Untitled"} · ${r.amount ? fmt(Number(r.amount)) : "—"}</p>
                ))}
              </div>
            )}
            {result.unmatchedItems.length > 0 && (
              <div>
                <p style={{ fontSize: 13, fontWeight: 500, margin: "0 0 6px", color: THEME.accentStrong }}>On the report but never captured ({result.unmatchedItems.length})</p>
                {result.unmatchedItems.map((it, i) => (
                  <p key={i} style={{ fontSize: 13, color: THEME.ink2, margin: "0 0 4px" }}>{it.date} · {it.expenseType} · {it.vendor || ""} · ${fmt(Number(it.amount) || 0)}</p>
                ))}
              </div>
            )}
            <button onClick={() => onApply(result)} style={primaryBtn}>Apply mileage total</button>
          </div>
        )}

        <button onClick={onClose} style={{ ...secondaryBtn, width: "100%", marginTop: 16 }}>Close</button>
      </div>
    </div>
  );
}

/* ---------- Time punch ---------- */
function TimePunch({ trip, timeEntries, onAdd, onDelete }) {
  const [running, setRunning] = useState(trip.activePunch || null);
  const [now, setNow] = useState(Date.now());
  const [manualDate, setManualDate] = useState(todayStr());
  const [manualHours, setManualHours] = useState("");

  useEffect(() => {
    if (!running) return;
    const id = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(id);
  }, [running]);

  const tripEntries = timeEntries.filter((e) => e.tripId === trip.id).sort((a, b) => a.date.localeCompare(b.date));

  function isWeekend(dateStr) {
    const d = new Date(dateStr + "T12:00:00");
    const day = d.getDay();
    return day === 0 || day === 6;
  }
  function start() {
    const startedAt = Date.now();
    setRunning({ startedAt });
    onAdd(null, { activePunch: { startedAt } });
  }
  function stop() {
    if (!running) return;
    const ms = Date.now() - running.startedAt;
    const hours = Math.round((ms / 3600000) * 100) / 100;
    const date = new Date(running.startedAt).toISOString().slice(0, 10);
    onAdd({ id: uid(), tripId: trip.id, date, hours, weekend: isWeekend(date), source: "punch" }, { activePunch: null });
    setRunning(null);
  }
  function addManual() {
    const hours = Number(manualHours);
    if (!hours || !manualDate) return;
    onAdd({ id: uid(), tripId: trip.id, date: manualDate, hours, weekend: isWeekend(manualDate), source: "manual" }, {});
    setManualHours("");
  }

  const totalHours = tripEntries.reduce((s, e) => s + e.hours, 0);
  const over8Days = [...new Set(tripEntries.filter((e) => e.hours > 8).map((e) => e.date))];
  const weekendHours = tripEntries.filter((e) => e.weekend).reduce((s, e) => s + e.hours, 0);

  return (
    <div style={{ background: THEME.surface, borderRadius: 8, padding: "1rem", marginBottom: 20, border: `1px solid ${THEME.line}` }}>
      <p style={{ fontSize: 13, fontWeight: 600, margin: "0 0 10px", color: THEME.ink }}>Time with rep</p>

      {running ? (
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }}>
          <div style={{ width: 9, height: 9, borderRadius: "50%", background: THEME.accent, flexShrink: 0 }} />
          <span style={{ fontFamily: MONO, fontSize: 20, fontWeight: 500, flex: 1, color: THEME.ink }}>{elapsedLabel(now - running.startedAt)}</span>
          <button onClick={stop} style={{ padding: "8px 14px", borderRadius: 5, border: `1.5px solid ${THEME.accent}`, background: "transparent", color: THEME.accentStrong, fontFamily: FONT, cursor: "pointer", fontSize: 13, display: "flex", alignItems: "center", gap: 6 }}>
            <Icon name="stop" size={12} strokeWidth={1.8} /> Stop
          </button>
        </div>
      ) : (
        <button onClick={start} style={{ width: "100%", marginBottom: 12, display: "flex", alignItems: "center", justifyContent: "center", gap: 7, padding: 11, borderRadius: 5, border: `1.5px solid ${THEME.accent}`, background: "transparent", color: THEME.accentStrong, fontFamily: FONT, cursor: "pointer", fontSize: 14 }}>
          <Icon name="play" size={14} strokeWidth={1.8} /> Start punch
        </button>
      )}

      <div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
        <input type="date" value={manualDate} onChange={(e) => setManualDate(e.target.value)} style={{ flex: 1, fontSize: 13, padding: "8px 6px", borderRadius: 5, border: `1px solid ${THEME.line}`, background: THEME.surface2, fontFamily: FONT }} />
        <input type="number" step="0.25" placeholder="hrs" value={manualHours} onChange={(e) => setManualHours(e.target.value)} style={{ width: 64, fontSize: 13, padding: "8px 6px", borderRadius: 5, border: `1px solid ${THEME.line}`, background: THEME.surface2, fontFamily: MONO, boxSizing: "border-box" }} />
        <button onClick={addManual} style={{ padding: "6px 12px", borderRadius: 5, border: `1px solid ${THEME.lineStrong}`, background: "transparent", cursor: "pointer", color: THEME.ink }}>
          <Icon name="plus" size={14} strokeWidth={2} />
        </button>
      </div>

      {tripEntries.length > 0 && (
        <div style={{ marginBottom: 10 }}>
          {tripEntries.map((e) => (
            <div key={e.id} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: 13, padding: "4px 0", borderBottom: `1px solid ${THEME.line}` }}>
              <span style={{ color: THEME.ink2 }}>{e.date}{e.weekend ? " · weekend" : ""}{e.source === "punch" ? " · punched" : ""}</span>
              <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
                <span style={{ fontFamily: MONO, color: THEME.ink }}>{e.hours} hrs</span>
                <span onClick={() => onDelete(e.id)} style={{ cursor: "pointer", color: THEME.ink3, display: "flex" }}><Icon name="x" size={13} strokeWidth={2} /></span>
              </span>
            </div>
          ))}
        </div>
      )}

      <div style={{ display: "flex", gap: 16, fontSize: 12, color: THEME.ink3 }}>
        <span>Total: {totalHours} hrs</span>
        {over8Days.length > 0 && <span>Over 8hrs: {over8Days.length} day{over8Days.length === 1 ? "" : "s"}</span>}
        {weekendHours > 0 && <span>Weekend: {weekendHours} hrs</span>}
      </div>
    </div>
  );
}

/* ---------- Trip detail (field mode) ---------- */
function TripDetail({ trip, receipts, timeEntries, onAddTimeEntry, onDeleteTimeEntry, onBack, onUpdateTrip, onEditReceipt, onExport, onBulkUpdateReceipts, onShowReconcile }) {
  const tripReceipts = receipts.filter((r) => receiptTouchesTrip(r, trip.id));
  const totals = {};
  CATEGORIES.forEach((c) => { totals[c.key] = 0; });
  tripReceipts.forEach((r) => { if (r.category && totals[r.category] != null) totals[r.category] += amountForTrip(r, trip.id); });

  const [miles, setMiles] = useState(trip.miles || "");
  useEffect(() => setMiles(trip.miles || ""), [trip.id]);
  function saveMiles(val) { setMiles(val); onUpdateTrip({ ...trip, miles: val }); }

  const mileageTotal = trip.mileageReimbursement != null ? trip.mileageReimbursement : 0;
  const grandTotal = mileageTotal + Object.values(totals).reduce((a, b) => a + b, 0);

  return (
    <div>
      <div style={{ marginBottom: 20 }}>
        <div style={{ fontSize: 11.5, color: THEME.ink3, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 3 }}>Rep</div>
        <div style={{ fontSize: 14, color: THEME.ink, marginBottom: 12 }}>{trip.rep || "—"}</div>
        <div style={{ fontSize: 11.5, color: THEME.ink3, textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 3 }}>Designer</div>
        <div style={{ fontSize: 14, color: THEME.ink }}>{trip.designer || "—"}</div>
      </div>

      <TimePunch trip={trip} timeEntries={timeEntries}
        onAdd={(entry, tripPatch) => { if (entry) onAddTimeEntry(entry); if (tripPatch && Object.keys(tripPatch).length) onUpdateTrip({ ...trip, ...tripPatch }); }}
        onDelete={onDeleteTimeEntry} />

      <div style={{ marginBottom: 24 }}>
        <label style={labelStyle}>Personal car miles</label>
        <input type="number" step="0.1" value={miles} onChange={(e) => saveMiles(e.target.value)} placeholder="0"
          style={{ width: "100%", fontFamily: MONO, fontSize: 20, fontWeight: 500, color: THEME.ink, border: "none", borderBottom: `1.5px solid ${THEME.lineStrong}`, background: "transparent", padding: "4px 2px", boxSizing: "border-box", outline: "none" }} />
        <p style={{ fontSize: 12, color: THEME.ink3, margin: "8px 0 0" }}>
          {trip.mileageReimbursement != null ? `Reconciled: $${fmt(trip.mileageReimbursement)} from your expense report` : "$ amount pending — reconcile against your expense report once it's filed"}
        </p>
      </div>

      <div>
        {CATEGORIES.filter((c) => c.key !== "miles").map((c) => (
          <div key={c.key} style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", padding: "9px 0", borderBottom: `1px solid ${THEME.line}`, gap: 14 }}>
            <span style={{ fontSize: 14, color: THEME.ink2 }}>{c.label}</span>
            <span style={{ fontFamily: MONO, fontSize: 15, color: THEME.ink, whiteSpace: "nowrap" }}>${fmt(totals[c.key])}</span>
          </div>
        ))}
      </div>

      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", padding: "16px 0 22px", borderTop: `2px solid ${THEME.ink}`, marginTop: 2 }}>
        <span style={{ fontSize: 15, color: THEME.ink, fontWeight: 500 }}>Total</span>
        <span style={{ fontFamily: MONO, fontSize: 27, fontWeight: 600, color: THEME.ink }}>${fmt(grandTotal)}</span>
      </div>
      {trip.mileageReimbursement == null && Number(miles) > 0 && (
        <p style={{ fontSize: 12, color: THEME.ink3, margin: "-14px 0 20px" }}>Plus {fmtMi(Number(miles))} mi, pending reconciliation</p>
      )}

      <div style={{ display: "flex", gap: 10, marginBottom: 24 }}>
        <button onClick={onShowReconcile} style={{ ...secondaryBtn, display: "flex", alignItems: "center", justifyContent: "center", gap: 7 }}>
          <Icon name="filecheck" size={15} strokeWidth={1.7} /> Reconcile
        </button>
        <button onClick={() => onExport(trip, tripReceipts, totals, miles, mileageTotal)} style={{ ...primaryBtn, display: "flex", alignItems: "center", justifyContent: "center", gap: 7 }}>
          <Icon name="download" size={15} strokeWidth={1.9} /> Export
        </button>
      </div>

      <h3 style={{ fontSize: 15, fontWeight: 600, margin: "4px 0 12px", color: THEME.ink }}>Receipts ({tripReceipts.length})</h3>
      {tripReceipts.length === 0 ? (
        <div style={{ textAlign: "center", padding: "40px 16px", color: THEME.ink2 }}>
          <p style={{ fontSize: 14, margin: 0 }}>Receipts tagged to this trip will show up here.</p>
        </div>
      ) : (
        <div>{tripReceipts.map((r) => <ReceiptRow key={r.id} receipt={r} onClick={() => onEditReceipt(r)} />)}</div>
      )}
    </div>
  );
}

function TripsList({ trips, receipts, onOpen }) {
  if (trips.length === 0) {
    return <EmptyState icon="mappin" title="No trips yet" body="Create a trip to start tagging receipts and building a worksheet." />;
  }
  return (
    <div>
      {trips.map((t) => {
        const tReceipts = receipts.filter((r) => receiptTouchesTrip(r, t.id));
        const total = tReceipts.reduce((s, r) => s + amountForTrip(r, t.id), 0);
        return (
          <div key={t.id} onClick={() => onOpen(t)} style={{ display: "flex", alignItems: "baseline", gap: 14, padding: "16px 0", borderBottom: `1px solid ${THEME.line}`, cursor: "pointer" }}>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 16, fontWeight: 600, color: THEME.ink, marginBottom: 3 }}>{t.name}</div>
              <div style={{ fontSize: 13, color: THEME.ink2, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{t.rep || "No rep set"} · {tReceipts.length} receipt{tReceipts.length === 1 ? "" : "s"}</div>
            </div>
            <div style={{ fontFamily: MONO, fontSize: 16, fontWeight: 500, color: THEME.ink, flexShrink: 0 }}>${fmt(total)}</div>
          </div>
        );
      })}
    </div>
  );
}

function InboxList({ receipts, onEdit }) {
  const unassigned = receipts.filter((r) => (r.split ? !(r.splits && r.splits.some((sp) => sp.tripId)) : !r.tripId));
  return (
    <div>
      <p style={{ fontSize: 13, color: THEME.ink2, margin: "-4px 0 18px" }}>Captured, not yet filed to a trip.</p>
      {unassigned.length === 0 ? (
        <EmptyState icon="inbox" title="Inbox is clear" body="Untagged receipts land here — snap one anytime, sort it later." />
      ) : (
        <div>{unassigned.map((r) => <ReceiptRow key={r.id} receipt={r} onClick={() => onEdit(r)} />)}</div>
      )}
    </div>
  );
}

function SettingsPanel({ settings, onUpdate, onSwitchToReview }) {
  return (
    <div>
      <label style={labelStyle}>Default designer name</label>
      <input type="text" value={settings.designerName} onChange={(e) => onUpdate({ ...settings, designerName: e.target.value })} placeholder="Your name" style={{ ...inputStyle, marginBottom: 30 }} />
      <div style={{ display: "flex", gap: 10, alignItems: "flex-start", padding: 14, borderRadius: 5, background: THEME.surface, border: `1px solid ${THEME.line}`, marginBottom: 20 }}>
        <div style={{ color: THEME.ink3, flexShrink: 0, marginTop: 2 }}><Icon name="lock" size={16} strokeWidth={1.7} /></div>
        <p style={{ fontSize: 13, color: THEME.ink2, margin: 0, lineHeight: 1.5 }}>Everything here stays on this device. Nothing syncs to Concur or any company system.</p>
      </div>
      <span onClick={onSwitchToReview} style={{ fontSize: 12, color: THEME.accentStrong, cursor: "pointer" }}>Switch to review view</span>
    </div>
  );
}

/* ---------- Review mode (desktop, table-dense) ---------- */
function ReviewTripsTable({ trips, receipts, timeEntries, onOpen, onNew }) {
  const th = { padding: "8px 10px", fontWeight: 500, color: THEME.ink2, textAlign: "left" };
  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
        <h2 style={{ margin: 0, fontSize: 21, fontWeight: 600, color: THEME.ink }}>Trips</h2>
        <button onClick={onNew} style={{ display: "flex", alignItems: "center", gap: 6, ...primaryBtn, flex: "none" }}><Icon name="plus" size={14} strokeWidth={2.1} /> New trip</button>
      </div>
      {trips.length === 0 ? (
        <EmptyState icon="mappin" title="No trips yet" body="Create a trip to start tagging receipts and building a worksheet." />
      ) : (
        <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 14 }}>
          <thead><tr style={{ borderBottom: `1px solid ${THEME.line}` }}>
            <th style={th}>Trip</th><th style={th}>Rep(s)</th><th style={th}>Receipts</th><th style={th}>Hours</th><th style={{ ...th, textAlign: "right" }}>Total</th><th style={th}>Mileage</th>
          </tr></thead>
          <tbody>
            {trips.map((t) => {
              const tReceipts = receipts.filter((r) => receiptTouchesTrip(r, t.id));
              const total = tReceipts.reduce((s, r) => s + amountForTrip(r, t.id), 0);
              const hours = timeEntries.filter((e) => e.tripId === t.id).reduce((s, e) => s + e.hours, 0);
              return (
                <tr key={t.id} onClick={() => onOpen(t)} style={{ borderBottom: `1px solid ${THEME.line}`, cursor: "pointer" }}>
                  <td style={{ padding: 10, color: THEME.ink }}>{t.name}</td>
                  <td style={{ padding: 10, color: THEME.ink2 }}>{t.rep || "—"}</td>
                  <td style={{ padding: 10, fontFamily: MONO, color: THEME.ink }}>{tReceipts.length}</td>
                  <td style={{ padding: 10, fontFamily: MONO, color: THEME.ink }}>{hours || "—"}</td>
                  <td style={{ padding: 10, textAlign: "right", fontFamily: MONO, color: THEME.ink }}>${fmt(total)}</td>
                  <td style={{ padding: 10, color: THEME.ink3, fontFamily: MONO }}>{t.mileageReimbursement != null ? `$${fmt(t.mileageReimbursement)}` : t.miles ? `${fmtMi(Number(t.miles))} mi pending` : "—"}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      )}
    </div>
  );
}

function ReviewTripDetail({ trip, receipts, timeEntries, onBack, onSaveReceipt, onDeleteReceipt, onExport, onShowReconcile }) {
  const tripReceipts = receipts.filter((r) => receiptTouchesTrip(r, trip.id)).sort((a, b) => (a.date || "").localeCompare(b.date || ""));
  const totals = {};
  CATEGORIES.forEach((c) => { totals[c.key] = 0; });
  tripReceipts.forEach((r) => { if (r.category && totals[r.category] != null) totals[r.category] += amountForTrip(r, trip.id); });
  const grandTotal = (trip.mileageReimbursement || 0) + Object.values(totals).reduce((a, b) => a + b, 0);

  function updateField(receipt, field, val) { onSaveReceipt({ ...receipt, [field]: val }); }
  const th = { padding: "6px 8px", textAlign: "left", color: THEME.ink2, fontWeight: 500 };
  const cellInput = { fontSize: 13, padding: "4px 6px", border: `1px solid ${THEME.line}`, borderRadius: 4, fontFamily: FONT, background: THEME.surface2 };

  return (
    <div>
      <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 16 }}>
        <button onClick={onBack} style={{ width: 36, height: 36, borderRadius: 5, border: `1px solid ${THEME.line}`, background: THEME.surface, display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", color: THEME.ink }}>
          <Icon name="back" size={18} strokeWidth={1.9} />
        </button>
        <h2 style={{ margin: 0, flex: 1, fontSize: 21, fontWeight: 600, color: THEME.ink }}>{trip.name}</h2>
        <button onClick={onShowReconcile} style={{ ...plainBtn, display: "flex", alignItems: "center", gap: 7 }}><Icon name="filecheck" size={15} strokeWidth={1.7} /> Reconcile</button>
        <button onClick={() => onExport(trip, tripReceipts, totals, trip.miles, trip.mileageReimbursement || 0)} style={{ ...primaryBtn, flex: "none", display: "flex", alignItems: "center", gap: 7 }}>
          <Icon name="download" size={15} strokeWidth={1.9} /> Export .xlsx
        </button>
      </div>

      <div style={{ display: "flex", gap: 24, marginBottom: 16, fontSize: 13, color: THEME.ink2 }}>
        <span>Designer: <strong style={{ color: THEME.ink }}>{trip.designer || "—"}</strong></span>
        <span>Rep(s): <strong style={{ color: THEME.ink }}>{trip.rep || "—"}</strong></span>
        <span>Miles: <strong style={{ color: THEME.ink, fontFamily: MONO }}>{trip.miles || "0"}</strong></span>
        <span>Total: <strong style={{ color: THEME.ink, fontFamily: MONO }}>${fmt(grandTotal)}</strong></span>
      </div>

      <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13, marginBottom: 24 }}>
        <thead><tr style={{ borderBottom: `1px solid ${THEME.line}` }}>
          <th style={th}></th><th style={th}>Date</th><th style={th}>Vendor</th><th style={th}>Category</th><th style={{ ...th, textAlign: "right" }}>Amount</th><th style={th}>Notes</th><th style={th}></th>
        </tr></thead>
        <tbody>
          {tripReceipts.map((r) => (
            <tr key={r.id} style={{ borderBottom: `1px solid ${THEME.line}` }}>
              <td style={{ padding: "6px 8px" }}>{r.image ? <img src={r.image} alt="" style={{ width: 28, height: 28, borderRadius: 4, objectFit: "cover" }} /> : <span style={{ color: THEME.ink3 }}><Icon name="receipt" size={16} strokeWidth={1.6} /></span>}</td>
              <td style={{ padding: "6px 8px" }}><input type="date" value={r.date || ""} onChange={(e) => updateField(r, "date", e.target.value)} style={cellInput} /></td>
              <td style={{ padding: "6px 8px" }}><input type="text" value={r.vendor || ""} onChange={(e) => updateField(r, "vendor", e.target.value)} style={{ ...cellInput, width: 120 }} /></td>
              <td style={{ padding: "6px 8px" }}>
                <select value={r.category || ""} onChange={(e) => updateField(r, "category", e.target.value)} style={cellInput}>
                  <option value="">—</option>
                  {CATEGORIES.map((c) => <option key={c.key} value={c.key}>{c.label}</option>)}
                </select>
              </td>
              <td style={{ padding: "6px 8px", textAlign: "right" }}>
                <input type="number" step="0.01" value={r.amount || ""} onChange={(e) => updateField(r, "amount", e.target.value)} style={{ ...cellInput, width: 70, textAlign: "right", fontFamily: MONO }} />
                {r.split && <span style={{ marginLeft: 6 }}><Badge tone="accent">Split</Badge></span>}
              </td>
              <td style={{ padding: "6px 8px" }}><input type="text" value={r.notes || ""} onChange={(e) => updateField(r, "notes", e.target.value)} style={{ ...cellInput, width: 160 }} /></td>
              <td style={{ padding: "6px 8px" }}><span onClick={() => onDeleteReceipt(r.id)} style={{ cursor: "pointer", color: THEME.ink3, display: "flex" }}><Icon name="trash" size={15} strokeWidth={1.6} /></span></td>
            </tr>
          ))}
        </tbody>
      </table>

      <div style={{ display: "grid", gridTemplateColumns: "repeat(5, minmax(0,1fr))", gap: 10 }}>
        {CATEGORIES.filter((c) => c.key !== "miles").map((c) => (
          <div key={c.key} style={{ background: THEME.surface, border: `1px solid ${THEME.line}`, borderRadius: 5, padding: "0.6rem" }}>
            <p style={{ fontSize: 11, color: THEME.ink3, margin: "0 0 2px" }}>{c.label}</p>
            <p style={{ fontFamily: MONO, fontSize: 15, fontWeight: 500, margin: 0, color: THEME.ink }}>${fmt(totals[c.key])}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ---------- App ---------- */
function App() {
  const [trips, setTrips] = useTrips();
  const [receipts, setReceipts] = useReceipts();
  const [settings, setSettings] = useSettings();
  const [timeEntries, setTimeEntries] = useTimeEntries();
  const [mode, modeOverride, setModeOverride] = useViewMode();

  const [tab, setTab] = useState("trips");
  const [activeTripId, setActiveTripId] = useState(null);
  const [showNewTrip, setShowNewTrip] = useState(false);
  const [captureChoice, setCaptureChoice] = useState(false);
  const [showCamera, setShowCamera] = useState(false);
  const [showReconcile, setShowReconcile] = useState(false);
  const [editingReceipt, setEditingReceipt] = useState(null);
  const fileInputRef = useRef(null);

  if (trips === null || receipts === null || settings === null || timeEntries === null) {
    return <div style={{ padding: "2rem", textAlign: "center", color: THEME.ink2, fontFamily: FONT }}>Loading…</div>;
  }

  const activeTrip = trips.find((t) => t.id === activeTripId);

  function createTrip(trip) { setTrips([trip, ...trips]); setShowNewTrip(false); setActiveTripId(trip.id); }
  function updateTrip(updated) { setTrips(trips.map((t) => (t.id === updated.id ? updated : t))); }
  function openNewReceipt(imageDataUrl) {
    const r = { id: uid(), image: imageDataUrl || null, vendor: "", date: todayStr(), amount: "", category: "", tripId: activeTripId || "", notes: "", createdAt: Date.now() };
    setEditingReceipt(r);
  }
  function saveReceipt(r) {
    const exists = receipts.some((x) => x.id === r.id);
    setReceipts(exists ? receipts.map((x) => (x.id === r.id ? r : x)) : [r, ...receipts]);
    setEditingReceipt(null);
  }
  function deleteReceipt(id) { setReceipts(receipts.filter((r) => r.id !== id)); setEditingReceipt(null); }
  function bulkUpdateReceipts(updated) {
    const byId = Object.fromEntries(updated.map((r) => [r.id, r]));
    setReceipts(receipts.map((r) => (byId[r.id] ? byId[r.id] : r)));
  }
  function addTimeEntry(entry) { setTimeEntries([entry, ...timeEntries]); }
  function deleteTimeEntry(id) { setTimeEntries(timeEntries.filter((e) => e.id !== id)); }

  function handleFileUpload(e) {
    const file = e.target.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => openNewReceipt(reader.result);
    reader.readAsDataURL(file);
    e.target.value = "";
  }

  function applyReconcile(trip, result) {
    updateTrip({ ...trip, mileageReimbursement: result.mileageReimbursement });
    const noteUpdates = result.matched.map(({ receipt, item }) => {
      const diningMatch = result.diningNotes.find((d) => d.date === item.date && Math.abs(d.amount - item.amount) < 0.01);
      if (diningMatch && !receipt.notes) return { ...receipt, notes: `Dined with ${diningMatch.attendees.join(", ")}` };
      return null;
    }).filter(Boolean);
    if (noteUpdates.length) bulkUpdateReceipts(noteUpdates);
    setShowReconcile(false);
  }

  async function exportWorksheet(trip, tripReceipts, totals, miles, mileageTotal) {
    const XLSX = window.XLSX;
    if (!XLSX) { alert("Export library still loading — try again in a second."); return; }
    const rows = [];
    const header = ["PERSONAL CAR MILES", "", "Airfare", "Checked Luggage Fee", "Rental Car", "Gas", "Parking", "Taxi/Rideshare", "Tolls", "Lodging", "Meals", "", "DATES OF DAYS WITH REP", "TIME WITH REP OVER 8HRS", "HRS OF WEEKEND TRAVEL", "NAMES"];
    rows.push(header);

    const dates = [...new Set(tripReceipts.filter((r) => r.date).map((r) => r.date))].sort();
    const catCols = ["airfare", "luggage", "rental", "gas", "parking", "taxi", "tolls", "lodging", "meals"];
    const colLists = {};
    catCols.forEach((k) => { colLists[k] = tripReceipts.filter((r) => r.category === k).map((r) => amountForTrip(r, trip.id)).filter((v) => v > 0); });

    const tripTimeEntries = timeEntries.filter((e) => e.tripId === trip.id);
    const over8ByDate = {};
    tripTimeEntries.forEach((e) => { if (e.hours > 8) over8ByDate[e.date] = e.hours; });
    const over8List = Object.entries(over8ByDate).sort((a, b) => a[0].localeCompare(b[0])).map(([, h]) => h);
    const weekendList = tripTimeEntries.filter((e) => e.weekend).sort((a, b) => a.date.localeCompare(b.date)).map((e) => e.hours);

    const maxRows = Math.max(1, dates.length, over8List.length, weekendList.length, ...catCols.map((k) => colLists[k].length));
    for (let i = 0; i < maxRows; i++) {
      const row = [];
      row.push(i === 0 ? Number(miles) || 0 : "");
      row.push("");
      catCols.forEach((k) => row.push(colLists[k][i] != null ? colLists[k][i] : ""));
      row.push("");
      row.push(dates[i] || "");
      row.push(over8List[i] != null ? over8List[i] : "");
      row.push(weekendList[i] != null ? weekendList[i] : "");
      if (i === 0) row.push("NAME OF DESIGNER:");
      else if (i === 1) row.push(trip.designer || "");
      else if (i === 2) row.push("NAME OF REP:");
      else if (i === 3) row.push(trip.rep || "");
      else if (i === 4 && trip.mileageReimbursement == null) row.push("Mileage pending — reconcile against expense report");
      else row.push("");
      rows.push(row);
    }

    const ws = XLSX.utils.aoa_to_sheet(rows);
    const wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
    const wbout = XLSX.write(wb, { bookType: "xlsx", type: "array" });
    const blob = new Blob([wbout], { type: "application/octet-stream" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = `${trip.name.replace(/[^a-z0-9]+/gi, "_")}_worksheet.xlsx`;
    document.body.appendChild(a); a.click(); document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  const unassignedCount = receipts.filter((r) => (r.split ? !(r.splits && r.splits.some((sp) => sp.tripId)) : !r.tripId)).length;

  // Reps typed in manually (not in the base roster) on past trips, so they show up
  // as normal checkbox options instead of needing to be re-typed every time.
  const customRepNames = [...new Set(
    trips.flatMap((t) => (t.rep || "").split(",").map((s) => s.trim()).filter(Boolean))
  )].filter((n) => !REP_NAMES.includes(n));

  const rootFontStyle = { fontFamily: FONT, background: THEME.bg, color: THEME.ink, minHeight: "100vh", position: "relative", WebkitFontSmoothing: "antialiased" };

  if (mode === "review") {
    return (
      <div style={rootFontStyle}>
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
        <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap" />
        <div style={{ maxWidth: 1000, margin: "0 auto", padding: "1.5rem" }}>
          <div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 12 }}>
            <div style={{ display: "flex", gap: 4, fontSize: 12 }}>
              {[["Auto", null], ["Field view", "field"], ["Review view", "review"]].map(([label, val]) => (
                <span key={label} onClick={() => setModeOverride(val)} style={{ cursor: "pointer", color: modeOverride === val ? THEME.accentStrong : THEME.ink3, padding: "4px 8px" }}>{label}</span>
              ))}
            </div>
          </div>

          {activeTrip ? (
            <ReviewTripDetail trip={activeTrip} receipts={receipts} timeEntries={timeEntries}
              onBack={() => setActiveTripId(null)} onSaveReceipt={saveReceipt} onDeleteReceipt={deleteReceipt}
              onExport={exportWorksheet} onShowReconcile={() => setShowReconcile(true)} />
          ) : (
            <ReviewTripsTable trips={trips} receipts={receipts} timeEntries={timeEntries} onOpen={(t) => setActiveTripId(t.id)} onNew={() => setShowNewTrip(true)} />
          )}

          {showNewTrip && <NewTripModal onClose={() => setShowNewTrip(false)} onCreate={createTrip} extraNames={customRepNames} />}
          {showReconcile && activeTrip && (
            <ReconcileModal trip={activeTrip} tripReceipts={receipts.filter((r) => receiptTouchesTrip(r, activeTrip.id))}
              onClose={() => setShowReconcile(false)} onApply={(result) => applyReconcile(activeTrip, result)} />
          )}
        </div>
      </div>
    );
  }

  return (
    <div style={rootFontStyle}>
      <link rel="preconnect" href="https://fonts.googleapis.com" />
      <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
      <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap" />

      <div style={{ position: "sticky", top: 0, zIndex: 10, background: THEME.bg, padding: "18px 20px 12px", display: "flex", alignItems: "center", gap: 10, maxWidth: 480, margin: "0 auto", boxSizing: "border-box" }}>
        {activeTrip && (
          <button onClick={() => setActiveTripId(null)} style={{ width: 36, height: 36, borderRadius: 5, border: `1px solid ${THEME.line}`, background: THEME.surface, display: "flex", alignItems: "center", justifyContent: "center", color: THEME.ink, flexShrink: 0, cursor: "pointer", padding: 0 }}>
            <Icon name="back" size={18} strokeWidth={1.9} />
          </button>
        )}
        <h1 style={{ margin: 0, fontSize: 21, fontWeight: 600, letterSpacing: "-0.01em", flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: THEME.ink }}>
          {activeTrip ? activeTrip.name : tab === "trips" ? "Trips" : tab === "inbox" ? "Inbox" : "Settings"}
        </h1>
        {!activeTrip && tab === "trips" && (
          <button onClick={() => setShowNewTrip(true)} style={{ display: "flex", alignItems: "center", gap: 5, padding: "8px 12px", borderRadius: 5, border: `1.5px solid ${THEME.accent}`, background: "transparent", color: THEME.accentStrong, fontSize: 13, fontWeight: 500, fontFamily: FONT, cursor: "pointer", whiteSpace: "nowrap", flexShrink: 0 }}>
            <Icon name="plus" size={14} strokeWidth={2.1} /> New trip
          </button>
        )}
      </div>

      <div style={{ padding: "6px 20px 130px", maxWidth: 480, margin: "0 auto", width: "100%", boxSizing: "border-box" }}>
        {activeTrip ? (
          <TripDetail trip={activeTrip} receipts={receipts} timeEntries={timeEntries}
            onAddTimeEntry={addTimeEntry} onDeleteTimeEntry={deleteTimeEntry}
            onBack={() => setActiveTripId(null)} onUpdateTrip={updateTrip} onEditReceipt={setEditingReceipt}
            onExport={exportWorksheet} onBulkUpdateReceipts={bulkUpdateReceipts} onShowReconcile={() => setShowReconcile(true)} />
        ) : tab === "trips" ? (
          <TripsList trips={trips} receipts={receipts} onOpen={(t) => setActiveTripId(t.id)} />
        ) : tab === "inbox" ? (
          <InboxList receipts={receipts} onEdit={setEditingReceipt} />
        ) : (
          <SettingsPanel settings={settings} onUpdate={setSettings} onSwitchToReview={() => setModeOverride("review")} />
        )}
      </div>

      <div style={{ position: "fixed", left: 0, right: 0, bottom: 0, background: THEME.surface2, borderTop: `1px solid ${THEME.line}`, padding: "9px 16px calc(9px + env(safe-area-inset-bottom))", zIndex: 20 }}>
        <div style={{ maxWidth: 480, margin: "0 auto", position: "relative", display: "flex", alignItems: "center" }}>
          <button onClick={() => { setActiveTripId(null); setTab("trips"); }} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 3, background: "transparent", border: "none", padding: "6px 0", cursor: "pointer", color: tab === "trips" && !activeTrip ? THEME.accent : THEME.ink2, fontFamily: FONT }}>
            <Icon name="mappin" size={20} strokeWidth={1.7} />
            <span style={{ fontSize: 11 }}>Trips</span>
          </button>
          <button onClick={() => { setActiveTripId(null); setTab("inbox"); }} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 3, background: "transparent", border: "none", padding: "6px 0", cursor: "pointer", color: tab === "inbox" && !activeTrip ? THEME.accent : THEME.ink2, position: "relative", fontFamily: FONT }}>
            <Icon name="inbox" size={20} strokeWidth={1.7} />
            <span style={{ fontSize: 11 }}>Inbox</span>
            {unassignedCount > 0 && (
              <span style={{ position: "absolute", top: 0, right: "28%", minWidth: 15, height: 15, padding: "0 3px", borderRadius: 8, background: THEME.accent, color: THEME.onAccent, fontSize: 10, fontWeight: 600, display: "flex", alignItems: "center", justifyContent: "center" }}>{unassignedCount}</span>
            )}
          </button>
          <div style={{ flex: 1 }} />
          <button onClick={() => { setActiveTripId(null); setTab("settings"); }} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 3, background: "transparent", border: "none", padding: "6px 0", cursor: "pointer", color: tab === "settings" && !activeTrip ? THEME.accent : THEME.ink2, fontFamily: FONT }}>
            <Icon name="settings" size={20} strokeWidth={1.7} />
            <span style={{ fontSize: 11 }}>Settings</span>
          </button>
          <button onClick={() => setCaptureChoice(true)} aria-label="Capture receipt" style={{ position: "absolute", left: "50%", top: -26, transform: "translateX(-50%)", width: 54, height: 54, borderRadius: "50%", background: THEME.accent, border: `3px solid ${THEME.bg}`, display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", boxShadow: "0 3px 10px rgba(40,20,10,0.22)", padding: 0 }}>
            <Icon name="camera" size={22} strokeWidth={1.8} color={THEME.onAccent} />
          </button>
        </div>
      </div>

      <input ref={fileInputRef} type="file" accept="image/*" onChange={handleFileUpload} style={{ display: "none" }} />

      {captureChoice && (
        <CaptureChoiceModal
          onTakePhoto={() => { setCaptureChoice(false); setShowCamera(true); }}
          onUpload={() => { setCaptureChoice(false); fileInputRef.current?.click(); }}
          onClose={() => setCaptureChoice(false)}
        />
      )}
      {showCamera && <CameraCapture onClose={() => setShowCamera(false)} onCapture={(img) => { setShowCamera(false); openNewReceipt(img); }} />}
      {showNewTrip && <NewTripModal onClose={() => setShowNewTrip(false)} onCreate={createTrip} extraNames={customRepNames} />}
      {showReconcile && activeTrip && (
        <ReconcileModal trip={activeTrip} tripReceipts={receipts.filter((r) => receiptTouchesTrip(r, activeTrip.id))}
          onClose={() => setShowReconcile(false)} onApply={(result) => applyReconcile(activeTrip, result)} />
      )}
      {editingReceipt && (
        <ReceiptEditor receipt={editingReceipt} trips={trips} onSave={saveReceipt} onDelete={deleteReceipt} onClose={() => setEditingReceipt(null)} />
      )}
    </div>
  );
}
