/* ============================================================================
   MOWDEW — Reschedule negotiation kit (shared)
   The negotiation model (pending → proposed ⇄ counter → approved / declined /
   cancelled) + the atoms it's built from: state chips, time-change block,
   mini calendar + time picker, decline / propose / reassign forms, the
   append-only thread, a desktop Modal, and the in-app notification system
   (data + bell + feed rows) for all three personas.

   Load AFTER mowdew-kit.jsx (+ mowdew-owner-data.jsx where CUSTOMERS is used).
   Screens that compose these live in mowdew-resched-screens.jsx.
   ========================================================================= */

/* ─── Requests — one coherent May 2026 world (today = Tue, May 12) ──────
   thread turns: { by: 'homeowner'|'owner', kind: 'request'|'propose'|
   'counter'|'approve'|'decline'|'cancel', to?, note?, at }                */
const RESCHED_REQS = [
  { id: 'q1', cust: 'c2', worker: 'w1', state: 'proposed', age: '2h ago',
    orig: 'Mon, May 18 · 10 AM',
    thread: [
      { by: 'homeowner', kind: 'request', to: 'Thu, May 21 · morning', note: 'Pool party Wednesday — could Diego come Thursday instead?', at: 'Tue 9:15 AM' },
      { by: 'owner', kind: 'propose', to: 'Sat, May 23 · 9 AM', note: "Thursday's full — would Saturday morning work?", at: 'Tue 11:02 AM' },
    ] },
  { id: 'q2', cust: 'c5', worker: 'w2', state: 'pending', age: '5h ago',
    orig: 'Wed, May 20 · 1 PM',
    thread: [
      { by: 'homeowner', kind: 'request', to: 'Fri, May 22 · 1 PM', note: 'Gate will be locked Wednesday.', at: 'Mon 6:30 PM' },
    ] },
  { id: 'q3', cust: 'c3', worker: 'w2', state: 'pending', age: '1d ago', rounds: 2,
    orig: 'Mon, May 18 · 8:30 AM',
    thread: [
      { by: 'homeowner', kind: 'request', to: 'Thu, May 21 · 8:30 AM', note: "We're repaving the driveway Monday.", at: 'Sun 4:10 PM' },
      { by: 'owner', kind: 'propose', to: 'Sat, May 23 · 8:30 AM', note: 'Marco is booked Thursday — Saturday instead?', at: 'Sun 6:45 PM' },
      { by: 'homeowner', kind: 'counter', to: 'Sun, May 24 · afternoon', note: "We're out Saturday — Sunday afternoon?", at: 'Mon 8:20 AM' },
    ] },
  { id: 'q4', cust: 'c6', worker: 'w1', state: 'approved', age: '4d ago',
    orig: 'Tue, May 12 · 9 AM', final: 'Fri, May 15 · 2 PM',
    thread: [
      { by: 'homeowner', kind: 'request', to: 'Fri, May 15 · 2 PM', note: 'Hosting a brunch Tuesday morning.', at: 'Fri 2:12 PM' },
      { by: 'owner', kind: 'approve', to: 'Fri, May 15 · 2 PM', at: 'Fri 3:05 PM' },
    ] },
  { id: 'q5', cust: 'c8', worker: 'w1', state: 'declined', age: '5d ago',
    orig: 'Wed, May 13 · 11 AM',
    thread: [
      { by: 'homeowner', kind: 'request', to: 'Thu, May 14 · 11 AM', note: 'Painters are coming Wednesday.', at: 'Thu 7:40 PM' },
      { by: 'owner', kind: 'decline', note: 'Too short notice — that day was already dispatched. Kept your Wednesday slot.', at: 'Thu 8:15 PM' },
    ] },
  { id: 'q6', cust: 'c1', worker: 'w1', state: 'cancelled', age: '1w ago',
    orig: 'Mon, May 11 · 9 AM',
    thread: [
      { by: 'homeowner', kind: 'request', to: 'Wed, May 13 · 9 AM', note: 'We might be travelling.', at: 'Mon 8:00 AM' },
      { by: 'homeowner', kind: 'cancel', note: 'Never mind — plans changed, Monday works.', at: 'Mon 9:30 AM' },
    ] },
];
function reqLatestTo(q) { const t = [...q.thread].reverse().find(x => x.to); return t ? t.to : q.orig; }
function reqNeedsOwner(q) { return q.state === 'pending'; }
function reqWaiting(q) { return q.state === 'proposed'; }
function reqResolved(q) { return q.state === 'approved' || q.state === 'declined' || q.state === 'cancelled'; }
function needsOwnerCount() { return RESCHED_REQS.filter(reqNeedsOwner).length; }

