// forecast-app.jsx — Trunkrs forecast wizard
// Chunks 2–3: TopNav + Stepper

// ---- Inline SVG icons ----
function IconDownload() {
  return (
    <svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
      <path d="M10 3v9M6 9l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
      <path d="M3 15h14" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
    </svg>
  );
}

function IconChevronDown({ size = 16, style }) {
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden="true" style={style}>
      <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  );
}

function IconHelpCircle() {
  return (
    <svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
      <circle cx="10" cy="10" r="8" stroke="currentColor" strokeWidth="1.5"/>
      <path d="M8 8c0-1.1.9-2 2-2s2 .9 2 2c0 .8-.5 1.5-1.2 1.8L10 10.5V12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
      <circle cx="10" cy="14.5" r=".75" fill="currentColor"/>
    </svg>
  );
}

// ---- Generic icon (img-based, or mask-based when mask=true) ------------
function Icon({ name, size = 18, mask = false }) {
  if (mask) {
    const url = `url(assets/icons/${name}.svg)`;
    return (
      <span
        className="ic-mask"
        style={{ width: size, height: size, WebkitMaskImage: url, maskImage: url }}
        aria-hidden="true"
      />
    );
  }
  return <img src={`assets/icons/${name}.svg`} width={size} height={size} alt="" aria-hidden="true" />;
}

// ---- Rich tooltip ---------------------------------------------------------
function Tip({ children, title, sub, body, style }) {
  const [on, setOn] = React.useState(false);
  const hasContent = !!(title || body);
  return (
    <span
      style={{ position: 'relative', display: 'flex', alignItems: 'center', cursor: 'default', ...style }}
      onMouseEnter={() => { if (hasContent) setOn(true); }}
      onMouseLeave={() => setOn(false)}
    >
      {children}
      {on && hasContent && (
        <div style={{
          position: 'absolute', bottom: 'calc(100% + 10px)',
          left: '50%', transform: 'translateX(-50%)',
          background: '#fff', color: 'var(--trk-fg-1)',
          padding: '10px 12px', borderRadius: '4px',
          border: '1px solid var(--trk-gray-42)',
          boxShadow: 'var(--trk-elevation-2)', zIndex: 200,
          width: 240, pointerEvents: 'none',
          fontSize: 12, lineHeight: '17px',
        }}>
          <div style={{ fontWeight: 700, marginBottom: 3 }}>{title}</div>
          {sub && <div style={{ color: 'var(--trk-fg-2)', fontSize: 12, marginBottom: 5 }}>{sub}</div>}
          <div style={{ whiteSpace: 'normal', lineHeight: '15px', color: 'var(--trk-fg-2)' }}>{body}</div>
          <div style={{
            position: 'absolute', bottom: -5, left: '50%', transform: 'translateX(-50%) rotate(45deg)',
            width: 10, height: 10, background: '#fff',
            borderRight: '1px solid var(--trk-gray-42)', borderBottom: '1px solid var(--trk-gray-42)',
          }} />
        </div>
      )}
    </span>
  );
}

// ---- TopNav ---------------------------------------------------------------
function TopNav() {
  return (
    <header className="topnav" role="banner">
      <div className="topnav-inner">

        <div className="topnav-logo">
          <img src="assets/logos/logo-horizontal-color-dark.svg" alt="Trunkrs" />
        </div>

        <nav className="topnav-nav" aria-label="Main navigation">
          <a>
            Shipments
            <span className="nav-caret"><IconChevronDown /></span>
          </a>
          <a>Collections</a>
          <a className="active" aria-current="page">Forecast</a>
          <a>Invoice</a>
          <a>Cases</a>
          <a>
            Settings
            <span className="nav-caret"><IconChevronDown /></span>
          </a>
        </nav>

        <div className="topnav-right">
          <button className="topnav-lang" aria-label="Language: Dutch">
            <span className="nl-flag" role="img" aria-label="Dutch flag" />
            <span>NL</span>
            <IconChevronDown size={14} style={{ opacity: 0.7 }} />
          </button>
          <button className="topnav-dl" aria-label="Downloads">
            <IconDownload />
          </button>
          <button className="topnav-btn">
            <IconHelpCircle />
            Help Center
          </button>
          <button className="topnav-btn">
            MerchantName
            <IconChevronDown size={14} />
          </button>
        </div>

      </div>
    </header>
  );
}

// ---- Mock data ------------------------------------------------------------
const LAST_WEEK = {
  labels: ['4 Mon', '5 Tue', '6 Wed', '7 Thu', '8 Fri'],
  sub:    ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
  trk:    [390, 420, 480, 355, 510],
  yours:  [400, 410, 500, 370, 490],
  actual: [385, 432, 471, 338, 524],
};

// ---- Forecast page: the rolling 3-week window -----------------------------
// The submit form always shows the current week plus the next two, computed
// from today. Merchants submit whenever they like; if they don't, the Trunkrs
// forecast is used as-is. The day before today, today, and tomorrow are locked
// — those volumes are already committed to trucks and drivers.
const WD_SHORT  = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
const MONTHS    = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const TRK_SHAPE = [205, 228, 215, 240, 295]; // Trunkrs baseline, Mon–Fri
// Absolute Trunkrs baseline override for a specific week offset (skips scaling).
// Week +1 (next week) is an unusually low-volume week — good for exercising the
// axis floor. It's inside the editable 3-week window, so it shows in the review
// form too. Keyed by week offset, shared by buildForecastWeeks + buildSchedule.
const TRK_OVERRIDE = { '1': [18, 15, 20, 16, 22] };
const LOCK_REASON = "no more changes can be made to this day's forecast";

function addDays(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return x; }
function startOfIsoWeek(d) {
  const x = new Date(d.getFullYear(), d.getMonth(), d.getDate());
  x.setDate(x.getDate() - ((x.getDay() + 6) % 7)); // back to Monday
  return x;
}
function isoWeekNumber(d) {
  const x = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
  x.setUTCDate(x.getUTCDate() + 4 - (x.getUTCDay() || 7)); // nearest Thursday
  const yearStart = new Date(Date.UTC(x.getUTCFullYear(), 0, 1));
  return Math.ceil((((x - yearStart) / 86400000) + 1) / 7);
}
function isoDate(d) { const p = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; }
function weekDateRange(mon) {
  const fri = addDays(mon, 4);
  return mon.getMonth() === fri.getMonth()
    ? `${mon.getDate()} – ${fri.getDate()} ${MONTHS[fri.getMonth()]}`
    : `${mon.getDate()} ${MONTHS[mon.getMonth()]} – ${fri.getDate()} ${MONTHS[fri.getMonth()]}`;
}

function buildForecastWeeks() {
  const now      = new Date();
  const today    = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const firstEditable = addDays(today, 2); // day-after-tomorrow onward is editable
  const week0Mon = startOfIsoWeek(today);

  return [0, 1, 2].map(w => {
    const monday = addDays(week0Mon, w * 7);
    const days   = WD_SHORT.map((_, j) => addDays(monday, j));
    const fri    = days[4];
    const dates  = monday.getMonth() === fri.getMonth()
      ? `Mon ${monday.getDate()} – Fri ${fri.getDate()} ${MONTHS[fri.getMonth()]}`
      : `Mon ${monday.getDate()} ${MONTHS[monday.getMonth()]} – Fri ${fri.getDate()} ${MONTHS[fri.getMonth()]}`;
    return {
      key:   `fw${w}`,
      label: `Week ${isoWeekNumber(monday)}`,
      dates,
      data: {
        labels: days.map((dt, j) => `${dt.getDate()} ${WD_SHORT[j]}`),
        sub:    [...WD_SHORT],
        iso:    days.map(isoDate),
        trk:    TRK_OVERRIDE[String(w)] || TRK_SHAPE.map(v => Math.round(v * (1 + w * 0.04))),
        yours:  [null, null, null, null, null],
        closed: [],
        locked: days.map((dt, i) => (dt.getTime() < firstEditable.getTime() ? i : -1)).filter(i => i >= 0),
        lockReason: LOCK_REASON,
      },
    };
  });
}

const FORECAST_WEEKS = buildForecastWeeks();

// ---- Dashboard schedule: rolling history + upcoming, anchored to today ------
// Mirrors buildForecastWeeks' dynamic dating so the dashboard trend never shows
// stale week numbers. Weeks run from 5 weeks ago (graded, with actuals) through
// the current week to two upcoming weeks — 5 behind + this week + 2 ahead. The
// two ahead are editable in quick-edit mode.
//   editPolicy: 'past' = no edit; 'within7' = current week (edit + warn);
//               'open' = upcoming submitted (edit freely); 'none' = pending.
// "yours" is sparse — a value only where the merchant overrode Trunkrs (null
// otherwise) — a realistic scatter of isolated edits and short runs, which is
// what drives the tether/run rendering in the charts.
function buildSchedule() {
  const now      = new Date();
  const today    = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const week0Mon = startOfIsoWeek(today);
  // Same near-term lock as the review form (buildForecastWeeks): the day before
  // today, today, and tomorrow are already committed to trucks/drivers — the
  // first editable day is the day after tomorrow. Quick edit must honour this.
  const firstEditable = addDays(today, 2);

  // Per-week overrides vs Trunkrs, keyed by week offset: { i: dayIndex, d: delta }.
  // Only PAST weeks carry edits (the merchant's submitted history). The current
  // and upcoming weeks start blank here so they match the review page, where
  // "your forecast" is null until the merchant actually changes a day.
  const EDITS = {
    '-5': [{ i: 2, d: 22 }],                                     // single isolated Wed
    '-4': [{ i: 0, d: -18 }, { i: 1, d: -12 }],                  // Mon–Tue run
    '-3': [{ i: 0, d: 25 }, { i: 3, d: -25 }, { i: 4, d: -15 }], // isolated Mon + Thu–Fri run
    '-2': [{ i: 1, d: 30 }],                                     // single isolated Tue
    '-1': [{ i: 2, d: 41 }, { i: 4, d: -55 }],                   // two isolated (Wed, Fri)
  };
  // Deterministic actual-vs-forecast gap per week. Small values stay within the
  // day tolerance (on target); the larger ones deliberately blow it so the grade
  // varies and the weekly overview has real misses to explain. Grades below are
  // on-target days × 2 (see weekGrade): -5→8, -4→10, -3→6, -2→8, -1→4.
  const ACT_WOBBLE = {
    '-5': [ 10,  -7,  88,  -9,   6],   // Wed miss                → 4/5 → 8
    '-4': [  8,  -6,  11,  -8,   5],   // all on target           → 5/5 → 10
    '-3': [-90,   8,  12,  -7,  92],   // Mon + Fri miss          → 3/5 → 6
    '-2': [  7,  -6,  10,  96,   5],   // Thu miss                → 4/5 → 8
    '-1': [  9, -88,  90,  -8, -92],   // Tue + Wed + Fri miss    → 2/5 → 4
  };
  const DEFAULT_WOBBLE = [9, -6, 11, -8, 5];

  const specs = [
    { off: -5, status: 'submitted', policy: 'past'    },
    { off: -4, status: 'submitted', policy: 'past'    },
    { off: -3, status: 'submitted', policy: 'past'    },
    { off: -2, status: 'submitted', policy: 'past'    },
    { off: -1, status: 'submitted', policy: 'past'    },
    { off:  0, status: 'submitted', policy: 'within7' },
    { off:  1, status: 'submitted', policy: 'open'    },
    { off:  2, status: 'submitted', policy: 'open'    },
  ];

  return specs.map(({ off, status, policy }) => {
    const monday    = addDays(week0Mon, off * 7);
    const isPending = status === 'pending';
    const trk       = TRK_OVERRIDE[String(off)] || TRK_SHAPE.map(v => Math.round(v * (1 + off * 0.03)));
    const edits     = EDITS[String(off)] || [];

    const days = WD_SHORT.map((wd, j) => {
      const dt    = addDays(monday, j);
      const ov    = edits.find(e => e.i === j);
      const yours = (isPending || !ov) ? null : trk[j] + ov.d;
      const base  = yours == null ? trk[j] : yours;
      return {
        date:   `${wd} ${dt.getDate()} ${MONTHS[dt.getMonth()]}`,
        iso:    isoDate(dt),
        yours,
        trk:    trk[j],
        // Any day that has already elapsed carries an actual — so the current
        // week's past days show actuals in the schedule modal (and trend) just
        // like the fully-past weeks, and today/future stay null until they land.
        actual: dt.getTime() < today.getTime() ? base + (ACT_WOBBLE[String(off)] || DEFAULT_WOBBLE)[j] : null,
        locked: dt.getTime() < firstEditable.getTime(),
      };
    });

    const week = {
      week:       isoWeekNumber(monday),
      start:      isoDate(monday),
      label:      `Week ${isoWeekNumber(monday)}`,
      dates:      weekDateRange(monday),
      status,
      volume:     isPending ? null : days.reduce((a, d) => a + (d.yours == null ? d.trk : d.yours), 0),
      grade:      weekGrade(days),
      editPolicy: policy,
      days,
    };
    if (isPending) week.due = `${WD_SHORT[0]} ${monday.getDate()} ${MONTHS[monday.getMonth()]}`;
    return week;
  });
}
const SCHEDULE = buildSchedule();

