/* ============================================================
   CurrencyDesk OS — Till & Cash Drawer
   Count the physical drawer down to the cent: every bill and coin
   of every currency. Saves a dated snapshot, reconciles against the
   ledger's expected float, closes the trading day, and keeps a
   year of daily history you can scroll back through.
   Day Close is folded in here as the "Reconcile & close" tab.
   ============================================================ */
(function () {
  const { useState, useMemo, useEffect, useRef } = React;
  /* `TODAY` is deliberately not imported here. It is a wall-clock snapshot
     taken when the page loaded, and every date on this screen is a TRADING
     DAY — the till session's `business_date`, which the server decides. A
     drawer counted at 00:20 belongs to the session that was open, not to the
     calendar. See the note on wallClock/businessDate in cdos-base.jsx. */
  const { CD, Ic, fmt, num, crossRate, perCadLive, dDiff, STAFF } = window.CDOS;

  // denominations per currency — { v: face value in that ccy, t: 'bill' | 'coin' }
  const DEN = {
    CAD: [[100, 'bill'], [50, 'bill'], [20, 'bill'], [10, 'bill'], [5, 'bill'], [2, 'coin'], [1, 'coin'], [0.25, 'coin'], [0.10, 'coin'], [0.05, 'coin']],
    USD: [[100, 'bill'], [50, 'bill'], [20, 'bill'], [10, 'bill'], [5, 'bill'], [1, 'bill'], [0.25, 'coin'], [0.10, 'coin'], [0.05, 'coin'], [0.01, 'coin']],
    EUR: [[500, 'bill'], [200, 'bill'], [100, 'bill'], [50, 'bill'], [20, 'bill'], [10, 'bill'], [5, 'bill'], [2, 'coin'], [1, 'coin'], [0.50, 'coin'], [0.20, 'coin'], [0.10, 'coin'], [0.05, 'coin'], [0.02, 'coin'], [0.01, 'coin']],
    GBP: [[50, 'bill'], [20, 'bill'], [10, 'bill'], [5, 'bill'], [2, 'coin'], [1, 'coin'], [0.50, 'coin'], [0.20, 'coin'], [0.10, 'coin'], [0.05, 'coin']],
    INR: [[2000, 'bill'], [500, 'bill'], [200, 'bill'], [100, 'bill'], [50, 'bill'], [20, 'bill'], [10, 'bill'], [10, 'coin'], [5, 'coin'], [2, 'coin'], [1, 'coin']],
    PHP: [[1000, 'bill'], [500, 'bill'], [200, 'bill'], [100, 'bill'], [50, 'bill'], [20, 'bill'], [20, 'coin'], [10, 'coin'], [5, 'coin'], [1, 'coin'], [0.25, 'coin']],
    CNY: [[100, 'bill'], [50, 'bill'], [20, 'bill'], [10, 'bill'], [5, 'bill'], [1, 'bill'], [1, 'coin'], [0.5, 'coin'], [0.1, 'coin']],
    MXN: [[1000, 'bill'], [500, 'bill'], [200, 'bill'], [100, 'bill'], [50, 'bill'], [20, 'bill'], [20, 'coin'], [10, 'coin'], [5, 'coin'], [2, 'coin'], [1, 'coin'], [0.5, 'coin']],
    AED: [[1000, 'bill'], [500, 'bill'], [200, 'bill'], [100, 'bill'], [50, 'bill'], [20, 'bill'], [10, 'bill'], [5, 'bill'], [1, 'coin'], [0.5, 'coin'], [0.25, 'coin']],
  };
  const CCYS = Object.keys(DEN);
  const flagOf = (c) => { try { return (typeof CUR !== 'undefined' ? (CUR.find(x => x.code === c) || {}).flag : '') || ''; } catch (e) { return ''; } };
  const denLabel = (v) => v >= 1 ? (Number.isInteger(v) ? String(v) : v.toFixed(2)) : (Math.round(v * 100) + '¢');
  /* `cadOf` stood here — a module-level conversion into Canadian dollars,
     used by every total on this screen. It is gone. The rate board is quoted
     in units per CANADIAN dollar (`PER_CAD`), so a conversion built on it
     can only produce a total for a desk whose books are kept in that
     currency; everywhere else it produced a Canadian figure and the screen
     printed a Canadian symbol in front of it. What replaced it is `homeOf`
     inside TillDrawer, which returns NULL where it cannot answer, and every
     total on this screen checks. See docs/ABSENT_FIGURES.md. */
  const HKEY = 'cdos_till_history_v2', CKEY = 'cdos_till_counts';
  const SHIFT_KEY = 'cdos_till_operator_v1', HANDOFF_KEY = 'cdos_till_handoffs_v1';
  const shiftStamp = () => new Date().toLocaleString('en-CA', { hour12: false }).replace(',', '');
  // ledger principals are stored tenant-scoped ("tnt-yorkfx:a.singh"); a teller
  // reading their own close-out should see the person, not the scoping
  const personOf = (id) => String(id || '').split(':').pop() || '—';
  const clockOf = (ms) => ms ? new Date(ms).toLocaleTimeString('en-CA', { hour: '2-digit', minute: '2-digit', hour12: true }) : '';

  /* WHAT THIS DRAWER HAS BEEN COUNTED AT, AND ONLY THAT.

     `seedHistory()` stood here. On first open of the Cash Drawer it wrote
     a YEAR of invented daily counts into `cdos_till_history_v2` —
     CAD 238,500, USD 84,000, EUR 31,000 and six more, wobbled by a sine
     function, one row per non-Sunday, including a row for today.

     It was there so the History tab would have something to scroll. What
     it actually did was become a second book, and the End-of-Day Sign-Off
     read out of it: the sheet's "Counted" column was this map, so a desk
     that had counted nothing printed a full drawer against the ledger's
     real balances and a variance in the hundreds of thousands, above two
     signature lines. See docs/CASH_OWNERSHIP_INVARIANTS.md — two books do
     not crash, they disagree.

     So there is no seed. A drawer that has been counted has rows here; a
     drawer that has not is empty, and the History tab says so. The
     authoritative record of a count is the ledger's anyway
     (`ledger_till_counts`, reachable as `latestCounts` on
     GET /api/ledger/till-session); this local map is a convenience for
     the count sheet and nothing on a generated document may read it. */
  function loadHistory() {
    try { const saved = JSON.parse(localStorage.getItem(HKEY) || 'null'); return (saved && typeof saved === 'object' && !Array.isArray(saved)) ? saved : {}; }
    catch (e) { return {}; }
  }

  /* One denomination row. Hoisted to module scope (NOT defined inside
     TillDrawer) so its component identity is stable across renders —
     otherwise React remounts every row on each keystroke and the input
     loses focus after a single digit, making multi-digit counts impossible. */
  function DenRow({ d, ccy, counts, setCount }) {
    const cnt = parseInt((counts[ccy] || {})[d.i], 10) || 0;
    const sub = d.v * cnt;
    return (<div className="flex items-center gap-2 py-1.5" style={{ borderTop: `1px solid ${CD.lineSoft}` }}>
      <span className="grid place-items-center flex-none" style={{ width: 44, fontFamily: 'Space Mono, monospace', fontSize: 12.5, fontWeight: 700, color: CD.ink }}>{ccy === 'CAD' || ccy === 'USD' || d.v >= 1 ? (d.v < 1 ? denLabel(d.v) : (['CAD', 'USD', 'GBP', 'EUR', 'AED'].includes(ccy) ? '$' : '') + denLabel(d.v)) : denLabel(d.v)}</span>
      <span className="text-[9px] px-1.5 py-0.5 flex-none" style={{ borderRadius: 4, background: d.t === 'bill' ? CD.lineSoft : 'transparent', border: d.t === 'coin' ? `1px solid ${CD.line}` : 'none', color: CD.mute, fontFamily: 'Space Mono, monospace', textTransform: 'uppercase', letterSpacing: '0.05em' }}>{d.t}</span>
      <div className="flex items-center flex-none" style={{ marginLeft: 'auto' }}>
        <button onClick={() => setCount(ccy, d.i, String(Math.max(0, cnt - 1)))} className="till-step grid place-items-center" style={{ width: 26, height: 28, border: `1px solid ${CD.line}`, borderRadius: '7px 0 0 7px', color: CD.mute }}>−</button>
        <input type="number" value={(counts[ccy] || {})[d.i] ?? ''} onChange={e => setCount(ccy, d.i, e.target.value)} placeholder="0" className="text-center outline-none" style={{ width: 52, height: 28, border: `1px solid ${CD.line}`, borderLeft: 0, borderRight: 0, fontVariantNumeric: 'tabular-nums', fontFamily: 'Space Mono, monospace', fontSize: 13 }} />
        <button onClick={() => setCount(ccy, d.i, String(cnt + 1))} className="till-step grid place-items-center" style={{ width: 26, height: 28, border: `1px solid ${CD.line}`, borderRadius: '0 7px 7px 0', color: CD.mute }}>+</button>
      </div>
      <span style={{ width: 92, textAlign: 'right', fontFamily: 'Space Mono, monospace', fontSize: 12.5, color: sub ? CD.ink : CD.faint, fontVariantNumeric: 'tabular-nums', fontWeight: sub ? 600 : 400 }}>{sub ? num(sub) : '—'}</span>
    </div>);
  }

  /* =====================================================================
     SHIFT HANDOFF — who's on the drawer, and passing it on.
     Mom-and-pop friendly: usually the same person all day, so the operator
     just sits in the header. When the drawer changes hands, a quick modal
     asks whether to count first (optional unless the owner requires it),
     then records the handoff with any variance. Nothing bank-heavy.
  ===================================================================== */
  function HandoffModal({ tillId, tillName, current, roster, me, expected, requireCount, onClose, onDone }) {
    const [step, setStep] = useState('who');      // who → count? → done
    const [toName, setToName] = useState('');
    const [counts, setCounts] = useState(() => { const o = {}; expected.forEach(x => { o[x.ccy] = ''; }); return o; });
    /* A handoff variance is a cross-currency total, so it needs the same
       care as everything else on this screen: it exists in the desk's own
       money or it does not exist. */
    const pack = window.CDOS.deskPack();
    const homeCcy = (pack && pack.homeCurrency) || null;
    const homeLocal = (amt, ccy) => !homeCcy ? null : ccy === homeCcy ? (+amt || 0) : (homeCcy === 'CAD' ? (+amt || 0) / (crossRate('CAD', ccy) || 1) : null);
    const sumHome = (parts) => { const v = parts.map(([a, c]) => homeLocal(a, c)); return v.length && v.every(x => x != null) ? v.reduce((s, x) => s + x, 0) : null; };
    const to = roster.find(s => s.name === toName);
    const expHome = sumHome(expected.map(x => [x.units, x.ccy]));
    const countedHome = sumHome(expected.map(x => [counts[x.ccy] === '' ? x.units : (parseFloat(counts[x.ccy]) || 0), x.ccy]));
    const variance = expHome == null || countedHome == null ? null : +(countedHome - expHome).toFixed(2);
    const anyEntered = expected.some(x => counts[x.ccy] !== '' && counts[x.ccy] != null);

    const finish = (counted) => {
      const rec = {
        id: 'ho' + Date.now(), tillId, tillName,
        from: current ? current.operator : '—', to: toName, toRole: to ? to.role : '',
        at: shiftStamp(), atMs: Date.now(), by: me.name,
        counted, expectedHome: expHome == null ? null : +expHome.toFixed(2),
        countedHome: counted && countedHome != null ? +countedHome.toFixed(2) : null,
        currency: homeCcy,
        variance: counted ? variance : null
      };
      onDone(rec);
    };

    return ReactDOM.createPortal(
      <div className="fixed inset-0 flex items-center justify-center p-4" style={{ background: 'var(--cd-scrim)', zIndex: 9400 }} onMouseDown={onClose}>
        <div onMouseDown={e => e.stopPropagation()} className="w-full flex flex-col" style={{ maxWidth: 460, maxHeight: 'calc(100vh - 40px)', background: CD.paper, border: `1px solid ${CD.ink}`, borderRadius: 16, boxShadow: '0 24px 70px var(--cd-scrim)', overflow: 'hidden' }}>
          {/* header */}
          <div className="flex items-center gap-3 px-5 py-3.5 flex-none" style={{ borderBottom: `1px solid ${CD.line}`, background: 'var(--cd-panel)' }}>
            <span className="grid place-items-center" style={{ width: 32, height: 32, background: CD.ink, borderRadius: 9 }}><Ic n="users" s={17} c="var(--cd-on-ink)" /></span>
            <div className="flex-1 min-w-0">
              <div className="font-semibold leading-tight" style={{ color: CD.ink }}>Hand off the drawer</div>
              <div className="text-[11px]" style={{ color: CD.mute }}>{tillName} · leaving <b style={{ color: CD.ink }}>{current ? current.operator : '—'}</b></div>
            </div>
            <button onClick={onClose} className="p-1.5"><Ic n="x" s={18} c={CD.mute} /></button>
          </div>

          {step === 'who' && (<div className="px-5 py-4">
            <div className="text-[11px] mb-2 font-semibold uppercase tracking-wider" style={{ color: CD.faint, fontFamily: 'Space Mono, monospace' }}>Who's taking the drawer?</div>
            <div className="flex flex-col gap-1.5">
              {roster.map(s => { const on = toName === s.name; const isMe = s.name === me.name; return (
                <button key={s.name} onClick={() => setToName(s.name)} className="flex items-center gap-3 px-3 py-2.5 text-left" style={{ border: `1px solid ${on ? CD.ink : CD.line}`, background: on ? CD.ink : 'var(--cd-panel)', borderRadius: 10 }}>
                  <span className="grid place-items-center flex-none font-semibold" style={{ width: 30, height: 30, borderRadius: 8, background: on ? 'var(--cd-on-ink-faint)' : CD.lineSoft, color: on ? 'var(--cd-on-ink)' : CD.ink, fontSize: 11 }}>{s.name.split(/[ .]+/).filter(Boolean).map(x => x[0]).join('').slice(0, 2).toUpperCase()}</span>
                  <span className="flex-1 min-w-0"><span className="block text-[13.5px] font-medium" style={{ color: on ? 'var(--cd-on-ink)' : CD.ink }}>{s.name}{isMe ? ' (me)' : ''}</span><span className="block text-[11px]" style={{ color: on ? 'var(--cd-on-ink-soft)' : CD.mute }}>{s.role}</span></span>
                  {on && <Ic n="check" s={16} c="var(--cd-on-ink)" />}
                </button>); })}
            </div>
            <button disabled={!toName || toName === (current && current.operator)} onClick={() => setStep('count')} className="w-full mt-3 py-2.5 text-sm font-semibold text-white" style={{ background: (toName && toName !== (current && current.operator)) ? CD.ink : 'var(--cd-disabled)', borderRadius: 10 }}>Continue</button>
            {toName && toName === (current && current.operator) && <div className="text-[11px] mt-1.5 text-center" style={{ color: CD.faint }}>{toName} is already on the drawer.</div>}
          </div>)}

          {step === 'count' && (<>
            <div className="px-5 py-4 overflow-auto">
              <div className="flex items-center justify-between mb-1"><div className="text-[13.5px] font-semibold" style={{ color: CD.ink }}>Count the drawer before handing to {toName}?</div></div>
              <div className="text-[12px] mb-3" style={{ color: CD.mute }}>{requireCount ? 'Your shop requires a count at handoff.' : 'Optional — skip it if it’s a quick swap and the same drawer.'}</div>

              {/* compact count: prefilled to expected, edit only what's off */}
              <div style={{ border: `1px solid ${CD.line}`, borderRadius: 11, overflow: 'hidden' }}>
                {expected.map((x, i) => { const val = counts[x.ccy] === '' ? '' : counts[x.ccy]; const cnt = val === '' ? x.units : (parseFloat(val) || 0); /* This row's own over/short. Measured in the drawer's own units when
                     the desk's currency cannot price it — a variance nobody can
                     express is still a variance, and hiding it would be worse
                     than showing it in the currency it is actually in. */
                  const home = homeLocal(cnt, x.ccy), exp = homeLocal(x.units, x.ccy);
                  const dv = home == null || exp == null ? +(cnt - (x.units || 0)).toFixed(2) : +(home - exp).toFixed(2);
                  const off = Math.abs(dv) > 0.005; return (
                  <div key={x.ccy} className="flex items-center gap-2 px-3 py-2" style={{ borderTop: i ? `1px solid ${CD.lineSoft}` : 'none', background: 'var(--cd-panel)' }}>
                    <span className="font-semibold flex-none" style={{ width: 42, fontFamily: 'Space Mono, monospace', fontSize: 12.5, color: CD.ink }}>{x.ccy}</span>
                    <span className="flex-1 text-[11px]" style={{ color: CD.mute }}>expected <b style={{ color: CD.ink, fontVariantNumeric: 'tabular-nums' }}>{num(x.units)}</b></span>
                    <input value={val} onChange={e => setCounts(o => ({ ...o, [x.ccy]: e.target.value }))} inputMode="decimal" placeholder={num(x.units)} className="text-right outline-none text-[13px] px-2 py-1.5" style={{ width: 96, border: `1px solid ${off ? CD.flag : CD.line}`, borderRadius: 7, fontVariantNumeric: 'tabular-nums', color: off ? CD.flag : CD.ink }} />
                  </div>); })}
              </div>

              <div className="flex items-center justify-between mt-3 px-3 py-2.5" style={{ background: variance != null && Math.abs(variance) > 0.005 ? CD.flagSoft : variance == null && anyEntered ? 'var(--cd-chip)' : CD.greenSoft, borderRadius: 10 }}>
                <span className="text-[12px] font-medium" style={{ color: variance != null && Math.abs(variance) > 0.005 ? CD.flag : variance == null && anyEntered ? CD.mute : CD.green }}>{!anyEntered ? 'Not counted yet' : variance == null ? 'Counted — no single currency to state the variance in' : Math.abs(variance) < 0.005 ? '✓ Balances to expected' : (variance > 0 ? 'Over by ' : 'Short by ') + fmt(Math.abs(variance), homeCcy)}</span>
                <span className="text-[12px]" style={{ color: CD.mute, fontFamily: 'Space Mono, monospace' }}>{countedHome == null ? '—' : fmt(countedHome, homeCcy)}</span>
              </div>
            </div>
            <div className="px-5 py-3.5 flex items-center justify-between gap-2 flex-none" style={{ borderTop: `1px solid ${CD.line}`, background: 'var(--cd-panel)' }}>
              <button onClick={() => setStep('who')} className="text-[13px] px-3 py-2 font-medium" style={{ color: CD.mute }}>Back</button>
              <div className="flex items-center gap-2">
                {!requireCount && <button onClick={() => finish(false)} className="text-[13px] px-3.5 py-2 font-medium" style={{ border: `1px solid ${CD.line}`, borderRadius: 9, color: CD.ink, background: 'var(--cd-on-ink)' }}>Skip — hand off as-is</button>}
                <button onClick={() => finish(true)} className="flex items-center gap-1.5 text-[13px] px-4 py-2 font-semibold text-white" style={{ background: CD.green, borderRadius: 9 }}><Ic n="check" s={15} c="var(--cd-on-ink)" /> {anyEntered ? 'Confirm count & hand off' : 'Confirm as counted'}</button>
              </div>
            </div>
          </>)}
        </div>
      </div>, document.body);
  }

  function TillDrawer({ rows: allRows, log, day, onCloseDay, onOpenNextDay, me, canCloseDay = true, baseline, setBaseline, receipts, stationName, stationTill, branches, station, setStation, onOpenReport, settings, onMoveCash, onOpenVault, moves, serverBacked, cashVersion, onSessionSync, ledgerScope, onSelectLedgerTill }) {
    const rows = useMemo(() => allRows.filter(r => r.status !== 'void'), [allRows]);
    const [tab, setTab] = useState('count');
    /* ---------------- SERVER TILL SESSION ----------------
       On a server-backed desk the drawer is not a spreadsheet: the till holds
       an authoritative balance per currency, and every count, movement and
       close is recorded against ONE open session. Three rules hold here:
         · the till is opened deliberately, by a person, never by a render;
         · nothing is posted without a session id we actually hold;
         · the close writes the counted figure back as the till balance, so a
           currency that was not counted must never be sent.
       ------------------------------------------------------------------ */
    const [serverBalances, setServerBalances] = useState(null);
    const [serverSession, setServerSession] = useState(null);
    const [serverBalanceError, setServerBalanceError] = useState('');
    /* THE DESK'S OWN MONEY, AND THE DAY'S OWN BOOK.

       This screen totalled every drawer "in CAD" and labelled it so — a
       Canadian answer printed on a London desk's close-out, in front of a
       pound sign. Home currency is the jurisdiction pack's to state, so it
       is read from the pack and every total on this screen is labelled with
       it. A desk whose pack has not arrived says the currency is unknown
       rather than assuming one; see docs/ABSENT_FIGURES.md.

       `dayEarned` is the trading day's earnings from GET /api/ledger/summary
       — commission plus the margin each disposal actually booked. The close
       used to compute this here, as `(mid - paid)` against a live market
       rate, and store it on the day summary: on a desk with one voided deal
       the closed banner read "Earned today $223.00". */
    const deskFacts = window.CDOS.useDeskFacts();
    const homeCcy = useMemo(() => {
      const pack = window.CDOS.deskPack();
      return (pack && pack.homeCurrency) || (window.CDOS.reportingLimit(settings) || {}).currency || null;
    }, [settings, deskFacts]);
    /* Every figure this screen totals is a sum across currencies, and a sum
       across currencies needs a rate. The browser's board is quoted per
       CANADIAN dollar, so it can only produce that sum for a desk whose
       home currency IS the Canadian dollar. Elsewhere there is no total to
       show and the screen says which currencies it is holding instead. */
    const canTotal = homeCcy === 'CAD';
    const homeOf = (amt, ccy) => !homeCcy ? null : ccy === homeCcy ? (+amt || 0) : (canTotal ? (+amt || 0) / (crossRate('CAD', ccy) || 1) : null);
    const totalHome = (parts) => {
      const values = parts.map(([amt, ccy]) => homeOf(amt, ccy));
      return values.length && values.every(v => v != null) ? values.reduce((s, v) => s + v, 0) : null;
    };
    /* A total this screen cannot produce, said once. */
    const fmtHome = (v) => v == null ? '—' : fmt(v, homeCcy);
    const [dayBook, setDayBook] = useState({ loading: true, summary: null, error: '' });
    const [sessionErr, setSessionErr] = useState('');     // last failed till action, shown on screen
    const [tillBusy, setTillBusy] = useState('');         // 'open' | 'count' | 'close'
    /* The till picker below switches THIS DESK's local station — ids this
       browser invented ("b01t1"). The server names its tills separately and
       decides which one to answer for from the workspace the OS sends. The two
       cannot be matched up, so when the branch has more than one server till the
       picker's label and the money on this screen are about different drawers.
       That is what let a count taken at Till 2 be closed onto Till 1's balance,
       so the drawer says plainly which till the ledger is answering for. */
    const ledgerTill = (ledgerScope && ledgerScope.active) || null;
    const ledgerTillWorkspace = ledgerTill ? ledgerTill.workspaceId : null;
    const ledgerAmbiguous = !!(ledgerScope && ledgerScope.ambiguous);
    const sessionSyncRef = useRef(onSessionSync);
    useEffect(() => { sessionSyncRef.current = onSessionSync; }, [onSessionSync]);
    // hydrate the count fields from whatever the server last recorded for this
    // session, so a reload (or a second teller's screen) shows the same counts
    const applyLatestCounts = (result) => {
      const latest = (result && result.latestCounts) || {};
      if (!result || !result.session || result.session.status !== 'open' || !Object.keys(latest).length) return;
      const nextQuick = {}, nextMode = {}, nextAt = {}, nextSaved = {};
      Object.entries(latest).forEach(([currency, record]) => {
        nextQuick[currency] = String(record.counted);
        nextMode[currency] = 'total';
        const at = new Date(record.countedAt).getTime();
        nextAt[currency] = at;
        nextSaved[currency] = { amt: Number(record.counted), ts: at, by: personOf(record.actorId) };
      });
      setQuick(current => ({ ...current, ...nextQuick }));
      setMode(current => ({ ...current, ...nextMode }));
      setCountedAt(current => ({ ...current, ...nextAt }));
      setLastSaved(current => ({ ...current, ...nextSaved }));
    };
    // every session-shaped response (open / count / close) lands here
    const applySession = (result) => {
      const session = (result && result.session) || null;
      setServerSession(session);
      applyLatestCounts(result);
      if (sessionSyncRef.current) sessionSyncRef.current(session);
      return session;
    };
    /* What the till earned on the trading day it is in. Re-read whenever the
       session changes, because the figure on a closed-out banner has to be
       the one the ledger holds for the day that was just closed. */
    useEffect(() => {
      if (!serverBacked || !window.CDOS.Backend) { setDayBook({ loading: false, summary: null, error: 'this desk is not on the ledger' }); return; }
      let live = true;
      window.CDOS.Backend.loadLedgerSummary(window.CDOS.businessDayWindow(1))
        .then(summary => { if (live) setDayBook({ loading: false, summary, error: '' }); })
        .catch(error => { if (live) setDayBook({ loading: false, summary: null, error: (error && error.message) || 'the ledger could not be reached' }); });
      return () => { live = false; };
    }, [serverBacked, deskFacts, serverSession && serverSession.sessionId, serverSession && serverSession.status]);
    const refreshTill = () => {
      if (!serverBacked || !window.CDOS.Backend) return Promise.resolve(null);
      return window.CDOS.Backend.loadTill().then(result => {
        setServerBalances(result.balances || {});
        setServerBalanceError('');
        return applySession(result);
      });
    };
    useEffect(() => {
      if (!serverBacked || !window.CDOS.Backend) {
        setServerBalances(null);
        setServerSession(null);
        setServerBalanceError('');
        return;
      }
      let active = true;
      window.CDOS.Backend.loadTill()
        .then(result => {
          if (!active) return;
          setServerBalances(result.balances || {});
          setServerBalanceError('');
          applySession(result);
        })
        .catch(error => {
          if (!active) return;
          setServerBalances(null);
          setServerSession(null);
          setServerBalanceError(error.message || 'Server balances unavailable');
        });
      return () => { active = false; };
      // cashVersion ticks on every posted movement, so the expected float on
      // this screen follows money leaving the vault without a remount; the
      // ledger workspace joins it because the first fetch can happen before the
      // OS has learned which till the server is answering for
    }, [serverBacked, station && station.tillId, day && day.num, cashVersion, ledgerTillWorkspace]);

    // opening the till is an act, not a side effect of looking at the screen
    const openTill = async () => {
      if (!serverBacked || !window.CDOS.Backend || tillBusy) return;
      setTillBusy('open');
      setSessionErr('');
      try {
        const session = applySession(await window.CDOS.Backend.openTillSession());
        const balances = await window.CDOS.Backend.loadTillBalances();
        setServerBalances(balances.balances || {});
        setServerBalanceError('');
        log && log('Till opened', `Session ${session ? session.sessionNumber : '—'} · ${tillNm}`);
      } catch (error) {
        setSessionErr(error.message || 'The server would not open this till.');
      } finally {
        setTillBusy('');
      }
    };

    /* A new session starts uncounted. Carrying the closed session's figures
       forward would let somebody close a fresh drawer without touching it,
       and the close writes those figures back as the balance. */
    const clearAllCounts = () => {
      setQuick({}); setCounts({}); setCountedAt({}); setRevealExp({});
      try { localStorage.setItem(CKEY, '{}'); localStorage.setItem('cdos_till_quick', '{}'); localStorage.setItem('cdos_till_counted_at', '{}'); } catch (e) {}
    };
    const startNextSession = async () => {
      if (opening || busyRef.current) return;
      busyRef.current = true;
      setOpening(true);
      setSessionErr('');
      try {
        clearAllCounts();
        if (onOpenNextDay) await onOpenNextDay();
        if (serverBacked) await refreshTill();
      } catch (error) {
        setSessionErr(error.message || 'The server would not open the next session.');
      } finally {
        setOpening(false);
        busyRef.current = false;
      }
    };
    // switch tills at THIS location only (same logic as the header) — change store = sign out
    const [tillMenu, setTillMenu] = useState(false);
    const tillMenuRef = useRef(null);
    useEffect(() => { if (!tillMenu) return; const h = (e) => { if (tillMenuRef.current && !tillMenuRef.current.contains(e.target)) setTillMenu(false); }; document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, [tillMenu]);
    const _ab = (branches || []).find(b => b.id === (station && station.branchId)) || (branches || [])[0];
    const _tills = ((_ab && _ab.tills) || []).filter(t => t.status !== 'closed');
    const [pendingTill, setPendingTill] = useState(null);   // option key awaiting switch confirmation
    const [switchNote, setSwitchNote] = useState('');       // what the last switch did to the screen
    /* THE TILLS THIS MENU MAY OFFER.
       On a server-backed desk they are the ledger's, named the way the ledger
       names them, and picking one re-points the ledger. The menu used to list
       this browser's own demo tills and announce who was on them from a roster
       compiled into the page — "Till 2 — Express · M. Costa on now" for a
       drawer that had never held a session and a person the ledger has never
       heard of. Occupancy now comes from the till's session or is left blank;
       see tillOccupant in cdos-backend.js. */
    const onLedgerTills = !!(ledgerScope && (ledgerScope.workspaces || []).length && onSelectLedgerTill);
    const occupantOf = (session) => (window.CDOS.Backend && window.CDOS.Backend.tillOccupant ? window.CDOS.Backend.tillOccupant(session) : '');
    const tillOptions = onLedgerTills
      ? ledgerScope.workspaces.map(w => ({
          key: w.workspaceId,
          name: w.tillId,
          occupant: occupantOf(w.session),
          note: w.session && w.session.status === 'open' ? '' : (w.session ? 'not open' : 'never opened'),
          on: !!(ledgerTill && ledgerTill.workspaceId === w.workspaceId),
        }))
      : _tills.map(t => ({
          key: t.id,
          name: t.name,
          // the desk's own record of who is POSTED here, said as that rather
          // than as a live session this browser has no way to know about
          occupant: '',
          note: t.teller ? 'posted to ' + t.teller : '',
          on: !!(station && station.tillId === t.id),
        }));
    const activeOption = tillOptions.find(o => o.on) || null;
    const pickTill = (key) => { setTillMenu(false); if (activeOption && key === activeOption.key) return; setPendingTill(key); };
    const confirmPickTill = async () => {
      const key = pendingTill;
      setPendingTill(null);
      if (!key) return;
      if (!onLedgerTills) {
        setStation && setStation({ branchId: _ab.id, tillId: key });
        const t = (_ab.tills || []).find(x => x.id === key) || {};
        log && log('Till switched', (_ab.name || '') + ' · ' + (t.name || ''));
        return;
      }
      try {
        const resolved = await onSelectLedgerTill(key);
        if (!resolved) return;
        /* A count typed into these fields and not yet saved was counted at
           the drawer being left, and it is the one thing on this screen that
           could still be carried onto the wrong till. Nothing was posted, so
           dropping it loses no record; carrying it would put one drawer's
           cash into another drawer's variance. The fields are then rehydrated
           from whatever the NEW till last recorded, which is the only count
           that belongs to it. */
        const dropped = countedCcys.slice();
        clearAllCounts();
        setMode({});
        setLastSaved({});
        try { localStorage.setItem('cdos_till_lastcount_v1', '{}'); localStorage.setItem('cdos_till_mode', '{}'); } catch (e) {}
        setServerBalances(resolved.balances || {});
        setServerBalanceError('');
        applySession(resolved);
        setSessionErr('');
        setSwitchNote(`Now counting ${resolved.tillId}.` + (dropped.length
          ? ` The ${dropped.join(', ')} figure${dropped.length === 1 ? '' : 's'} on screen stayed with the drawer you counted ${dropped.length === 1 ? 'it' : 'them'} at — count this one afresh.`
          : ''));
      } catch (error) {
        // the OS logs the refusal; the drawer says it out loud, because the
        // screen deliberately did not move and that needs explaining
        setSessionErr(error.message || 'The ledger would not move this session to that till.');
      }
    };
    /* Open on the desk's own currency where the pack has told us what it
       is. This opened on CAD everywhere, which on a Dubai desk meant the
       first drawer a teller saw was one they do not keep. */
    const [ccy, setCcy] = useState(() => { const pack = window.CDOS.deskPack(); return (pack && CCYS.includes(pack.homeCurrency)) ? pack.homeCurrency : 'CAD'; });
    const [counts, setCounts] = useState(() => { try { return JSON.parse(localStorage.getItem(CKEY) || '{}') || {}; } catch (e) { return {}; } });
    const [quick, setQuick] = useState(() => { try { return JSON.parse(localStorage.getItem('cdos_till_quick') || '{}') || {}; } catch (e) { return {}; } });
    const [mode, setMode] = useState(() => { try { return JSON.parse(localStorage.getItem('cdos_till_mode') || '{}') || {}; } catch (e) { return {}; } });
    // per-currency timestamp of when each drawer was last counted (ms epoch)
    const [countedAt, setCountedAt] = useState(() => { try { return JSON.parse(localStorage.getItem('cdos_till_counted_at') || '{}') || {}; } catch (e) { return {}; } });
    // last SAVED count per currency — amount + when + by whom (survives reloads)
    const [lastSaved, setLastSaved] = useState(() => { try { return JSON.parse(localStorage.getItem('cdos_till_lastcount_v1') || '{}') || {}; } catch (e) { return {}; } });
    // blind-count: expected float stays blurred per-currency until the teller reveals it
    const [revealExp, setRevealExp] = useState({});
    const [confirmClose, setConfirmClose] = useState(false);
    const [, forceTick] = useState(0); // ticks every 30s so "x min ago" labels stay fresh
    const [history, setHistory] = useState(loadHistory);
    // ---- shift operator (who's on the drawer) ----
    const tillId = (station && station.tillId) || 'main';
    // what to call this drawer in a log line: the ledger's name for it when
    // there is a ledger, because that is the drawer the entry is about
    const tillNm = (serverBacked && ledgerTill && ledgerTill.tillId)
      || stationTill || (((_ab && _ab.tills) || []).find(t => t.id === tillId) || {}).name || 'This till';
    const roster = useMemo(() => ((settings && settings.employees && settings.employees.length ? settings.employees : STAFF) || []).filter(s => s.active !== false), [settings]);
    const requireCount = !!(settings && settings.requireCountOnHandoff);
    // blind count: expected float stays blurred until revealed (Settings › Cash drawer)
    const blind = !(settings && settings.tillBlindCount === false);
    const [shifts, setShifts] = useState(() => { try { return JSON.parse(localStorage.getItem(SHIFT_KEY) || '{}') || {}; } catch (e) { return {}; } });
    const [handoffs, setHandoffs] = useState(() => { try { return JSON.parse(localStorage.getItem(HANDOFF_KEY) || '[]') || []; } catch (e) { return []; } });
    const [handoffOpen, setHandoffOpen] = useState(false);
    // the operator strip is usually the same person all day, so let it be dismissed
    // to a small chip; the owner can pop it back open when the drawer changes hands.
    const [stripOpen, setStripOpen] = useState(() => { try { return localStorage.getItem('cdos_till_strip_open') !== '0'; } catch (e) { return true; } });
    useEffect(() => { try { localStorage.setItem('cdos_till_strip_open', stripOpen ? '1' : '0'); } catch (e) {} }, [stripOpen]);
    useEffect(() => { try { localStorage.setItem(SHIFT_KEY, JSON.stringify(shifts)); } catch (e) {} }, [shifts]);
    useEffect(() => { try { localStorage.setItem(HANDOFF_KEY, JSON.stringify(handoffs)); } catch (e) {} }, [handoffs]);
    /* WHO IS ON THIS DRAWER.

       On a server-backed desk the answer is the session's, and only the
       session's: the person who opened this till, at the time the ledger
       recorded. The strip used to read a local shift map seeded from the demo
       roster's `teller` field, so switching drawers renamed the operator to
       "M. Costa" while A. Singh was signed in and the server was recording
       actor_id = a.singh — the screen and the audit trail naming two different
       people over the same cash. When the ledger has no open session for this
       till there is no operator to name, and the strip says so rather than
       falling back to a roster. */
    const serverShift = serverBacked && serverSession && serverSession.status === 'open'
      ? { operator: personOf(serverSession.openedBy), role: '', since: new Date(serverSession.openedAt).getTime() }
      : null;
    const current = serverBacked ? serverShift : shifts[tillId];
    // seed the operator for this till on first view — from the till's assigned
    // teller, else me. Local desks only: with a ledger, see above.
    useEffect(() => {
      if (serverBacked) return;
      if (!shifts[tillId] && tillId) {
        const seededName = (((_ab && _ab.tills) || []).find(t => t.id === tillId) || {}).teller || me.name;
        const r = roster.find(s => s.name === seededName);
        setShifts(o => ({ ...o, [tillId]: { operator: seededName, role: r ? r.role : (seededName === me.name ? me.role : 'Cashier'), since: Date.now() } }));
      }
    }, [tillId, serverBacked]);
    const doHandoff = (rec) => {
      setHandoffs(h => [rec, ...h].slice(0, 200));
      setShifts(o => ({ ...o, [tillId]: { operator: rec.to, role: rec.toRole, since: rec.atMs } }));
      log && log('Drawer handed off', `${tillNm} · ${rec.from} → ${rec.to}${rec.counted ? (Math.abs(rec.variance || 0) < 0.005 ? ' · counted ✓' : ' · counted · ' + (rec.variance > 0 ? '+' : '') + fmtHome(rec.variance)) : ' · no count'}`);
      setHandoffOpen(false);
    };
    const sinceOperator = current && current.since ? (() => { const m = Math.round((Date.now() - current.since) / 60000); if (m < 1) return 'just now'; if (m < 60) return `${m} min`; const h = Math.floor(m / 60); return `${h}h ${m % 60}m`; })() : '';
    const [viewDay, setViewDay] = useState(null);
    const [saved, setSaved] = useState(false);
    const [opening, setOpening] = useState(false);
    const [closing, setClosing] = useState(false);
    // when the day's open/closed state actually flips, clear the transient
    // button flags — backstop in case a render slips through
    useEffect(() => { setOpening(false); setClosing(false); busyRef.current = false; }, [day && day.closed, day && day.num]);
    // synchronous re-entrancy lock: a quick second click (or one that lands
    // before React re-renders) reads a stale `opening`/`closing` and slips past
    // the state guard — this ref blocks it in the same tick so we never queue a
    // second open/close and never get stuck mid-animation on a later loop.
    const busyRef = useRef(false);
    const saveLock = useRef(0);
    const savedTimer = useRef(null);
    useEffect(() => () => { if (savedTimer.current) clearTimeout(savedTimer.current); }, []);
    useEffect(() => { try { localStorage.setItem(CKEY, JSON.stringify(counts)); } catch (e) {} }, [counts]);
    useEffect(() => { try { localStorage.setItem('cdos_till_quick', JSON.stringify(quick)); } catch (e) {} }, [quick]);
    useEffect(() => { try { localStorage.setItem('cdos_till_mode', JSON.stringify(mode)); } catch (e) {} }, [mode]);
    useEffect(() => { try { localStorage.setItem('cdos_till_counted_at', JSON.stringify(countedAt)); } catch (e) {} }, [countedAt]);
    useEffect(() => { const id = setInterval(() => forceTick(t => t + 1), 30000); return () => clearInterval(id); }, []);
    const ccyMode = (c) => mode[c] || (settings && settings.tillCountMode) || 'denom';

    // stamp the moment a drawer was last touched, so we can show staleness
    const stampCount = (c) => setCountedAt(o => ({ ...o, [c]: Date.now() }));
    const clearCount = (c) => { setQuick(o => ({ ...o, [c]: '' })); setCounts(o => ({ ...o, [c]: {} })); setCountedAt(o => { const n = { ...o }; delete n[c]; return n; }); };
    const setCount = (c, idx, val) => { setCounts(o => ({ ...o, [c]: { ...(o[c] || {}), [idx]: val } })); stampCount(c); };
    // relative + absolute labels for "last counted"
    const sinceLabel = (ts) => { if (!ts) return null; const s = Math.max(0, Math.round((Date.now() - ts) / 1000)); if (s < 45) return 'just now'; const m = Math.round(s / 60); if (m < 60) return `${m} min ago`; const h = Math.floor(m / 60); if (h < 24) return `${h}h ${m % 60}m ago`; const d = Math.floor(h / 24); return `${d} day${d > 1 ? 's' : ''} ago`; };
    const atLabel = (ts) => ts ? new Date(ts).toLocaleTimeString('en-CA', { hour: '2-digit', minute: '2-digit', hour12: false }) : null;
    // total held per currency — quick-entered total overrides the denomination count
    const denTotal = (c) => (DEN[c] || []).reduce((s, [v], i) => s + v * (parseInt((counts[c] || {})[i], 10) || 0), 0);
    const ccyTotal = (c) => ccyMode(c) === 'total' ? (parseFloat(quick[c]) || 0) : denTotal(c);
    const isCounted = (c) => ccyMode(c) === 'total' ? (parseFloat(quick[c]) > 0) : Object.values(counts[c] || {}).some(v => parseInt(v, 10) > 0);
    const countedCcys = useMemo(() => CCYS.filter(isCounted), [counts, quick, mode]);
    const grandHome = useMemo(() => totalHome(countedCcys.map(c => [ccyTotal(c), c])), [counts, quick, mode, countedCcys, homeCcy]);

    // expected float per currency — DERIVED from the one shared source of truth
    // (opening baseline + wholesale receipts + posted ledger legs, void-aware).
    // Identical to the figure the Vault shows because both call position().
    const serverBalancesReady = !serverBacked || serverBalances !== null;
    // the currencies the server actually keeps a till balance for. The close
    // must name exactly this set — a superset is rejected, and a subset means
    // an untouched currency would be written back at whatever we sent.
    const serverCcys = useMemo(() => Object.keys(serverBalances || {}).sort(), [serverBalances]);
    const sessionOpen = !serverBacked || !!(serverSession && serverSession.status === 'open');
    const sessionClosed = serverBacked && !!(serverSession && serverSession.status === 'closed');
    // "the book is shut" and "no session has been opened yet" are different
    // states and must not render the same. The OS marks the day closed when
    // there is no session, because the ledger will refuse to post — but only a
    // genuinely closed session earns the closed-out screen.
    const bookClosed = serverBacked ? sessionClosed : !!(day && day.closed);
    /* What the drawer is expected to hold, or null when nothing can say.
       Null, not zero: zero is a claim about somebody's cash and only the
       ledger gets to make it. A currency the ledger has never heard of, and
       a desk running with no ledger at all, both have no expected float —
       and a count sheet that prints 0.00 there invites a teller to write a
       variance against a number nobody stood behind. See ABSENT_FIGURES.md. */
    const expectedOf = (c) => serverBacked && serverBalances
      && Object.prototype.hasOwnProperty.call(serverBalances, c)
      ? Number(serverBalances[c])
      : null;
    // reveal + read out the expected float (audible cue for a blind / hands-busy count)
    const announceExpected = (c) => {
      try {
        const amt = expectedOf(c);
        const AC = window.AudioContext || window.webkitAudioContext;
        if (AC) { const ctx = new AC(); const o = ctx.createOscillator(); const g = ctx.createGain(); o.connect(g); g.connect(ctx.destination); o.type = 'sine'; o.frequency.value = 880; g.gain.value = 0.06; o.start(); g.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.18); o.stop(ctx.currentTime + 0.2); }
      } catch (e) {}
    };
    const toggleReveal = (c) => { const nx = !revealExp[c]; setRevealExp(o => ({ ...o, [c]: nx })); if (nx) announceExpected(c); };

    /* ---------------- COUNT TAB ---------------- */
    const dlist = DEN[ccy] || [];
    const bills = dlist.map((d, i) => ({ v: d[0], t: d[1], i })).filter(d => d.t === 'bill');
    const coins = dlist.map((d, i) => ({ v: d[0], t: d[1], i })).filter(d => d.t === 'coin');
    const persistLocalSnapshot = () => {
      const now = Date.now();
      const byCcy = {}; let grand = 0;
      countedCcys.forEach(c => { const t = ccyTotal(c); byCcy[c] = t; });
      const grandLocal = totalHome(countedCcys.map(c => [ccyTotal(c), c]));
      /* Filed under the TRADING DAY, not the wall clock. A count taken at
         00:20 on a session opened the previous evening belongs to that
         session's business date, and filing it under the calendar date is
         how a drawer's history came to show two rows for one count. */
      const bookDate = (serverSession && serverSession.businessDate) || window.CDOS.businessDate();
      const snap = { byCcy, grand: grandLocal == null ? null : Math.round(grandLocal), currency: homeCcy || null, at: new Date().toLocaleString('en-CA', { hour12: false }).replace(',', ''), by: me.name, denoms: JSON.parse(JSON.stringify(counts)) };
      setHistory(h => { const n = { ...h, [bookDate]: snap }; try { localStorage.setItem(HKEY, JSON.stringify(n)); } catch (e) {} return n; });
      // remember each currency's saved count — the reconcile table shows this as
      // "last count": the amount that was on record, when, and by whom
      setLastSaved(o => { const n = { ...o }; countedCcys.forEach(c => { n[c] = { amt: ccyTotal(c), ts: now, by: me.name }; }); try { localStorage.setItem('cdos_till_lastcount_v1', JSON.stringify(n)); } catch (e) {} return n; });
      log && log('Drawer counted', `${grandLocal == null ? countedCcys.join(', ') : fmt(grandLocal, homeCcy)} across ${countedCcys.length} currenc${countedCcys.length === 1 ? 'y' : 'ies'}`);
      setSaved(true);
      savedTimer.current = setTimeout(() => setSaved(false), 1900);
    };
    const saveSnapshot = async () => {
      const now = Date.now();
      if (saved || now - saveLock.current < 1800 || !countedCcys.length) return;
      saveLock.current = now;
      if (serverBacked) {
        // the server only accepts counts for currencies it holds a balance for;
        // anything else in the drawer stays a local figure until the ledger
        // carries that currency
        const serverCounts = Object.fromEntries(
          countedCcys.filter(c => serverCcys.includes(c)).map(c => [c, ccyTotal(c).toFixed(2)]));
        if (Object.keys(serverCounts).length) {
          if (!serverSession || serverSession.status !== 'open') {
            setSessionErr('Open the till before saving a count — there is no session to record it against.');
            saveLock.current = 0;
            return;
          }
          setTillBusy('count');
          try {
            applySession(await window.CDOS.Backend.saveTillCount({
              idempotencyKey: `till-count-${serverSession.sessionId}-${now}`,
              counts: serverCounts,
            }));
            setSessionErr('');
          } catch (error) {
            setSessionErr(error.message || 'The server rejected the count.');
            log && log('Drawer count failed', error.message || 'Server rejected the count');
            saveLock.current = 0;
            return;
          } finally {
            setTillBusy('');
          }
        }
      }
      persistLocalSnapshot();
    };

    /* ---------------- RECONCILE / CLOSE TAB ----------------
       Server-backed, the reconcile table IS the close payload: exactly the
       currencies the server holds a till balance for, no more and no less.
       Locally it stays the old derived list. */
    const reconCcys = serverBacked
      ? serverCcys
      : CCYS.filter(c => expectedOf(c) || countedCcys.includes(c) || c === homeCcy);
    const recon = reconCcys.map(c => { const expected = expectedOf(c); const counted = countedCcys.includes(c) ? ccyTotal(c) : null; const variance = counted == null ? null : counted - expected; return { c, expected, counted, variance }; });
    // counted here but not carried by the server ledger — recorded locally only,
    // and deliberately kept out of the close so it can't disturb a real balance
    const offLedgerCcys = serverBacked ? countedCcys.filter(c => !serverCcys.includes(c)) : [];
    // a drawer is "off" only when its CAD variance exceeds the owner's tolerance (Settings › Cash drawer)
    const tolHome = Math.max(0, +((settings && settings.tillVarianceTol)) || 0);
    /* A drawer is "off" when its variance exceeds the owner's tolerance,
       measured in the desk's own money. Where this screen cannot express the
       variance in that money it falls back to the currency's own units — a
       tolerance nobody can apply is not a reason to declare a drawer clean. */
    const offOf = (r) => { if (r.variance == null) return false; const home = homeOf(r.variance, r.c); return Math.abs(home == null ? r.variance : home) > tolHome + 0.005; };
    const offRows = recon.filter(offOf);
    const countedN = recon.filter(r => r.counted != null).length;
    const closeBlocked = serverBacked
      ? (!serverBalancesReady || !sessionOpen || !recon.length || countedN < recon.length)
      : (!!(settings && settings.requireCountOnClose) && countedN < recon.length);
    // grand totals across all drawers, expressed in CAD, for the reconcile total row + close modal
    /* Cross-currency totals for the reconcile footer. Null where any row
       cannot be expressed in the desk's own money — a total quietly missing
       two of nine currencies looks complete and is not. */
    const totalExpHome = totalHome(recon.filter(r => r.expected != null).map(r => [r.expected, r.c]));
    const totalCountHome = totalHome(recon.filter(r => r.counted != null).map(r => [r.counted, r.c]));
    const totalVarHome = totalHome(recon.filter(r => r.variance != null).map(r => [r.variance, r.c]));
    const doClose = async () => {
      const offSumHome = totalHome(offRows.map(r => [r.variance, r.c]));
      /* WHAT THE DESK MADE TODAY IS NOT THIS SCREEN'S TO WORK OUT.

         What stood here summed `(inAmt × mid) − outAmt` over the browser's
         copy of the day's rows and called it earnings. That is a spread
         against a market mid, which is a different number from what the
         desk earned — the desk earns what the disposal booked against the
         stock's own cost (docs/COST_BASIS.md) — and it was summed over
         `r.date === TODAY`, the WALL CLOCK, on a screen whose whole subject
         is the business date. On a till with one voided $1,000 deal the
         closed banner read "Earned today $223.00".

         The figure now comes from GET /api/ledger/summary over the trading
         day, and is null when the book has nothing to say. It is carried on
         the summary as `earnedHome` with its currency beside it, so nothing
         downstream can render it as a bare dollar amount. */
      const book = dayBook.summary;
      const earnedHome = book && book.earningsHome != null ? Number(book.earningsHome) : null;
      const summary = {
        txns: book ? book.posted : null,
        reversed: book ? book.reversed : null,
        ccys: recon.length,
        counted: countedN,
        offCount: offRows.length,
        offSum: offSumHome == null ? null : Math.round(offSumHome),
        grand: grandHome == null ? null : Math.round(grandHome),
        earnedHome,
        earnedCurrency: (book && book.homeCurrency) || homeCcy || null,
        earnedWhy: book ? (earnedHome == null ? 'nothing posted on this trading day' : null) : (dayBook.error || 'the ledger could not be reached'),
        businessDate: (serverSession && serverSession.businessDate) || window.CDOS.businessDate(),
        note: offRows.length
          ? `${offRows.length} drawer(s) off${offSumHome == null ? '' : ` · ${fmt(offSumHome, homeCcy)} net`}`
          : 'All counted drawers balanced',
      };
      if (serverBacked) {
        if (!serverSession || serverSession.status !== 'open') {
          throw new Error('There is no open till session to close. Reload the drawer.');
        }
        // The close writes each counted figure back as the authoritative till
        // balance. An uncounted currency has no figure — sending a placeholder
        // would silently overwrite real money with a guess, so refuse instead.
        const missing = recon.filter(r => r.counted == null).map(r => r.c);
        if (missing.length) {
          throw new Error(`Count ${missing.join(', ')} before closing — the close writes your count back as the till balance.`);
        }
        applySession(await window.CDOS.Backend.closeTillSession(serverSession.sessionId, {
          idempotencyKey: `till-close-${serverSession.sessionId}`,
          counts: Object.fromEntries(recon.map(r => [r.c, r.counted.toFixed(2)])),
          note: summary.note,
        }));
        const balances = await window.CDOS.Backend.loadTillBalances();
        setServerBalances(balances.balances || {});
        setSessionErr('');
      }
      onCloseDay && onCloseDay(summary);
      persistLocalSnapshot();
    };
    // first click opens a review modal; the irreversible commit lives in confirmAndClose
    const clickClose = () => { if (!canCloseDay || closing || busyRef.current || closeBlocked) return; setConfirmClose(true); };
    const confirmAndClose = async () => {
      if (closing || busyRef.current) return;
      busyRef.current = true;
      setConfirmClose(false);
      setClosing(true);
      try {
        await doClose();
      } catch (error) {
        setSessionErr(error.message || 'The server rejected the till close.');
        log && log('Day close failed', error.message || 'Server rejected the till close');
      } finally {
        setClosing(false);
        busyRef.current = false;
      }
    };

    /* ---------------- HISTORY ---------------- */
    const histKeys = useMemo(() => Object.keys(history).sort().reverse(), [history]);
    /* Days whose total this desk cannot express in its own money are left
       OUT of the sparkline rather than plotted as zero — a dip to nothing on
       a chart of cash held is a story, and it would be a false one. */
    const histSeries = useMemo(() => Object.keys(history).sort().slice(-90).map(k => history[k].grand).filter(v => v != null), [history]);

    /* The close-out banner's three figures, each with the reason it is
       absent when it is. The ledger is asked directly rather than trusting
       what the close stored, so a screen reopened days later still shows the
       book's answer for that trading day rather than a frozen copy. */
    const closedBook = dayBook.summary;
    const closedEarned = closedBook && closedBook.earningsHome != null ? Number(closedBook.earningsHome)
      : (day.summary && day.summary.earnedHome != null ? Number(day.summary.earnedHome) : null);
    const closedEarnedCcy = (closedBook && closedBook.homeCurrency) || (day.summary && day.summary.earnedCurrency) || homeCcy;
    const closedEarnedWhy = dayBook.error || (day.summary && day.summary.earnedWhy) || (dayBook.loading ? 'reading the ledger…' : 'nothing posted on this trading day');
    const closedTxns = closedBook ? closedBook.posted : (day.summary && day.summary.txns != null ? day.summary.txns : null);

    const TABS = [['count', 'Cash drawer', 'wallet'], ['reconcile', 'Reconcile & close', 'coins'], ['history', 'History', 'clock']];

    return (<div className="flex flex-col" style={{ height: '100%', background: CD.paper, position: 'relative' }}>
      {/* header + tabs */}
      <div className="px-4 pt-3 flex-none" style={{ background: CD.panel }}>
        <div className="flex items-center gap-2.5 pb-3">
          <span className="grid place-items-center" style={{ width: 30, height: 30, background: '#fff', boxShadow: 'inset 0 0 0 1px ' + CD.line, borderRadius: 8 }}><Ic n="tilldrawer" s={17} c="var(--cd-on-ink)" /></span>
          <div className="min-w-0"><div className="font-semibold leading-tight" style={{ color: CD.ink }}>Cash Drawer</div><div className="text-[11px] flex items-center gap-1 flex-wrap" style={{ color: CD.mute }}>
            {stationName ? <b style={{ color: CD.ink }}>{stationName}</b> : null}
            {stationName ? <span>·</span> : null}
            {/* The title of this window IS the ledger's name for the drawer
                when there is a ledger. It used to be a local till name that
                could — and did — say "Till 2 — Express" over Till 1's money. */}
            {tillOptions.length ? (<span ref={tillMenuRef} style={{ position: 'relative', display: 'inline-flex' }}>
              <button onClick={() => setTillMenu(o => !o)} title="Switch till at this location" style={{ display: 'inline-flex', alignItems: 'center', gap: 3, border: 0, background: tillMenu ? 'var(--cd-hover)' : 'transparent', borderRadius: 6, padding: '1px 5px', margin: '0 -3px', cursor: 'pointer', color: 'inherit', fontWeight: 600 }}>{(activeOption && activeOption.name) || (onLedgerTills ? 'Till' : stationTill || 'Till')}<span style={{ display: 'inline-flex', transform: 'rotate(90deg)' }}><Ic n="chev" s={10} c={CD.faint} /></span></button>
              {tillMenu && (<div style={{ position: 'absolute', top: '100%', left: 0, marginTop: 5, width: 230, background: 'var(--cd-panel)', border: `1px solid ${CD.line}`, borderRadius: 11, boxShadow: '0 14px 36px var(--cd-shade)', zIndex: 9999, overflow: 'hidden' }}>
                <div style={{ padding: '8px 11px', fontSize: 9.5, textTransform: 'uppercase', letterSpacing: '0.08em', color: CD.faint, fontFamily: 'Space Mono, monospace', borderBottom: `1px solid ${CD.lineSoft}` }}>{(onLedgerTills && ledgerTill && ledgerTill.branchName) || (_ab ? _ab.name : '')} · switch till</div>
                {tillOptions.map(o => (<button key={o.key} onClick={() => pickTill(o.key)} style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 8, padding: '7px 11px', background: o.on ? CD.brassSoft : 'transparent', border: 0, cursor: 'pointer', textAlign: 'left' }}><Ic n="wallet" s={12} c={o.on ? CD.ink : CD.mute} /><span style={{ flex: 1, fontSize: 12, color: CD.ink }}>{o.name}{o.on ? <span style={{ color: CD.faint }}> · you</span> : (o.occupant ? <span style={{ color: CD.faint }}> · {o.occupant} on now</span> : (o.note ? <span style={{ color: CD.faint }}> · {o.note}</span> : null))}</span>{o.on && <Ic n="check" s={12} c={CD.ink} />}</button>))}
                <div style={{ padding: '7px 11px', borderTop: `1px solid ${CD.lineSoft}`, fontSize: 10, color: CD.faint, display: 'flex', alignItems: 'center', gap: 5 }}><Ic n="logout" s={10} c={CD.faint} /> To change store, sign out &amp; back in.</div>
              </div>)}
            </span>) : (stationTill ? <b style={{ color: CD.ink }}>{stationTill}</b> : null)}
            <span>· {serverBacked ? `Session ${serverSession ? serverSession.sessionNumber : '—'}` : `Day ${day && day.num || 1}`}{bookClosed ? ' · closed' : serverBacked && !serverSession ? ' · not opened' : ''} · {countedCcys.length} drawer(s) counted</span>
            {serverBacked && <span style={{ color: serverBalanceError ? CD.flag : serverBalancesReady ? CD.green : CD.mute }}>· {serverBalanceError ? 'server balances unavailable' : serverBalancesReady ? 'server balances live' : 'loading server balances'}</span>}
            {/* the ledger's own name for the drawer these figures belong to —
                the picker to the left names a local till, which is a different
                thing and can disagree */}
            {serverBacked && ledgerTill && <span title="The till the server ledger is answering for" style={{ color: ledgerAmbiguous ? CD.brass : CD.faint }}>· ledger till <b style={{ fontFamily: 'Space Mono, monospace', color: ledgerAmbiguous ? CD.brass : CD.mute }}>{ledgerTill.tillId}</b></span>}
          </div></div>
          <div className="flex items-center gap-1.5 flex-none ml-auto">
            {(() => {
              // the drawer's rail balance — changes the moment cash is issued or returned.
              // floats are ISSUED AT THE VAULT (Vault / Branch Network), never from here:
              // the drawer only shows what it holds and links to where floats happen.
              const _tRec = ((_ab && _ab.tills) || []).find(t => t.id === (station && station.tillId));
              // server-backed, the drawer holds what the LEDGER says it holds —
              // the local rail figure is a demo store and would quietly disagree
              const _tc = serverBacked
                ? (serverBalances ? totalHome(serverCcys.map(c => [Number(serverBalances[c]), c])) : null)
                : (_tRec && window.CDOS._stations && window.CDOS._stations.tillCad ? window.CDOS._stations.tillCad(_tRec) : null);
              return (<>
                {_tc != null && <span className="flex items-center gap-1.5 px-2.5 py-1.5 text-[11px]" style={{ border: `1px solid ${CD.lineSoft}`, borderRadius: 8, background: 'var(--cd-chip)', color: CD.mute }} title={serverBacked ? 'What the server ledger says this drawer holds, across every ledger currency' : 'What this drawer holds on the cash rail — floats are issued and returned at the vault'}>{serverBacked ? 'In drawer · ledger' : 'Float in drawer'} <b style={{ color: CD.ink, fontFamily: 'Space Mono, monospace', fontVariantNumeric: 'tabular-nums' }}>{fmtHome(_tc)}</b></span>}
                {/* the drawer's own end of the cash rail — same modal, same
                    ledger movement, preset to this branch and this till */}
                {onMoveCash && <button onClick={() => onMoveCash({ kind: 'issue', bId: _ab && _ab.id, tId: station && station.tillId })} title="Issue a float from the vault, or return cash to it — recorded on the ledger" className="flex items-center gap-1.5 px-2.5 py-1.5 text-[11px] font-medium" style={{ border: `1px solid ${CD.line}`, borderRadius: 8, color: CD.ink, background: 'var(--cd-panel)' }}><Ic n="swap" s={13} c={CD.mute} /> Move cash</button>}
                <button onClick={() => onOpenVault && onOpenVault()} title="Floats are issued & returned at the vault" className="flex items-center gap-1 text-[11px] px-2.5 py-1.5" style={{ color: CD.mute, border: 0, background: 'transparent' }}><Ic n="vaultsafe" s={13} c={CD.mute} /> Vault ›</button>
              </>);
            })()}
          </div>
        </div>
        <div className="fld-bar" style={{ '--ft': '#17140F', margin: '2px -16px 0', padding: '0 16px' }}>
          {TABS.map(([id, label, ic]) => <button key={id} onClick={() => setTab(id)} className={'fld-tab' + (tab === id ? ' on' : '')}><Ic n={ic} s={13} c={tab === id ? 'var(--cd-on-ink)' : CD.mute} /> {label}</button>)}
        </div>
      </div>

      {/* ===== SESSION STRIP — the open-and-close, made visible =====
          A server-backed drawer is either inside an open session or it is not,
          and nothing about the day makes sense until the teller can see which.
          No session is not an error state — it is the state before someone
          opens the till. */}
      {serverBacked && (<>
        {/* This branch runs more than one drawer, so say which one plainly.
            The picker above now names the ledger's tills, so the title and the
            money agree by construction — this is a reminder of which drawer is
            being counted, not a warning that two things disagree. */}
        {ledgerAmbiguous && ledgerTill && (
          <div className="flex items-start gap-2 px-4 py-2.5 flex-none text-[11.5px]" style={{ background: CD.brassSoft, borderBottom: `1px solid ${CD.line}`, color: CD.ink }}>
            <Ic n="alert" s={13} c={CD.brass} />
            <span className="flex-1 min-w-0">
              This branch has {ledgerScope.workspaces.length} tills on the ledger. Everything on this screen — the expected figures, the counts, the close — belongs to <b style={{ fontFamily: 'Space Mono, monospace' }}>{ledgerTill.tillId}</b>
              {ledgerTill.branchName ? ` at ${ledgerTill.branchName}` : ''}.
              <span style={{ color: CD.mute }}> Use the till name in the header to move to another drawer; the ledger moves with it.</span>
            </span>
          </div>
        )}
        {switchNote && (
          <div className="flex items-center gap-2 px-4 py-2 flex-none text-[11.5px]" style={{ background: CD.brassSoft, borderBottom: `1px solid ${CD.line}`, color: CD.ink }}>
            <Ic n="wallet" s={13} c={CD.brass} />
            <span className="flex-1 min-w-0">{switchNote}</span>
            <button onClick={() => setSwitchNote('')} className="flex-none grid place-items-center" style={{ width: 22, height: 22, borderRadius: 6, border: 0, background: 'transparent', color: CD.mute }}><Ic n="x" s={12} c={CD.mute} /></button>
          </div>
        )}
        {!serverSession && serverBalancesReady && (
          <div className="flex items-center gap-3 px-4 py-2.5 flex-none" style={{ background: CD.brassSoft, borderBottom: `1px solid ${CD.line}` }}>
            <span className="grid place-items-center flex-none" style={{ width: 28, height: 28, borderRadius: 8, background: CD.brass }}><Ic n="lock" s={14} c="var(--cd-on-ink)" /></span>
            <div className="flex-1 min-w-0 leading-tight">
              <div className="text-[12.5px] font-semibold" style={{ color: CD.ink }}>This till hasn’t been opened</div>
              <div className="text-[11px]" style={{ color: CD.mute }}>Open it to start a session — counts, cash movements and posted deals all record against it.</div>
            </div>
            <button onClick={openTill} disabled={!!tillBusy} className="flex items-center gap-1.5 px-3.5 py-2 text-[12.5px] font-semibold text-white flex-none" style={{ background: tillBusy === 'open' ? CD.green : CD.ink, borderRadius: 9, cursor: tillBusy ? 'wait' : 'pointer' }}><Ic n={tillBusy === 'open' ? 'checkcircle' : 'power'} s={14} c="var(--cd-on-ink)" /> {tillBusy === 'open' ? 'Opening…' : 'Open the till'}</button>
          </div>
        )}
        {serverSession && sessionOpen && (
          <div className="flex items-center gap-2 px-4 py-1.5 flex-none text-[11px]" style={{ background: 'var(--cd-panel)', borderBottom: `1px solid ${CD.line}`, color: CD.mute }}>
            <span style={{ width: 6, height: 6, borderRadius: '50%', background: CD.green, flex: 'none' }}></span>
            <span>Session <b style={{ color: CD.ink, fontFamily: 'Space Mono, monospace' }}>#{serverSession.sessionNumber}</b> open · {serverSession.businessDate} · opened by {personOf(serverSession.openedBy)} at {clockOf(new Date(serverSession.openedAt).getTime())}</span>
            <span className="ml-auto flex-none" style={{ color: CD.faint }}>{serverCcys.length} ledger currenc{serverCcys.length === 1 ? 'y' : 'ies'} · {countedN}/{recon.length} counted</span>
          </div>
        )}
        {sessionClosed && (
          <div className="flex items-center gap-2 px-4 py-1.5 flex-none text-[11px]" style={{ background: CD.greenSoft, borderBottom: `1px solid ${CD.line}`, color: '#3a7a56' }}>
            <Ic n="lock" s={12} c="#3a7a56" />
            <span>Session <b style={{ fontFamily: 'Space Mono, monospace' }}>#{serverSession.sessionNumber}</b> closed by {personOf(serverSession.closedBy)} at {clockOf(new Date(serverSession.closedAt).getTime())}{serverSession.closeNote ? ` · ${serverSession.closeNote}` : ''}</span>
          </div>
        )}
        {(sessionErr || serverBalanceError) && (
          <div className="flex items-center gap-2 px-4 py-2 flex-none text-[11.5px]" style={{ background: CD.flagSoft, borderBottom: `1px solid ${CD.line}`, color: CD.flag }}>
            <Ic n="alert" s={13} c={CD.flag} />
            <span className="flex-1 min-w-0">{sessionErr || serverBalanceError}</span>
            <button onClick={() => { setSessionErr(''); refreshTill().catch(error => setServerBalanceError(error.message || 'Server balances unavailable')); }} className="flex-none text-[11px] font-semibold px-2.5 py-1" style={{ border: `1px solid ${CD.flag}`, borderRadius: 7, color: CD.flag, background: 'transparent' }}>Retry</button>
          </div>
        )}
      </>)}

      {/* who's on the drawer — mom-and-pop operator strip (dismissible to a chip) */}
      {/* The modal promises the switch is stamped to the audit trail with your
          name and the time. It is: confirmPickTill awaits the ledger's own
          till-selection call, which writes that row before the screen moves. */}
      {pendingTill && window.CDOS._stations && window.CDOS._stations.ConfirmStationModal && (() => {
        const opt = tillOptions.find(o => o.key === pendingTill);
        return opt ? React.createElement(window.CDOS._stations.ConfirmStationModal, {
          branch: { name: (onLedgerTills && ledgerTill && ledgerTill.branchName) || (_ab && _ab.name) },
          till: { name: opt.name, operator: opt.occupant },
          me,
          onClose: () => setPendingTill(null),
          onConfirm: confirmPickTill,
        }) : null;
      })()}
      {stripOpen ? (
      <div className="flex items-center justify-between gap-2 px-4 py-2 flex-none" style={{ background: 'var(--cd-panel)', borderBottom: `1px solid ${CD.line}` }}>
        <div className="flex items-center gap-2.5 min-w-0">
          <span className="grid place-items-center flex-none font-semibold" style={{ width: 28, height: 28, borderRadius: 8, background: CD.ink, color: 'var(--cd-on-ink)', fontSize: 10.5 }}>{current ? current.operator.split(/[ .]+/).filter(Boolean).map(x => x[0]).join('').slice(0, 2).toUpperCase() : '—'}</span>
          <div className="min-w-0 leading-tight">
            <div className="text-[12.5px]" style={{ color: CD.ink }}><span style={{ color: CD.mute }}>On the drawer:</span> <b>{current ? current.operator : '—'}</b>{current && current.role ? <span className="text-[11px]" style={{ color: CD.faint }}> · {current.role}</span> : null}</div>
            <div className="text-[10.5px]" style={{ color: CD.faint }}>{current && current.since ? `since ${clockOf(current.since)} · ${sinceOperator}` : (serverBacked ? 'nobody on this till — no open session on the ledger' : 'unassigned')}{requireCount ? ' · count required at handoff' : ''}</div>
          </div>
        </div>
        <div className="flex items-center gap-1.5 flex-none">
          <button onClick={() => setHandoffOpen(true)} title={serverBacked ? 'Records a handover note and a count for this desk. Who the LEDGER holds responsible stays the operator on the open session — close it and reopen it as the next person to change that.' : 'Hand the drawer to the next operator'} className="flex items-center gap-1.5 text-[12px] font-medium px-3 py-1.5" style={{ border: `1px solid ${CD.line}`, borderRadius: 8, color: CD.ink, background: CD.panel }}><Ic n="users" s={13} c={CD.mute} /> Hand off</button>
          <button onClick={() => setStripOpen(false)} title="Hide operator bar" className="grid place-items-center" style={{ width: 28, height: 28, borderRadius: 8, border: `1px solid ${CD.line}`, background: CD.panel, color: CD.mute }}><Ic n="x" s={13} c={CD.mute} /></button>
        </div>
      </div>
      ) : (
      <div className="flex-none px-4 py-1.5" style={{ background: 'var(--cd-panel)', borderBottom: `1px solid ${CD.line}` }}>
        <button onClick={() => setStripOpen(true)} title="Show who's on the drawer" className="flex items-center gap-1.5 text-[11px] font-medium px-2.5 py-1" style={{ border: `1px solid ${CD.line}`, borderRadius: 999, color: CD.mute, background: CD.panel }}><span className="grid place-items-center flex-none font-semibold" style={{ width: 16, height: 16, borderRadius: 5, background: CD.ink, color: 'var(--cd-on-ink)', fontSize: 8 }}>{current ? current.operator.split(/[ .]+/).filter(Boolean).map(x => x[0]).join('').slice(0, 2).toUpperCase() : '—'}</span>{current ? current.operator : 'Operator'} · on the drawer</button>
      </div>
      )}

      <div className="flex-1 overflow-auto">

        {/* ===== COUNT ===== */}
        {tab === 'count' && (<div className="p-4 pb-0">
          {/* currency chips */}
          <div className="flex flex-wrap gap-1.5 mb-3">
            {CCYS.map(c => { const on = c === ccy; const has = countedCcys.includes(c); return (
              <button key={c} onClick={() => setCcy(c)} className="till-chip flex items-center gap-1.5 px-2.5 py-1.5 text-xs" style={{ borderRadius: 8, border: `1px solid ${on ? CD.ink : CD.line}`, background: on ? CD.ink : CD.panel, color: on ? 'var(--cd-on-ink)' : CD.mute, fontFamily: 'Space Mono, monospace' }}>
                <span style={{ fontFamily: 'system-ui' }}>{flagOf(c)}</span>{c}{has && <span style={{ width: 5, height: 5, borderRadius: '50%', background: on ? 'var(--cd-panel)' : CD.green }}></span>}
              </button>); })}
          </div>
          {/* denomination columns */}
          {/* count mode: by denomination, or a quick total */}
          <div className="flex items-center justify-between mb-3">
            <div className="inline-flex" style={{ border: `1px solid ${CD.line}`, borderRadius: 8, overflow: 'hidden' }}>
              {[['denom', 'Count denominations'], ['total', 'Enter total']].map(([m, l]) => <button key={m} onClick={() => setMode(o => ({ ...o, [ccy]: m }))} className="text-[11.5px] px-3 py-1.5" style={{ background: ccyMode(ccy) === m ? CD.ink : 'transparent', color: ccyMode(ccy) === m ? 'var(--cd-on-ink)' : CD.mute, fontFamily: 'Space Mono, monospace' }}>{l}</button>)}
            </div>
            {isCounted(ccy) && <button onClick={() => clearCount(ccy)} className="text-[11px]" style={{ color: CD.mute }}>Clear {ccy}</button>}
          </div>

          {ccyMode(ccy) === 'total' ? (
            <div className="p-4" style={{ background: CD.panel, border: `1px solid ${CD.line}`, borderRadius: 12 }}>
              <div className="text-[11px] mb-2" style={{ color: CD.mute }}>Skip the breakdown — just enter the total {ccy} you counted in the drawer.</div>
              <div className="flex items-center" style={{ border: `1px solid ${CD.ink}`, borderRadius: 10, maxWidth: 340 }}>
                <span className="px-3 text-sm" style={{ color: CD.mute, fontFamily: 'Space Mono, monospace', borderRight: `1px solid ${CD.line}` }}>{ccy}</span>
                <input type="number" autoFocus value={quick[ccy] ?? ''} onChange={e => { setQuick(o => ({ ...o, [ccy]: e.target.value })); stampCount(ccy); }} placeholder="0.00" className="flex-1 min-w-0 px-3 py-2.5 text-2xl font-bold text-right outline-none" style={{ fontVariantNumeric: 'tabular-nums', fontFamily: 'Space Mono, monospace' }} />
              </div>
              {ccy !== homeCcy && homeOf(parseFloat(quick[ccy]) || 0, ccy) != null && <div className="mt-2 text-[12px]" style={{ color: CD.mute }}>≈ {fmt(homeOf(parseFloat(quick[ccy]) || 0, ccy), homeCcy)}</div>}
            </div>
          ) : (
          <div className="grid md:grid-cols-2 gap-x-5 gap-y-0">
            <div>
              <div className="text-[10px] uppercase tracking-widest mb-1 mt-1" style={{ color: CD.faint, fontFamily: 'Space Mono, monospace' }}>Notes</div>
              {bills.map(d => <DenRow key={d.i} d={d} ccy={ccy} counts={counts} setCount={setCount} />)}
            </div>
            <div>
              <div className="text-[10px] uppercase tracking-widest mb-1 mt-1" style={{ color: CD.faint, fontFamily: 'Space Mono, monospace' }}>Coin</div>
              {coins.map(d => <DenRow key={d.i} d={d} ccy={ccy} counts={counts} setCount={setCount} />)}
            </div>
          </div>
          )}
          {/* per-currency footer — counted (typeable), expected, then the grand total */}
          <div style={{ position: 'sticky', bottom: 0, background: CD.paper, paddingTop: 10, marginTop: 8 }}>
            <div className="flex items-end justify-between gap-4 pt-3" style={{ borderTop: `1px solid ${CD.line}` }}>
              <div>
                <div className="text-[10px] uppercase tracking-widest" style={{ color: CD.faint, fontFamily: 'Space Mono, monospace' }}>{ccy} counted</div>
                <div className="flex items-center" style={{ border: `1px solid ${CD.line}`, borderRadius: 8, maxWidth: 220, marginTop: 3, background: ccyMode(ccy) === 'total' ? 'var(--cd-panel)' : 'transparent' }}>
                  <input type="text" inputMode="decimal" value={ccyMode(ccy) === 'total' ? (quick[ccy] ?? '') : num(ccyTotal(ccy))} readOnly={ccyMode(ccy) !== 'total'} onFocus={() => { if (ccyMode(ccy) !== 'total') { setMode(o => ({ ...o, [ccy]: 'total' })); setQuick(o => ({ ...o, [ccy]: String(denTotal(ccy) || '') })); } }} onChange={e => { setQuick(o => ({ ...o, [ccy]: e.target.value.replace(/[^0-9.]/g, '') })); stampCount(ccy); }} className="min-w-0 px-2.5 py-1.5 text-lg font-bold text-right outline-none bg-transparent" style={{ width: 150, fontVariantNumeric: 'tabular-nums', fontFamily: 'Space Mono, monospace', color: CD.ink, cursor: ccyMode(ccy) !== 'total' ? 'pointer' : 'text' }} />
                  <span className="px-2.5 text-[11px]" style={{ color: CD.mute, fontFamily: 'Space Mono, monospace', borderLeft: `1px solid ${CD.line}` }}>{ccy}</span>
                </div>
                <div className="text-[11px] mt-1.5 flex items-center gap-2 flex-wrap" style={{ color: CD.faint }}>
                  <button onClick={() => toggleReveal(ccy)} title={revealExp[ccy] ? 'Hide expected — keep the count blind' : 'Reveal & read out the expected float'} className="inline-flex items-center gap-1.5" style={{ border: 0, background: 'transparent', padding: 0, cursor: 'pointer', color: 'inherit' }}>
                    <span>Expected</span>
                    <b style={{ color: CD.mute, fontFamily: 'Space Mono, monospace', filter: (blind && !revealExp[ccy]) ? 'blur(6px)' : 'none', transition: 'filter .15s', userSelect: 'none' }}>{num(expectedOf(ccy))} {ccy}</b>
                    {isCounted(ccy) && (() => { const v = ccyTotal(ccy) - expectedOf(ccy); const off = Math.abs(v) > 0.005; return <b style={{ color: revealExp[ccy] ? (off ? CD.flag : CD.green) : CD.faint, fontFamily: 'Space Mono, monospace', filter: (blind && !revealExp[ccy]) ? 'blur(6px)' : 'none', transition: 'filter .15s', userSelect: 'none' }}>{off ? `${v > 0 ? '+' : ''}${num(v)} ${v > 0 ? 'over' : 'short'}` : '\u2713 balanced'}</b>; })()}
                    <Ic n={revealExp[ccy] ? 'power' : 'lock'} s={11} c={CD.faint} />
                  </button>
                  {ccy !== homeCcy && isCounted(ccy) && homeOf(ccyTotal(ccy), ccy) != null && <span>· ≈ {fmt(homeOf(ccyTotal(ccy), ccy), homeCcy)}</span>}
                </div>
                <div className="text-[11px] mt-1" style={{ color: CD.faint }}>{(() => { const ts = countedAt[ccy]; return ts ? <span>Last counted <b style={{ color: CD.mute, fontFamily: 'Space Mono, monospace' }}>{sinceLabel(ts)}</b> · {atLabel(ts)}</span> : <span>Not counted yet today</span>; })()}</div>
              </div>
              <div className="text-right flex-none">
                <div className="text-[10px] uppercase tracking-widest" style={{ color: CD.faint, fontFamily: 'Space Mono, monospace' }}>Total drawer{homeCcy ? ' · ' + homeCcy : ''}</div>
                <div style={{ fontSize: 24, fontWeight: 800, color: grandHome == null ? CD.faint : CD.ink, fontVariantNumeric: 'tabular-nums', lineHeight: 1.1 }} title={grandHome == null ? 'This desk keeps its books in a currency the rate board cannot total against' : ''}>{fmtHome(grandHome)}</div>
                <button onClick={saveSnapshot} disabled={saved || !sessionOpen || tillBusy === 'count'} title={!sessionOpen ? 'Open the till before saving a count' : ''} className="till-save mt-2 flex items-center gap-1.5 px-3.5 py-2 text-sm font-semibold text-white" style={{ background: saved ? CD.green : sessionOpen ? CD.ink : 'var(--cd-disabled)', borderRadius: 9, marginLeft: 'auto', cursor: sessionOpen ? 'pointer' : 'not-allowed', transition: 'background .25s ease, transform .12s ease' }}><Ic n={saved ? 'checkcircle' : 'checkcircle'} s={15} c="var(--cd-on-ink)" /> {saved ? 'Saved' : tillBusy === 'count' ? 'Saving…' : 'Save count'}</button>
              </div>
            </div>
            <div className="text-[10.5px] py-2" style={{ color: CD.faint }}>Counting denominations updates the total live · or tap the counted figure to type it in directly · expected comes from the ledger float.</div>
          </div>
        </div>)}

        {/* THE CLOSED-OUT FIGURES. Read from the ledger for the trading day,
            with the day summary the close wrote as a fallback — and both are
            allowed to be absent. `day.summary.earned` (a browser-derived
            spread) is deliberately NOT consulted: the close no longer writes
            it, and an old one left in localStorage is exactly the stale
            second figure this pass exists to remove. */}
        {/* ===== RECONCILE / CLOSE ===== */}
        {tab === 'reconcile' && (bookClosed ? (<div className="p-5 flex flex-col" style={{ minHeight: '100%' }}>
          {/* pleasant green closed banner */}
          <div className="flex items-start gap-3 p-4" style={{ background: CD.greenSoft, border: `1px solid ${CD.green}`, borderRadius: 14 }}>
            <span className="grid place-items-center flex-none" style={{ width: 40, height: 40, background: CD.green, borderRadius: 11 }}><Ic n="checkcircle" s={21} c="var(--cd-on-ink)" /></span>
            <div className="flex-1">
              <div className="text-[15px] font-semibold" style={{ color: '#1c5c3a' }}>{serverBacked && serverSession ? `Session #${serverSession.sessionNumber}` : `Day ${day.num || 1}`} closed — nicely done</div>
              <div className="text-[12px] mt-0.5" style={{ color: '#3a7a56' }}>Closed by {(serverBacked && serverSession && serverSession.closedBy) ? personOf(serverSession.closedBy) : (day.closedBy || '—')} · {(serverBacked && serverSession && serverSession.closedAt) ? new Date(serverSession.closedAt).toLocaleString('en-CA', { hour12: false }).replace(',', '') : (day.closedAt || '')}{(serverBacked && serverSession) ? ` · trading day ${serverSession.businessDate}` : ''}. {serverBacked ? 'Your counts are now the till’s balances on the ledger. The book is locked until the next session is opened.' : 'The book is locked until you open the next day.'}</div>
            </div>
            {/* EARNED TODAY, FROM THE BOOK — and absent when the book has no
                answer. This tile read "$223.00" for a till whose only deal
                had been voided, because the figure beside it was summed here
                against a market mid over the wall-clock date. */}
            <div className="text-right flex-none">
              <div className="text-[10px] uppercase tracking-widest" style={{ color: '#3a7a56', fontFamily: 'Space Mono, monospace' }}>Earned today</div>
              {closedEarned == null
                ? <div style={{ fontSize: 22, fontWeight: 800, color: '#7a9a89', fontVariantNumeric: 'tabular-nums' }} title={closedEarnedWhy}>—</div>
                : <div style={{ fontSize: 22, fontWeight: 800, color: '#1c5c3a', fontVariantNumeric: 'tabular-nums' }}>{fmt(closedEarned, closedEarnedCcy)}</div>}
              <div className="text-[10px]" style={{ color: '#3a7a56' }}>{closedEarned == null ? closedEarnedWhy : 'commission + realized margin'}</div>
            </div>
          </div>
          <div className="grid grid-cols-3 gap-3 mt-4">
            <Kpi label={`Drawer value${homeCcy ? ' · ' + homeCcy : ''}`} value={fmtHome((day.summary && day.summary.grand != null) ? day.summary.grand : grandHome)} />
            <Kpi label="Transactions" value={closedTxns == null ? '—' : `${closedTxns}`} sub={closedTxns == null ? (dayBook.error || 'the ledger has no answer') : (dayBook.summary && dayBook.summary.reversed ? `${dayBook.summary.reversed} voided` : null)} />
            <Kpi label="Drawers off" value={(day.summary && day.summary.offCount) || 0} tone={((day.summary && day.summary.offCount) || 0) > 0 ? CD.flag : CD.green} sub={(day.summary && day.summary.note) || 'balanced'} />
          </div>

          {/* big, deliberate (not automatic) end-of-day report generation */}
          {onOpenReport && (
            <button onClick={() => onOpenReport('endofday')} className="flex items-center gap-3.5 mt-4 w-full text-left" style={{ background: CD.ink, borderRadius: 14, padding: '17px 18px', cursor: 'pointer', transition: 'transform .12s, box-shadow .12s' }}
              onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-1px)'; e.currentTarget.style.boxShadow = '0 12px 28px -12px var(--cd-scrim)'; }}
              onMouseLeave={e => { e.currentTarget.style.transform = 'none'; e.currentTarget.style.boxShadow = 'none'; }}>
              <span className="grid place-items-center flex-none" style={{ width: 46, height: 46, background: CD.green, borderRadius: 12 }}><Ic n="receipt" s={23} c="var(--cd-on-ink)" /></span>
              <span className="flex-1 min-w-0">
                <span className="block text-[15.5px] font-semibold" style={{ color: 'var(--cd-on-ink)' }}>Generate End-of-Day Sign-Off sheet</span>
                <span className="block text-[12px] mt-0.5" style={{ color: 'var(--cd-on-ink-soft)' }}>The signed close-out: drawer counts, day totals &amp; compliance — ready to print, sign and file.</span>
              </span>
              <span className="flex items-center gap-1.5 flex-none px-3 py-2 text-[12.5px] font-semibold" style={{ background: 'var(--cd-on-ink-faint)', color: 'var(--cd-on-ink)', borderRadius: 9 }}>Open <Ic n="arrowright" s={15} c="var(--cd-on-ink)" /></span>
            </button>
          )}
          {/* spacer pushes the open-next-day control to the bottom, out of accidental reach */}
          <div style={{ flex: 1, minHeight: 28 }}></div>
          <div className="flex items-center justify-between gap-3 pt-3" style={{ borderTop: `1px solid ${CD.line}` }}>
            <p className="text-[11px]" style={{ color: CD.faint, maxWidth: 360 }}>Ready for a new session? Opening the next {serverBacked ? 'session' : 'day'} unlocks the book and starts {serverBacked ? `session #${(day.num || 1) + 1}` : `Day ${(day.num || 1) + 1}`}{serverBacked ? ' with a fresh, uncounted drawer' : ''}.</p>
            {canCloseDay
              ? <button onClick={startNextSession} disabled={opening} className="till-save flex items-center gap-2 px-4 py-2.5 text-sm font-semibold flex-none" style={{ background: opening ? CD.green : 'transparent', color: opening ? 'var(--cd-on-ink)' : CD.ink, border: `1px solid ${opening ? CD.green : CD.line}`, borderRadius: 9, transition: 'background .25s ease, color .25s ease, border-color .25s ease' }} onMouseEnter={e => { if (!opening) { e.currentTarget.style.background = CD.ink; e.currentTarget.style.color = '#fff'; } }} onMouseLeave={e => { if (!opening) { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = CD.ink; } }}><Ic n={opening ? 'checkcircle' : 'calendar'} s={15} c={opening ? 'var(--cd-on-ink)' : 'currentColor'} /> {opening ? 'Opening…' : 'Open next day'}</button>
              : <span className="flex items-center gap-2 px-3 py-2 text-[12px] flex-none" style={{ color: CD.mute }}><Ic n="lock" s={14} c={CD.faint} /> Owner / permitted staff only</span>}
          </div>
        </div>) : (<div className="p-4">
          <div className="flex items-center justify-between mb-3">
            <div><div className="text-sm font-semibold" style={{ color: CD.ink }}>Reconcile & close — Day {day && day.num || 1}</div><div className="text-[11px]" style={{ color: CD.mute }}>{serverBacked ? 'Expected is the authoritative server till balance' : "Opening comes from the vault · expected = opening + today's deals"} · counted is your physical count · {countedN} of {recon.length} counted</div></div>
            <span className="text-[11px] px-2.5 py-1" style={{ background: countedN === recon.length ? CD.greenSoft : 'var(--cd-chip)', color: countedN === recon.length ? CD.green : CD.mute, borderRadius: 999, fontFamily: 'Space Mono, monospace' }}>{countedN}/{recon.length} counted</span>
          </div>
          <div className="overflow-hidden" style={{ border: `1px solid ${CD.line}`, background: CD.panel, borderRadius: 10 }}>
            <table className="w-full text-sm border-collapse"><thead><tr style={{ background: 'var(--cd-chip)', color: CD.mute }} className="text-[11px] uppercase tracking-wide text-left"><th className="px-3 py-2">Currency</th><th className="px-3 py-2 text-right"><span title={serverBacked ? 'Expected balance comes directly from the authoritative server ledger' : 'Issued by the vault — not editable here'} className="inline-flex items-center gap-1">{serverBacked ? 'Source' : 'Opening · vault'} <Ic n="lock" s={10} c={CD.faint} /></span></th><th className="px-3 py-2 text-right">Expected</th><th className="px-3 py-2 text-right">Last count</th><th className="px-3 py-2 text-right">Counted now</th><th className="px-3 py-2 text-right">Variance</th></tr></thead>
              <tbody>{recon.map(r => { const ls = lastSaved[r.c]; return (<tr key={r.c} style={{ borderTop: `1px solid ${CD.lineSoft}` }}>
                <td className="px-3 py-2 font-medium" style={{ color: CD.ink }}><span style={{ fontFamily: 'system-ui' }}>{flagOf(r.c)}</span> {r.c}</td>
                <td className="px-3 py-2 text-right" title={serverBacked ? 'Authoritative server ledger' : 'What the vault issued to this drawer — change it with Issue float / Return at the vault, never by typing'} style={{ fontVariantNumeric: 'tabular-nums', color: serverBacked ? CD.green : CD.mute }}>{serverBacked ? 'Server ledger' : '—'}</td>
                <td className="px-3 py-2 text-right font-semibold" style={{ fontVariantNumeric: 'tabular-nums', color: r.expected == null ? CD.faint : CD.ink }}>{r.expected == null ? '—' : num(r.expected)}</td>
                <td className="px-3 py-2 text-right" style={{ color: ls ? CD.mute : CD.faint }}>{ls ? <span title={`${num(ls.amt)} ${r.c} · ${atLabel(ls.ts)}${ls.by ? ' · ' + ls.by : ''}`} style={{ cursor: 'help', borderBottom: `1px dotted ${CD.line}`, fontVariantNumeric: 'tabular-nums' }}>{sinceLabel(ls.ts)}</span> : '— never'}</td>
                <td className="px-3 py-2 text-right" style={{ fontVariantNumeric: 'tabular-nums', color: r.counted == null ? CD.faint : CD.ink, fontWeight: r.counted == null ? 400 : 700 }}>{r.counted == null ? <button onClick={() => { setCcy(r.c); setTab('count'); }} className="text-[11px] underline" style={{ color: CD.mute }}>count →</button> : <span title={countedAt[r.c] ? 'Counted ' + sinceLabel(countedAt[r.c]) + ' · ' + atLabel(countedAt[r.c]) : ''} style={{ cursor: countedAt[r.c] ? 'help' : 'default' }}>{num(r.counted)}</span>}</td>
                <td className="px-3 py-2 text-right font-semibold" style={{ fontVariantNumeric: 'tabular-nums', color: r.variance == null ? CD.mute : !offOf(r) ? CD.green : CD.flag }}>{r.variance == null ? '—' : (r.variance > 0 ? '+' : '') + num(r.variance)}</td>
              </tr>); })}</tbody><tfoot><tr style={{ borderTop: `2px solid ${CD.line}`, background: 'var(--cd-chip)' }}><td className="px-3 py-2 font-semibold" style={{ color: CD.ink }}>Total{homeCcy ? ' · ' + homeCcy : ''}</td><td></td><td className="px-3 py-2 text-right font-semibold" style={{ fontVariantNumeric: 'tabular-nums', color: CD.mute }}>{totalExpHome == null ? '—' : num(totalExpHome)}</td><td></td><td className="px-3 py-2 text-right font-bold" style={{ fontVariantNumeric: 'tabular-nums', color: CD.ink }}>{totalCountHome == null ? '—' : num(totalCountHome)}</td><td className="px-3 py-2 text-right font-bold" style={{ fontVariantNumeric: 'tabular-nums', color: totalVarHome == null ? CD.faint : Math.abs(totalVarHome) <= tolHome + 0.005 ? CD.green : CD.flag }}>{totalVarHome == null ? '—' : (totalVarHome > 0 ? '+' : '') + num(totalVarHome)}</td></tr></tfoot></table>
          </div>
          <div className="flex items-center gap-3 mt-3">
            {canCloseDay
              ? <button onClick={clickClose} disabled={closing || closeBlocked} title={closeBlocked ? (!serverBalancesReady ? (serverBalanceError || 'Waiting for server balances') : !sessionOpen ? 'There is no open till session to close' : serverBacked ? 'Count every server-backed currency before closing' : 'Count every drawer first — required in Settings › Cash drawer') : ''} className="till-save flex items-center gap-2 px-4 py-2 text-sm font-semibold text-white" style={{ background: closing ? CD.green : CD.ink, borderRadius: 9, transition: 'background .22s ease' }} onMouseEnter={e => { if (!closing) e.currentTarget.style.background = CD.flag; }} onMouseLeave={e => { if (!closing) e.currentTarget.style.background = CD.ink; }}><Ic n={closing ? 'checkcircle' : 'lock'} s={14} c="var(--cd-on-ink)" /> {closing ? 'Closing…' : 'Close day & lock book'}</button>
              : <span className="flex items-center gap-2 px-3 py-2 text-[12px]" style={{ background: 'var(--cd-chip)', borderRadius: 9, color: CD.mute }}><Ic n="lock" s={14} c={CD.faint} /> Closing the day isn’t in your role — the owner enables it in Settings › Permissions.</span>}
            {canCloseDay && closeBlocked && <span className="text-[11px]" style={{ color: serverBalanceError ? CD.flag : CD.mute }}>{!serverBalancesReady ? (serverBalanceError || 'Waiting for server balances') : !sessionOpen ? 'Open the till before closing it' : !recon.length ? 'This till has no ledger balances yet' : `Count all ${recon.length} drawer${recon.length === 1 ? '' : 's'} before closing — the close writes your count back as the balance`}</span>}
            {canCloseDay && offRows.length > 0 && <span className="text-[11px]" style={{ color: CD.flag }}>{offRows.length} drawer(s) off</span>}
            {canCloseDay && offRows.length === 0 && countedN > 0 && <span className="text-[11px]" style={{ color: CD.green }}>All counted drawers balanced</span>}
          </div>
          {offLedgerCcys.length > 0 && <div className="mt-2 text-[11px] px-3 py-2" style={{ background: 'var(--cd-chip)', borderRadius: 8, color: CD.mute }}>Counted but not on the server ledger: <b style={{ color: CD.ink }}>{offLedgerCcys.join(', ')}</b>. These are recorded in your local history only and are left out of the close, so they can’t disturb a real balance.</div>}
          <p className="mt-2 text-[11px]" style={{ color: CD.faint }}>{serverBacked ? <>The server ledger is the source of <b>expected</b> cash, including posted exchanges, reversals and vault movements. Closing writes each <b>counted</b> figure back as the till’s balance.</> : <>The vault issues your <b>opening</b> and the day's deals produce <b>expected</b>.</>} Your physical count is <b>counted</b>, and <b>variance</b> = counted − expected — it lands on the operator. Closing locks the ledger until the next day is opened.</p>
        </div>))}

        {/* ===== HISTORY ===== */}
        {tab === 'history' && (<div className="p-4">
          {/* the cash rail — every issue & return for THIS drawer */}
          {(() => {
            const _tRec = ((_ab && _ab.tills) || []).find(t => t.id === (station && station.tillId));
            const tLabel = _ab && _tRec ? _ab.code + ' · ' + _tRec.name.replace(/\s+—.*/, '') : null;
            const railMoves = tLabel ? (moves || []).filter(m => m.from === tLabel || m.to === tLabel) : [];
            return (<div className="mb-4">
              <div className="flex items-center justify-between mb-2">
                <div className="text-[11px] uppercase tracking-widest font-semibold flex items-center gap-1.5" style={{ color: CD.faint, fontFamily: 'Space Mono, monospace' }}><Ic n="swap" s={12} c={CD.faint} /> Cash rail · this drawer</div>
                {railMoves.length > 0 && <span className="text-[11px]" style={{ color: CD.faint }}>{railMoves.length} recorded</span>}
              </div>
              {railMoves.length === 0
                ? <div className="text-[12px] px-3 py-3" style={{ color: CD.mute, background: CD.panel, border: `1px solid ${CD.line}`, borderRadius: 10 }}>No floats yet — cash issued to or returned from this drawer is recorded here.</div>
                : <div style={{ border: `1px solid ${CD.line}`, borderRadius: 11, overflow: 'hidden' }}>
                    {railMoves.slice(0, 8).map((m, i) => { const out = m.from === tLabel; return (
                      <div key={m.id} className="flex items-center gap-2.5 px-3 py-2.5" style={{ borderTop: i ? `1px solid ${CD.lineSoft}` : 'none', background: 'var(--cd-panel)' }}>
                        <span className="grid place-items-center flex-none" style={{ width: 26, height: 26, borderRadius: 7, background: out ? CD.lineSoft : CD.greenSoft }}><Ic n={out ? 'arrowup' : 'arrowdown'} s={13} c={out ? CD.mute : CD.green} /></span>
                        <div className="flex-1 min-w-0 leading-tight">
                          <div className="text-[12.5px]" style={{ color: CD.ink }}><b>{out ? 'Returned to vault' : 'Float issued'}</b> <span style={{ color: CD.faint }}>{out ? '→' : '←'}</span> {out ? m.to : m.from}</div>
                          <div className="text-[10.5px]" style={{ color: CD.faint, fontFamily: 'Space Mono, monospace' }}>{m.ref} · {m.date} · {m.by}</div>
                        </div>
                        <span className="text-[12px] font-bold flex-none" style={{ fontFamily: 'Space Mono', fontVariantNumeric: 'tabular-nums', color: out ? CD.mute : CD.green }}>{out ? '−' : '+'}{num(m.amount)} {m.ccy}</span>
                      </div>); })}
                  </div>}
            </div>);
          })()}
          {/* shift handoff log */}
          <div className="mb-4">
            <div className="flex items-center justify-between mb-2">
              <div className="text-[11px] uppercase tracking-widest font-semibold flex items-center gap-1.5" style={{ color: CD.faint, fontFamily: 'Space Mono, monospace' }}><Ic n="users" s={12} c={CD.faint} /> Shift handoffs</div>
              {handoffs.length > 0 && <span className="text-[11px]" style={{ color: CD.faint }}>{handoffs.length} recorded</span>}
            </div>
            {handoffs.length === 0
              ? <div className="text-[12px] px-3 py-3" style={{ color: CD.mute, background: CD.panel, border: `1px solid ${CD.line}`, borderRadius: 10 }}>No handoffs yet — the drawer has stayed with {current ? current.operator : 'one operator'}.</div>
              : <div style={{ border: `1px solid ${CD.line}`, borderRadius: 11, overflow: 'hidden' }}>
                  {handoffs.slice(0, 8).map((h, i) => (
                    <div key={h.id} className="flex items-center gap-2.5 px-3 py-2.5" style={{ borderTop: i ? `1px solid ${CD.lineSoft}` : 'none', background: 'var(--cd-panel)' }}>
                      <span className="grid place-items-center flex-none" style={{ width: 26, height: 26, borderRadius: 7, background: h.counted ? (Math.abs(h.variance || 0) < 0.005 ? CD.greenSoft : CD.flagSoft) : CD.lineSoft }}><Ic n={h.counted ? (Math.abs(h.variance || 0) < 0.005 ? 'check' : 'alert') : 'arrowright'} s={13} c={h.counted ? (Math.abs(h.variance || 0) < 0.005 ? CD.green : CD.flag) : CD.mute} /></span>
                      <div className="flex-1 min-w-0 leading-tight">
                        <div className="text-[12.5px]" style={{ color: CD.ink }}><b>{h.from}</b> <span style={{ color: CD.faint }}>→</span> <b>{h.to}</b>{h.tillName ? <span className="text-[11px]" style={{ color: CD.faint }}> · {h.tillName}</span> : null}</div>
                        <div className="text-[10.5px]" style={{ color: CD.faint }}>{h.at} · by {h.by}</div>
                      </div>
                      <span className="text-[11px] flex-none text-right" style={{ fontFamily: 'Space Mono, monospace', color: h.counted ? (Math.abs(h.variance || 0) < 0.005 ? CD.green : CD.flag) : CD.mute }}>{h.counted ? (Math.abs(h.variance || 0) < 0.005 ? '✓ balanced' : (h.variance > 0 ? '+' : '') + fmtHome(h.variance)) : 'no count'}</span>
                    </div>))}
                </div>}
          </div>
          {viewDay ? (<div>
            <button onClick={() => setViewDay(null)} className="flex items-center gap-1.5 text-[12px] mb-3" style={{ color: CD.mute }}><Ic n="arrowleft" s={14} /> All days</button>
            <div className="text-sm font-semibold mb-1" style={{ color: CD.ink }}>{viewDay}{history[viewDay].grand == null ? '' : ' · ' + fmt(history[viewDay].grand, history[viewDay].currency || homeCcy)}</div>
            <div className="text-[11px] mb-3" style={{ color: CD.faint }}>Counted by {history[viewDay].by} · {history[viewDay].at}</div>
            <div className="grid sm:grid-cols-2 gap-2">
              {Object.entries(history[viewDay].byCcy).sort((a, b) => (homeOf(b[1], b[0]) || 0) - (homeOf(a[1], a[0]) || 0)).map(([c, amt]) => (
                <div key={c} className="flex items-center justify-between px-3 py-2.5" style={{ background: CD.panel, border: `1px solid ${CD.line}`, borderRadius: 9 }}>
                  <span className="flex items-center gap-2 text-sm" style={{ color: CD.ink }}><span style={{ fontFamily: 'system-ui' }}>{flagOf(c)}</span> {c}</span>
                  <span className="text-right"><span style={{ fontFamily: 'Space Mono, monospace', fontWeight: 700, color: CD.ink, fontVariantNumeric: 'tabular-nums' }}>{num(amt)}</span> {c !== homeCcy && homeOf(amt, c) != null && <span className="text-[11px]" style={{ color: CD.mute }}> · {fmt(homeOf(amt, c), homeCcy)}</span>}</span>
                </div>))}
            </div>
          </div>) : (<div>
            {/* 90-day trend */}
            <div className="p-4 mb-3" style={{ background: CD.panel, border: `1px solid ${CD.line}`, borderRadius: 12 }}>
              <div className="flex items-center justify-between mb-2"><span className="text-[10px] uppercase tracking-widest" style={{ color: CD.faint, fontFamily: 'Space Mono, monospace' }}>Drawer value · last 90 days</span><span style={{ fontFamily: 'Space Mono, monospace', fontSize: 13, fontWeight: 700, color: CD.ink }}>{fmtHome(histSeries[histSeries.length - 1])}</span></div>
              <HistChart data={histSeries} />
            </div>
            <div className="overflow-hidden" style={{ border: `1px solid ${CD.line}`, borderRadius: 10, background: CD.panel }}>
              <table className="w-full text-sm border-collapse"><thead><tr style={{ background: 'var(--cd-chip)', color: CD.mute }} className="text-[11px] uppercase tracking-wide text-left"><th className="px-3 py-2">Date</th><th className="px-3 py-2 text-right">Total (CAD)</th><th className="px-3 py-2">Currencies</th><th className="px-3 py-2">Counted by</th></tr></thead>
                <tbody>{histKeys.slice(0, 60).map(k => { const h = history[k]; return (<tr key={k} onClick={() => setViewDay(k)} className="cursor-pointer" style={{ borderTop: `1px solid ${CD.lineSoft}` }} onMouseEnter={e => e.currentTarget.style.background = 'var(--cd-paper-soft)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
                  <td className="px-3 py-2 font-medium" style={{ color: CD.ink, fontVariantNumeric: 'tabular-nums' }}>{k}{k === ((serverSession && serverSession.businessDate) || window.CDOS.businessDate()) && <span className="ml-2 text-[9px] px-1.5 py-0.5" style={{ background: CD.greenSoft, color: CD.green, borderRadius: 4, fontFamily: 'Space Mono, monospace' }}>TODAY</span>}</td>
                  <td className="px-3 py-2 text-right font-semibold" style={{ color: CD.ink, fontVariantNumeric: 'tabular-nums' }}>{h.grand == null ? '—' : fmt(h.grand, h.currency || homeCcy)}</td>
                  <td className="px-3 py-2" style={{ color: CD.mute }}>{Object.keys(h.byCcy).length}</td>
                  <td className="px-3 py-2" style={{ color: CD.mute }}>{h.by}</td>
                </tr>); })}</tbody></table>
            </div>
            <p className="mt-2 text-[11px]" style={{ color: CD.faint }}>A snapshot is saved each time you count the drawer. Click any day to see its full denomination breakdown.</p>
          </div>)}
        </div>)}
      </div>
      {/* ===== CLOSE-OUT REVIEW MODAL ===== */}
      {confirmClose && (<div style={{ position: 'absolute', inset: 0, zIndex: 60, background: 'rgba(24,21,17,0.46)', display: 'grid', placeItems: 'center', padding: 20 }} onClick={() => setConfirmClose(false)}>
        <div onClick={e => e.stopPropagation()} className="flex flex-col" style={{ width: 'min(580px, 100%)', maxHeight: '100%', background: CD.paper, border: `1px solid ${CD.line}`, borderRadius: 16, boxShadow: '0 24px 60px rgba(0,0,0,0.30)', overflow: 'hidden' }}>
          <div className="px-5 pt-4 pb-3 flex items-start gap-3 flex-none" style={{ borderBottom: `1px solid ${CD.line}` }}>
            <span className="grid place-items-center flex-none" style={{ width: 34, height: 34, background: CD.ink, borderRadius: 9 }}><Ic n="lock" s={16} c="var(--cd-on-ink)" /></span>
            <div className="flex-1">
              <div className="text-[15px] font-semibold" style={{ color: CD.ink }}>Close out the day — Day {day && day.num || 1}</div>
              <div className="text-[11.5px] mt-0.5" style={{ color: CD.mute }}>Review each drawer against expected, then lock the book. You won't be able to post again until you open the next day.</div>
            </div>
          </div>
          <div className="overflow-auto px-5 py-3" style={{ flex: 1 }}>
            <table className="w-full text-sm border-collapse">
              <thead><tr className="text-[10px] uppercase tracking-wide text-left" style={{ color: CD.faint, fontFamily: 'Space Mono, monospace' }}><th className="py-1.5">Drawer</th><th className="py-1.5 text-right">Expected</th><th className="py-1.5 text-right">Counted</th><th className="py-1.5 text-right">Last counted</th><th className="py-1.5 text-right">Variance</th></tr></thead>
              <tbody>{recon.map(r => { const ts = countedAt[r.c]; return (<tr key={r.c} style={{ borderTop: `1px solid ${CD.lineSoft}` }}>
                <td className="py-1.5 font-medium" style={{ color: CD.ink }}><span style={{ fontFamily: 'system-ui' }}>{flagOf(r.c)}</span> {r.c}</td>
                <td className="py-1.5 text-right" style={{ fontVariantNumeric: 'tabular-nums', color: CD.mute, fontFamily: 'Space Mono, monospace' }}>{num(r.expected)}</td>
                <td className="py-1.5 text-right" style={{ fontVariantNumeric: 'tabular-nums', color: r.counted == null ? CD.faint : CD.ink, fontFamily: 'Space Mono, monospace' }}>{r.counted == null ? '— not counted' : num(r.counted)}</td>
                <td className="py-1.5 text-right text-[11px]" style={{ color: ts ? CD.mute : CD.faint }}>{ts ? `${sinceLabel(ts)} · ${atLabel(ts)}` : '—'}</td>
                <td className="py-1.5 text-right font-semibold" style={{ fontVariantNumeric: 'tabular-nums', color: r.variance == null ? CD.faint : Math.abs(r.variance) < 0.005 ? CD.green : CD.flag, fontFamily: 'Space Mono, monospace' }}>{r.variance == null ? '—' : (r.variance > 0 ? '+' : '') + num(r.variance)}</td>
              </tr>); })}</tbody>
              <tfoot><tr style={{ borderTop: `2px solid ${CD.line}` }}><td className="py-2 font-semibold" style={{ color: CD.ink }}>Total{homeCcy ? ' · ' + homeCcy : ''}</td><td className="py-2 text-right font-semibold" style={{ fontVariantNumeric: 'tabular-nums', color: CD.mute, fontFamily: 'Space Mono, monospace' }}>{totalExpHome == null ? '—' : num(totalExpHome)}</td><td className="py-2 text-right font-bold" style={{ fontVariantNumeric: 'tabular-nums', color: CD.ink, fontFamily: 'Space Mono, monospace' }}>{totalCountHome == null ? '—' : num(totalCountHome)}</td><td></td><td className="py-2 text-right font-bold" style={{ fontVariantNumeric: 'tabular-nums', color: totalVarHome == null ? CD.faint : Math.abs(totalVarHome) < 0.005 ? CD.green : CD.flag, fontFamily: 'Space Mono, monospace' }}>{totalVarHome == null ? '—' : (totalVarHome > 0 ? '+' : '') + num(totalVarHome)}</td></tr></tfoot>
            </table>
            {countedN < recon.length && <div className="mt-3 text-[11.5px] px-3 py-2" style={{ background: 'var(--cd-chip)', borderRadius: 8, color: CD.mute }}>{recon.length - countedN} drawer(s) not counted yet — they'll lock at expected with no variance. Go back and count them first if you want a true close.</div>}
          </div>
          <div className="px-5 py-3 flex items-center justify-between gap-3 flex-none" style={{ borderTop: `1px solid ${CD.line}` }}>
            <button onClick={() => { setConfirmClose(false); setTab('count'); }} className="text-sm font-medium px-3.5 py-2" style={{ color: CD.ink, border: `1px solid ${CD.line}`, borderRadius: 9, background: 'transparent' }}>← Back to count</button>
            <button onClick={confirmAndClose} className="till-save flex items-center gap-2 px-4 py-2 text-sm font-semibold text-white" style={{ background: CD.ink, borderRadius: 9 }} onMouseEnter={e => e.currentTarget.style.background = CD.flag} onMouseLeave={e => e.currentTarget.style.background = CD.ink}><Ic n="lock" s={14} c="var(--cd-on-ink)" /> Close day & lock book</button>
          </div>
        </div>
      </div>)}
      {handoffOpen && <HandoffModal tillId={tillId} tillName={tillNm} current={current} roster={roster} me={me} requireCount={requireCount} expected={CCYS.map(c => ({ ccy: c, units: expectedOf(c) })).filter(x => Math.abs(x.units || 0) > 0.005 || x.ccy === homeCcy)} onClose={() => setHandoffOpen(false)} onDone={doHandoff} />}
    </div>);
  }

  function HistChart({ data, h = 70 }) {
    if (!data || data.length < 2) return <div style={{ height: h, display: 'grid', placeItems: 'center', color: CD.faint, fontSize: 11 }}>No history</div>;
    const w = 600, max = Math.max(...data), min = Math.min(...data), rng = max - min || 1;
    const X = (i) => (i / (data.length - 1)) * w, Y = (v) => h - ((v - min) / rng) * (h - 8) - 4;
    const line = data.map((v, i) => `${i === 0 ? 'M' : 'L'}${X(i).toFixed(1)},${Y(v).toFixed(1)}`).join(' ');
    return (<svg width="100%" height={h} viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" style={{ display: 'block' }}>
      <defs><linearGradient id="tillArea" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor={CD.ink} stopOpacity="0.12" /><stop offset="100%" stopColor={CD.ink} stopOpacity="0" /></linearGradient></defs>
      <path d={`${line} L${w},${h} L0,${h} Z`} fill="url(#tillArea)" /><path d={line} fill="none" stroke={CD.ink} strokeWidth="1.4" strokeLinejoin="round" />
    </svg>);
  }
  function Kpi({ label, value, sub, tone }) {
    return (<div style={{ border: `1px solid ${CD.line}`, background: CD.panel, borderRadius: 10, padding: '12px 14px' }}>
      <div className="text-[11px]" style={{ color: CD.mute }}>{label}</div>
      <div className="text-xl font-semibold mt-0.5" style={{ color: tone || CD.ink, fontVariantNumeric: 'tabular-nums' }}>{value}</div>
      {sub && <div className="text-[10.5px] mt-0.5" style={{ color: CD.faint }}>{sub}</div>}
    </div>);
  }

  window.CDOS = Object.assign(window.CDOS || {}, { TillDrawer });
})();