/* ─── State chip — viewer-aware labels ──────────────────────────────── */
const RESCHED_CHIP = {
  pending:   { tone: 'soon',    owner: 'Needs you',           homeowner: 'Pending',            ownerShort: 'Needs you', homeShort: 'Pending' },
  proposed:  { tone: 'soon',    owner: 'Waiting on customer', homeowner: 'New time suggested', ownerShort: 'Waiting',   homeShort: 'New time' },
  approved:  { tone: 'done',    owner: 'Confirmed',           homeowner: 'Confirmed', icon: 'check', ownerShort: 'Confirmed', homeShort: 'Confirmed' },
  declined:  { tone: 'alert',   owner: 'Declined',            homeowner: 'Declined',  ownerShort: 'Declined',  homeShort: 'Declined' },
  cancelled: { tone: 'neutral', owner: 'Cancelled',           homeowner: 'Cancelled', ownerShort: 'Cancelled', homeShort: 'Cancelled' },
};
function ReschedChip({ state, viewer = 'owner', compact = false }) {
  const m = RESCHED_CHIP[state] || RESCHED_CHIP.pending;
  const label = compact ? (viewer === 'owner' ? m.ownerShort : m.homeShort) : m[viewer];
  return <Chip tone={m.tone} icon={m.icon} style={{ flexShrink: 0 }}>{label}</Chip>;
}

/* ─── Time change — FROM → TO, the calendar handshake ───────────────── */
function TimeChange({ from, to, toLabel = 'TO', toTone = 'green', compact = false }) {
  const toColor = toTone === 'alert' ? 'var(--alert-700)' : toTone === 'aqua' ? 'var(--aqua-700)' : 'var(--green-700)';
  return (
    <div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: compact ? '6px 10px' : '8px 14px', background: 'var(--paper)', borderRadius: 13, padding: compact ? '10px 12px' : '13px 16px', border: '1px solid var(--ink-100)' }}>
      <div style={{ minWidth: 0 }}>
        <div style={{ fontFamily: 'var(--font-mono)', fontSize: 9.5, color: 'var(--ink-500)', letterSpacing: '.08em' }}>FROM</div>
        <div style={{ fontFamily: 'var(--font-mono)', fontWeight: 600, fontSize: compact ? 11.5 : 13, marginTop: 3, color: 'var(--ink-700)', whiteSpace: 'nowrap', textDecoration: 'line-through', textDecorationColor: 'var(--ink-300)', textDecorationThickness: 1.5 }}>{from}</div>
      </div>
      <Icon name="arrow-right" size={compact ? 15 : 18} style={{ color: 'var(--ink-300)', flexShrink: 0 }} />
      <div style={{ minWidth: 0 }}>
        <div style={{ fontFamily: 'var(--font-mono)', fontSize: 9.5, color: toColor, letterSpacing: '.08em' }}>{toLabel}</div>
        <div style={{ fontFamily: 'var(--font-mono)', fontWeight: 700, fontSize: compact ? 11.5 : 13, marginTop: 3, color: toColor, whiteSpace: 'nowrap' }}>{to}</div>
      </div>
    </div>
  );
}