// ---- Holidays (dev-simulated) ---------------------------------------------
// Closed days are real in production — Trunkrs doesn't deliver on Dutch public
// holidays — but the rolling 8-week demo window only rarely contains one, so
// there's usually nothing to look at. The dev panel drops a closed day on any
// date; from there down, every surface reads it exactly as it would a real one.
const HOLIDAY_KEY      = 'trk_fo_dev_holiday';
const HOLIDAY_DATE_KEY = 'trk_fo_dev_holiday_date';
// Default to Thursday of next week: inside the trend window, and inside the
// review form's editable range, so both surfaces show it the moment you toggle.
const DEFAULT_HOLIDAY_DATE = isoDate(addDays(startOfIsoWeek(new Date()), 10));

// Easter Sunday (anonymous Gregorian computus) — anchors the moving NL holidays.
function easterSunday(year) {
  const a = year % 19, b = Math.floor(year / 100), c = year % 100;
  const d = Math.floor(b / 4), e = b % 4, f = Math.floor((b + 8) / 25);
  const g = Math.floor((b - f + 1) / 3);
  const h = (19 * a + b - d - g + 15) % 30;
  const i = Math.floor(c / 4), k = c % 4;
  const l = (32 + 2 * e + 2 * i - h - k) % 7;
  const m = Math.floor((a + 11 * h + 22 * l) / 451);
  const month = Math.floor((h + l - 7 * m + 114) / 31);
  return new Date(year, month - 1, ((h + l - 7 * m + 114) % 31) + 1);
}

// Fallback name for a closed day that isn't a recognised Dutch public holiday.
// Callers compare against it to avoid copy like "Public holiday — public holiday".
const GENERIC_HOLIDAY = 'Public holiday';

const NL_FIXED = {
  '01-01': 'Nieuwjaarsdag',   '04-27': 'Koningsdag',      '05-05': 'Bevrijdingsdag',
  '12-25': 'Eerste Kerstdag', '12-26': 'Tweede Kerstdag',
};
const NL_EASTER_OFFSET = {
  '-2': 'Goede Vrijdag',   '0': 'Eerste Paasdag',     '1': 'Tweede Paasdag',
  '39': 'Hemelvaartsdag', '49': 'Eerste Pinksterdag', '50': 'Tweede Pinksterdag',
};

// Name the chosen date when it happens to be a genuine Dutch public holiday, so
// the demo copy reads true; anything else is just an unnamed closed day.
function nlHolidayName(iso) {
  const dt = new Date(iso + 'T00:00:00');
  if (isNaN(dt)) return GENERIC_HOLIDAY;
  if (NL_FIXED[iso.slice(5)]) return NL_FIXED[iso.slice(5)];
  const offset = Math.round((dt - easterSunday(dt.getFullYear())) / 86400000);
  return NL_EASTER_OFFSET[String(offset)] || GENERIC_HOLIDAY;
}

// { columnIndex: holidayName } for a run of ISO dates — the shape the chart
// (axis chip + line gap) and the review table both consume.
function holidayMap(isoDates, holidayDate) {
  if (!holidayDate || !isoDates) return {};
  const i = isoDates.indexOf(holidayDate);
  return i === -1 ? {} : { [i]: nlHolidayName(holidayDate) };
}

// "Thu 14 May 2026" — the human date shown beside the holiday name.
function holidayDateLabel(iso) {
  const dt = new Date(iso + 'T00:00:00');
  if (isNaN(dt)) return iso;
  return `${_WEEKDAYS[dt.getDay()]} ${dt.getDate()} ${_MONTHS[dt.getMonth()]} ${dt.getFullYear()}`;
}

// ---- Stop mode window -----------------------------------------------------
// Dev control: a stopping merchant's forecast is zeroed for STOP_WEEKS weeks
// starting from the week that contains `stopStart` (an ISO yyyy-mm-dd date).
const STOP_WEEKS = 8;
const STOP_START_KEY = 'trk_fo_dev_stop_start';
// Current week's Monday — the first non-past week, anchored to today.
const DEFAULT_STOP_START = isoDate(startOfIsoWeek(new Date()));
const _MONTHS   = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const _WEEKDAYS = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];

function stopWindow(stopStart) {
  if (!stopStart) return null;
  const dt = new Date(stopStart + 'T00:00:00');
  if (isNaN(dt)) return null;
  dt.setDate(dt.getDate() - ((dt.getDay() + 6) % 7)); // back up to Monday
  const from = dt.getTime();
  return { from, to: from + STOP_WEEKS * 7 * 86400000 };
}

// Human-readable Monday the stoppage begins, e.g. "Mon 11 May 2026".
function stopStartLabel(stopStart) {
  const win = stopWindow(stopStart);
  if (!win) return '';
  const d = new Date(win.from);
  return `${_WEEKDAYS[d.getDay()]} ${d.getDate()} ${_MONTHS[d.getMonth()]} ${d.getFullYear()}`;
}

