/* enterprisex-core.jsx — Mira Enterprise (rebuild) domain core.
 * Pure logic, no React. Exposes window.MX. Modular by concern (§42).
 * Data source: REAL window.__MIRA_FUNDING__ (funding-opportunities.json).
 * NEVER merges the static FUNDING_CATALOG (§37/§41). Unknown stays unknown.
 */
(function () {
  'use strict';
  var MX = {};

  // ── Money (§18): store separately, British formatting, never £0 for missing ──
  var GBP_RATES = { GBP: 1, USD: 0.79, EUR: 0.86 };      // analytical only; conversion dated
  var GBP_AS_OF = '2026-07-26';
  function toGBP(amount, ccy) {
    if (amount == null || isNaN(amount)) return null;
    var r = GBP_RATES[(ccy || 'USD').toUpperCase()];
    return r == null ? null : Math.round(amount * r);
  }
  function fmtGBP(n) {
    if (n == null || isNaN(n)) return 'Amount not published';        // never £0
    if (n >= 1e9) return '£' + (n / 1e9).toFixed(n % 1e9 ? 1 : 0).replace(/\.0$/, '') + ' billion';
    if (n >= 1e6) return '£' + (n / 1e6).toFixed(n % 1e6 ? 1 : 0).replace(/\.0$/, '') + ' million';
    return '£' + Math.round(n).toLocaleString('en-GB');
  }
  var BUCKETS = [
    ['Under £5,000', 0, 5e3], ['£5,000–£25,000', 5e3, 25e3], ['£25,000–£100,000', 25e3, 1e5],
    ['£100,000–£500,000', 1e5, 5e5], ['£500,000–£1 million', 5e5, 1e6], ['£1 million–£5 million', 1e6, 5e6],
    ['£5 million–£25 million', 5e6, 25e6], ['£25 million–£100 million', 25e6, 1e8],
    ['£100 million–£500 million', 1e8, 5e8], ['£500 million+', 5e8, Infinity],
  ];
  function sizeBucket(gbp) { if (gbp == null) return 'Amount not published'; for (var i = 0; i < BUCKETS.length; i++) if (gbp >= BUCKETS[i][1] && gbp < BUCKETS[i][2]) return BUCKETS[i][0]; return 'Amount not published'; }
  MX.money = { toGBP: toGBP, fmtGBP: fmtGBP, sizeBucket: sizeBucket, GBP_AS_OF: GBP_AS_OF };

  // ── Load real opportunities (no static catalog) ─────────────────────────────
  function normOpp(raw, i) {
    var perAwardMax = raw.amount_max != null ? raw.amount_max : null;
    var perAwardMin = raw.amount_min != null ? raw.amount_min : null;
    var envelope = raw.total_budget != null ? raw.total_budget : null;
    // analytical amount prefers per-award; NEVER treats envelope as a grant (§18)
    var analytical = perAwardMax != null ? perAwardMax : (perAwardMin != null ? perAwardMin : null);
    var amountType = perAwardMax != null ? 'per_award_max' : (perAwardMin != null ? 'per_award_min' : (envelope != null ? 'total_programme_budget' : 'not_published'));
    var gbp = toGBP(analytical, raw.currency);
    var dl = raw.deadline && !isNaN(new Date(raw.deadline)) ? new Date(raw.deadline) : null;
    return {
      id: raw.id || ('opp-' + i),
      title: (raw.display_title || raw.title || '').trim() || 'Untitled opportunity',
      funder: (raw.funder || raw.source_name || 'Unknown funder').trim(),
      funderType: raw.funder_type || null,
      type: raw.opportunity_type || raw.record_type || 'opportunity',
      recordClass: raw.record_class || 'opportunity',
      status: raw.status || (dl && dl > new Date() ? 'verified_open' : 'status_unclear'),
      summary: raw.summary || null,
      sectors: Array.isArray(raw.sectors) ? raw.sectors : [],
      countries: Array.isArray(raw.countries) ? raw.countries : [],
      regions: Array.isArray(raw.regions) ? raw.regions : [],
      eligible: Array.isArray(raw.eligible) ? raw.eligible : [],
      eligibilityText: raw.eligibility_text || null,
      deadline: dl ? dl.toISOString() : null,
      amountMin: perAwardMin, amountMax: perAwardMax, totalBudget: envelope,
      currency: raw.currency || null, analyticalGBP: gbp, amountType: amountType,
      url: raw.url || raw.original_url || raw.source_url || null,
      sourceName: raw.source_name || null,
      trust: raw.trust_level || 'unclear',
      confidence: raw.confidence != null ? raw.confidence : null,
      evidence: raw.evidence_snippet || null,
      posted: raw.posted_date || raw.extracted_at || null,
    };
  }
  var _cache = null, _total = null, _sourceOfTruth = null;
  MX.total = function () { return _total; };
  MX.sourceOfTruth = function () { return _sourceOfTruth; };
  // Map a Supabase funding_opportunities row → the raw shape normOpp expects.
  function dbToRaw(r) {
    return {
      id: r.id, title: r.title, funder: r.funder, funder_type: r.donor_type || r.funder_type,
      opportunity_type: r.funding_type || r.record_type,
      record_class: r.record_type === 'portal' ? 'source' : 'opportunity',
      status: r.status, summary: r.summary_en, sectors: r.sectors, countries: r.countries, regions: r.regions,
      eligible: r.eligible_types, eligibility_text: null, deadline: r.deadline,
      amount_min: r.amount_min, amount_max: r.amount_max, total_budget: r.total_budget, currency: r.currency,
      url: r.url || r.official_url, source_name: (r.provenance && r.provenance.source_name) || null,
      trust_level: 'high', confidence: null,
      evidence_snippet: (r.provenance && r.provenance.source_name) ? ('Listed on ' + r.provenance.source_name) : null,
      posted_date: r.first_seen,
    };
  }
  // Authoritative store: Supabase (funding_opportunities). §A: production must
  // NOT treat the legacy JSON as source of truth. JSON is a dev/offline fallback
  // only. Server-side count via content-range; portals excluded.
  MX.loadOpportunities = function () {
    if (_cache) return Promise.resolve(_cache);
    function fallback() {
      _sourceOfTruth = 'legacy_json_fallback';
      return fetch('funding-opportunities.json').then(function (r) { return r.json(); })
        .then(function (j) { var arr = (j && j.opportunities) || j || []; _cache = arr.map(normOpp).filter(function (o) { return o.recordClass !== 'source'; }); _total = _cache.length; return _cache; })
        .catch(function () { _cache = []; return _cache; });
    }
    // Browser-safe PUBLISHABLE key (public by design; /api/config exposes a
    // SECRET key which Supabase blocks in-browser, so we use the publishable one).
    var url = 'https://xyhctbrsgxluwcytbran.supabase.co';
    var key = 'sb_publishable_Z01GBeNbfbpIu1G0AibFUw_6W3DuA-7';
    return Promise.resolve().then(function () {
      return fetch(url + '/rest/v1/funding_opportunities?select=*&record_type=neq.portal&order=last_seen.desc.nullslast', {
        headers: { apikey: key, Authorization: 'Bearer ' + key, Prefer: 'count=exact', Range: '0-999' },
      }).then(function (res) {
        if (!res.ok && res.status !== 206 && res.status !== 200) throw new Error('rest ' + res.status);
        var cr = res.headers.get('content-range'); if (cr && cr.indexOf('/') !== -1) _total = parseInt(cr.split('/')[1], 10);
        return res.json();
      }).then(function (rows) {
        if (!Array.isArray(rows) || !rows.length) return fallback();
        _sourceOfTruth = 'supabase';
        _cache = rows.map(function (r) { return normOpp(dbToRaw(r), r.id); }).filter(function (o) { return o.recordClass !== 'source'; });
        if (_total == null) _total = _cache.length;
        return _cache;
      });
    }).catch(fallback);
  };

  // ── Truthful database statistics (§3/§12) ──────────────────────────────────
  // Real counts computed SERVER-SIDE via PostgREST count=exact (scalable to
  // 20k–50k records — never load the universe into the browser to count it).
  // NOTHING here is hardcoded; if the DB holds 100, it reports 100.
  var _stats = null;
  MX.stats = function () {
    if (_stats) return Promise.resolve(_stats);
    var url = 'https://xyhctbrsgxluwcytbran.supabase.co';
    var key = 'sb_publishable_Z01GBeNbfbpIu1G0AibFUw_6W3DuA-7';
    function count(qs) {
      return fetch(url + '/rest/v1/' + qs, { headers: { apikey: key, Authorization: 'Bearer ' + key, Prefer: 'count=exact', Range: '0-0' } })
        .then(function (res) { var cr = res.headers.get('content-range'); return cr && cr.indexOf('/') !== -1 ? parseInt(cr.split('/')[1], 10) : null; })
        .catch(function () { return null; });
    }
    function scalar(qs) { return fetch(url + '/rest/v1/' + qs, { headers: { apikey: key, Authorization: 'Bearer ' + key } }).then(function (r) { return r.json(); }).catch(function () { return null; }); }
    var today = new Date().toISOString().slice(0, 10);
    var in30 = new Date(Date.now() + 30 * 864e5).toISOString().slice(0, 10);
    var since = new Date(Date.now() - 7 * 864e5).toISOString();
    var base = 'funding_opportunities?select=id&record_type=neq.portal';
    return Promise.all([
      count(base),
      count(base + '&status=eq.verified_open'),
      count(base + '&status=eq.forecast'),
      count(base + '&status=in.(awarded,closed,historical,archived,cancelled,recently_closed)'),
      count(base + '&status=eq.verified_open&deadline=gte.' + today + '&deadline=lt.' + in30),
      count(base + '&first_seen=gte.' + since),
      count('funding_sources?select=id'),
      scalar('funding_opportunities?select=last_seen&record_type=neq.portal&order=last_seen.desc.nullslast&limit=1'),
    ]).then(function (r) {
      var last = Array.isArray(r[7]) && r[7][0] && r[7][0].last_seen ? String(r[7][0].last_seen).slice(0, 10) : null;
      _stats = {
        total: r[0], open: r[1], forecast: r[2], historical: r[3] || 0,
        closingSoon: r[4], newSince: r[5], sources: r[6], updated: last,
        source: 'supabase',
      };
      // Distinct funders: server-side, from the funding_facets RPC — never by
      // counting a bulk client load (Funding no longer fetches one). Falls back
      // to the cache only if a another screen happens to have loaded it, and
      // stays null (rendered as "…", never a guess) if neither is available.
      return MX.fundingFacets().then(function (f) {
        if (f && Array.isArray(f.funders) && f.funders.length) _stats.funders = f.funders.length;
        else if (_cache) { var m = {}; _cache.forEach(function (o) { if (o.funder) m[o.funder] = 1; }); _stats.funders = Object.keys(m).length; }
        return _stats;
      }).catch(function () { return _stats; });
    }).catch(function () { return { total: _total, source: 'unavailable' }; });
  };

  // ── Server-side Funding query (Phase 2B) ────────────────────────────────────
  // Pagination, filtering, sorting, search, segment filtering and grouped segment
  // counts all run in Postgres via the funding_search RPC. The browser receives
  // ONLY the current page. Classification is computed server-side from the same
  // rules as MX.match, using the caller's org profile passed as a parameter (no
  // organisation data is stored or cross-tenant readable).
  var _SB_URL = 'https://xyhctbrsgxluwcytbran.supabase.co';
  var _SB_KEY = 'sb_publishable_Z01GBeNbfbpIu1G0AibFUw_6W3DuA-7';
  function _sbRpc(fn, body) {
    return fetch(_SB_URL + '/rest/v1/rpc/' + fn, {
      method: 'POST', headers: { apikey: _SB_KEY, Authorization: 'Bearer ' + _SB_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify(body || {}),
    }).then(function (r) { if (!r.ok) throw new Error('rpc ' + fn + ' ' + r.status); return r.json(); });
  }
  // The org profile the RPC needs: countries, applicant "kinds" (the same
  // synonym expansion MX.match uses), sectors and the preferred GBP range.
  MX.orgQueryProfile = function (org) {
    org = org || MX.org();
    var kinds = orgApplicantKinds(org);
    return { countries: org.countries || [], kinds: Object.keys(kinds), sectors: org.sectors || [],
      preferredMinGBP: org.preferredMinGBP || 0, preferredMaxGBP: org.preferredMaxGBP || 1e18 };
  };
  // params: { page, pageSize, search, segment, country, includeSupportedGlobal,
  //   opportunityType, funder, funderType, status, region, sector, elig,
  //   amountMin, amountMax, sortField, sortDirection }. Returns { records (mapped
  //   to the normalised opp shape, each with _seg), pagination, counts }.
  // Families that are never grants. Mira Grants is a GRANTS product: a works
  // tender or an individual-consultant EOI is not a funding opportunity for an
  // NGO, however active it is.
  //
  // This gate is the single chokepoint for every grants query in the product.
  // funding_search has always accepted `excludeFamilies`, and nothing passed it
  // — so a Sri Lanka search returned six World Bank procurement notices and no
  // grants, because six procurement notices are all Sri Lanka has in the corpus.
  // Filtering here rather than per-caller means a new screen cannot reintroduce
  // the leak by forgetting a parameter.
  //
  // Callers that genuinely want the wider universe (procurement intelligence,
  // the internal coverage audit) pass includeNonGrantFamilies: true explicitly.
  MX.NON_GRANT_FAMILIES = ['procurement', 'consulting'];

  MX.searchFunding = function (params) {
    params = params || {};
    var body = Object.assign({}, params, { org: MX.orgQueryProfile(params.org) });
    delete body._org;
    if (body.includeNonGrantFamilies) {
      delete body.includeNonGrantFamilies;
    } else if (!body.family && !(body.excludeFamilies && body.excludeFamilies.length)) {
      // Exclude rather than family:'grant' so forecast and partnership records —
      // which are grant-shaped and pursuable — are not silently dropped too.
      body.excludeFamilies = MX.NON_GRANT_FAMILIES.slice();
    }
    return _sbRpc('funding_search', { p: body }).then(function (res) {
      var recs = (res && res.records) || [];
      return {
        // normOpp keeps only the legacy display shape, which silently dropped the
        // taxonomy the server now returns (precise type, participation routes,
        // publisher date, availability). Carry those through verbatim — they are
        // what the decision-first table renders, and re-deriving them in the
        // browser would invent classifications the source never made.
        records: recs.map(function (r) {
          var o = normOpp(dbToRaw(r), r.id);
          o._seg = r._seg;
          ['opportunity_type', 'opportunity_family', 'procurement_subtype', 'participation_routes',
            'implementation_countries', 'funder_domicile', 'availability', 'source_published_at',
            'customer_surface', 'priority_band', 'applicant_eligibility', 'critical_unresolved',
            'south_asia_relevance', 'eligible_types', 'status', 'first_seen', 'last_seen',
          ].forEach(function (k) { if (r[k] !== undefined && o[k] === undefined) o[k] = r[k]; });
          return o;
        }),
        pagination: (res && res.pagination) || { page: 0, pageSize: 25, totalRecords: recs.length, totalPages: 1, hasNextPage: false, hasPreviousPage: false },
        counts: (res && res.counts) || null,
        queryTimestamp: res && res.queryTimestamp,
      };
    });
  };
  // Distinct filter options (server-side; never a bulk client load).
  var _facets = null;
  MX.fundingFacets = function () {
    if (_facets) return Promise.resolve(_facets);
    return _sbRpc('funding_facets', {}).then(function (f) { _facets = f || { countries: [], funders: [], types: [] }; return _facets; })
      .catch(function () { return { countries: [], funders: [], types: [] }; });
  };
  // Lazy-load a SINGLE record's heavy fields (summary/provenance) when the
  // preview opens — never sent in the table query (§4/§15).
  MX.fundingRecord = function (id) {
    return fetch(_SB_URL + '/rest/v1/funding_opportunities?id=eq.' + encodeURIComponent(id) + '&select=*&limit=1',
      { headers: { apikey: _SB_KEY, Authorization: 'Bearer ' + _SB_KEY } })
      .then(function (r) { return r.json(); })
      .then(function (rows) { return Array.isArray(rows) && rows[0] ? normOpp(dbToRaw(rows[0]), rows[0].id) : null; })
      .catch(function () { return null; });
  };


  // ── Phase 7A.1: authenticated Enterprise workspace + persistence ───────────
  // Everything below goes through the ONE canonical Supabase client owned by
  // globly.jsx (window.GloballyAuth). Enterprise creates no second client, no
  // second login, and never treats localStorage identity as authentication.
  //
  // PREVIEW vs PRODUCTION is explicit and never blurred: with no Supabase
  // session every persistence call returns {ok:false, reason:'preview'} and
  // writes nothing. It does not fabricate a workspace, does not write to the
  // protected tables, and never claims to have saved something it did not.
  var _authBridge = function () { return (typeof window !== 'undefined' && window.GloballyAuth) || null; };
  var _wsCache = null;

  MX.auth = {
    available: function () { return !!_authBridge(); },
    session: function () { var b = _authBridge(); return b ? b.getSession() : Promise.resolve(null); },
    user: function () { var b = _authBridge(); return b ? b.getUser() : Promise.resolve(null); },
    token: function () { var b = _authBridge(); return b ? b.getAccessToken() : Promise.resolve(null); },
    onChange: function (cb) { var b = _authBridge(); return b ? b.onAuthStateChange(cb) : function () {}; },
  };

  // Authenticated PostgREST call. Uses the USER's access token as the bearer so
  // auth.uid() is populated and RLS applies. Returns a controlled result rather
  // than throwing, so no caller can mistake a failure for a success.
  function _authedFetch(path, init) {
    return MX.auth.token().then(function (tok) {
      if (!tok) return { ok: false, reason: 'preview', error: 'Not signed in' };
      var o = init || {};
      return fetch(_SB_URL + '/rest/v1/' + path, Object.assign({}, o, {
        headers: Object.assign({
          apikey: _SB_KEY, Authorization: 'Bearer ' + tok, 'Content-Type': 'application/json',
        }, o.headers || {}),
      })).then(function (r) {
        return r.text().then(function (txt) {
          var body = null; try { body = txt ? JSON.parse(txt) : null; } catch (e) {}
          if (!r.ok) return { ok: false, reason: r.status === 401 || r.status === 403 ? 'forbidden' : 'error', status: r.status, error: (body && body.message) || txt };
          return { ok: true, data: body };
        });
      }).catch(function (e) { return { ok: false, reason: 'offline', error: e.message }; });
    });
  }

  // ensureWorkspace — the single provisioning entry point. Idempotent server-side
  // (ensure_enterprise_workspace); this only caches the result per page load.
  MX.ensureWorkspace = function (opts) {
    opts = opts || {};
    if (_wsCache && !opts.force) return Promise.resolve(_wsCache);
    return _authedFetch('rpc/ensure_enterprise_workspace', {
      method: 'POST',
      body: JSON.stringify({
        p_name: opts.name || null, p_org_website: opts.website || null,
        p_org_type: opts.orgType || null, p_onboarding_key: opts.onboardingKey || null,
      }),
    }).then(function (res) {
      if (!res.ok) return res;
      _wsCache = { ok: true, workspace: res.data.workspace, membership: res.data.membership, created: res.data.created, userId: res.data.user_id };
      return _wsCache;
    });
  };
  MX.getWorkspace = function () { return _wsCache; };
  MX.clearWorkspaceCache = function () { _wsCache = null; };

  function _withWs(fn) {
    return MX.ensureWorkspace().then(function (w) { return w.ok ? fn(w.workspace.id) : w; });
  }

  MX.listFundingConversations = function () {
    return _withWs(function (ws) {
      return _authedFetch('funding_conversations?workspace_id=eq.' + ws + '&archived_at=is.null&order=updated_at.desc&limit=50');
    });
  };
  MX.createFundingConversation = function (title, criteria) {
    return _withWs(function (ws) {
      return _authedFetch('funding_conversations', {
        method: 'POST', headers: { Prefer: 'return=representation' },
        body: JSON.stringify([{ workspace_id: ws, title: title || 'New conversation', structured_criteria: criteria || {} }]),
      }).then(function (r) { if (r.ok && Array.isArray(r.data)) r.data = r.data[0]; return r; });
    });
  };
  MX.addFundingMessage = function (conversationId, role, body, payload) {
    return _withWs(function (ws) {
      return _authedFetch('funding_conversation_messages', {
        method: 'POST', headers: { Prefer: 'return=representation' },
        body: JSON.stringify([{ conversation_id: conversationId, workspace_id: ws, role: role, body: body, structured_payload: payload || {} }]),
      }).then(function (r) { if (r.ok && Array.isArray(r.data)) r.data = r.data[0]; return r; });
    });
  };
  MX.listFundingMessages = function (conversationId) {
    return _authedFetch('funding_conversation_messages?conversation_id=eq.' + conversationId + '&order=created_at.asc&limit=200');
  };
  MX.listFundingShortlist = function () {
    return _withWs(function (ws) {
      return _authedFetch('funding_shortlist?workspace_id=eq.' + ws + '&archived_at=is.null&order=created_at.desc&limit=100');
    });
  };
  // Duplicate adds are expected (a user clicks Save twice). The partial unique
  // indexes make a second active row impossible; a 23505 is reported as an
  // already-saved success rather than surfaced as an error.
  MX.addToFundingShortlist = function (opportunityKey, opts) {
    opts = opts || {};
    return _withWs(function (ws) {
      var row = { workspace_id: ws, opportunity_key: opportunityKey };
      if (opts.programmeWorkspaceId) row.programme_workspace_id = opts.programmeWorkspaceId;
      if (opts.conversationId) row.source_conversation_id = opts.conversationId;
      return _authedFetch('funding_shortlist', {
        method: 'POST', headers: { Prefer: 'return=representation' }, body: JSON.stringify([row]),
      }).then(function (r) {
        if (!r.ok && /duplicate key|23505/.test(String(r.error || ''))) return { ok: true, alreadySaved: true };
        if (r.ok && Array.isArray(r.data)) r.data = r.data[0];
        return r;
      });
    });
  };
  MX.archiveFundingShortlistItem = function (id) {
    return _authedFetch('funding_shortlist?id=eq.' + id, {
      method: 'PATCH', body: JSON.stringify({ archived_at: new Date().toISOString() }),
    });
  };

  // ── Organisation entity (§14) — real if onboarded, else a clearly-labelled demo ─
  MX.org = function () {
    var demo = { name: 'Islamic Relief', _demo: true, types: ['International NGO'], countries: ['Pakistan', 'Kenya', 'Bangladesh', 'Somalia'],
      eligibleCountries: null, sectors: ['Health', 'Humanitarian', 'Education', 'Climate'], typicalProjectGBP: 400000,
      preferredMinGBP: 25000, preferredMaxGBP: 2000000, yearsOperating: 20, consortiumCapable: true, leadCapable: true,
      coFinanceCapable: true, priorDonors: ['World Bank', 'ECHO'], languages: ['English'] };
    try { var raw = localStorage.getItem('mx.org'); if (raw) { var o = JSON.parse(raw); o._demo = false; return o; } } catch (e) {}
    return demo;
  };
  MX.saveOrg = function (o) { try { localStorage.setItem('mx.org', JSON.stringify(o)); } catch (e) {} };

  // ── Matching engine (§16/§17/§41): dimensions, hard-eligibility override ─────
  var APPLICANT_SYNONYMS = { 'International NGO': ['ngo', 'nonprofit', 'non-profit', 'civil society'], 'Local NGO': ['ngo', 'nonprofit'], 'University': ['university', 'academic', 'research', 'higher education'], 'Research institution': ['research', 'institute'], 'Social enterprise': ['company', 'enterprise', 'business', 'for-profit'], 'Charity': ['charity', 'ngo', 'nonprofit'] };
  function orgApplicantKinds(org) { var out = {}; (org.types || []).forEach(function (t) { (APPLICANT_SYNONYMS[t] || [t.toLowerCase()]).forEach(function (k) { out[k] = 1; }); }); return out; }
  function dim(verdict, note) { return { verdict: verdict, note: note }; }

  MX.match = function (opp, org) {
    var reasons = [], concerns = [], missing = [], dims = {};
    // Geography (hard if opp names countries and none overlap)
    var geoHard = false;
    if (opp.countries.length) {
      var hit = opp.countries.filter(function (c) { return (org.countries || []).indexOf(c) !== -1; });
      if (hit.length) { dims.geography = dim('strong', 'Eligible country: ' + hit.join(', ')); reasons.push(hit[0] + ' is one of your operating countries'); }
      else if (opp.regions.indexOf('Global') !== -1) { dims.geography = dim('possible', 'Global — country not specifically named'); }
      // A named country is usually the PROJECT location, not a confirmed
      // applicant-country restriction. §3: do not assert Not eligible without a
      // confirmed hard rule — treat unconfirmed geography as unclear, not a fail.
      else { dims.geography = dim('unclear', 'Names ' + opp.countries.slice(0, 3).join(', ') + ' — a confirmed applicant-country restriction is not published'); missing.push('Confirmed geographic eligibility'); }
    } else if (opp.regions.indexOf('Global') !== -1) { dims.geography = dim('strong', 'Open globally'); }
    else { dims.geography = dim('unclear', 'Geography not published'); missing.push('Eligible geography'); }

    // Applicant type (hard if opp lists eligible types and none match)
    var appHard = false;
    if (opp.eligible.length) {
      var kinds = orgApplicantKinds(org);
      var ok = opp.eligible.some(function (e) { return kinds[String(e).toLowerCase()]; });
      if (ok) { dims.applicantType = dim('strong', 'Your organisation type is eligible'); reasons.push('Your organisation type is an eligible applicant'); }
      else { dims.applicantType = dim('ineligible', 'Eligible applicants: ' + opp.eligible.join(', ')); appHard = true; concerns.push('Your organisation type may not be an eligible applicant'); }
    } else { dims.applicantType = dim('unclear', 'Applicant types not published'); missing.push('Eligible applicant types'); }

    // Sector (soft)
    if (opp.sectors.length) {
      var s = opp.sectors.filter(function (x) { return (org.sectors || []).indexOf(x) !== -1; });
      if (s.length) { dims.sector = dim('strong', 'Sector match: ' + s.join(', ')); reasons.push(s[0] + ' is a primary sector for you'); }
      else { dims.sector = dim('weak', 'Sectors (' + opp.sectors.join(', ') + ') differ from yours'); concerns.push('Sector focus differs from your programmes'); }
    } else { dims.sector = dim('unclear', 'Sector not published'); missing.push('Sector'); }

    // Funding size (soft) — per-award only, never envelope
    if (opp.analyticalGBP != null) {
      var within = opp.analyticalGBP >= (org.preferredMinGBP || 0) && opp.analyticalGBP <= (org.preferredMaxGBP || Infinity);
      dims.fundingSize = within ? dim('strong', fmtGBP(opp.analyticalGBP) + ' fits your typical range') : dim('weak', fmtGBP(opp.analyticalGBP) + ' is outside your typical range');
      if (within) reasons.push('Award size fits your typical project range'); else concerns.push('Award size is outside your usual range');
    } else if (opp.amountType === 'total_programme_budget') { dims.fundingSize = dim('unclear', 'Only a total programme budget is published — per-award size unknown'); missing.push('Per-award amount'); }
    else { dims.fundingSize = dim('unclear', 'Amount not published'); missing.push('Award amount'); }

    // Deadline feasibility (soft)
    if (opp.deadline) {
      var days = Math.round((new Date(opp.deadline) - Date.now()) / 86400000);
      if (days < 0) { dims.deadline = dim('ineligible', 'Deadline passed'); concerns.push('The published deadline has passed'); }
      else if (days <= 21) { dims.deadline = dim('weak', 'Closes in ' + days + ' days — tight'); concerns.push('Deadline is in ' + days + ' days'); }
      else { dims.deadline = dim('strong', 'Closes in ' + days + ' days'); }
    } else { dims.deadline = dim('unclear', 'Deadline not published'); missing.push('Deadline'); }

    // Evidence confidence
    dims.evidence = (opp.trust === 'high' || opp.trust === 'verified') ? dim('strong', 'From a verified official source') : dim('unclear', 'Source trust ' + (opp.trust || 'unclear'));

    // Overall with hard override (§16/§17/§41)
    var hardFail = geoHard || appHard || (dims.deadline.verdict === 'ineligible');
    var overall;
    if (hardFail) overall = 'Not eligible';
    else {
      var strong = Object.keys(dims).filter(function (k) { return dims[k].verdict === 'strong'; }).length;
      var weak = Object.keys(dims).filter(function (k) { return dims[k].verdict === 'weak'; }).length;
      var unclear = Object.keys(dims).filter(function (k) { return dims[k].verdict === 'unclear'; }).length;
      // §10: geography must be reasonably SUPPORTED for a Strong candidate.
      // Unconfirmed geography (e.g. a US-only grant vs a Pakistan/Kenya org)
      // can never be Strong merely because applicant type says "NGO".
      var geoOk = dims.geography && (dims.geography.verdict === 'strong' || dims.geography.verdict === 'possible');
      if (unclear >= 4) overall = 'Insufficient information';
      else if (!geoOk) overall = (unclear >= 2 ? 'Insufficient information' : (strong >= 2 ? 'Worth reviewing' : 'Monitoring'));
      else if (strong >= 4 && weak === 0) overall = 'Strong candidate';
      else if (strong >= 2) overall = 'Worth reviewing';
      else overall = 'Monitoring';
    }
    return { overall: overall, dims: dims, reasons: reasons, concerns: concerns, missing: missing, hardFail: hardFail };
  };
  MX.ORDER = ['Strong candidate', 'Worth reviewing', 'Monitoring', 'Insufficient information', 'Probably not suitable', 'Not eligible'];

  // ── Entity derivation ───────────────────────────────────────────────────────
  MX.countries = function (opps) { var m = {}; opps.forEach(function (o) { o.countries.forEach(function (c) { (m[c] = m[c] || { name: c, opps: [] }).opps.push(o); }); }); return m; };
  MX.funders = function (opps) { var m = {}; opps.forEach(function (o) { (m[o.funder] = m[o.funder] || { name: o.funder, type: o.funderType, opps: [] }).opps.push(o); }); return m; };

  // ── Funder profile (Phase A) — header facts from real records only ──────────
  MX.funderProfile = function (name, opps) {
    var mine = opps.filter(function (o) { return o.funder === name; });
    var awards = mine.map(function (o) { return o.analyticalGBP; }).filter(function (x) { return x != null; }).sort(function (a, b) { return a - b; });
    var budgets = mine.filter(function (o) { return o.amountType === 'total_programme_budget'; }).map(function (o) { return toGBP(o.totalBudget, o.currency); }).filter(function (x) { return x != null; });
    var countries = {}, sectors = {}; mine.forEach(function (o) { o.countries.forEach(function (c) { countries[c] = 1; }); o.sectors.forEach(function (s) { sectors[s] = 1; }); });
    var open = mine.filter(function (o) { return o.status === 'verified_open' || o.status === 'likely_open'; }).length;
    var forecast = mine.filter(function (o) { return o.status === 'forecast'; }).length;
    return {
      name: name, type: (mine[0] && mine[0].funderType) || null,
      countries: Object.keys(countries), sectors: Object.keys(sectors),
      awardRange: awards.length ? { min: awards[0], max: awards[awards.length - 1], n: awards.length } : null,
      programmeBudgets: budgets, active: mine.length, open: open, forecast: forecast, opps: mine,
    };
  };

  // ── Coverage (§19) — computed from real records, unknown stays unknown ───────
  MX.coverage = function (opps) {
    var n = opps.length || 1;
    var withAmount = opps.filter(function (o) { return o.analyticalGBP != null; }).length;
    var withElig = opps.filter(function (o) { return o.eligible.length || o.eligibilityText; }).length;
    var sources = {}; opps.forEach(function (o) { if (o.sourceName) sources[o.sourceName] = 1; });
    return { records: opps.length, sources: Object.keys(sources).length, amountPct: Math.round(100 * withAmount / n), eligPct: Math.round(100 * withElig / n), asOf: new Date().toISOString() };
  };

  // ── NL query parser (§5/§6) — entity + lens + filters, correctable ──────────
  // Canonical geography — a FIXED list (never derived only from ingested data),
  // so a query for a country Mira has no records for is still recognised (§1).
  var COUNTRIES = {
    'Sri Lanka': ['sri lanka', 'srilanka'], 'Kenya': ['kenya', 'kenyan'], 'Pakistan': ['pakistan', 'pakistani'],
    'Bangladesh': ['bangladesh'], 'India': ['india', 'indian'], 'Nepal': ['nepal'], 'Bhutan': ['bhutan'],
    'Afghanistan': ['afghanistan'], 'Myanmar': ['myanmar', 'burma'], 'Indonesia': ['indonesia'], 'Philippines': ['philippines'],
    'Vietnam': ['vietnam'], 'Cambodia': ['cambodia'], 'Malaysia': ['malaysia'], 'Thailand': ['thailand'],
    'United Kingdom': ['united kingdom', ' uk ', 'britain', 'british', 'england'], 'United States': ['united states', ' usa', ' u.s', 'american'],
    'Nigeria': ['nigeria'], 'Somalia': ['somalia'], 'Ethiopia': ['ethiopia'], 'Uganda': ['uganda'], 'Ghana': ['ghana'],
    'Tanzania': ['tanzania'], 'South Africa': ['south africa'], 'Democratic Republic of Congo': ['dr congo', 'drc', 'democratic republic of congo'],
    'Yemen': ['yemen'], 'Syria': ['syria'], 'Lebanon': ['lebanon'], 'Palestine': ['palestine', 'gaza'], 'Sudan': ['sudan'],
    'Ukraine': ['ukraine'], 'Turkey': ['turkey', 'turkiye'], 'Egypt': ['egypt'], 'Morocco': ['morocco'], 'Jordan': ['jordan'],
  };
  var REGIONS = {
    'South Asia': ['south asia', 'south-asia'], 'Southeast Asia': ['southeast asia', 'south-east asia', 'south east asia'],
    'Sub-Saharan Africa': ['sub-saharan', 'sub saharan'], 'East Africa': ['east africa'], 'West Africa': ['west africa'],
    'Middle East': ['middle east', 'mena'], 'Latin America': ['latin america'], 'Europe': ['europe'], 'Global': ['global', 'worldwide'],
  };
  function padded(ql) { return ' ' + ql.replace(/[.,]/g, ' ') + ' '; }
  MX.parse = function (q, opps) {
    q = String(q || '').trim(); var ql = q.toLowerCase(); var pql = padded(ql);
    var out = { raw: q, entity: null, lens: 'funding', filters: {}, confidence: 'medium' };
    if (/\brisk|risks?\b/.test(ql)) out.lens = 'risk';
    else if (/\bsource|sources|evidence\b/.test(ql)) out.lens = 'sources';
    else if (/\bchanged|change|briefing|update\b/.test(ql)) out.lens = 'briefing';
    else if (/\bdonor|funder(s)?\b/.test(ql)) out.lens = 'donors';
    // funder (explicit, before country so "world bank ... in pakistan" keeps both)
    var funders = MX.funders(opps || []); var funder = null;
    Object.keys(funders).forEach(function (f) { if (f.length > 4 && ql.indexOf(f.toLowerCase()) !== -1) funder = f; });
    if (/\bworld bank\b/.test(ql)) funder = funder || 'World Bank';
    // country (canonical list; longest alias wins; word-boundary-ish)
    var country = null, clen = 0;
    Object.keys(COUNTRIES).forEach(function (c) { COUNTRIES[c].forEach(function (a) { if (a && (pql.indexOf(a) !== -1 || pql.indexOf(' ' + a.trim() + ' ') !== -1) && a.length > clen) { country = c; clen = a.length; } }); });
    // region
    var region = null; Object.keys(REGIONS).forEach(function (r) { REGIONS[r].forEach(function (a) { if (pql.indexOf(a) !== -1) region = r; }); });
    if (country) { out.filters.country = country; if (!funder) out.entity = { type: 'country', id: country }; }
    if (region && !country) { out.filters.region = region; }
    if (funder) out.entity = { type: 'funder', id: funder };
    // funding type
    var ftypes = { grant: ['grant', 'grants'], tender: ['tender', 'tenders', 'procurement', 'rfp', 'rfq'], fellowship: ['fellowship'], prize: ['prize', 'challenge'], loan: ['loan'] };
    Object.keys(ftypes).forEach(function (t) { ftypes[t].forEach(function (a) { if (pql.indexOf(' ' + a + ' ') !== -1) out.filters.fundingType = t; }); });
    // amounts
    var amt = ql.match(/(?:under|below|less than)\s*£?\s*([\d,.]+)\s*(k|thousand|m|million|bn|billion)?/);
    if (amt) out.filters.amountMax = scaleAmt(amt[1], amt[2]);
    var amt2 = ql.match(/(?:over|above|more than)\s*£?\s*([\d,.]+)\s*(k|thousand|m|million|bn|billion)?/);
    if (amt2) out.filters.amountMin = scaleAmt(amt2[1], amt2[2]);
    // deadline
    var dl = ql.match(/(?:closing|closes?|deadline|next)\s*(?:in\s*)?(\d+)\s*days?/);
    if (dl) out.filters.deadlineDays = parseInt(dl[1], 10); else if (/closing soon|soon/.test(ql)) out.filters.deadlineDays = 30;
    // sector
    ['Health', 'Climate', 'Education', 'Humanitarian', 'Agriculture', 'AI', 'Energy'].forEach(function (s) { if (ql.indexOf(s.toLowerCase()) !== -1) out.filters.sector = s; });
    if (/\bopen\b/.test(ql)) out.filters.status = 'open';
    if (/\bnew\b|since i last/.test(ql)) out.filters.newOnly = true;
    return out;
  };
  function scaleAmt(num, unit) { var n = parseFloat(String(num).replace(/,/g, '')); if (!unit) return n; unit = unit[0]; return unit === 'k' ? n * 1e3 : unit === 't' ? n * 1e3 : unit === 'm' ? n * 1e6 : unit === 'b' ? n * 1e9 : n; }

  var COUNTRY_REGION = { 'Sri Lanka': 'South Asia', 'Pakistan': 'South Asia', 'India': 'South Asia', 'Bangladesh': 'South Asia', 'Nepal': 'South Asia', 'Bhutan': 'South Asia', 'Afghanistan': 'South Asia', 'Indonesia': 'Southeast Asia', 'Philippines': 'Southeast Asia', 'Vietnam': 'Southeast Asia', 'Cambodia': 'Southeast Asia', 'Malaysia': 'Southeast Asia', 'Thailand': 'Southeast Asia', 'Myanmar': 'Southeast Asia', 'Kenya': 'East Africa', 'Ethiopia': 'East Africa', 'Uganda': 'East Africa', 'Tanzania': 'East Africa', 'Somalia': 'East Africa', 'Nigeria': 'West Africa', 'Ghana': 'West Africa' };
  function regionOf(c) { return COUNTRY_REGION[c] || null; }
  MX.applyFilters = function (opps, f) {
    return opps.filter(function (o) {
      // §2: explicit country/region are HARD constraints, evaluated FIRST.
      // Valid = opp targeted at the country, OR a global opp (eligible there).
      if (f.country && o.countries.indexOf(f.country) === -1 && o.regions.indexOf('Global') === -1) return false;
      if (f.region && o.regions.indexOf(f.region) === -1 && o.regions.indexOf('Global') === -1 && !o.countries.some(function (c) { return regionOf(c) === f.region; })) return false;
      // Funding type: exclude only when the record's type is DOCUMENTED and conflicts.
      // Records with an undocumented type stay as candidates (never hidden on inference).
      if (f.fundingType === 'grant' && o.type && /tender|procure|loan|credit|prize/i.test(o.type) && !/grant/i.test(o.type)) return false;
      if (f.fundingType === 'tender' && o.type && /grant|fellowship|prize/i.test(o.type) && !/tender|procure|project/i.test(o.type)) return false;
      if (f.sector && o.sectors.indexOf(f.sector) === -1) return false;
      if (f.amountMax != null && (o.analyticalGBP == null || o.analyticalGBP > f.amountMax)) return false;
      if (f.amountMin != null && (o.analyticalGBP == null || o.analyticalGBP < f.amountMin)) return false;
      if (f.status === 'open' && !(o.status === 'verified_open' || o.status === 'likely_open')) return false;
      if (f.deadlineDays != null) { if (!o.deadline) return false; var d = Math.round((new Date(o.deadline) - Date.now()) / 86400000); if (d < 0 || d > f.deadlineDays) return false; }
      return true;
    });
  };

  // ── Store (§7/§12/§23): pins, saves, applications, alerts — localStorage now ─
  function lsGet(k, d) { try { var v = localStorage.getItem(k); return v ? JSON.parse(v) : d; } catch (e) { return d; } }
  function lsSet(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} }
  MX.store = {
    pins: function () { return lsGet('mx.pins', []); },
    pin: function (p) { var a = lsGet('mx.pins', []); if (!a.some(function (x) { return x.key === p.key; })) { a.push(Object.assign({ at: Date.now(), lastSeen: Date.now() }, p)); lsSet('mx.pins', a); } return a; },
    unpin: function (key) { lsSet('mx.pins', lsGet('mx.pins', []).filter(function (x) { return x.key !== key; })); },
    saves: function () { return lsGet('mx.saves', []); },
    save: function (opp) { var a = lsGet('mx.saves', []); if (!a.some(function (x) { return x.id === opp.id; })) { a.push({ id: opp.id, title: opp.title, funder: opp.funder, at: Date.now() }); lsSet('mx.saves', a); } return a; },
    unsave: function (id) { lsSet('mx.saves', lsGet('mx.saves', []).filter(function (x) { return x.id !== id; })); },
    isSaved: function (id) { return lsGet('mx.saves', []).some(function (x) { return x.id === id; }); },
    apps: function () { return lsGet('mx.apps', []); },
    // "Pursuit" record (user-facing name); table/keys stay mx.apps for now.
    createApp: function (opp) {
      var a = lsGet('mx.apps', []); var ex = a.filter(function (x) { return x.oppId === opp.id; })[0]; if (ex) return ex;
      var acts = [{ at: Date.now(), text: 'Opportunity added to Pursuits' }];
      try {
        var m = MX.match(opp, MX.org());
        if (m.dims.applicantType && m.dims.applicantType.verdict === 'ineligible') acts.push({ at: Date.now(), text: 'Mira flagged an applicant-type restriction to resolve', mira: true });
        else if (m.missing.indexOf('Confirmed geographic eligibility') !== -1) acts.push({ at: Date.now(), text: 'Mira identified an unresolved applicant-country rule', mira: true });
      } catch (e) {}
      var app = { id: 'app-' + Date.now(), oppId: opp.id, title: opp.title, funder: opp.funder, deadline: opp.deadline, stage: 'Saved', createdAt: Date.now(), assessed: true, tasks: [], notes: [], dismissedTasks: [], activity: acts };
      a.push(app); lsSet('mx.apps', a); return app;
    },
    updateApp: function (id, patch) { var a = lsGet('mx.apps', []); a = a.map(function (x) { return x.id === id ? Object.assign(x, patch) : x; }); lsSet('mx.apps', a); return a; },
    addActivity: function (id, text) { var a = lsGet('mx.apps', []); a = a.map(function (x) { if (x.id === id) { x.activity = (x.activity || []).concat([{ at: Date.now(), text: text }]); } return x; }); lsSet('mx.apps', a); return a; },
    alerts: function () { return lsGet('mx.alerts', []); },
    addAlert: function (al) { var a = lsGet('mx.alerts', []); a.push(Object.assign({ id: 'al-' + Date.now(), at: Date.now() }, al)); lsSet('mx.alerts', a); return a; },
  };
  MX.APP_STAGES = ['Saved', 'Reviewing', 'Go / no-go', 'Preparing', 'Internal review', 'Ready to submit', 'Submitted', 'Won', 'Lost', 'Withdrawn'];

  // ── Go/no-go brief (§13) — from visible evidence only, no win prediction ─────
  MX.goNoGo = function (opp, org) {
    var m = MX.match(opp, org);
    var rec = m.hardFail ? 'Do not pursue — hard eligibility fails' : (m.overall === 'Strong candidate' ? 'Proceed to review full guidelines' : m.overall === 'Worth reviewing' ? 'Review, then decide' : 'Monitor only');
    return { strategicFit: m.dims.sector, eligibility: m.hardFail ? 'Fails' : 'Passes checked dimensions', fundingFit: m.dims.fundingSize, keyRisks: m.concerns, missing: m.missing, recommendation: rec, note: 'Mira cannot predict award success; this is a structured readiness view from published evidence only.' };
  };

  // ── Pursuit intelligence (§9-12): readiness, inferred state, tasks, actions ──
  MX.readiness = function (app, opp, org) {
    var m = MX.match(opp, org);
    var d = opp.deadline ? Math.round((new Date(opp.deadline) - Date.now()) / 86400000) : null;
    var geoUnclear = m.missing.indexOf('Confirmed geographic eligibility') !== -1;
    return [
      { label: 'Eligibility verified', status: m.hardFail ? 'missing' : (geoUnclear ? 'unclear' : 'verified'), note: m.hardFail ? 'A hard eligibility rule fails' : (geoUnclear ? 'Applicant-country rule not confirmed' : 'No confirmed blocker') },
      { label: 'Application route confirmed', status: 'unclear', note: 'Globally is a discovery intermediary — confirm the submission route on the official portal' },
      { label: 'Award suitability', status: opp.analyticalGBP != null ? 'verified' : 'unclear', note: opp.analyticalGBP != null ? fmtGBP(opp.analyticalGBP) : 'Award value not published' },
      { label: 'Required documents available', status: 'unclear', note: 'Document list not yet confirmed from the official guidance' },
      { label: 'Internal owner assigned', status: app.owner ? 'verified' : 'missing', note: app.owner || 'No owner assigned' },
      { label: 'Deadline feasible', status: d == null ? 'unclear' : (d > 21 ? 'verified' : (d >= 0 ? 'unclear' : 'missing')), note: d == null ? 'No published deadline' : (d >= 0 ? 'Closes in ' + d + ' days' : 'Deadline passed') },
      { label: 'Consortium readiness', status: org.consortiumCapable ? 'verified' : 'unclear', note: org.consortiumCapable ? 'Your profile records consortium capability' : 'Consortium capability not confirmed' },
      { label: 'Co-financing readiness', status: org.coFinanceCapable ? 'verified' : 'unclear', note: org.coFinanceCapable ? 'Your profile records co-financing capacity' : 'Co-financing capacity not confirmed' },
    ];
  };
  MX.pursuitState = function (app, opp, org) {
    var confirmed = { Submitted: 'SUBMITTED', Won: 'WON', Lost: 'LOST', Withdrawn: 'WITHDRAWN' };
    if (confirmed[app.stage]) return { state: confirmed[app.stage], source: 'User confirmed' };
    if (!opp) return { state: 'WATCHING', source: 'Mira inferred' };
    var m = MX.match(opp, org);
    var ready = MX.readiness(app, opp, org);
    var unresolved = ready.filter(function (r) { return r.status !== 'verified'; }).length;
    var d = opp.deadline ? Math.round((new Date(opp.deadline) - Date.now()) / 86400000) : null;
    var hasWork = (app.tasks && app.tasks.length) || (app.notes && app.notes.length);
    if (d != null && d >= 0 && d <= 21 && unresolved > 3) return { state: 'DEADLINE RISK', source: 'Mira inferred' };
    if (m.hardFail || (app.decision == null && unresolved >= 5 && hasWork)) return { state: 'AWAITING DECISION', source: 'Mira inferred' };
    if (hasWork || app.decision === 'Go') return { state: 'PREPARING', source: 'Mira inferred' };
    if (app.assessed) return { state: 'ASSESSING', source: 'Mira inferred' };
    return { state: 'WATCHING', source: 'Mira inferred' };
  };
  MX.suggestedTasks = function (app, opp, org) {
    var m = MX.match(opp, org), out = [];
    if (m.dims.applicantType && m.dims.applicantType.verdict === 'ineligible') out.push('Identify an eligible lead applicant or consortium route');
    if (m.missing.indexOf('Confirmed geographic eligibility') !== -1) out.push('Confirm whether international NGOs may participate in this call');
    out.push('Download the official applicant guidance');
    if (m.missing.indexOf('Award amount') !== -1 || m.missing.indexOf('Per-award amount') !== -1) out.push('Confirm the published award value');
    if (!app.owner) out.push('Assign an internal funding lead');
    out.push('Schedule an internal go/no-go review');
    var have = (app.tasks || []).map(function (t) { return t.text; });
    var dismissed = app.dismissedTasks || [];
    return out.filter(function (t) { return have.indexOf(t) === -1 && dismissed.indexOf(t) === -1; });
  };
  MX.nextBestAction = function (app, opp, org) {
    if (!opp) return 'Open the opportunity to assess eligibility';
    var m = MX.match(opp, org);
    if (m.dims.applicantType && m.dims.applicantType.verdict === 'ineligible') return 'Confirm whether an eligible consortium or lead-applicant route exists — your organisation type is not directly eligible';
    if (m.missing.indexOf('Confirmed geographic eligibility') !== -1) return 'Confirm whether international NGOs may participate in this call';
    if (!app.owner) return 'Assign an internal funding lead to own this pursuit';
    var d = opp.deadline ? Math.round((new Date(opp.deadline) - Date.now()) / 86400000) : null;
    if (d != null && d <= 30 && d >= 0) return 'Decide go/no-go — deadline in ' + d + ' days';
    return 'Review the official applicant guidance and confirm required documents';
  };

  window.MX = MX;
})();