/* ─── Mini calendar — May / Jun 2026, real weekday layout ───────────── */
const CAL_MONTHS = [
  { label: 'May 2026', days: 31, startCol: 5, today: 12 },  // May 1, 2026 = Friday
  { label: 'June 2026', days: 30, startCol: 1 },            // Jun 1, 2026 = Monday
];
function MiniCalendar({ value, onChange }) {
  const [mi, setMi] = React.useState(0);
  const m = CAL_MONTHS[mi];
  const pick = (d) => onChange(`${m.label.split(' ')[0]} ${d}`);
  const selected = (d) => value === `${m.label.split(' ')[0]} ${d}`;
  const disabled = (d) => mi === 0 && m.today && d <= m.today;
  return (
    <div style={{ background: '#fff', border: '1px solid var(--ink-100)', borderRadius: 14, padding: '12px 12px 10px' }}>
      <div style={{ display: 'flex', alignItems: 'center', marginBottom: 8 }}>
        <button onClick={() => setMi(0)} disabled={mi === 0} aria-label="Previous month" style={{ width: 30, height: 30, borderRadius: 8, border: 'none', background: 'transparent', color: mi === 0 ? 'var(--ink-200)' : 'var(--ink-700)', display: 'grid', placeItems: 'center', cursor: mi === 0 ? 'default' : 'pointer' }}><Icon name="arrow-left" size={15} strokeWidth={2.4} /></button>
        <span style={{ flex: 1, textAlign: 'center', fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 14, letterSpacing: '-0.01em' }}>{m.label}</span>
        <button onClick={() => setMi(1)} disabled={mi === 1} aria-label="Next month" style={{ width: 30, height: 30, borderRadius: 8, border: 'none', background: 'transparent', color: mi === 1 ? 'var(--ink-200)' : 'var(--ink-700)', display: 'grid', placeItems: 'center', cursor: mi === 1 ? 'default' : 'pointer' }}><Icon name="arrow-right" size={15} strokeWidth={2.4} /></button>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 2, marginBottom: 4 }}>
        {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d, i) => (
          <span key={i} style={{ textAlign: 'center', fontFamily: 'var(--font-mono)', fontSize: 9.5, color: 'var(--ink-400,#A8A498)', padding: '2px 0' }}>{d}</span>
        ))}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 2 }}>
        {Array.from({ length: m.startCol }).map((_, i) => <span key={'b' + i} />)}
        {Array.from({ length: m.days }).map((_, i) => {
          const d = i + 1; const on = selected(d); const off = disabled(d); const isToday = mi === 0 && d === m.today;
          return (
            <button key={d} onClick={() => !off && pick(d)} disabled={off}
              style={{ height: 34, borderRadius: 9, border: isToday && !on ? '1.5px solid var(--green-300)' : 'none', cursor: off ? 'default' : 'pointer',
                background: on ? 'var(--green-700)' : 'transparent', color: off ? 'var(--ink-200)' : on ? '#fff' : 'var(--ink-900)',
                fontFamily: 'var(--font-body)', fontSize: 12.5, fontWeight: on ? 800 : 600 }}>
              {d}
            </button>
          );
        })}
      </div>
    </div>
  );
}

/* ─── Time chips ────────────────────────────────────────────────────── */
const TIME_SLOTS = ['8 AM', '9 AM', '10 AM', '11 AM', '1 PM', '2 PM', '4 PM'];
function TimeChips({ value, onChange }) {
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 7 }}>
      {TIME_SLOTS.map(t => {
        const on = value === t;
        return (
          <button key={t} onClick={() => onChange(t)}
            style={{ padding: '9px 13px', borderRadius: 10, border: on ? '2px solid var(--green-500)' : '1.5px solid var(--ink-200)', background: on ? 'var(--green-50)' : '#fff', color: on ? 'var(--green-800)' : 'var(--ink-700)', fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>
            {t}
          </button>
        );
      })}
    </div>
  );
}