// Helpers to synthesize schedule rows for stop-window weeks beyond SCHEDULE.
const _WEEK20_MONDAY = new Date('2026-05-11T00:00:00').getTime(); // Week 20 anchor
function isoLocal(d) {
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
function weekNumberFor(ms) {
  return 20 + Math.round((ms - _WEEK20_MONDAY) / (7 * 86400000));
}
function weekRangeLabel(monday) {
  const fri = new Date(monday.getTime() + 4 * 86400000);
  const m1 = _MONTHS[monday.getMonth()], m2 = _MONTHS[fri.getMonth()];
  return m1 === m2
    ? `${monday.getDate()} – ${fri.getDate()} ${m2}`
    : `${monday.getDate()} ${m1} – ${fri.getDate()} ${m2}`;
}

// SCHEDULE plus synthesized zeroed rows so the full 8-week stoppage is visible.
function scheduleWithStopWindow(stopMode, stopStart) {
  if (!stopMode) return SCHEDULE;
  const win = stopWindow(stopStart);
  if (!win) return SCHEDULE;
  const have = new Set(SCHEDULE.map(w => w.start));
  const extra = [];
  const cur = new Date(win.from);
  for (let i = 0; i < STOP_WEEKS; i++) {
    const iso = isoLocal(cur);
    if (!have.has(iso)) {
      const n = weekNumberFor(cur.getTime());
      extra.push({ week: n, start: iso, label: `Week ${n}`, dates: weekRangeLabel(new Date(cur)),
                   status: 'stopped', volume: null, grade: null, editPolicy: 'none', synthetic: true });
    }
    cur.setDate(cur.getDate() + 7);
  }
  return [...SCHEDULE, ...extra].sort((a, b) => (a.start < b.start ? -1 : 1));
}

// A week is zeroed when stop mode is on, its Monday falls inside the window,
// and it hasn't already ended (past weeks keep their grades).
function isWeekStopped(w, stopMode, stopStart) {
  if (!stopMode || !w || !w.start || w.editPolicy === 'past') return false;
  const win = stopWindow(stopStart);
  if (!win) return false;
  const ms = new Date(w.start + 'T00:00:00').getTime();
  return ms >= win.from && ms < win.to;
}

const REASONS = [
  { id: 'promo',    title: 'Promotion / sale', desc: 'Marketing campaign or discount',      icon: 'trend-up'    },
  { id: 'stockout', title: 'Stock-out',         desc: 'Low stock limits the volume',         icon: 'warning'     },
  { id: 'launch',   title: 'New launch',        desc: 'New product or category',             icon: 'package'     },
  { id: 'weather',  title: 'Weather event',     desc: 'Storm or hot weather affects demand', icon: 'info-circle' },
  { id: 'holiday',  title: 'Holiday / event',   desc: 'Public holiday or special event',     icon: 'calendar'    },
  { id: 'closed',   title: 'Closed for a holiday', desc: 'Business closed — little or no volume', icon: 'lock'    },
  { id: 'structural', title: 'Structural volume change', desc: 'A lasting shift in baseline, not a one-off', icon: 'history' },
  { id: 'other',    title: 'Something else',    desc: 'Explain in the notes below',          icon: 'edit'        },
];

// ---- Grade (Dutch cijfer, even only) — derived from the tolerance rules ----
// A day is "on target" when |forecast − actual| is within the allowed
// difference, banded by the actual's size:
//     actual < 250        → within 50
//     250 ≤ actual < 500  → within 20% of actual
//     actual ≥ 500        → within 100
// The forecast judged is what the customer committed: their override, or the
// Trunkrs number on days they accepted it. Grade = on-target days × 2 (five
// operating days → even 0·2·4·6·8·10). Pass line at 6 (3 of 5 days on target).
function dayForecast(d) { return d.yours == null ? d.trk : d.yours; }
function dayAllowedDiff(actual) {
  if (actual == null) return null;
  if (actual < 250) return 50;
  if (actual < 500) return Math.round(actual * 0.2);
  return 100;
}
// Per-day verdict, or null when the day can't be judged yet (no actual).
function dayVerdict(d) {
  const forecast = dayForecast(d);
  if (forecast == null || d.actual == null) return null;
  const diff    = Math.abs(forecast - d.actual);
  const allowed = dayAllowedDiff(d.actual);
  return { forecast, actual: d.actual, diff, allowed, within: diff <= allowed };
}
// Week grade + hit breakdown, or null until every operating day has an actual.
function weekGradeInfo(days) {
  const ds = days ?? [];
  if (!ds.length || ds.some(d => dayVerdict(d) == null)) return null;
  const verdicts = ds.map(dayVerdict);
  const hits = verdicts.filter(v => v.within).length;
  return { hits, total: verdicts.length, grade: Math.round((hits / verdicts.length) * 5) * 2 };
}
function weekGrade(days) { return weekGradeInfo(days)?.grade ?? null; }

// ---- Dev: forced week grade -----------------------------------------------
// The demo actuals only ever produce 8/10/6/8/4, so the edge grades — 0 above
// all, which is the loudest state on the chart — can't be reviewed. Rather than
// stamp a fake `grade` (which would contradict the day rows in the detail
// modal), this rewrites the week's *actuals* so the target grade is genuinely
// earned: every consumer — band, pill, KPI, table, per-day breakdown — then
// agrees, because they all derive from the same verdicts.
const GRADE_MISS_DELTA = 120;                 // clears every tolerance band
const GRADE_HIT_DELTA  = [9, -6, 11, -8, 5];  // comfortably inside tolerance

function withGradeOverride(schedule, grade) {
  if (grade == null) return schedule;
  // The most recent graded week — also the one behind the "Last week grade" KPI.
  let idx = -1;
  schedule.forEach((w, i) => { if (w.grade != null) idx = i; });
  if (idx === -1) return schedule;
  const wk = schedule[idx];
  const misses = wk.days.length - grade / 2;   // grade = hits × 2
  const days = wk.days.map((d, j) => {
    const forecast = d.yours == null ? d.trk : d.yours;
    const delta = j < misses
      ? GRADE_MISS_DELTA * (j % 2 ? -1 : 1)
      : GRADE_HIT_DELTA[j % GRADE_HIT_DELTA.length];
    return { ...d, actual: forecast + delta };
  });
  return schedule.map((w, i) => (i === idx ? { ...w, days, grade: weekGrade(days) } : w));
}

// 6–10 pass (green), 4 borderline (amber), 0–2 fail (red).
function gradeBand(g) {
  if (g == null) return 'neutral';
  if (g >= 8) return 'success';
  if (g === 6) return 'warning';
  return 'danger';
}

// ---- GradeBox -------------------------------------------------------------
function GradeBox({ grade, size = 'md', label, sub }) {
  const band = gradeBand(grade);
  return (
    <div className={`grade-box grade-${band} grade-${size}`}>
      {label && <div className="grade-label">{label}</div>}
      <div className="grade-row">
        <div className="grade-figure">
          <span className="grade-num">{grade == null ? '—' : grade}</span>
        </div>
        {sub && <div className="grade-sub">{sub}</div>}
      </div>
    </div>
  );
}

// ---- StatusBadge ----------------------------------------------------------
function StatusBadge({ status }) {
  if (status === 'submitted') return <span className="sb sb-success"><span className="sb-dot" />Submitted</span>;
  if (status === 'pending')   return <span className="sb"><span className="sb-dot" />Pending</span>;
  if (status === 'overdue')   return <span className="sb sb-danger"><span className="sb-dot" />Overdue</span>;
  if (status === 'stopped')   return <span className="sb sb-danger"><span className="sb-dot" />Stopped</span>;
  return null;
}

// ---- MotdTicker -----------------------------------------------------------
const MOTD = [
  { icon: 'package',     text: 'Your forecast helps us choose the right number of trucks for your deliveries.' },
  { icon: 'calendar',    text: 'The sooner you flag a change, the more easily we can plan around it.' },
  { icon: 'trend-up',    text: 'Better forecasts mean better on-time delivery rates for your customers.' },
  { icon: 'info-circle', text: "No need to review every week — we'll use the Trunkrs forecast unless you change it." },
  { icon: 'package',     text: 'Big sale or launch coming? Adjust those days so we can plan ahead.' },
];

function MotdTicker() {
  const [idx, setIdx]   = React.useState(0);
  const [fade, setFade] = React.useState('in');

  React.useEffect(() => {
    const t = setInterval(() => {
      setFade('out');
      setTimeout(() => {
        setIdx(i => (i + 1) % MOTD.length);
        setFade('in');
      }, 280);
    }, 7000);
    return () => clearInterval(t);
  }, []);

  const msg = MOTD[idx];
  return (
    <div className="rib" role="status" aria-live="polite">
      <span className="rib-ic">
        <span className="ic" style={{
          WebkitMaskImage: `url('assets/icons/${msg.icon}.svg')`,
          maskImage:        `url('assets/icons/${msg.icon}.svg')`,
        }} />
      </span>
      <div className={`rib-msg ${fade}`}>
        <span>{msg.text}</span>
      </div>
      <div className="rib-dots" aria-hidden="true">
        {MOTD.map((_, i) => <span key={i} className={`rib-d${i === idx ? ' on' : ''}`} />)}
      </div>
    </div>
  );
}

// ---- Editor table ---------------------------------------------------------
function isSignificant(v, trk) {
  if (v == null || v === trk || trk == null) return false;
  const d = Math.abs(v - trk);
  return trk > 100 ? d > trk * 0.10 : d > 50;
}

function Editor({ data, predictions, onChange, onHover, hoverIndex, weekBreak, weekBreakLabel, disabled = false }) {
  return (
    <div className="editor" onMouseLeave={() => onHover && onHover(null)}>
      <div className="lab">Date</div>
      <div className="lab">Your forecast</div>
      <div className="lab" style={{ textAlign: 'right' }}>Trunkrs</div>
      <div className="lab" style={{ textAlign: 'center' }}>Diff</div>

      {data.labels.map((lab, i) => {
        const closed = (data.closed || []).includes(i);
        const locked = (data.locked || []).includes(i);
        const yours  = predictions[i];
        const trk    = data.trk[i];
        const changed = !closed && !locked && isSignificant(yours, trk);
        const delta   = yours == null ? null : (yours - (trk || 0));
        const hover   = hoverIndex === i;

        if (locked && !disabled) {
          return (
            <React.Fragment key={i}>
              <div className="row-date locked">{lab}</div>
              <div className="day-locked">
                <Icon name="lock" size={16} mask />
                <span><strong>Locked</strong> — {data.lockReason || LOCK_REASON}</span>
              </div>
            </React.Fragment>
          );
        }

        if (closed) {
          const lbl       = (data.closedLabels && data.closedLabels[i]) || 'Closed';
          const isHoliday = lbl !== 'Closed';
          return (
            <React.Fragment key={i}>
              <div className="row-date" style={{ opacity: 0.5 }}>
                {lab}
              </div>
              {isHoliday && data.holidayInfo ? (
                <Tip
                  title={data.holidayInfo.name}
                  sub={data.holidayInfo.date}
                  body="No deliveries on that day. Trunkrs redistributed the volume to the surrounding days."
                  style={{ gridColumn: 'span 3', borderRadius: 4, padding: '8px 10px', gap: 8, background: 'var(--trk-yellow-fade-light)', fontSize: 12 }}
                >
                  <Icon name="calendar" size={14} />
                  <span style={{ color: '#403516', fontWeight: 700 }}>
                    {lbl === GENERIC_HOLIDAY
                      ? `${lbl} — no delivery`
                      : `${lbl} — public holiday, no delivery`}
                  </span>
                </Tip>
              ) : (
                <div style={{
                  gridColumn: 'span 3', fontSize: 12, color: 'var(--trk-fg-2)',
                  padding: '8px 10px', background: 'var(--trk-bg)',
                  borderRadius: 4, display: 'flex', alignItems: 'center', gap: 8,
                }}>
                  <Icon name="info-circle" size={14} />
                  <span>No delivery — closed</span>
                </div>
              )}
            </React.Fragment>
          );
        }

        return (
          <React.Fragment key={i}>
            {weekBreak != null && i === weekBreak && (
              <div className="week-sep">{weekBreakLabel || ''}</div>
            )}
            <div className="row-date"
                 onMouseEnter={() => onHover && onHover(i)}
                 style={hover ? { color: 'var(--trk-light-violet-50)' } : null}>
              {lab}
            </div>
            <div className="tt" onMouseEnter={() => onHover && onHover(i)}>
              <div className="inp-row">
                <button className="btn btn-secondary btn-sq" disabled={disabled} onClick={() => onChange(i, Math.max(0, (yours == null ? (trk || 0) : yours) - 5))} aria-label="Decrease by 5"><Icon name="subtract" size={16} /></button>
                <input
                  className="inp-field"
                  type="number"
                  value={yours == null ? '' : yours}
                  placeholder={trk != null ? String(trk) : ''}
                  onChange={e => { const r = e.target.value; onChange(i, r === '' ? null : parseInt(r, 10)); }}
                  min="0"
                  disabled={disabled}
                />
                <button className="btn btn-secondary btn-sq" disabled={disabled} onClick={() => onChange(i, (yours == null ? (trk || 0) : yours) + 5)} aria-label="Increase by 5"><Icon name="add" size={16} /></button>
              </div>
              {!disabled && changed && delta != null && (
                <div className="tt-bub warning">
                  <strong>Significant change</strong>
                  {delta > 0 ? '+' : ''}{delta} parcels vs Trunkrs<br/>
                  Reason required to submit
                </div>
              )}
            </div>
            <div className="trk-pred" onMouseEnter={() => onHover && onHover(i)}>{trk ?? '—'}</div>
            <div
              className={'delta' + (delta != null && delta > 0 ? ' up' : delta != null && delta < 0 ? ' down' : '')}
              onMouseEnter={() => onHover && onHover(i)}
            >
              {delta == null ? '—' : delta === 0 ? '0' : (delta > 0 ? '+' : '') + delta}
            </div>
          </React.Fragment>
        );
      })}
    </div>
  );
}

// ---- Amend modal ----------------------------------------------------------
function AmendModal({ onClose, onSubmit, changedDays = [] }) {
  const weeks = React.useMemo(() => {
    const map = new Map();
    for (const day of changedDays) {
      const key = day.weekLabel || 'Changes';
      if (!map.has(key)) map.set(key, []);
      map.get(key).push(day);
    }
    return Array.from(map.entries()).map(([label, days]) => ({ label, days }));
  }, [changedDays]);

  const total = weeks.length || 1;
  const [weekIdx, setWeekIdx] = React.useState(0);
  const [answers, setAnswers] = React.useState(() =>
    Array.from({ length: total }, () => ({ reason: null, note: '' }))
  );

  const currentWeek = weeks[weekIdx];
  const answer  = answers[weekIdx] ?? { reason: null, note: '' };
  const isLast  = weekIdx === total - 1;
  const canAdvance = answer.reason && (answer.reason !== 'other' || answer.note.trim());

  function update(patch) {
    setAnswers(prev => prev.map((a, i) => i === weekIdx ? { ...a, ...patch } : a));
  }

  function handleNext() {
    if (isLast) onSubmit(answers);
    else setWeekIdx(i => i + 1);
  }

  return (
    <div className="scrim" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        <div className="modal-hd">
          <div className="ttl">
            <h3>Why are you changing it?</h3>
            <p>{total > 1
              ? `${currentWeek?.label} · ${weekIdx + 1} of ${total} weeks`
              : 'A short reason helps Trunkrs plan and helps our model learn.'}</p>
          </div>
          <button className="x" onClick={onClose} aria-label="Close"><Icon name="close" size={18} /></button>
        </div>

        <div className="modal-bd">
          <div className="amend-steps">
              {weeks.map((wk, i) => (
                <React.Fragment key={i}>
                  <div className={`amend-step-pill ${i < weekIdx ? 'done' : i === weekIdx ? 'current' : 'future'}`}>
                    {wk.label}
                  </div>
                  {i < weeks.length - 1 && <div className={`amend-step-line ${i < weekIdx ? 'done' : 'pending'}`} />}
                </React.Fragment>
              ))}
          </div>

          {currentWeek && (
            <div className="amend-week-table">
              <div className="awt-hd">
                <span>Day</span>
                <span>Yours</span>
                <span>Trunkrs</span>
                <span>Diff</span>
              </div>
              {currentWeek.days.map((d, i) => (
                <div className="awt-row" key={i}>
                  <span className="awt-col awt-date">{d.date}</span>
                  <span className="awt-col">{d.yours?.toLocaleString()}</span>
                  <span className="awt-col">{d.trk?.toLocaleString() ?? '—'}</span>
                  <span className={`awt-col awt-delta ${d.delta > 0 ? 'pos' : 'neg'}`}>
                    {d.delta > 0 ? '+' : ''}{d.delta}
                  </span>
                </div>
              ))}
            </div>
          )}

          <div className="reason-grid">
            {REASONS.map(r => (
              <div
                key={r.id}
                className={'reason-card' + (answer.reason === r.id ? ' is-selected' : '')}
                onClick={() => update({ reason: r.id })}
                role="radio" aria-checked={answer.reason === r.id} tabIndex={0}
                onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); update({ reason: r.id }); } }}
              >
                <div className="rc-ic"><Icon name={r.icon} size={18} /></div>
                <div>
                  <div className="rc-t">{r.title}</div>
                  <div className="rc-m">{r.desc}</div>
                </div>
              </div>
            ))}
          </div>

          <div className="ta-wrap">
            <textarea
              value={answer.note}
              onChange={e => update({ note: e.target.value })}
              placeholder={answer.reason === 'other'
                ? 'Tell us what changed and when…'
                : 'Add details (optional) — sale dates, expected extra volume, areas affected…'}
            />
          </div>
          <div className="ta-hint">Your reason goes to your Trunkrs success manager. We may contact you if the change is more than 20%.</div>
        </div>

        <div className="modal-ft">
          <button className="btn btn-tertiary btn-lg" onClick={onClose}>Cancel</button>
          <button className="btn btn-primary btn-lg" disabled={!canAdvance} onClick={handleNext}>
            {isLast ? 'Submit changes' : 'Next'}
          </button>
        </div>
      </div>
    </div>
  );
}