/* ─── Propose / counter form — calendar + time + note ───────────────── */
function ProposeForm({ submitLabel = 'Propose this time', notePlaceholder = 'Add a note (optional)…', onSubmit }) {
  const [date, setDate] = React.useState(null);
  const [time, setTime] = React.useState(null);
  const [note, setNote] = React.useState('');
  return (
    <div style={{ display: 'grid', gap: 12 }}>
      <MiniCalendar value={date} onChange={setDate} />
      <TimeChips value={time} onChange={setTime} />
      <textarea value={note} onChange={e => setNote(e.target.value)} className="input" rows={2} placeholder={notePlaceholder} style={{ resize: 'none', lineHeight: 1.5, fontSize: 14 }} />
      <button onClick={() => onSubmit(`${date} · ${time}`, note.trim())} disabled={!date || !time} className="btn btn-primary btn-block">
        <Icon name="send" size={17} />{submitLabel}{date && time ? ` — ${date}, ${time}` : ''}
      </button>
    </div>
  );
}

/* ─── Decline form — reason required, quick chips ───────────────────── */
function DeclineForm({ quick, onSubmit, submitLabel = 'Decline with this reason' }) {
  const chips = quick || ['Pro unavailable that day', 'Too short notice', 'Weather plan that week'];
  const [reason, setReason] = React.useState('');
  return (
    <div style={{ display: 'grid', gap: 12 }}>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 7 }}>
        {chips.map(c => (
          <button key={c} onClick={() => setReason(c)}
            style={{ padding: '8px 12px', borderRadius: 99, border: reason === c ? '2px solid var(--alert-500)' : '1.5px solid var(--ink-200)', background: reason === c ? 'var(--alert-50)' : '#fff', color: reason === c ? 'var(--alert-700)' : 'var(--ink-700)', fontSize: 12.5, fontWeight: 700, cursor: 'pointer' }}>
            {c}
          </button>
        ))}
      </div>
      <textarea value={reason} onChange={e => setReason(e.target.value)} className="input" rows={2} placeholder="Why? The customer sees this — keep it kind." style={{ resize: 'none', lineHeight: 1.5, fontSize: 14 }} />
      <button onClick={() => onSubmit(reason.trim())} disabled={!reason.trim()} className="btn btn-block" style={{ background: 'var(--alert-500)', color: '#fff', boxShadow: 'var(--shadow-md)' }}>
        <Icon name="x" size={17} />{submitLabel}
      </button>
    </div>
  );
}

/* ─── Reassign form — pro picker, optional approve-combo ────────────── */
function ReassignForm({ currentWorker, withApprove = false, onSubmit }) {
  const [wid, setWid] = React.useState(null);
  const w = WORKERS.find(x => x.id === wid);
  return (
    <div style={{ display: 'grid', gap: 12 }}>
      <div style={{ display: 'grid', gap: 9 }}>
        {WORKERS.map(nw => {
          const cur = nw.id === currentWorker;
          const on = wid === nw.id;
          return (
            <button key={nw.id} onClick={() => !cur && setWid(nw.id)} disabled={cur}
              style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', borderRadius: 'var(--radius-lg)', border: on ? '2px solid var(--green-500)' : '1.5px solid var(--ink-100)', background: on ? 'var(--green-50)' : cur ? 'var(--ink-50)' : '#fff', cursor: cur ? 'default' : 'pointer', textAlign: 'left', opacity: cur ? 0.7 : 1 }}>
              <Avatar name={nw.name} size={40} />
              <div style={{ flex: 1 }}>
                <div style={{ fontWeight: 700, fontSize: 14.5 }}>{nw.name}{nw.you ? ' · you' : ''}</div>
                <div style={{ fontSize: 12, color: 'var(--ink-500)' }}>{nw.role} · {nw.today} stops today</div>
              </div>
              {cur
                ? <span style={{ fontFamily: 'var(--font-mono)', fontSize: 9.5, fontWeight: 600, letterSpacing: '.08em', color: 'var(--ink-500)', background: '#fff', border: '1px solid var(--ink-100)', borderRadius: 7, padding: '3px 8px' }}>CURRENT</span>
                : <span style={{ width: 22, height: 22, borderRadius: '50%', border: on ? 'none' : '2px solid var(--ink-200)', background: on ? 'var(--green-700)' : 'transparent', display: 'grid', placeItems: 'center', color: '#fff' }}>{on && <Icon name="check" size={13} strokeWidth={3} />}</span>}
            </button>
          );
        })}
      </div>
      {withApprove ? (
        <div style={{ display: 'grid', gap: 9 }}>
          <button onClick={() => onSubmit(wid, true)} disabled={!wid} className="btn btn-primary btn-block"><Icon name="check" size={17} />Approve &amp; reassign{w ? ` to ${w.name.split(' ')[0]}` : ''}</button>
          <button onClick={() => onSubmit(wid, false)} disabled={!wid} className="btn btn-secondary btn-block">Reassign only — decide the time later</button>
        </div>
      ) : (
        <button onClick={() => onSubmit(wid, false)} disabled={!wid} className="btn btn-primary btn-block"><Icon name="users" size={17} />Reassign{w ? ` to ${w.name.split(' ')[0]}` : ''}</button>
      )}
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: 'var(--ink-500)' }}>
        <Icon name="route" size={14} style={{ color: 'var(--blue-700)', flexShrink: 0 }} />
        Both Pros are notified and routes rebalance automatically.
      </div>
    </div>
  );
}