// ---- HintBanner -----------------------------------------------------------
function HintBanner({ storageKey, children, elevated, className = '' }) {
  const [dismissed, setDismissed] = React.useState(() => !!localStorage.getItem(storageKey));
  if (dismissed) return null;
  function dismiss() {
    localStorage.setItem(storageKey, '1');
    setDismissed(true);
  }
  return (
    <div className={`co hint-banner${className ? ' ' + className : ''}`} data-intent="info" style={elevated ? { boxShadow: 'var(--trk-elevation-1)' } : undefined}>
      <span className="co-ic">
        <span className="ic" style={{ WebkitMaskImage: 'url(assets/icons/info-circle.svg)', maskImage: 'url(assets/icons/info-circle.svg)' }} />
      </span>
      <div className="co-bd">
        <div className="co-m">{children}</div>
      </div>
      <button className="hint-x" onClick={dismiss} aria-label="Dismiss tip">
        <Icon name="close" size={14} />
      </button>
    </div>
  );
}

// ---- WelcomeModal ---------------------------------------------------------
const WELCOME_KEY = 'trk_forecast_welcomed';

const WELCOME_STEPS = [
  {
    title: 'Forecasting has changed',
    body: [
      { icon: 'remove-circle', text: 'No more CSV uploads — your forecast is calculated automatically from your shipment history and seasonal patterns.' },
      { icon: 'trend-up',      text: 'Trunkrs keeps a forecast ready for the next three weeks. Our numbers are almost always fine to use — review and submit only if you want to change something.' },
      { icon: 'check',         text: 'Submit whenever it suits you; if you don\'t, we use the Trunkrs forecast. Your accuracy score is shown every week so you can see how well it tracked reality.' },
    ],
  },
  {
    title: 'Here\'s how it works',
    body: [
      { step: '1', label: 'Review three weeks', text: 'The current week and the next two are always shown. Accept the Trunkrs forecast as-is or edit any day.' },
      { step: '2', label: 'Near days are locked', text: 'The day before today, today, and tomorrow can\'t be changed — those volumes are already planned into trucks and drivers.' },
      { step: '3', label: 'Bigger changes', text: 'If a change is large, we\'ll ask for a quick reason so we can plan and improve the model.' },
    ],
  },
];