/* ─── Thread — append-only negotiation history (Outlook-invite style) ── */
const TURN_META = {
  request: { icon: 'calendar-check', color: 'var(--yellow-700)', bg: 'var(--yellow-50)', verb: 'asked to move the visit' },
  propose: { icon: 'clock', color: 'var(--yellow-700)', bg: 'var(--yellow-50)', verb: 'suggested a new time' },
  counter: { icon: 'history', color: 'var(--yellow-700)', bg: 'var(--yellow-50)', verb: 'proposed another time' },
  approve: { icon: 'check-circle', color: 'var(--green-700)', bg: 'var(--green-50)', verb: 'approved the new time' },
  reassign: { icon: 'users', color: 'var(--blue-700)', bg: 'var(--blue-50)', verb: 'reassigned the stop' },
  decline: { icon: 'x', color: 'var(--alert-700)', bg: 'var(--alert-50)', verb: 'declined' },
  cancel:  { icon: 'x', color: 'var(--ink-500)', bg: 'var(--ink-50)', verb: 'cancelled the request' },
};
/* names.owner / names.homeowner — what to call each side from this viewer */
function NegotiationThread({ thread, names, compact = false }) {
  return (
    <div style={{ position: 'relative' }}>
      <div style={{ position: 'absolute', left: 13, top: 10, bottom: 10, width: 2, background: 'var(--ink-100)' }} />
      <div style={{ display: 'grid', gap: compact ? 9 : 11 }}>
        {thread.map((t, i) => {
          const m = TURN_META[t.kind] || TURN_META.request;
          const who = names[t.by] || t.by;
          const verb = t.kind === 'reassign' && t.toWho ? `moved this stop to ${t.toWho}'s route` : m.verb;
          return (
            <div key={i} style={{ display: 'flex', gap: 10, position: 'relative' }}>
              <span style={{ width: 28, height: 28, borderRadius: '50%', background: m.bg, display: 'grid', placeItems: 'center', color: m.color, flexShrink: 0, zIndex: 1, border: '2px solid #fff', boxShadow: 'var(--shadow-sm)' }}>
                <Icon name={m.icon} size={14} strokeWidth={2.4} />
              </span>
              <div style={{ flex: 1, minWidth: 0, paddingTop: 2 }}>
                <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
                  <span style={{ fontSize: compact ? 12.5 : 13, fontWeight: 700 }}>{who} {verb}</span>
                  {t.to && t.kind !== 'approve' && <span style={{ fontFamily: 'var(--font-mono)', fontSize: compact ? 10.5 : 11, fontWeight: 600, color: m.color, background: m.bg, borderRadius: 7, padding: '2px 7px', whiteSpace: 'nowrap' }}>{t.to}</span>}
                  <span style={{ fontFamily: 'var(--font-mono)', fontSize: 9.5, color: 'var(--ink-400,#A8A498)', marginLeft: 'auto', flexShrink: 0 }}>{t.at}</span>
                </div>
                {t.note && <div style={{ fontSize: compact ? 12.5 : 13, color: 'var(--ink-600,#6E6B60)', lineHeight: 1.45, marginTop: 3 }}>"{t.note}"</div>}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* ─── Modal — desktop dialog (mobile uses Sheet from mowdew-frame) ──── */
function Modal({ open, onClose, title, children, width = 470 }) {
  if (!open) return null;
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 90, background: 'rgba(27,26,23,.42)', display: 'grid', placeItems: 'center', animation: 'tnFade .16s ease', padding: 20 }}>
      <div onClick={e => e.stopPropagation()} style={{ width: '100%', maxWidth: width, maxHeight: '88vh', overflowY: 'auto', background: 'var(--surface)', borderRadius: 22, padding: '20px 22px 22px', boxShadow: 'var(--shadow-xl)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
          <h2 style={{ fontSize: 19, flex: 1 }}>{title}</h2>
          <button onClick={onClose} aria-label="Close" style={{ width: 34, height: 34, borderRadius: 10, border: '1px solid var(--ink-100)', background: '#fff', display: 'grid', placeItems: 'center', cursor: 'pointer', color: 'var(--ink-500)' }}><Icon name="x" size={17} /></button>
        </div>
        {children}
      </div>
    </div>
  );
}

/* ============================================================================
   IN-APP NOTIFICATIONS — shared system, three personas
   ========================================================================= */
const NOTIFS = {
  owner: [
    { id: 'no1', icon: 'history', tone: 'yellow', title: 'New time proposed', body: 'Ada proposed Sun, May 24 · afternoon instead of your Saturday offer.', at: '1d ago', unread: true, link: 'resched' },
    { id: 'no2', icon: 'calendar-check', tone: 'yellow', title: 'New reschedule request', body: 'Tom wants to move his pool visit from Wed, May 20 to Fri, May 22.', at: '5h ago', unread: true, link: 'resched' },
    { id: 'no3', icon: 'calendar', tone: 'aqua', title: 'Time to bill — 3 customers due', body: 'Review who\u2019s being billed, then send them all.', at: 'Today 8:00 AM', unread: false, link: 'billing' },
    { id: 'no4', icon: 'check-circle', tone: 'green', title: 'Reschedule confirmed', body: 'The Cohens accepted — pool visit moved to Fri, May 15 · 2 PM. Your route is updated.', at: '4d ago', unread: false, link: 'resched' },
  ],
  homeowner: [
    { id: 'nh1', icon: 'clock', tone: 'yellow', title: 'New time suggested', body: 'Diego can\u2019t do Thursday — he suggested Sat, May 23 · 9 AM. Tap to accept or propose another.', at: '2h ago', unread: true, link: 'resched' },
    { id: 'nh2', icon: 'invoice', tone: 'aqua', title: 'New invoice', body: 'Marta Lawn & Pool sent INV-0007 — $260.00 due May 26.', at: 'Today 9:12 AM', unread: true, link: 'invoice' },
    { id: 'nh3', icon: 'check-circle', tone: 'green', title: 'Visit verified', body: 'Monday\u2019s pool visit is on the record — 24 min on-site, before & after photos.', at: '1w ago', unread: false, link: 'feed' },
  ],
  pro: [
    { id: 'np1', icon: 'route', tone: 'blue', title: 'Your route changed', body: 'Marin Vista pool moved to 2:30 PM today.', at: '1h ago', unread: true, link: 'today' },
    { id: 'np2', icon: 'plus', tone: 'green', title: 'New stop assigned', body: 'You picked up Cypress Ct pool — weekly, today 1:20 PM.', at: '2h ago', unread: true, link: 'today' },
    { id: 'np3', icon: 'calendar-check', tone: 'yellow', title: 'Schedule update', body: 'Brookhaven Ln garden — next Monday\u2019s visit moved to Thu, May 21.', at: '1d ago', unread: false, link: 'today' },
  ],
};
const NOTIF_TONE = {
  yellow: { bg: 'var(--yellow-50)', fg: 'var(--yellow-700)' },
  green:  { bg: 'var(--green-50)', fg: 'var(--green-700)' },
  aqua:   { bg: 'var(--aqua-50)', fg: 'var(--aqua-700)' },
  blue:   { bg: 'var(--blue-50)', fg: 'var(--blue-700)' },
  alert:  { bg: 'var(--alert-50)', fg: 'var(--alert-700)' },
  ink:    { bg: 'var(--ink-50)', fg: 'var(--ink-700)' },
};
/* read-state shared across screens for the session */
function notifReadStore() { return (window.__notifRead = window.__notifRead || {}); }
function notifUnreadCount(persona) {
  const read = notifReadStore();
  return NOTIFS[persona].filter(n => n.unread && !read[n.id]).length;
}

/* ─── Bell with unread badge ────────────────────────────────────────── */
function NotifBell({ count = 0, onClick, size = 42, onDark = false }) {
  return (
    <button onClick={onClick} aria-label={count ? `Notifications — ${count} unread` : 'Notifications'}
      style={{ position: 'relative', width: size, height: size, borderRadius: '50%', border: onDark ? '1px solid rgba(255,255,255,.18)' : '1px solid var(--ink-100)', background: onDark ? 'rgba(255,255,255,.08)' : '#fff', display: 'grid', placeItems: 'center', cursor: 'pointer', color: onDark ? '#fff' : 'var(--ink-900)', flexShrink: 0 }}>
      <Icon name="bell" size={Math.round(size * 0.48)} />
      {count > 0 && (
        <span style={{ position: 'absolute', top: -3, right: -3, minWidth: 19, height: 19, borderRadius: 99, background: 'var(--alert-500)', color: '#fff', fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 11, display: 'grid', placeItems: 'center', padding: '0 5px', border: `2px solid ${onDark ? 'var(--ink-900)' : '#fff'}` }}>{count}</span>
      )}
    </button>
  );
}

/* ─── One notification row ──────────────────────────────────────────── */
function NotifRow({ n, read, onOpen, first = false, compact = false }) {
  const t = NOTIF_TONE[n.tone] || NOTIF_TONE.ink;
  const isUnread = n.unread && !read;
  return (
    <div onClick={onOpen} style={{ display: 'flex', gap: 12, padding: compact ? '12px 14px' : '13px 16px', borderTop: first ? 'none' : '1px solid var(--ink-100)', cursor: 'pointer', background: isUnread ? '#fff' : 'var(--paper)' }}>
      <span style={{ width: 38, height: 38, borderRadius: 11, background: t.bg, display: 'grid', placeItems: 'center', color: t.fg, flexShrink: 0 }}>
        <Icon name={n.icon} size={19} strokeWidth={2.2} />
      </span>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
          <span style={{ fontWeight: isUnread ? 800 : 700, fontSize: compact ? 13.5 : 14, color: 'var(--ink-900)' }}>{n.title}</span>
          <span style={{ fontFamily: 'var(--font-mono)', fontSize: 9.5, color: 'var(--ink-400,#A8A498)', marginLeft: 'auto', flexShrink: 0 }}>{n.at}</span>
        </div>
        <div style={{ fontSize: compact ? 12.5 : 13, color: isUnread ? 'var(--ink-700)' : 'var(--ink-500)', lineHeight: 1.45, marginTop: 2 }}>{n.body}</div>
      </div>
      {isUnread && <span style={{ width: 9, height: 9, borderRadius: '50%', background: 'var(--blue-500)', flexShrink: 0, marginTop: 6 }} aria-label="Unread" />}
    </div>
  );
}

/* ─── Empty state — a mascot moment ─────────────────────────────────── */
function NotifEmpty({ persona = 'owner' }) {
  const M = persona === 'homeowner' || persona === 'pro' ? Dewy : Sprout;
  return (
    <div style={{ textAlign: 'center', padding: '38px 20px 30px' }}>
      <M size={96} mood="zzz" />
      <div style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 16, marginTop: 8 }}>All quiet.</div>
      <div style={{ fontSize: 13, color: 'var(--ink-500)', marginTop: 4 }}>When something needs you, it shows up here.</div>
    </div>
  );
}

/* ─── Feed — list + mark-all-read + clear (shows the empty state) ───── */
function NotifFeed({ persona, onOpen, compact = false, frame = true }) {
  const [, force] = React.useState(0);
  const [cleared, setCleared] = React.useState(false);
  const read = notifReadStore();
  const items = cleared ? [] : NOTIFS[persona];
  const unread = items.filter(n => n.unread && !read[n.id]).length;
  const open = (n) => { read[n.id] = true; force(x => x + 1); onOpen && onOpen(n); };
  const markAll = () => { items.forEach(n => { read[n.id] = true; }); force(x => x + 1); };
  const body = items.length === 0 ? <NotifEmpty persona={persona} /> : items.map((n, i) => (
    <NotifRow key={n.id} n={n} read={!!read[n.id]} first={i === 0} compact={compact} onOpen={() => open(n)} />
  ));
  if (!frame) return <div>{body}</div>;
  return (
    <div className="card" style={{ overflow: 'hidden' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: compact ? '11px 14px' : '12px 16px', borderBottom: '1px solid var(--ink-100)' }}>
        <Icon name="bell" size={16} style={{ color: 'var(--ink-700)' }} />
        <span style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 14 }}>Notifications</span>
        {unread > 0 && <span style={{ background: 'var(--alert-500)', color: '#fff', borderRadius: 99, minWidth: 18, height: 18, fontSize: 11, fontWeight: 800, display: 'grid', placeItems: 'center', padding: '0 5px' }}>{unread}</span>}
        <span style={{ marginLeft: 'auto', display: 'inline-flex', gap: 4 }}>
          {unread > 0 && <button onClick={markAll} style={{ border: 'none', background: 'none', color: 'var(--green-700)', fontWeight: 700, fontSize: 12, cursor: 'pointer', padding: '4px 6px' }}>Mark all read</button>}
          {items.length > 0 && <button onClick={() => setCleared(true)} style={{ border: 'none', background: 'none', color: 'var(--ink-500)', fontWeight: 700, fontSize: 12, cursor: 'pointer', padding: '4px 6px' }}>Clear</button>}
        </span>
      </div>
      {body}
    </div>
  );
}

Object.assign(window, {
  RESCHED_REQS, reqLatestTo, reqNeedsOwner, reqWaiting, reqResolved, needsOwnerCount,
  RESCHED_CHIP, ReschedChip, TimeChange, MiniCalendar, TimeChips, TIME_SLOTS,
  ProposeForm, DeclineForm, ReassignForm, NegotiationThread, TURN_META, Modal,
  NOTIFS, NOTIF_TONE, notifReadStore, notifUnreadCount, NotifBell, NotifRow, NotifEmpty, NotifFeed,
});