function WelcomeModal({ onDismiss, steps = WELCOME_STEPS }) {
  const [screen, setScreen] = React.useState(0);
  const s = steps[screen];
  const isLast = screen === steps.length - 1;

  function finish() {
    localStorage.setItem(WELCOME_KEY, '1');
    onDismiss();
  }

  return (
    <div className="scrim" onClick={finish}>
      <div className="modal modal-welcome" onClick={e => e.stopPropagation()}>

        <div className="modal-hd">
          <div className="ttl">
            <h3>{s.title}</h3>
          </div>
          <button className="x" onClick={finish} aria-label="Close">
            <Icon name="close" size={18} />
          </button>
        </div>

        <div className="modal-bd">
          {screen === 0 && (
            <img src="assets/Forecasting.svg" alt="" aria-hidden="true" className="wlc-illu" />
          )}
          {screen === 0 && (
            <ul className="wlc-list">
              {s.body.map((item, i) => (
                <li key={i} className="wlc-row">
                  <span className="wlc-ic">
                    <span className="ic" style={{ WebkitMaskImage: `url(assets/icons/${item.icon}.svg)`, maskImage: `url(assets/icons/${item.icon}.svg)` }} />
                  </span>
                  <span className="wlc-text">{item.text}</span>
                </li>
              ))}
            </ul>
          )}
          {screen === 1 && (
            <ol className="wlc-steps">
              {s.body.map((item, i) => (
                <li key={i} className="wlc-step-row">
                  <span className="wlc-step-num">{item.step}</span>
                  <div>
                    <div className="wlc-step-label">{item.label}</div>
                    <div className="wlc-step-text">{item.text}</div>
                  </div>
                </li>
              ))}
            </ol>
          )}
        </div>

        <div className="modal-ft">
          <div className="wlc-dots">
            {steps.map((_, i) => (
              <span key={i} className={`wlc-dot${i === screen ? ' is-active' : ''}`} />
            ))}
          </div>
          <div style={{ display: 'flex', gap: 8 }}>
            {screen > 0 && (
              <button className="btn btn-tertiary btn-lg" onClick={() => setScreen(s => s - 1)}>Back</button>
            )}
            {isLast ? (
              <button className="btn btn-primary btn-lg" onClick={finish}>Get started</button>
            ) : (
              <button className="btn btn-primary btn-lg" onClick={() => setScreen(s => s + 1)}>
                Next
                <span className="ic-mask" style={{ WebkitMaskImage: "url('assets/icons/arrow-right.svg')", maskImage: "url('assets/icons/arrow-right.svg')" }} />
              </button>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

// ---- Disclosure (single-item accordion, per DS) ---------------------------
// Leading chevron rotates 0→90° on open; label bold when open; panel indented
// to align under the label. Used to tuck secondary detail behind "Show more".
function Disclosure({ title, defaultOpen = false, children }) {
  const [open, setOpen] = React.useState(defaultOpen);
  return (
    <div className={`disc${open ? ' is-open' : ''}`}>
      <button type="button" className="disc-hd" aria-expanded={open} onClick={() => setOpen(o => !o)}>
        <span className="disc-chev" aria-hidden="true">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
            <path d="M9 6l6 6-6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </span>
        <span className="disc-title">{title}</span>
      </button>
      {open && <div className="disc-panel">{children}</div>}
    </div>
  );
}

// ---- WeekDetailModal ------------------------------------------------------
function WeekDetailModal({ week, holiday = null, onClose }) {
  const [legendHover, setLegendHover] = React.useState(null);

  const days       = week.days ?? [];
  const holidays   = holidayMap(days.map(d => d.iso), holiday);
  const hasActuals = days.some(d => d.actual != null);
  const isPending  = week.status === 'pending';

  const dayLabels = days.map(d => d.date.split(' ')[1]);
  const daySub    = days.map(d => d.date.split(' ')[0]);

  const gradeInfo = weekGradeInfo(days);
  const yourGrade = gradeInfo?.grade ?? (week.grade ?? null);
  // Days that blew the tolerance, in date order — the "why" behind the grade.
  const missedDays = days
    .map(d => ({ d, v: dayVerdict(d) }))
    .filter(x => x.v && !x.v.within);

  const chartSeries = hasActuals
    ? [
        { key: 'actual', label: 'Actual',           color: '#1ED771', values: days.map(d => d.actual) },
        { key: 'trk',    label: 'Trunkrs forecast', color: '#8664FF', values: days.map(d => d.trk),   style: 'dashed' },
        { key: 'yours',  label: 'Your forecast',    color: '#220C4A', values: days.map(d => d.yours) },
      ]
    : [
        { key: 'trk',    label: 'Trunkrs forecast', color: '#8664FF', values: days.map(d => d.trk),   style: 'dashed' },
        { key: 'yours',  label: 'Your forecast',    color: '#220C4A', values: days.map(d => d.yours) },
      ];

  // Graded weeks show the tolerance verdict per day (the rules that set the
  // grade); other weeks keep the plain forecast-vs-Trunkrs difference.
  const graded = hasActuals && !isPending;
  const DayTable = () => (
    <table className="detail-tbl" style={{ marginTop: 16 }}>
      <thead>
        <tr>
          <th className="dtbl-day">Day</th>
          <th style={{ textAlign: 'right' }}>Forecast</th>
          <th style={{ textAlign: 'right' }}>Trunkrs</th>
          {hasActuals && <th style={{ textAlign: 'right' }}>Actual</th>}
          {!isPending && <th style={{ textAlign: 'right' }}>{graded ? 'Difference' : 'Diff'}</th>}
          {graded && <th style={{ textAlign: 'right' }}>Allowed</th>}
          {graded && <th style={{ textAlign: 'right' }}>On target</th>}
        </tr>
      </thead>
      <tbody>
        {days.map((d, i) => {
          const v = graded ? dayVerdict(d) : null;
          // Graded: signed error vs the forecast actually judged (yours ?? trk).
          // Otherwise: the merchant's override vs the Trunkrs baseline.
          const rowDiff = graded
            ? (v ? d.actual - v.forecast : null)
            : (d.yours != null ? d.yours - d.trk : null);
          const rowPct = graded
            ? null
            : (d.trk > 0 && d.yours != null ? ((d.yours - d.trk) / d.trk * 100) : null);
          const allowedTag = v && d.actual >= 250 && d.actual < 500;
          return (
            <tr key={i} className={v && !v.within ? 'dtbl-miss' : undefined}>
              <td className="dtbl-day">{d.date}</td>
              <td style={{ textAlign: 'right' }}>
                {d.yours != null
                  ? d.yours.toLocaleString()
                  : <span className="muted" title="You accepted the Trunkrs forecast">{d.trk?.toLocaleString() ?? '—'} <em className="dtbl-auto">auto</em></span>}
              </td>
              <td style={{ textAlign: 'right' }}>{d.trk?.toLocaleString() ?? '—'}</td>
              {hasActuals && <td style={{ textAlign: 'right' }}>{d.actual?.toLocaleString() ?? <span className="muted">—</span>}</td>}
              {!isPending && (
                <td style={{ textAlign: 'right' }}>
                  {rowDiff != null ? (
                    <span className={`dtbl-diff ${rowDiff > 0 ? 'pos' : rowDiff < 0 ? 'neg' : ''}`}>
                      {rowDiff > 0 ? '+' : ''}{rowDiff}
                      {rowPct != null && <span className="dtbl-pct"> ({rowPct > 0 ? '+' : ''}{rowPct.toFixed(1)}%)</span>}
                    </span>
                  ) : <span className="muted">—</span>}
                </td>
              )}
              {graded && (
                <td style={{ textAlign: 'right' }}>
                  {v ? <>±{v.allowed}{allowedTag && <span className="dtbl-pct"> (20%)</span>}</> : <span className="muted">—</span>}
                </td>
              )}
              {graded && (
                <td style={{ textAlign: 'right' }}>
                  {v
                    ? (v.within
                        ? <span className="sb tol-yes"><span className="sb-dot" />On target</span>
                        : <span className="sb tol-no"><span className="sb-dot" />Off by {v.diff}</span>)
                    : <span className="muted">—</span>}
                </td>
              )}
            </tr>
          );
        })}
      </tbody>
    </table>
  );

  return (
    <div className="scrim" onClick={onClose}>
      <div className="modal modal-xl" onClick={e => e.stopPropagation()}>

        <div className="modal-hd">
          <div className="ttl">
            <h3>{week.label} <span className="wk-detail-date">{week.dates}</span></h3>
          </div>
          <button className="x" onClick={onClose} aria-label="Close">
            <Icon name="close" size={18} />
          </button>
        </div>

        <div className="modal-bd">
          {isPending ? (
            <>
              <div className="co" data-intent="info">
                <span className="co-ic"><span className="ic" style={{ WebkitMaskImage: 'url(assets/icons/info-circle.svg)', maskImage: 'url(assets/icons/info-circle.svg)' }} /></span>
                <div className="co-bd">
                  <div className="co-m">We've set this week's forecast from your history and seasonality — it'll be used as-is unless you submit a change. Actual shipments will appear here once the week is underway.</div>
                </div>
              </div>
              <DayTable />
            </>
          ) : (
            <>
              <div className="split" style={{ gridTemplateColumns: '1fr 1fr' }}>
                <div>
                  <ForecastChart
                    height={200}
                    labels={dayLabels}
                    sublabels={daySub}
                    series={chartSeries}
                    holidays={holidays}
                    topSeriesKey={legendHover}
                    tetherYours
                  />
                  <div className="legend" style={{ marginTop: -16 }}>
                    {hasActuals && <span className="legend-item" style={{ cursor: 'pointer' }} onMouseEnter={() => setLegendHover('actual')} onMouseLeave={() => setLegendHover(null)}><span className="legend-swatch actual" />Actual</span>}
                    <span className="legend-item" style={{ cursor: 'pointer' }} onMouseEnter={() => setLegendHover('yours')} onMouseLeave={() => setLegendHover(null)}><span className="legend-swatch you" />Your forecast</span>
                    <span className="legend-item" style={{ cursor: 'pointer' }} onMouseEnter={() => setLegendHover('trk')} onMouseLeave={() => setLegendHover(null)}><span className="legend-swatch dashed" />Trunkrs forecast</span>
                  </div>
                </div>

                {hasActuals ? (
                  <div className="grade-detail">
                    <div className="grade-detail-hd">Your forecast grade</div>
                    <div className="grade-detail-main">
                      <GradeBox grade={yourGrade} size="lg" />
                      <div className="grade-detail-exp">
                        {gradeInfo ? (
                          <p><strong>{gradeInfo.hits} of {gradeInfo.total} days</strong> landed within the allowed difference — {gradeInfo.hits} × 2 = <strong>{gradeInfo.grade}</strong>.</p>
                        ) : (
                          <p>How closely your forecast matched actual shipments this week.</p>
                        )}
                      </div>
                    </div>
                    <Disclosure title="How this grade was calculated">
                      {missedDays.length > 0 && (
                        <div className="grade-miss">
                          <div className="grade-miss-hd">Why it wasn't higher</div>
                          {missedDays.map(({ d, v }, i) => (
                            <div className="grade-miss-row" key={i}>
                              <span className="gm-day">{d.date}</span>
                              <span className="gm-detail">off by <strong>{v.diff}</strong> · allowed {v.allowed}</span>
                            </div>
                          ))}
                          <div className="grade-miss-foot">Each day outside its allowed difference costs 2 points.</div>
                        </div>
                      )}
                      <div className="grade-rules">
                        <span className="grade-rules-hd">Allowed difference per day</span>
                        <span className="grade-rule">under 250 → <strong>50</strong></span>
                        <span className="grade-rule">250–500 → <strong>20%</strong></span>
                        <span className="grade-rule">over 500 → <strong>100</strong></span>
                      </div>
                    </Disclosure>
                  </div>
                ) : (
                  <div>
                    <div className="co" data-intent="info">
                      <span className="co-ic"><span className="ic" style={{ WebkitMaskImage: 'url(assets/icons/info-circle.svg)', maskImage: 'url(assets/icons/info-circle.svg)' }} /></span>
                      <div className="co-bd"><div className="co-m">Accuracy will be calculated once actual shipment data is available.</div></div>
                    </div>
                  </div>
                )}
              </div>

              <DayTable />
            </>
          )}
        </div>

        <div className="modal-ft">
          <button className="btn btn-tertiary btn-lg" onClick={onClose}>Close</button>
        </div>
      </div>
    </div>
  );
}

// ---- QuickEditInline --------------------------------------------------------
// Direct-manipulation editor: a slim stepper anchored above the clicked chart
// column (no close button). The edit applies live; dismiss by clicking another
// day, clicking away, or Escape. Committing happens once via the toolbar Save.
function QuickEditInline({ dayIndex, day, trk, currentValue, isDraft, isWithin7, cx, plotTop, boundTop, onClose, onChange }) {
  const ref = React.useRef(null);
  const [val, setVal] = React.useState(currentValue != null ? currentValue : (trk ?? 0));

  // Re-sync the field when the selection jumps to a different day in place.
  React.useEffect(() => { setVal(currentValue != null ? currentValue : (trk ?? 0)); }, [dayIndex]);

  React.useEffect(() => {
    function onKey(e)  { if (e.key === 'Escape') onClose(); }
    function onDown(e) { if (ref.current && !ref.current.contains(e.target)) onClose(); }
    function onScroll() { onClose(); }
    document.addEventListener('keydown', onKey);
    document.addEventListener('mousedown', onDown);
    window.addEventListener('scroll', onScroll, true);
    return () => {
      document.removeEventListener('keydown', onKey);
      document.removeEventListener('mousedown', onDown);
      window.removeEventListener('scroll', onScroll, true);
    };
  }, [onClose]);

  function set(v) {
    const clamped = Math.max(0, v);
    setVal(clamped);
    onChange(dayIndex, clamped);
  }

  const isSignif = isSignificant(val, trk);
  const delta    = trk != null ? val - trk : null;
  const W        = 280;   // wider, so the reference can sit beside the stepper

  // One always-present status line, never an appearing one. The popover is
  // pinned to its column by the bottom edge, so anything that changes its
  // height drags everything above that edge upward — and the reason warning
  // used to appear the moment you crossed the threshold, sliding the +/-
  // buttons out from under a repeat-clicking cursor. A line that swaps text
  // instead of materialising keeps the height fixed.
  //
  // All three states describe the same thing — your number against Trunkrs —
  // so they read as one progression. The consequence is only ever named
  // alongside the change that causes it; an "no reason needed" on its own
  // raises a question the merchant hasn't thought to ask yet.
  const status = isSignif
    ? { icon: 'warning', tone: 'warn', text: "Large change — we'll ask why" }
    : delta === 0
      ? { icon: 'check', tone: 'ok', text: 'Matches the Trunkrs forecast' }
      : { icon: 'check', tone: 'ok', text: 'Close to the Trunkrs forecast' };

  // Measure real height so the caret lands on the column top; render offscreen
  // for the first frame. Height no longer varies with the value, so crossing
  // the threshold does not reposition anything.
  const [pos, setPos] = React.useState(null);
  React.useLayoutEffect(() => {
    const h = ref.current?.offsetHeight ?? 112;
    const left = Math.max(8, Math.min(cx - W / 2, window.innerWidth - W - 8));
    // Sits above the column, but never rides up past `boundTop` — the toolbar
    // holding Save. There is only ~96px of clearance above the plot, so even a
    // trimmed popover can overrun it; when it would, it drops down and covers a
    // little of the chart instead, which is the cheaper thing to hide.
    const top  = Math.max(boundTop ?? 8, plotTop - h - 8);
    setPos({ left, top, caretLeft: Math.max(14, Math.min(cx - left, W - 14)) });
  }, [cx, plotTop, boundTop, isWithin7, isDraft]);

  const style = pos
    ? { left: pos.left, top: pos.top, width: W }
    : { left: -9999, top: -9999, width: W };
  const caretLeft = pos ? pos.caretLeft : W / 2;

  return (
    <div ref={ref} className="qe-inline" style={style}>
      <div className="qe-inline-hd">
        <span className="qe-inline-day">{day.date}</span>
        {/* Both are properties of the day, not of the value being typed, so
            they belong together in the header where they can't shift the
            controls. dayMeta gives a single type, so they're exclusive. */}
        {isWithin7 && <span className="qe-inline-tag">Within 7 days</span>}
        {isDraft   && <span className="qe-inline-tag qe-inline-tag--draft">Pre-fills the wizard</span>}
      </div>
      {/* The reference sits beside the stepper rather than under it: the popover
          grows upward from the plot, so every row it sheds is a row that would
          otherwise reach into the toolbar and cover Save. Both ref lines fit
          inside the 36px stepper row, so the delta appearing costs no height. */}
      <div className="qe-inline-main">
        <div className="inp-row">
          <button className="btn btn-secondary btn-sq" onClick={() => set(val - 5)} aria-label="Decrease by 5"><Icon name="subtract" size={16} /></button>
          <input className="inp-field" type="number" value={val} min="0"
            onChange={e => set(parseInt(e.target.value, 10) || 0)}
            onKeyDown={e => { if (e.key === 'Enter') onClose(); }} autoFocus />
          <button className="btn btn-secondary btn-sq" onClick={() => set(val + 5)} aria-label="Increase by 5"><Icon name="add" size={16} /></button>
        </div>
        <div className="qe-inline-ref">
          <span>Trunkrs <strong>{trk ?? '—'}</strong></span>
          {delta != null && delta !== 0 && (
            <span className={`qe-inline-delta ${delta > 0 ? 'pos' : 'neg'}`}>{delta > 0 ? '+' : ''}{delta}</span>
          )}
        </div>
      </div>
      <div className={`qe-inline-status is-${status.tone}`}>
        <Icon name={status.icon} size={14} mask />
        <span>{status.text}</span>
      </div>
      <span className="qe-inline-caret" style={{ left: caretLeft }} />
    </div>
  );
}

// ---- QuickEditSummaryModal --------------------------------------------------
function QuickEditSummaryModal({ allDays, dayMeta, qeValues, onCancel, onConfirm }) {
  const editedDays = Object.keys(qeValues).map(Number).filter(i => qeValues[i] != null).sort((a, b) => a - b);
  const updateDays = editedDays.filter(i => dayMeta[i]?.type === 'within7');
  const draftDays  = editedDays.filter(i => dayMeta[i]?.type === 'draft');
  return (
    <div className="scrim" onClick={onCancel}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        <div className="modal-hd">
          <div className="ttl">
            <h3>Confirm your changes</h3>
            <p>Review what will happen before confirming.</p>
          </div>
          <button className="x" onClick={onCancel} aria-label="Close"><Icon name="close" size={18} /></button>
        </div>
        <div className="modal-bd" style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          {editedDays.length > 0 && (
            <div className="amend-week-table">
              <div className="awt-hd">
                <span>Day</span>
                <span>New</span>
                <span>Trunkrs</span>
                <span>Diff</span>
              </div>
              {editedDays.map(i => {
                const nv    = qeValues[i];
                const trk   = allDays[i].trk;
                const delta = nv - (trk ?? 0);
                return (
                  <div className="awt-row" key={i}>
                    <span className="awt-col awt-date">{allDays[i].date}</span>
                    <span className="awt-col">{nv.toLocaleString()}</span>
                    <span className="awt-col">{trk != null ? trk.toLocaleString() : '—'}</span>
                    <span className={`awt-col awt-delta ${delta > 0 ? 'pos' : delta < 0 ? 'neg' : ''}`}>
                      {delta > 0 ? '+' : ''}{delta}
                    </span>
                  </div>
                );
              })}
            </div>
          )}
          {updateDays.length > 0 && (
            <div className="co" data-intent="warning">
              <span className="co-ic"><span className="ic" style={{ WebkitMaskImage: 'url(assets/icons/warning.svg)', maskImage: 'url(assets/icons/warning.svg)' }} /></span>
              <div className="co-bd">
                <div className="co-t">You are making a change within 7 days</div>
                <div className="co-m">{updateDays.map(i => allDays[i].date).join(' · ')} — We may contact you to discuss whether we can accommodate this. If you want to be sure it's possible, please contact your account manager.</div>
              </div>
            </div>
          )}
          {draftDays.length > 0 && (
            <div className="co" data-intent="info">
              <span className="co-ic"><span className="ic" style={{ WebkitMaskImage: 'url(assets/icons/info-circle.svg)', maskImage: 'url(assets/icons/info-circle.svg)' }} /></span>
              <div className="co-bd">
                <div className="co-t">{draftDays.length} day{draftDays.length === 1 ? '' : 's'} pre-filled for the wizard</div>
                <div className="co-m">{draftDays.map(i => allDays[i].date).join(' · ')} — drafts only; you'll confirm them when going through the submission wizard.</div>
              </div>
            </div>
          )}
        </div>
        <div className="modal-ft">
          <button className="btn btn-tertiary btn-lg" onClick={onCancel}>Cancel</button>
          <button className="btn btn-primary btn-lg" onClick={onConfirm}>Confirm</button>
        </div>
      </div>
    </div>
  );
}

// ---- ForecastWeekBlock (one 5-day week: chart left, editor right) ----------
function ForecastWeekBlock({ week, predictions, setPredictions, disabled = false }) {
  const [hoverIdx, setHoverIdx]       = React.useState(null);
  const [legendHover, setLegendHover] = React.useState(null);
  const data     = week.data;
  const isClosed = i => (data.closed || []).includes(i);
  const isLocked = i => (data.locked || []).includes(i);
  // Your forecast is sparse: only days you actually changed carry a value.
  // Untouched (null), locked and closed days plot nothing — the chart tethers
  // each changed day to the Trunkrs baseline instead of drawing a full line.
  const yoursSeries = predictions.map((v, i) => (isClosed(i) ? null : v));
  // A labelled closure (a holiday) gets the axis chip + line gap; an unlabelled
  // one keeps the plain grey column wash.
  const unlabelledClosures = (data.closed || []).filter(i => !(data.closedLabels || {})[i]);
  const changedCount = predictions.reduce((a, v, i) => a + (!isClosed(i) && !isLocked(i) && v != null && v !== data.trk[i] ? 1 : 0), 0);

  const series = [
    { key: 'trk',   label: 'Trunkrs forecast', color: '#8664FF', values: data.trk.map(v => v ?? 0), style: 'dashed' },
    { key: 'yours', label: 'Your forecast',    color: '#220C4A', values: yoursSeries },
  ];

  return (
    <div className="fc-week">
      <div className="fc-week-hd">
        <div className="fc-week-title">
          <h3>{week.label}</h3>
          <span className="fc-week-dates">{week.dates}</span>
        </div>
        {!disabled && changedCount > 0 && (
          <span className="sb sb-caution"><span className="sb-dot" />{`${changedCount} day${changedCount === 1 ? '' : 's'} changed`}</span>
        )}
      </div>
      <div className="split">
        <div>
          <ForecastChart
            height={180}
            labels={data.labels.map(l => l.split(' ')[0])}
            sublabels={data.sub}
            series={series}
            highlightIndex={hoverIdx}
            onHover={setHoverIdx}
            nonOperating={unlabelledClosures}
            holidays={data.closedLabels || {}}
            topSeriesKey={legendHover}
            tetherYours
          />
          <div className="legend" style={{ marginTop: 8 }}>
            <span className="legend-item" style={{ cursor: 'pointer' }} onMouseEnter={() => setLegendHover('yours')} onMouseLeave={() => setLegendHover(null)}><span className="legend-swatch you" />Your forecast</span>
            <span className="legend-item" style={{ cursor: 'pointer' }} onMouseEnter={() => setLegendHover('trk')} onMouseLeave={() => setLegendHover(null)}><span className="legend-swatch dashed" />Trunkrs forecast</span>
          </div>
        </div>
        <div>
          <Editor
            data={data}
            predictions={predictions}
            disabled={disabled}
            onChange={(i, v) => { const next = [...predictions]; next[i] = v; setPredictions(next); }}
            onHover={setHoverIdx}
            hoverIndex={hoverIdx}
          />
        </div>
      </div>
    </div>
  );
}

// ---- ForecastPage (single page — replaces the wizard) ----------------------
function ForecastPage({ stopMode = false, stopStart, holiday = null, onExit }) {
  // A holiday closes the day: it joins `closed`, which the review table already
  // renders as a no-delivery row and `isEditable` already excludes from submission.
  const weeks = React.useMemo(() => FORECAST_WEEKS.map(wk => {
    const map  = holidayMap(wk.data.iso, holiday);
    const idxs = Object.keys(map).map(Number);
    if (!idxs.length) return wk;
    return {
      ...wk,
      data: {
        ...wk.data,
        closed:       [...(wk.data.closed || []), ...idxs],
        closedLabels: { ...(wk.data.closedLabels || {}), ...map },
        holidayInfo:  { name: map[idxs[0]], date: holidayDateLabel(holiday) },
      },
    };
  }), [holiday]);

  const zeros = () => FORECAST_WEEKS.map(w => w.data.trk.map(() => 0));
  const blanks = () => FORECAST_WEEKS.map(w => [...w.data.yours]);
  const [preds, setPreds]         = React.useState(() => stopMode ? zeros() : blanks());
  const [amendOpen, setAmendOpen] = React.useState(false);

  // Toggling stop mode while on the page re-zeros / clears the form.
  React.useEffect(() => { setPreds(stopMode ? zeros() : blanks()); }, [stopMode]);

  function setWeekPreds(wi, arr) {
    setPreds(prev => prev.map((w, i) => i === wi ? arr : w));
  }

  const isEditable = (wk, i) => !(wk.data.closed || []).includes(i) && !(wk.data.locked || []).includes(i);

  const changedDays = weeks.flatMap((wk, wi) =>
    preds[wi]
      .map((v, i) => ({ v, i }))
      .filter(({ v, i }) => isEditable(wk, i) && isSignificant(v, wk.data.trk[i]))
      .map(({ v, i }) => ({ date: wk.data.labels[i], yours: v, trk: wk.data.trk[i], delta: v - (wk.data.trk[i] ?? 0), weekLabel: wk.label })));

  // Any day that differs from Trunkrs at all — drives the "N days changed" pill,
  // independent of the significance threshold that gates the reason prompt.
  const anyChangedDays = weeks.flatMap((wk, wi) =>
    preds[wi].filter((v, i) => isEditable(wk, i) && v != null && v !== wk.data.trk[i]));

  function finish() {
    Toast.show({ type: 'full', intent: 'success', title: 'Forecast submitted', message: 'We have received your forecast for the next three weeks and will use it to plan trucks and drivers.' });
    onExit();
  }

  function handleSubmit() {
    if (stopMode) { finish(); return; }
    // Untouched days aren't "empty" — they keep the Trunkrs forecast, which is
    // the expected default. Only interrupt to collect a reason for big changes.
    if (changedDays.length > 0) { setAmendOpen(true); return; }
    finish();
  }

  return (
    <>
      <button className="btn btn-tertiary btn-lg" style={{ marginBottom: 'var(--trk-space-m)', alignSelf: 'flex-start' }} onClick={onExit}>
        <span className="ic-mask" style={{ WebkitMaskImage: "url('assets/icons/arrow-left.svg')", maskImage: "url('assets/icons/arrow-left.svg')" }} />
        Go back to dashboard
      </button>

      <div className="card">
        <div className="card-hd">
          <div style={{ flex: 1, minWidth: 0 }}>
            <h2>Review your forecast</h2>
            <div className="sub">The next three weeks · keep the Trunkrs forecast as-is, or change any day. The nearest days are locked.</div>
          </div>
          <div style={{ flex: '0 0 50%', minWidth: 0, marginLeft: 'auto' }}>
            <MotdTicker />
          </div>
        </div>
        <div className="card-bd">
          {stopMode ? (
            <div className="co" data-intent="danger" style={{ marginBottom: 'var(--trk-space-m)' }}>
              <span className="co-ic"><span className="ic" style={{ WebkitMaskImage: 'url(assets/icons/danger.svg)', maskImage: 'url(assets/icons/danger.svg)' }} /></span>
              <div className="co-bd">
                <div className="co-t">This client is stopping from {stopStartLabel(stopStart)}</div>
                <div className="co-m">The forecast is set to 0 for the 8 weeks from <strong>{stopStartLabel(stopStart)}</strong>. Day inputs are locked while stop mode is on.</div>
              </div>
            </div>
          ) : (
            <HintBanner storageKey="trk_fo_hint_forecast">
              Trunkrs has calculated a forecast from your shipping history. Change this if needed — otherwise the Trunkrs forecast will be used.
            </HintBanner>
          )}
          {weeks.map((wk, wi) => (
            <ForecastWeekBlock
              key={wk.key}
              week={wk}
              predictions={preds[wi]}
              setPredictions={arr => setWeekPreds(wi, arr)}
              disabled={stopMode}
            />
          ))}
        </div>
        {/* No edits = nothing to submit, so no ribbon at all — leaving via the
            "Go back to dashboard" link is the complete flow. The ribbon only
            appears once there's a real change (or stop mode) to commit. */}
        {(stopMode || anyChangedDays.length > 0) && (
          <div className="action-ribbon">
            <div className="info">
              {stopMode
                ? <><span className="dot" />Stop mode is on — all days set to 0.</>
                : <><span className="dot" />{`${anyChangedDays.length} day${anyChangedDays.length === 1 ? '' : 's'} changed vs Trunkrs`}{changedDays.length > 0 ? ' · a reason is required' : ''}</>}
            </div>
            <div className="actions">
              <button className="btn btn-primary btn-lg" onClick={handleSubmit}>
                <Icon name="check" size={16} mask />
                Submit forecast
              </button>
            </div>
          </div>
        )}
      </div>

      {amendOpen && (
        <AmendModal
          onClose={() => setAmendOpen(false)}
          onSubmit={() => { setAmendOpen(false); finish(); }}
          changedDays={changedDays}
        />
      )}
    </>
  );
}

// ---- Dashboard ------------------------------------------------------------
function Dashboard({ onQuickEditModeChange, stopMode = false, stopStart, holiday = null, gradeOverride = null, onSubmitForecast }) {
  // A stopping client's weeks inside the 8-week window (and not already ended) are set to 0.
  const isStoppedWeek = w => isWeekStopped(w, stopMode, stopStart);
  // Everything on this page reads `schedule`, not SCHEDULE, so the dev grade
  // override reaches the trend, the KPI, the table and the detail modal alike.
  const schedule = React.useMemo(() => withGradeOverride(SCHEDULE, gradeOverride), [gradeOverride]);
  const lastAccurate   = [...schedule].reverse().find(w => w.grade != null);
  const lastGrade      = lastAccurate ? lastAccurate.grade : null;
  const [viewWeek, setViewWeek] = React.useState(null);

  // Chart data — lifted so QE logic can access it
  const allDays     = schedule.flatMap(w => w.days);
  const trendLabels = allDays.map(d => d.date.split(' ')[1]);
  const trendSub    = allDays.map(d => d.date.split(' ')[0]);
  const trendGroup  = allDays.map((_, i) => i % 5 === 0 ? `Wk ${schedule[Math.floor(i / 5)].week}` : '');
  const trendTips   = allDays.map(d => d.date);
  // Per-day weekly grade (repeated across the week's 5 days; null for ungraded
  // weeks) — drives the slim grade bars on the chart's secondary right axis.
  const trendGrades = allDays.map((_, i) => schedule[Math.floor(i / 5)]?.grade ?? null);
  // Closed days on the trend: the chart cuts the series and marks the axis.
  const trendHolidays  = holidayMap(allDays.map(d => d.iso), holiday);
  const holidayIndices = new Set(Object.keys(trendHolidays).map(Number));

  // Per-day edit category
  const dayMeta = allDays.map((_, i) => {
    const week = schedule[Math.floor(i / 5)];
    if (week.editPolicy === 'past')    return { type: 'past',    week };
    if (week.editPolicy === 'within7') return { type: 'within7', week };
    if (week.editPolicy === 'open')    return { type: 'open',    week };
    if (week.status === 'pending')     return { type: 'draft', week };
    return { type: 'none', week };
  });

  // The current week (whole thing) drives the "this week" band; near-term days
  // that the review form locks (already committed to trucks/drivers) are not
  // editable in quick edit either — they're excluded from clickable + editable.
  // A closed day has nothing to edit, tint or lock — it drops out of every set
  // so the holiday column carries the holiday marking and nothing else.
  const currentWeekIndices = new Set(dayMeta.map((m, i) => m.type === 'within7' ? i : -1).filter(i => i >= 0));
  const lockedIndices    = new Set([...currentWeekIndices].filter(i => allDays[i].locked && !holidayIndices.has(i)));
  const clickableIndices = new Set(dayMeta.map((m, i) => (m.type === 'within7' || m.type === 'open' || m.type === 'draft') ? i : -1).filter(i => i >= 0 && !lockedIndices.has(i) && !holidayIndices.has(i)));
  const within7Indices   = new Set([...currentWeekIndices].filter(i => !lockedIndices.has(i) && !holidayIndices.has(i)));

  // Quick-edit state
  const [qeActive,             setQeActive]            = React.useState(false);
  const [qeValues,             setQeValues]            = React.useState({});
  const [committedValues,      setCommittedValues]      = React.useState({});
  const [committedDraftIndices, setCommittedDraftIndices] = React.useState(new Set());
  const [legendHover, setLegendHover] = React.useState(null);
  const [qePopover,            setQePopover]           = React.useState(null);
  const [qeAmendOpen,          setQeAmendOpen]         = React.useState(false);
  const [qeAmendDays,          setQeAmendDays]         = React.useState([]);
  const [qeSummaryOpen,        setQeSummary]           = React.useState(false);

  const activeDraftIndices = new Set(Object.keys(qeValues).map(Number).filter(i => dayMeta[i]?.type === 'draft'));
  const draftIndices       = qeActive ? activeDraftIndices : committedDraftIndices;
  const editedYours    = allDays.map((d, i) => {
    // Stopping client: days in a zeroed week (inside the 8-week window) drop to 0.
    if (isStoppedWeek(dayMeta[i]?.week)) return 0;
    return (qeActive && qeValues[i] !== undefined) ? qeValues[i] : committedValues[i] !== undefined ? committedValues[i] : d.yours;
  });
  const qeEditedCount  = Object.keys(qeValues).length;
  const qeChangedCount = Object.keys(qeValues).reduce((n, i) => n + (isSignificant(qeValues[+i], allDays[+i].trk) ? 1 : 0), 0);

  const chartCardRef = React.useRef(null);
  const [qeHint, setQeHint] = React.useState(false);
  const qeHintTimer = React.useRef(null);

  function enterQE() {
    setQeValues({...committedValues});
    setQeActive(true);
    onQuickEditModeChange?.(true);
    // Draw attention to the chart — it's where the editing happens.
    setQeHint(true);
    clearTimeout(qeHintTimer.current);
    qeHintTimer.current = setTimeout(() => setQeHint(false), 3200);
    requestAnimationFrame(() => chartCardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }));
  }
  function exitQE()  { setQeActive(false); setQeValues({}); setQePopover(null); onQuickEditModeChange?.(false); setQeHint(false); }

  function resetQE() { setQeValues({}); setQePopover(null); }

  function handleColumnClick(idx, geom) {
    // The card header carries the Save button, so it's the popover's ceiling.
    const hd = chartCardRef.current?.querySelector('.card-hd');
    const boundTop = hd ? hd.getBoundingClientRect().bottom + 8 : 8;
    setQePopover({ index: idx, boundTop, ...geom });
  }
  function updateQEValue(idx, val)   { setQeValues(prev => ({ ...prev, [idx]: val })); }

  function handleSave() {
    setQePopover(null);
    if (qeChangedCount > 0) {
      const days = Object.keys(qeValues).map(Number)
        .filter(i => isSignificant(qeValues[i], allDays[i].trk))
        .map(i => ({ date: allDays[i].date, yours: qeValues[i], trk: allDays[i].trk, delta: qeValues[i] - (allDays[i].trk ?? 0), weekLabel: dayMeta[i].week.label }));
      setQeAmendDays(days);
      setQeAmendOpen(true);
      return;
    }
    setQeSummary(true);
  }

  function confirmQESave() {
    const newDraftIdxs = Object.keys(qeValues).map(Number).filter(i => dayMeta[i]?.type === 'draft');
    setCommittedValues(prev => ({ ...prev, ...qeValues }));
    setCommittedDraftIndices(prev => new Set([...prev, ...newDraftIdxs]));
    const editedCount = Object.keys(qeValues).filter(i => qeValues[i] != null).length;
    const draftCount  = Object.keys(qeValues).filter(i => dayMeta[+i]?.type === 'draft').length;
    const updateCount = editedCount - draftCount;
    const parts = [];
    if (updateCount) parts.push(`${updateCount} day${updateCount === 1 ? '' : 's'} updated`);
    if (draftCount)  parts.push(`${draftCount} day${draftCount === 1 ? '' : 's'} pre-filled for wizard`);
    const message = parts.join(' · ') || 'Your forecast has been updated.';
    Toast.show({ type: 'full', intent: 'success', title: 'Changes saved', message });
    setQeSummary(false);
    exitQE();
  }

  return (
    <div className="dash-stack">
      <div className="card" ref={chartCardRef}>
        <div className="card-hd">
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--trk-space-s)' }}>
              <h2 style={{ margin: 0 }}>Forecast trend</h2>
              {!qeActive && (
                <button className="btn btn-tertiary btn-sm" onClick={enterQE}>
                  <Icon name="edit" size={14} mask />
                  Quick edit
                </button>
              )}
            </div>
            <div className="sub">Daily forecast vs actual shipments across all weeks</div>
          </div>
          {qeActive ? (
            <div className="qe-toolbar">
              <button className="btn btn-primary" onClick={handleSave} disabled={qeEditedCount === 0}>
                Save{qeEditedCount > 0 ? ` (${qeEditedCount})` : ''}
              </button>
              <button className="btn btn-tertiary" onClick={exitQE}>Discard</button>
              <button className="btn btn-tertiary" onClick={resetQE} disabled={qeEditedCount === 0}>Reset</button>
            </div>
          ) : (
            <div className="ovw-stats">
              <div className="ovw-stat">
                <div className="kpi-lbl">Last week grade</div>
                <GradeBox grade={lastGrade} size="md" sub={lastAccurate ? <><span className="grade-sub-wk">{lastAccurate.label}</span><span className="grade-sub-dt">{lastAccurate.dates}</span></> : 'No graded weeks yet'} />
              </div>
            </div>
          )}
        </div>
        <div className="card-bd" style={{ paddingTop: 8 }}>
          {qeActive && (
            <div className="co" data-intent="info" role="status" style={{ marginBottom: 'var(--trk-space-m)' }}>
              <span className="co-ic"><span className="ic" style={{ WebkitMaskImage: 'url(assets/icons/edit.svg)', maskImage: 'url(assets/icons/edit.svg)' }} /></span>
              <div className="co-bd">
                <div className="co-t">Click any day on the chart to make a change</div>
                <div className="co-m">Select a day below to adjust its forecast. We'll ask for a quick reason before sending it to us.</div>
              </div>
            </div>
          )}
          <ForecastChart
            height={240}
            qeHint={qeHint}
            labels={trendLabels}
            sublabels={trendSub}
            grouplabels={trendGroup}
            tooltipLabels={trendTips}
            grades={trendGrades}
            holidays={trendHolidays}
            series={[
              { key: 'actual', label: 'Actual',           color: '#1ED771', values: allDays.map(d => d.actual) },
              { key: 'trk',    label: 'Trunkrs forecast', color: '#8664FF', values: allDays.map(d => d.trk), style: 'dashed' },
              { key: 'yours',  label: 'Your forecast',    color: '#220C4A', values: editedYours },
            ]}
            quickEditMode={qeActive}
            clickableIndices={clickableIndices}
            within7Indices={within7Indices}
            currentWeekIndices={currentWeekIndices}
            lockedIndices={lockedIndices}
            draftIndices={draftIndices}
            selectedEditIndex={qeActive ? (qePopover?.index ?? null) : null}
            topSeriesKey={legendHover}
            tetherYours={!qeActive}
            onColumnClick={handleColumnClick}
          />
          <div className="legend" style={{ marginTop: 8 }}>
            <span className="legend-item" style={{ cursor: 'pointer' }} onMouseEnter={() => setLegendHover('actual')} onMouseLeave={() => setLegendHover(null)}><span className="legend-swatch actual" />Actual</span>
            <span className="legend-item" style={{ cursor: 'pointer' }} onMouseEnter={() => setLegendHover('yours')} onMouseLeave={() => setLegendHover(null)}><span className="legend-swatch you" />Your forecast</span>
            <span className="legend-item" style={{ cursor: 'pointer' }} onMouseEnter={() => setLegendHover('trk')} onMouseLeave={() => setLegendHover(null)}><span className="legend-swatch dashed" />Trunkrs forecast</span>
            {draftIndices.size > 0 && (
              <span className="legend-item" style={{ cursor: 'pointer' }} onMouseEnter={() => setLegendHover('yours')} onMouseLeave={() => setLegendHover(null)}>
                <svg width="22" height="10" viewBox="0 0 22 10" fill="none" aria-hidden="true" style={{ display: 'inline-block', verticalAlign: 'middle', marginRight: 6 }}>
                  <line x1="0" y1="5" x2="22" y2="5" stroke="#220C4A" strokeWidth="1.5" strokeDasharray="1.5 3" strokeLinecap="round" />
                  <circle cx="11" cy="5" r="3" fill="white" stroke="#220C4A" strokeWidth="1.5" />
                </svg>
                Draft
              </span>
            )}
            {/* Same chip as the axis marker, so the legend and the column read
                as one mark rather than two things that happen to be yellow. */}
            {holidayIndices.size > 0 && (
              <span className="legend-item">
                <svg width="18" height="15" viewBox="0 0 18 15" fill="none" aria-hidden="true" style={{ display: 'inline-block', verticalAlign: 'middle', marginRight: 6 }}>
                  <rect x="0.5" y="0.5" width="17" height="14" rx="4" fill="#FFF5D6" stroke="#FFE38D" />
                  <svg x="3.5" y="2" width="11" height="11" viewBox="0 0 24 24">
                    <path fillRule="evenodd" clipRule="evenodd" d="M7 8V6H4V9L20 9V6H17V8H15V6H9V8H7ZM15 4H9V2H7V4H2V9V11V21H22V11V9V4H17V2H15V4ZM20 11V19H4V11L20 11Z" fill="#403516" />
                  </svg>
                </svg>
                Holiday — closed
              </span>
            )}
          </div>
          {qeActive && qePopover && (
            <QuickEditInline
              dayIndex={qePopover.index}
              day={allDays[qePopover.index]}
              trk={allDays[qePopover.index]?.trk}
              currentValue={qeValues[qePopover.index] !== undefined ? qeValues[qePopover.index] : allDays[qePopover.index]?.yours}
              isDraft={dayMeta[qePopover.index]?.type === 'draft'}
              isWithin7={dayMeta[qePopover.index]?.type === 'within7'}
              cx={qePopover.cx}
              plotTop={qePopover.plotTop}
              boundTop={qePopover.boundTop}
              onClose={() => setQePopover(null)}
              onChange={updateQEValue}
            />
          )}
        </div>
      </div>

      <HintBanner storageKey="trk_fo_hint_dashboard" elevated className={qeActive ? 'qe-dim' : ''}>
        Trunkrs forecast is always ready — are you expecting different numbers than we calculate? Use <strong>Review forecast</strong> and change the days where you expect a deviation; if you don't change it we'll use the Trunkrs numbers. Your weeks stay here so you can track accuracy over time.
      </HintBanner>

      <div className={`card${qeActive ? ' qe-dim' : ''}`}>
        <div className="card-hd">
          <div style={{ minWidth: 0 }}>
            <h2>Weekly grades</h2>
            <div className="sub">{stopMode ? <>This client is stopping from <strong>{stopStartLabel(stopStart)}</strong> — upcoming weeks set to 0</> : 'How your recent forecasts scored'}</div>
          </div>
          {!stopMode && (
            <div className="right" style={{ position: 'relative' }}>
              <button className="btn btn-lg btn-primary" onClick={onSubmitForecast}>
                <Icon name="edit" size={16} mask />
                Review forecast
              </button>
            </div>
          )}
        </div>
        <div className="card-bd">
          {stopMode && (
            <div className="co" data-intent="danger" style={{ marginBottom: 'var(--trk-space-m)' }}>
              <span className="co-ic"><span className="ic" style={{ WebkitMaskImage: 'url(assets/icons/danger.svg)', maskImage: 'url(assets/icons/danger.svg)' }} /></span>
              <div className="co-bd">
                <div className="co-t">This client is stopping from {stopStartLabel(stopStart)}</div>
                <div className="co-m">The forecast has been set to 0 for the 8 weeks from <strong>{stopStartLabel(stopStart)}</strong>. Weeks that have already ended keep their grades.</div>
              </div>
            </div>
          )}
          <table className="sched-tbl">
            <thead>
              <tr>
                <th>Week</th>
                <th style={{ textAlign: 'right' }}>Grade</th>
                <th />
              </tr>
            </thead>
            <tbody>
              {withGradeOverride(scheduleWithStopWindow(stopMode, stopStart), gradeOverride)
                .filter(w => stopMode || w.editPolicy !== 'open')   // drop the upcoming (not-yet-scored) weeks
                .slice().reverse()                                   // most recent first
                .map(w => {
                const grade   = w.grade ?? null;
                const stopped = isStoppedWeek(w);
                const isCurrent = w.editPolicy === 'within7';
                const edited  = !stopped && (w.days || []).some(d => d.yours != null);
                return (
                <tr key={w.week} className={isCurrent ? 'is-current' : undefined}>
                  <td>
                    <div className="wk-lbl">{w.label}{edited && <span className="wk-edited" title="You adjusted this week's forecast" />}</div>
                    <div className="wk-dates">{w.dates}</div>
                    {stopped && <div className="stop-tag">Forecast set to 0</div>}
                  </td>
                  <td style={{ textAlign: 'right' }}>
                    {grade != null
                      ? <div style={{ display: 'inline-flex', justifyContent: 'flex-end' }}><GradeBox grade={grade} size="sm" /></div>
                      : <span className="muted">—</span>}
                  </td>
                  <td style={{ textAlign: 'right' }}>
                    {!stopped && (w.status === 'submitted' || w.status === 'pending') && (
                      <button className="btn btn-tertiary btn-sm" onClick={() => setViewWeek(w)}>View</button>
                    )}
                  </td>
                </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </div>

      {viewWeek && <WeekDetailModal week={viewWeek} holiday={holiday} onClose={() => setViewWeek(null)} />}
      {qeAmendOpen && (
        <AmendModal
          onClose={() => setQeAmendOpen(false)}
          onSubmit={() => { setQeAmendOpen(false); setQeSummary(true); }}
          changedDays={qeAmendDays}
        />
      )}
      {qeSummaryOpen && (
        <QuickEditSummaryModal
          allDays={allDays}
          dayMeta={dayMeta}
          qeValues={qeValues}
          onCancel={() => setQeSummary(false)}
          onConfirm={confirmQESave}
        />
      )}
    </div>
  );
}


// ---- App ------------------------------------------------------------------
const STOP_KEY        = 'trk_fo_dev_stop';
const GRADE_KEY       = 'trk_fo_dev_grade';
const GRADE_VALUE_KEY = 'trk_fo_dev_grade_value';
// Grades are always even (hits × 2 out of 5 days), so these are all of them.
const GRADE_CHOICES   = [0, 2, 4, 6, 8, 10];

function App() {
  const [view, setView]                   = React.useState('dashboard');
  const [welcomeOpen, setWelcomeOpen]     = React.useState(() => !localStorage.getItem(WELCOME_KEY));
  const [quickEditMode, setQuickEditMode] = React.useState(false);
  const [stopMode, setStopMode]           = React.useState(() => localStorage.getItem(STOP_KEY) === '1');
  const [stopStart, setStopStart]         = React.useState(() => localStorage.getItem(STOP_START_KEY) || DEFAULT_STOP_START);
  const [holidayOn, setHolidayOn]         = React.useState(() => localStorage.getItem(HOLIDAY_KEY) === '1');
  const [holidayDate, setHolidayDate]     = React.useState(() => localStorage.getItem(HOLIDAY_DATE_KEY) || DEFAULT_HOLIDAY_DATE);

  const [gradeOn, setGradeOn]         = React.useState(() => localStorage.getItem(GRADE_KEY) === '1');
  const [gradeValue, setGradeValue]   = React.useState(() => {
    const v = parseInt(localStorage.getItem(GRADE_VALUE_KEY), 10);
    return GRADE_CHOICES.includes(v) ? v : 0;
  });

  // A single ISO date, or null when the toggle is off — the only holiday input
  // the rest of the app takes.
  const holiday = holidayOn ? holidayDate : null;
  // Likewise: a grade to force on the most recent graded week, or null.
  const gradeOverride = gradeOn ? gradeValue : null;

  function applyGradeOn(on) {
    setGradeOn(on);
    localStorage.setItem(GRADE_KEY, on ? '1' : '0');
  }
  function applyGradeValue(v) {
    setGradeValue(v);
    localStorage.setItem(GRADE_VALUE_KEY, String(v));
  }

  function applyHoliday(on) {
    setHolidayOn(on);
    localStorage.setItem(HOLIDAY_KEY, on ? '1' : '0');
  }
  function applyHolidayDate(date) {
    const next = date || DEFAULT_HOLIDAY_DATE;
    setHolidayDate(next);
    localStorage.setItem(HOLIDAY_DATE_KEY, next);
  }

  function applyStop(on) {
    setStopMode(on);
    localStorage.setItem(STOP_KEY, on ? '1' : '0');
  }
  function applyStopStart(date) {
    const next = date || DEFAULT_STOP_START;
    setStopStart(next);
    localStorage.setItem(STOP_START_KEY, next);
  }

  function startForecast() { setView('forecast'); }
  function goToDashboard() { setView('dashboard'); }

  return (
    <>
      <TopNav />
      <div className="page">
        <div className={`page-head${quickEditMode && view === 'dashboard' ? ' qe-dim' : ''}`}>
          <div className="page-head-left">
            <div className="title-row">
              <h1>Forecast</h1>
              <span className="pill"><span className="glow" /><strong>Current week:</strong> {FORECAST_WEEKS[0].label}</span>
            </div>
            <p className="lede">
              {view === 'dashboard'
                ? 'Trunkrs forecasts your shipments automatically — review it only if you want to change something.'
                : 'Review the Trunkrs forecast for the next three weeks and adjust any day you disagree with.'}
            </p>
          </div>
          <div className="page-head-right">
            <button className="btn btn-tertiary btn-sq" onClick={() => setWelcomeOpen(true)} aria-label="How it works">
              <IconHelpCircle />
            </button>
          </div>
        </div>

        {view === 'dashboard' && (
          <Dashboard
            onQuickEditModeChange={setQuickEditMode}
            stopMode={stopMode}
            stopStart={stopStart}
            holiday={holiday}
            gradeOverride={gradeOverride}
            onSubmitForecast={startForecast}
          />
        )}

        {view === 'forecast' && <ForecastPage stopMode={stopMode} stopStart={stopStart} holiday={holiday} onExit={goToDashboard} />}
      </div>

      {welcomeOpen && <WelcomeModal onDismiss={() => setWelcomeOpen(false)} steps={WELCOME_STEPS} />}
      {/* Stop mode is a backend-controlled state; the dev-panel toggle just simulates it — no confirmation. */}

      {/* DEV ONLY — remove before implementation */}
      <div className="dev-panel">
        <span className="dev-panel-tag">Dev</span>
        <label className="dev-toggle" title="Stop mode: the client is ending their service — sets the forecast to 0 for the next 8 weeks from the start date.">
          <input type="checkbox" checked={stopMode} onChange={e => applyStop(e.target.checked)} />
          <span className="dev-switch" />
          Stop
        </label>
        {stopMode && (
          <label className="dev-date" title="Stoppage start date — the forecast is zeroed for 8 weeks from the week containing this date.">
            from
            <input type="date" value={stopStart} onChange={e => applyStopStart(e.target.value)} />
          </label>
        )}
        <span className="dev-panel-sep" />
        {/* Holidays are real, but the rolling demo window rarely contains one —
            this drops a closed day anywhere so the marking can be seen. */}
        <label className="dev-toggle" title="Simulate a public holiday: no delivery that day. Marked on the charts and closed in the review table.">
          <input type="checkbox" checked={holidayOn} onChange={e => applyHoliday(e.target.checked)} />
          <span className="dev-switch" />
          Holiday
        </label>
        {holidayOn && (
          <label className="dev-date" title="Which day is closed. Named automatically when it's a genuine Dutch public holiday, otherwise shown as 'Public holiday'.">
            on
            <input type="date" value={holidayDate} onChange={e => applyHolidayDate(e.target.value)} />
          </label>
        )}
        <span className="dev-panel-sep" />
        {/* The demo actuals only ever score 8/10/6/8/4, so the edge grades —
            0 especially — can't otherwise be reviewed on the chart. */}
        <label className="dev-toggle" title="Force a grade on the most recent graded week by rewriting its actuals, so the band, the pill, the KPI, the table and the day-by-day breakdown all agree.">
          <input type="checkbox" checked={gradeOn} onChange={e => applyGradeOn(e.target.checked)} />
          <span className="dev-switch" />
          Grade
        </label>
        {gradeOn && (
          <label className="dev-date" title="Grades are always even — on-target days × 2 out of 5.">
            =
            <select value={gradeValue} onChange={e => applyGradeValue(parseInt(e.target.value, 10))}>
              {GRADE_CHOICES.map(v => <option key={v} value={v}>{v}</option>)}
            </select>
          </label>
        )}
        <span className="dev-panel-sep" />
        <button className="dev-reset" title="Reset onboarding" onClick={() => {
          ['trk_forecast_welcomed','trk_fo_hint_dashboard','trk_fo_hint_forecast']
            .forEach(k => localStorage.removeItem(k));
          location.reload();
        }}>Reset</button>
      </div>

    </>
  );
}

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