// NO PROD JSON API (bucket B — feed is a DataTables render payload or
//   {"html":...}, not data) — mock until backend (see PROD_API_INVENTORY.md)
// Represents: GET /payments/settings/ · AdminPaymentsController::show('settings') → settingsData() — see docs/ISYSTEM_REFERENCE.md §Batch 10.1
/* Traced Aug 2026 (architecture item 2). settingsData() assembles the PSP
   directory (Payments/ProvidersController) with per-provider metrics; the
   routing directory it also carries is routesData()
   (Payments/RoutesController), which the prototype models separately as
   /payments/rules.
   The Brands section below is the prototype's own: upstream a "brand" is a
   SKIN and is edited through SkinsController (HostSkinsUnified.jsx), not
   here. Kept because the whole prototype is brand-scoped, but it is not a
   surface isystem has, and pb-brands is one of the five content keys
   pbStore.clearContent() drops at cutover. */
/* Settings — system configuration. Brands management lives here. */

const BRAND_COLOR_PRESETS = [
  { label: "Gold",    value: "linear-gradient(135deg,#e6a82c,#c48a14)" },
  { label: "Red",     value: "linear-gradient(135deg,#d03c3c,#8f2020)" },
  { label: "Green",   value: "linear-gradient(135deg,#1f9d57,#0c5a30)" },
  { label: "Amber",   value: "linear-gradient(135deg,#f2c257,#9a6b0c)" },
  { label: "Indigo",  value: "linear-gradient(135deg,#5e76ff,#1f30a6)" },
  { label: "Teal",    value: "linear-gradient(135deg,#2aaea4,#075a52)" },
  { label: "Rose",    value: "linear-gradient(135deg,#ee3380,#8b0846)" },
  { label: "Purple",  value: "linear-gradient(135deg,#8b4bff,#4811a6)" },
  { label: "Slate",   value: "linear-gradient(135deg,#6b7a92,#28303b)" },
  { label: "Midnight",value: "linear-gradient(135deg,#2e5aac,#0b1d4f)" },
];

const CURRENCIES = ["EUR","USD","GBP","CAD","AUD","BRL","MXN","INR","JPY","ARS","LBP","BOB","PYG","TRY","THB","NGN"];
const MARKETS = ["MGA","UKGC","Curacao","Kahnawake","ADM/AAMS","DGA","Ontario","Colombia","Romania"];

/* =========================================================================
   Customer.io integration — per-skin (per-brand) configuration metadata.

   Backend does the heavy lifting (queued retryable event emitter + inbound
   webhook handlers). The ONLY part that surfaces in skin settings is what
   the PO doc, Section 2, mandates:
     2.1  every outbound trigger visible with a per-skin on/off, and the
          few that carry a tunable value get a dynamic input field;
     2.2  a master on/off toggle for Customer.io per skin;
     2.3  credentials (site ID, API key, webhook signing secret) stored
          securely, hidden after entry, isolated per skin.

   CUSTOMERIO_TRIGGERS mirrors the 19 outbound triggers in Section 1 of the
   doc, split into Phase 1 (ships first) and Phase 2. The `fields` array on a
   trigger describes the dynamic, per-skin-editable inputs from the
   "Configurable per skin" column — nothing here is hard-coded into the
   backend, operators tune it without code changes. */
/* Outbound-trigger categories — product-meaningful grouping that replaces the
   internal Phase 1/Phase 2 split in the UI. Used as a per-trigger tag and as a
   filter facet in the panel. */
const CUSTOMERIO_CATEGORIES = {
  engagement:   { label:"Engagement",   color:"#3b82f6" },
  monetisation: { label:"Monetisation", color:"#16a34a" },
  lifecycle:    { label:"Lifecycle",    color:"#7c3aed" },
  reward:       { label:"Reward",       color:"#e6a82c" },
  retention:    { label:"Retention",    color:"#0d9488" },
};

const CUSTOMERIO_TRIGGERS = [
  { key:"login",                     cat:"engagement",   phase:"P1", label:"Login",                    purpose:"Engagement baseline, last-seen tracking (already firing)" },
  { key:"logout",                    cat:"engagement",   phase:"P1", label:"Logout",                   purpose:"Session-end signal" },
  { key:"made_deposit",              cat:"monetisation", phase:"P1", label:"Made deposit",             purpose:"Every successful deposit — core monetisation event" },
  { key:"first_deposit_completed",   cat:"monetisation", phase:"P1", label:"First deposit completed",  purpose:"FTD — the welcome-bonus trigger" },
  { key:"requested_withdrawal",      cat:"monetisation", phase:"P1", label:"Requested withdrawal",     purpose:"Withdrawal-pending nudge; suppresses deposit promos" },
  { key:"big_win",                   cat:"engagement",   phase:"P1", label:"Big win",                  purpose:"Single round win/bet ≥ threshold; celebration",
    fields:[ { key:"multiplier", label:"Multiplier threshold", type:"number", default:100, suffix:"×", min:1 } ] },
  { key:"bonus_claimed",             cat:"monetisation", phase:"P1", label:"Bonus claimed",            purpose:"Conversion attribution; “claimed and played” follow-on" },

  { key:"user_registered",           cat:"lifecycle",    phase:"P2", label:"User registered",          purpose:"Registration → first-deposit nurture" },
  { key:"kyc_completed",             cat:"lifecycle",    phase:"P2", label:"KYC completed",            purpose:"KYC-unlock reward + post-KYC welcome" },
  { key:"withdrawal_completed",      cat:"lifecycle",    phase:"P2", label:"Withdrawal completed ★",   purpose:"Flagship trust moment — highest leverage, don’t delay" },
  { key:"weekly_net_loss_calculated", cat:"reward",      phase:"P2", label:"Weekly net loss calculated", purpose:"Drives the weekly cashback campaign",
    fields:[ { key:"cashbackPct", label:"Cashback %", type:"number", default:10, suffix:"%", min:0, max:100 } ] },
  { key:"daily_net_loss_calculated", cat:"reward",       phase:"P2", label:"Daily net loss calculated", purpose:"Drives the daily cashback campaign (general)",
    fields:[ { key:"cashbackPct", label:"Cashback %", type:"number", default:5, suffix:"%", min:0, max:100 } ] },
  { key:"daily_net_loss_per_provider", cat:"reward",     phase:"P2", label:"Daily net loss per provider", purpose:"Drives the daily cashback campaign per provider",
    fields:[
      { key:"cashbackPct", label:"Cashback %", type:"number", default:5, suffix:"%", min:0, max:100 },
      { key:"providerList", label:"Provider list", type:"text", default:"", placeholder:"e.g. Pragmatic, Evolution, Hacksaw" },
    ] },
  { key:"cashback_unclaimed_24h_reminder", cat:"retention", phase:"P2", label:"Cashback unclaimed reminder", purpose:"Follow-up if cashback stays unclaimed",
    fields:[ { key:"reminderDelayH", label:"Reminder delay", type:"number", default:24, suffix:"h", min:1 } ] },
  { key:"birthday",                  cat:"reward",       phase:"P2", label:"Birthday",                 purpose:"No-deposit free-spins reward" },
  { key:"account_anniversary",       cat:"reward",       phase:"P2", label:"Account anniversary",     purpose:"Loyalty reward" },
  { key:"dormancy_warning_7d",       cat:"retention",    phase:"P2", label:"Dormancy warning (early)", purpose:"Early “promo expires” nudge",
    fields:[ { key:"daysThreshold", label:"Days threshold", type:"number", default:7, suffix:"d", min:1 } ] },
  { key:"dormancy_warning_30d",      cat:"retention",    phase:"P2", label:"Dormancy warning (reactivation)", purpose:"Powers the flagship 30-day reactivation campaign",
    fields:[ { key:"daysThreshold", label:"Days threshold", type:"number", default:30, suffix:"d", min:1 } ] },
  { key:"daily_login_streak",        cat:"retention",    phase:"P2", label:"Daily login streak",       purpose:"Streak milestones",
    fields:[ { key:"milestoneDays", label:"Milestone days", type:"text", default:"3 / 7 / 14 / 30", placeholder:"3 / 7 / 14 / 30" } ] },
];

/* The 7 inbound actions Customer.io calls back to grant — shown read-only in
   the panel so operators know what the integration can do on their skin.
   These are handled entirely backend-side (bonus engine / delivery), gated
   by the three guardrails: re-check eligibility, respect cooldown, log it. */
const CUSTOMERIO_INBOUND = [
  { kind:"reward",  label:"Free spins",                   desc:"Grant free spins on a game/provider, with expiry" },
  { kind:"reward",  label:"Cashback",                     desc:"Credit a cashback amount to the player’s wallet" },
  { kind:"reward",  label:"Promo code",                   desc:"Generate a unique single-use code and attach it to the player" },
  { kind:"reward",  label:"Deposit-match / reload bonus", desc:"Arm a bonus that triggers on the player’s next deposit" },
  { kind:"reward",  label:"Wallet / bonus credit",        desc:"Credit a flat bonus amount directly to the wallet" },
  { kind:"channel", label:"Pop-up / in-app message",      desc:"Display a message inside the product (native in-app)" },
  { kind:"channel", label:"External (WhatsApp / Telegram)", desc:"Forward a message out to an external messaging platform" },
];

/* Build the default Customer.io config for a skin that has never been
   configured. Master off by default (opt-in per operator), every trigger
   off, tunable fields seeded with the doc's defaults, credentials blank. */
function defaultCustomerioConfig() {
  const triggers = {};
  CUSTOMERIO_TRIGGERS.forEach(t => {
    const values = {};
    (t.fields || []).forEach(f => { values[f.key] = f.default; });
    triggers[t.key] = { enabled: false, values };
  });
  return {
    enabled: false,
    credentials: { siteId:"", apiKey:"", webhookSecret:"" },
    triggers,
  };
}

/* Merge a stored (possibly partial / older-shape) config onto the current
   defaults so newly added triggers/fields appear without losing saved
   values. */
function normalizeCustomerioConfig(stored) {
  const base = defaultCustomerioConfig();
  if (!stored || typeof stored !== "object") return base;
  return {
    enabled: !!stored.enabled,
    credentials: { ...base.credentials, ...(stored.credentials || {}) },
    triggers: Object.fromEntries(CUSTOMERIO_TRIGGERS.map(t => {
      const def = base.triggers[t.key];
      const s = (stored.triggers || {})[t.key] || {};
      return [t.key, { enabled: !!s.enabled, values: { ...def.values, ...(s.values || {}) } }];
    })),
  };
}

/* NOTIFICATION_CHANNELS — a different concept from CUSTOMERIO_TRIGGERS
   above. Triggers decide WHEN Customer.io fires an event; this decides
   WHICH channels we actually deliver through per skin. Customer.io sends
   us a notification for a given skin + channel — if the channel is off
   here we drop it silently. Plain on/off per channel; credentials for
   Push (Firebase/APNs) are configured outside the backoffice, not here.
   Only Push and In-app live here — Email/WhatsApp/Telegram/SMS stay
   entirely inside Customer.io and never reach this UI. */
const NOTIFICATION_CHANNELS = [
  { key:"push", label:"Push notifications", desc:"Browser/app notification, even when the site is closed." },
  { key:"inapp", label:"In-app notifications", desc:"Pop-up + inbox shown directly inside the skin's own site." },
];

/* Build the default notification-channels config for a skin that has
   never been configured. Every channel off by default. No master toggle
   — unlike Customer.io there's no single shared upstream service, so
   each channel switches independently. */
function defaultNotificationChannelsConfig() {
  const channels = {};
  NOTIFICATION_CHANNELS.forEach(c => { channels[c.key] = { enabled: false }; });
  return { channels };
}

/* Merge a stored (possibly partial / older-shape) config onto the current
   defaults so a newly added channel appears without losing saved values
   — same pattern as normalizeCustomerioConfig above. */
function normalizeNotificationChannelsConfig(stored) {
  const base = defaultNotificationChannelsConfig();
  if (!stored || typeof stored !== "object") return base;
  return {
    channels: Object.fromEntries(NOTIFICATION_CHANNELS.map(c => {
      const s = (stored.channels || {})[c.key] || {};
      return [c.key, { enabled: !!s.enabled }];
    })),
  };
}

/* Third-party PSP providers we can route transactions through. The
   `kind` column drives which routes a provider can sit in. */
const PROVIDERS = {
  stripe:     { name: "Stripe",       kind: "Card" },
  adyen:      { name: "Adyen",        kind: "Card" },
  checkout:   { name: "Checkout.com", kind: "Card" },
  worldpay:   { name: "Worldpay",     kind: "Card" },
  nuvei:      { name: "Nuvei",        kind: "APM" },
  trustly:    { name: "Trustly",      kind: "Bank" },
  skrillpsp:  { name: "Skrill PSP",   kind: "Wallet" },
  coinify:    { name: "Coinify",      kind: "Crypto" },
};

/* Seed routing rules. Real prod data would be persisted; this mirrors
   the shape of a v1 route record. */
/* Routes carry an optional weights array aligned with `chain`. When
   `weighted` is false the chain is strict-priority (PSP 1, then 2, then
   3 on cascade). When true, traffic splits proportionally — the chain
   order then only matters as a fallback if a weighted PSP is excluded
   for liquidity / health reasons. Weights are percentages and should
   sum to 100 (validated at save time, not enforced here). */
const ROUTES = [
  { id:"r1", brand:"GB24",     method:"Visa",       currency:"EUR", amount:"0 – 5,000",     geos:["DE","IT","ES","FR"], licenses:["MGA"],          appliesTo:{kind:"global"},                            chain:["stripe","adyen","worldpay"], weighted:false, weights:[],         triggers:["declined"],      cascadeTimeout:"30s", factors:{performance:true,cost:true,liquidity:true,health:true},  mode:"AUTO",   ranking:"manual", status:"active", lastMatchedAt: Date.now() - 2*60_000,        volume30d: 47 },
  { id:"r2", brand:"GB24",     method:"Visa",       currency:"EUR", amount:"5,000 – 25,000",geos:["DE","IT"],           licenses:["MGA"],          appliesTo:{kind:"brand", brandId:"gb24"},             chain:["adyen","checkout"],          weighted:false, weights:[],         triggers:["declined","fraud_flag"], cascadeTimeout:"20s", factors:{performance:true,cost:false,liquidity:true,health:true}, mode:"MANUAL", ranking:"hybrid", status:"active", lastMatchedAt: Date.now() - 18*60_000,       volume30d: 12 },
  { id:"r3", brand:"PlaySpin", method:"Mastercard", currency:"BOB", amount:"any",           geos:["BO"],                licenses:["Curacao"],      appliesTo:{kind:"global"},                            chain:["checkout","stripe","adyen"], weighted:true,  weights:[60,30,10], triggers:["declined"],      cascadeTimeout:"20s", factors:{performance:true,cost:true,liquidity:true,health:true},  mode:"AUTO",   ranking:"hybrid", status:"active", lastMatchedAt: Date.now() - 4*60_000,        volume30d: 21 },
  { id:"r4", brand:"GoldenSky",method:"Bank Wire",  currency:"LBP", amount:"any",           geos:["LB"],                licenses:["Curacao"],      appliesTo:{kind:"global"},                            chain:["trustly"],                   weighted:false, weights:[],         triggers:[],                cascadeTimeout:"60s", factors:{performance:true,cost:true,liquidity:true,health:true},  mode:"AUTO",   ranking:"manual", status:"active", lastMatchedAt: Date.now() - 3*3600_000,      volume30d: 8 },
  { id:"r5", brand:"any",      method:"Skrill",     currency:"any", amount:"any",           geos:[],                    licenses:[],               appliesTo:{kind:"global"},                            chain:["skrillpsp"],                 weighted:false, weights:[],         triggers:["declined"],      cascadeTimeout:"30s", factors:{performance:true,cost:true,liquidity:true,health:true},  mode:"AUTO",   ranking:"manual", status:"active", lastMatchedAt: Date.now() - 4*86400_000,     volume30d: 11 },
  { id:"r6", brand:"any",      method:"Bitcoin",    currency:"any", amount:"any",           geos:[],                    licenses:["Curacao","Kahnawake"], appliesTo:{kind:"global"},                     chain:["coinify"],                   weighted:false, weights:[],         triggers:["error"],         cascadeTimeout:"Never", factors:{performance:true,cost:false,liquidity:true,health:true}, mode:"MANUAL", ranking:"manual", status:"paused", lastMatchedAt: Date.now() - 22*86400_000, volume30d: 0 },
];
// Seed a small audit trail on each route so the History tab is not
// empty on first load. Demo-only; real entries are appended by saveRoute.
(function seedRouteHistory() {
  const SAMPLE_USERS = [
    { id:"op1", email:"sandro@iwakiri.io" },
    { id:"op2", email:"arbi@iwakiri.io"   },
    { id:"op3", email:"sara@iwakiri.io"   },
  ];
  ROUTES.forEach((r, i) => {
    const created = Date.now() - (180 - i*12) * 86400_000;
    const edited  = Date.now() - (20  - i*3 ) * 86400_000;
    r.history = [
      {
        ts: edited, user_id: SAMPLE_USERS[i % 3].id, user_email: SAMPLE_USERS[i % 3].email,
        route_id: r.id,
        change_summary: i % 2 === 0
          ? [`Cascade triggers: added "${(r.triggers && r.triggers[0]) || "declined"}"`]
          : [`Status: ${r.status} (was: paused)`],
        full_snapshot: JSON.parse(JSON.stringify(r)),
      },
      {
        ts: created, user_id: SAMPLE_USERS[(i+1) % 3].id, user_email: SAMPLE_USERS[(i+1) % 3].email,
        route_id: r.id,
        change_summary: ["Route created"],
        full_snapshot: JSON.parse(JSON.stringify(r)),
      },
    ];
  });
})();

// Expose for read-only display in other pages (e.g. Methods → PSP route tab).
window.MOCK_ROUTES = ROUTES;

const TRIGGER_OPTIONS = [
  "declined", "fraud_flag", "error", "network", "psp_down", "low_liquidity",
  "insufficient_funds",
];
// One-sentence explanation per cascade trigger — what it means and how the
// engine recognises it. Surfaced as a tooltip on each trigger chip in the
// Route wizard's Cascading step.
const TRIGGER_EXPLANATIONS = {
  declined:           "PSP returned a generic decline (no specific reason code). Engine detects: response code in the 100/200-series 'do_not_honor' / 'card_declined' / 'transaction_not_allowed' family. Common cause: issuer-side refusal that does not block a retry on a different PSP.",
  fraud_flag:         "PSP or its fraud engine flagged the transaction as suspicious (e.g. velocity, geo-mismatch, blacklist). Engine detects: response code 'fraud_suspected' / 'risk_block' or non-zero fraud_score in the webhook. Usually a STOP signal — retrying on another PSP rarely helps and may worsen risk score.",
  error:              "Unexpected PSP-side error (HTTP 5xx, malformed response, internal exception). Engine detects: response status ≥ 500, or webhook 'kind': 'error' without a player-side reason. Safe to retry — the next PSP often succeeds.",
  network:            "Connection issue between PayBO and the PSP (DNS, TLS, socket reset, connection refused). Engine detects: any transport-layer exception before a response is received. Always safe to retry on the next PSP.",
  psp_down:           "PSP is currently marked OFFLINE or DEGRADED by health monitoring. Engine detects: status flips set in Settings → Providers (manual or auto from the 5xx/timeout alarm). Skips the PSP entirely and moves to the next chain position.",
  low_liquidity:      "PSP wallet is below its configured floor — withdrawals would fail for empty wallet. Engine detects: live wallet balance < floor threshold (set per-PSP in Liquidity & thresholds). Skips withdrawals to that PSP until balance is topped up.",
  insufficient_funds: "Player's funding instrument has insufficient balance (card / bank account / wallet). Engine detects: response code 'insufficient_funds' / 'do_not_honor_no_funds'. Usually a STOP signal — retrying on a different PSP cannot magically add funds to the player's account.",
};
const CASCADE_TIMEOUTS = ["5s", "10s", "15s", "20s", "30s", "45s", "60s", "Never"];

/* Alarm metrics — every entry here MUST be computable from data the
   project actually has (Transactions, Activity log, Providers, Routes,
   Reports). No "provider_timeout" / "failed_auth" placeholders. The
   sourcePage field documents where the value comes from so an engineer
   wiring the engine knows exactly which surface to read. */
const ALARM_METRICS = [
  {
    key:"decline_rate", group:"Transactions",
    label:"Decline rate (all transactions)",
    description:"Share of transactions in window ending in Declined / Failed / Rejected. Spikes mean a provider is misbehaving, a fraud burst, or a misconfigured limit.",
    unit:"%", suggestedOp:">", suggestedThreshold:"15", suggestedWindow:"1h",
    sourcePage:"Transactions",
  },
  {
    key:"approval_rate", group:"Transactions",
    label:"Approval rate (all transactions)",
    description:"Share of transactions in window ending in Balanced or Approved. Dropping below 90% on a busy brand usually means a provider issue.",
    unit:"%", suggestedOp:"<", suggestedThreshold:"90", suggestedWindow:"1h",
    sourcePage:"Transactions · Dashboard",
  },
  {
    key:"tx_amount", group:"Transactions",
    label:"Single transaction amount",
    description:"Fires the instant a single deposit or withdrawal at or above this amount is created. Use for VIP-deck wake-ups.",
    unit:"EUR", suggestedOp:"≥", suggestedThreshold:"10000", suggestedWindow:"—",
    sourcePage:"Transactions (event-based)",
  },
  {
    key:"deposit_volume_drop", group:"Transactions",
    label:"Deposit volume drop vs prior 7d",
    description:"Compares deposits in window against the same window 7 days ago. Negative percentage = drop. Trips on broken funnels or PSP outages.",
    unit:"% delta", suggestedOp:"<", suggestedThreshold:"-30", suggestedWindow:"1h",
    sourcePage:"Dashboard · Deposits KPI",
  },
  {
    key:"withdrawal_volume_spike", group:"Transactions",
    label:"Withdrawal volume spike vs prior 7d",
    description:"Compares withdrawals in window against the same window 7 days ago. Large positive spike can mean an exploit or a VIP cash-out event.",
    unit:"% delta", suggestedOp:">", suggestedThreshold:"200", suggestedWindow:"1h",
    sourcePage:"Dashboard · Withdrawals KPI",
  },
  {
    key:"sanctions_country", group:"Transactions",
    label:"Sanctioned-country attempt",
    description:"Fires the instant a transaction is attempted from a country on the sanctions list. Always treat as critical.",
    unit:"(event)", suggestedOp:"≥", suggestedThreshold:"1", suggestedWindow:"—",
    sourcePage:"Transactions (country field)",
  },
  {
    key:"method_decline_rate", group:"Per method",
    label:"Per-method decline rate",
    description:"Decline rate scoped to a single payment method (Visa, Mastercard, Bank Wire…). Catches one rail going bad while others are healthy.",
    unit:"%", suggestedOp:">", suggestedThreshold:"25", suggestedWindow:"1h",
    sourcePage:"Payment methods · Dashboard (Approval rate by method)",
    requiresMethod:true,
  },
  {
    key:"to_confirm_age", group:"Queue",
    label:"Oldest To-Confirm age",
    description:"Age of the oldest unresolved To-Confirm transaction. Trips when the manual-review queue is being neglected.",
    unit:"minutes", suggestedOp:">", suggestedThreshold:"30", suggestedWindow:"—",
    sourcePage:"Withdrawals (To Confirm tab)",
  },
  {
    key:"to_confirm_depth", group:"Queue",
    label:"To-Confirm queue depth",
    description:"Number of transactions sitting in To-Confirm. Trips when manual approvals stack up faster than the team can clear them.",
    unit:"count", suggestedOp:">", suggestedThreshold:"20", suggestedWindow:"—",
    sourcePage:"Withdrawals",
  },
  {
    key:"psp_wallet_pct_of_ceiling", group:"Providers (PSP)",
    label:"PSP wallet near ceiling (idle capital)",
    description:"Per-PSP. Fires when wallet balance is at or above this % of the operator-set ceiling. Means money is sitting idle and should be swept to treasury.",
    unit:"% of ceiling", suggestedOp:"≥", suggestedThreshold:"80", suggestedWindow:"—",
    sourcePage:"Settings → Providers (per-PSP wallet, ceiling)",
  },
  {
    key:"psp_wallet_vs_floor", group:"Providers (PSP)",
    label:"PSP wallet below floor (top-up needed)",
    description:"Per-PSP. Fires when wallet balance drops below the operator-set floor. Top up before withdrawals start failing for empty wallet.",
    unit:"EUR or 'floor'", suggestedOp:"<", suggestedThreshold:"floor", suggestedWindow:"—",
    sourcePage:"Settings → Providers (per-PSP wallet, floor)",
  },
  {
    key:"route_paused", group:"Routes",
    label:"Route auto-paused",
    description:"Fires the instant a configured route in Routes & cascading transitions to Paused. Means the cascade is no longer running for that brand × method.",
    unit:"(event)", suggestedOp:"=", suggestedThreshold:"paused", suggestedWindow:"—",
    sourcePage:"Settings → Routes & cascading",
  },
  {
    key:"cascade_exhausted", group:"Routes",
    label:"Cascade exhausted (every PSP in chain failed)",
    description:"Fires when a transaction walks every PSP in its route chain and still fails. Means the route's fallbacks are not protecting the player.",
    unit:"count", suggestedOp:"≥", suggestedThreshold:"1", suggestedWindow:"15m",
    sourcePage:"Settings → Routes & cascading",
  },
  {
    key:"operator_rejection_rate", group:"Operators",
    label:"Operator rejection rate",
    description:"Per-operator. % of decisions made by one operator that ended in Reject. A sudden swing usually flags either fraud activity or an over-cautious agent.",
    unit:"%", suggestedOp:">", suggestedThreshold:"40", suggestedWindow:"24h",
    sourcePage:"Activity log",
  },
  {
    key:"operator_action_burst", group:"Operators",
    label:"Operator action burst",
    description:"Number of decisions made by a single operator in window. Catches both over-zealous approvers and accounts whose credentials may be compromised.",
    unit:"count", suggestedOp:">", suggestedThreshold:"50", suggestedWindow:"1h",
    sourcePage:"Activity log",
  },

  /* ===== Volume & flow metrics — fire on rolling-window totals so the
     operator can wake up before something becomes a crisis. ===== */
  {
    key:"total_deposits_window", group:"Volume & flow",
    label:"Total deposit volume in window",
    description:"Sum of successful deposit amounts in the rolling window (€). Fires when the total crosses the threshold — useful for VIP traffic alerts, weekend cash-in surges, and fraud rings cycling cards.",
    unit:"EUR", suggestedOp:">", suggestedThreshold:"100000", suggestedWindow:"1h",
    sourcePage:"Transactions (sum of balanced deposits)",
  },
  {
    key:"total_withdrawals_window", group:"Volume & flow",
    label:"Total withdrawal volume in window",
    description:"Sum of successful withdrawal amounts in the rolling window (€). Fires when the total crosses the threshold — early signal of liquidity drain or coordinated cash-out.",
    unit:"EUR", suggestedOp:">", suggestedThreshold:"100000", suggestedWindow:"1h",
    sourcePage:"Transactions (sum of balanced withdrawals)",
  },
  {
    key:"net_position_window", group:"Volume & flow",
    label:"Net position in window (deposits − withdrawals)",
    description:"Net cash flow = total deposits − total withdrawals in the rolling window (€). Use 'less than' with a negative threshold (e.g. < -50000) to catch outflow spikes; 'greater than' with positive to catch suspicious inflow.",
    unit:"EUR", suggestedOp:"<", suggestedThreshold:"-50000", suggestedWindow:"1h",
    sourcePage:"Transactions (deposits − withdrawals)",
  },
  {
    key:"count_deposits_window", group:"Volume & flow",
    label:"Count of deposits in window",
    description:"Number of successful deposits in the rolling window. Fires when more than N players deposit in the period — catches bot rings, promo abuse, and traffic-spike events.",
    unit:"count", suggestedOp:">", suggestedThreshold:"500", suggestedWindow:"1h",
    sourcePage:"Transactions",
  },
  {
    key:"count_withdrawals_window", group:"Volume & flow",
    label:"Count of withdrawals in window",
    description:"Number of successful withdrawals in the rolling window. Sudden jumps are a leading indicator of liquidity stress.",
    unit:"count", suggestedOp:">", suggestedThreshold:"200", suggestedWindow:"1h",
    sourcePage:"Transactions",
  },

  /* ===== Per-status transaction alarms — count of transactions of a
     specific outcome in the rolling window. ===== */
  {
    key:"count_balanced_window", group:"Transaction status",
    label:"Count of balanced transactions in window",
    description:"Number of transactions reaching the BALANCED final state in the rolling window. Use with greater-than for traffic spikes, or less-than to detect dead funnels.",
    unit:"count", suggestedOp:">", suggestedThreshold:"500", suggestedWindow:"1h",
    sourcePage:"Transactions (status = balanced)",
  },
  {
    key:"count_failed_window", group:"Transaction status",
    label:"Count of failed transactions in window",
    description:"Number of transactions ending in REJECTED / FAILED / DECLINED in the rolling window. Trips on PSP outages, mis-priced limits, fraud bursts.",
    unit:"count", suggestedOp:">", suggestedThreshold:"50", suggestedWindow:"15m",
    sourcePage:"Transactions (status = failed/rejected)",
  },
  {
    key:"count_to_confirm_window", group:"Transaction status",
    label:"Count of new To-Confirm transactions in window",
    description:"Number of transactions entering the TO-CONFIRM queue in the rolling window. Spikes here usually precede an SLA breach on manual review.",
    unit:"count", suggestedOp:">", suggestedThreshold:"30", suggestedWindow:"15m",
    sourcePage:"Withdrawals · To Confirm",
  },
  {
    key:"tx_amount_above", group:"Transaction status",
    label:"Any transaction with amount above threshold",
    description:"Fires the instant a single transaction of any status is created with an amount at or above the threshold. Use to wake VIP desks or fraud teams on outsized stakes.",
    unit:"EUR", suggestedOp:">", suggestedThreshold:"25000", suggestedWindow:"—",
    sourcePage:"Transactions (event-based)",
  },
];
const ALARM_METRIC_BY_KEY = Object.fromEntries(ALARM_METRICS.map(m => [m.key, m]));
const ALARM_METRIC_GROUPS = (() => {
  const out = {};
  for (const m of ALARM_METRICS) { (out[m.group] = out[m.group] || []).push(m); }
  return out;
})();

const ALARM_OPS = [">", "<", "≥", "≤", "="];
const ALARM_WINDOWS = ["—", "5m", "15m", "30m", "1h", "6h", "24h"];
const ALARM_SEVERITIES = ["critical", "high", "medium", "low"];

/* Seed alarm rules. Severity drives badge color; channels drive the
   notification fan-out at fire-time. `destinations` lists the specific
   endpoints (rows from NOTIF_DESTINATIONS_DEFAULTS) the alarm posts to;
   the channels field is now derived from those destinations and is kept
   in sync by the editor. */
const ALARMS = [
  { id:"a1", name:"Decline rate spike",                  metric:"decline_rate",              op:">", threshold:"15",    window:"1h",  severity:"critical", destinations:["d1","d4"],        status:"on"  },
  { id:"a2", name:"Visa decline spike",                  metric:"method_decline_rate",       op:">", threshold:"25",    window:"1h",  severity:"high",     destinations:["d4"],             status:"on",  scopeMethod:"visa" },
  { id:"a3", name:"Deposits dropped vs last week",       metric:"deposit_volume_drop",       op:"<", threshold:"-30",   window:"1h",  severity:"medium",   destinations:["d1"],             status:"on"  },
  { id:"a4", name:"High-value transaction wake",         metric:"tx_amount",                 op:"≥", threshold:"10000", window:"—",   severity:"high",     destinations:["d2","d6","d10"],  status:"on"  },
  { id:"a5", name:"To-Confirm queue going stale",        metric:"to_confirm_age",            op:">", threshold:"30",    window:"—",   severity:"medium",   destinations:["d1"],             status:"off" },
  { id:"a6", name:"Wallet near ceiling (idle capital)",  metric:"psp_wallet_pct_of_ceiling", op:"≥", threshold:"80",    window:"—",   severity:"medium",   destinations:["d3","d4"],        status:"on"  },
  { id:"a7", name:"Wallet below floor (top-up)",         metric:"psp_wallet_vs_floor",       op:"<", threshold:"floor", window:"—",   severity:"high",     destinations:["d3","d6","d7"],   status:"on"  },
  { id:"a8", name:"Sanctioned-country attempt",          metric:"sanctions_country",         op:"≥", threshold:"1",     window:"—",   severity:"critical", destinations:["d2","d4","d9"],   status:"on"  },
];

const SEVERITY_CHIP = {
  critical: { bg: "var(--err-50, #fee2e2)",  fg: "var(--err-700, #991b1b)",  border: "var(--err-200, #fecaca)" },
  high:     { bg: "#ffedd5",                 fg: "#9a3412",                  border: "#fed7aa" },
  medium:   { bg: "var(--warn-50, #fef3c7)", fg: "#92400e",                  border: "#fcd34d" },
  low:      { bg: "var(--n-75)",             fg: "var(--text-secondary)",    border: "var(--border-default)" },
};

/* PSP configuration profiles — the master directory ("anagrafica") that
   Routes & cascading both read from. Liquidity is tracked because a PSP
   with an empty wallet cannot pay out, even if every other check passes.

   Liquidity model:
   - walletEur     · real-time balance held at the PSP
   - ceilingEur    · operator-set maximum we want sitting at this PSP.
                     Anything above is idle capital. Alarm at 80%.
   - floorEur      · operator-set minimum needed to honour withdrawals.
                     Drop below → top-up alarm before requests are rejected.
   - minWithdrawalEur · the PSP's own rule for the smallest amount we can
                        pull back to our treasury wallet.
   - settlement    · when funds settle (T+0 same day, T+1 next day, …).
   - payout        · headline payout capacity.

   Licenses include the operations each license authorises, so the
   operator can see "this PSP processes withdrawals under MGA but only
   deposits under Curaçao". */
const PSP_PROFILES = [
  { id:"stripe",   name:"Stripe",       kind:"Card",   status:"active",
    fees:{ dep:"1.4% + €0.25", wd:"1.0%",      settle:"€0.50 / payout" },
    limits:{ min:"€10", max:"€50,000",  daily:"€2.5M", monthly:"€60M" },
    liquidity:{ walletEur:1_240_000, ceilingEur:2_000_000, floorEur:300_000,  minWithdrawalEur:10_000, settlement:"T+1", payout:"€500K / day", sweepPct:80, floorAlert:true },
    geos:["DE","IT","FR","ES","UK","NL","+EU"],
    licenses:[{ name:"MGA", ops:["dep","wd","refund"] },{ name:"UKGC", ops:["dep","wd"] }],
    methods:["visa","mastercard"],
    health:{ status:"online",   successRate:98.4, avgLatencyMs:320, timeoutRate:0.3, lastCheck:"1m ago" },
    tech:{ endpoint:"https://api.stripe.com/v1/charges", timeoutMs:8000, webhook:"/webhooks/stripe" },
    notifChannels:["slack","email"],
    methodOverrides:[] },
  { id:"adyen",    name:"Adyen",        kind:"Card",   status:"active",
    fees:{ dep:"1.6% + €0.20", wd:"1.2%",      settle:"€0.40 / payout" },
    limits:{ min:"€5",  max:"€100,000", daily:"€5M",   monthly:"€150M" },
    liquidity:{ walletEur:2_890_000, ceilingEur:3_500_000, floorEur:600_000,  minWithdrawalEur:25_000, settlement:"T+1", payout:"€1.2M / day", sweepPct:80, floorAlert:true },
    geos:["UK","DE","NL","BE","ES","IT","+EU"],
    licenses:[{ name:"MGA", ops:["dep","wd","refund"] },{ name:"UKGC", ops:["dep","wd"] },{ name:"Curacao", ops:["dep","wd"] }],
    methods:["visa","mastercard"],
    health:{ status:"online",   successRate:98.9, avgLatencyMs:280, timeoutRate:0.2, lastCheck:"30s ago" },
    tech:{ endpoint:"https://checkout-test.adyen.com/v70/payments", timeoutMs:7000, webhook:"/webhooks/adyen" },
    notifChannels:["slack","email"],
    methodOverrides:[
      { method:"visa",       floorEur:200_000, ceilingEur:1_800_000, sweepPct:75 },
    ] },
  { id:"checkout", name:"Checkout.com", kind:"Card",   status:"active",
    fees:{ dep:"1.5%",         wd:"1.1%",      settle:"€0.35 / payout" },
    limits:{ min:"€10", max:"€75,000",  daily:"€3M",   monthly:"€90M" },
    liquidity:{ walletEur:620_000,   ceilingEur:1_500_000, floorEur:200_000,  minWithdrawalEur:15_000, settlement:"T+2", payout:"€700K / day", sweepPct:80, floorAlert:true },
    geos:["UK","FR","DE","SG","HK","+APAC"],
    licenses:[{ name:"UKGC", ops:["dep","wd"] },{ name:"Curacao", ops:["dep"] }],
    methods:["visa","mastercard"],
    health:{ status:"degraded",  successRate:94.1, avgLatencyMs:580, timeoutRate:1.8, lastCheck:"2m ago" },
    tech:{ endpoint:"https://api.checkout.com/payments", timeoutMs:9000, webhook:"/webhooks/checkout" },
    notifChannels:["slack"],
    methodOverrides:[] },
  { id:"worldpay", name:"Worldpay",     kind:"Card",   status:"paused",
    fees:{ dep:"1.7%",         wd:"1.4%",      settle:"€0.60 / payout" },
    limits:{ min:"€10", max:"€60,000",  daily:"€2M",   monthly:"€60M" },
    liquidity:{ walletEur:90_000,    ceilingEur:1_000_000, floorEur:150_000,  minWithdrawalEur:20_000, settlement:"T+2", payout:"€400K / day", sweepPct:80, floorAlert:true },
    geos:["UK","US","CA"],
    licenses:[{ name:"UKGC", ops:["dep","wd"] }],
    methods:["visa","mastercard"],
    health:{ status:"down",     successRate:0,    avgLatencyMs:0,   timeoutRate:100, lastCheck:"4m ago" },
    tech:{ endpoint:"https://api.worldpay.com/v6/payments", timeoutMs:10000, webhook:"/webhooks/worldpay" },
    notifChannels:["slack","email","in-app"],
    methodOverrides:[] },
  { id:"trustly",  name:"Trustly",      kind:"Bank",   status:"active",
    fees:{ dep:"€0.80 flat",   wd:"€1.20 flat",settle:"€0.20 / payout" },
    limits:{ min:"€20", max:"€200,000", daily:"€10M",  monthly:"€300M" },
    liquidity:{ walletEur:4_100_000, ceilingEur:5_000_000, floorEur:1_000_000,minWithdrawalEur:50_000, settlement:"T+0", payout:"€2M / day", sweepPct:85, floorAlert:true },
    geos:["SE","DE","NL","FI","NO","+EU"],
    licenses:[{ name:"MGA", ops:["dep","wd","refund"] },{ name:"DGA", ops:["dep","wd"] }],
    methods:["bankwire"],
    health:{ status:"online",   successRate:99.2, avgLatencyMs:410, timeoutRate:0.1, lastCheck:"45s ago" },
    tech:{ endpoint:"https://api.trustly.com/payments", timeoutMs:12000, webhook:"/webhooks/trustly" },
    notifChannels:["slack","email"],
    methodOverrides:[] },
  { id:"skrillpsp",name:"Skrill PSP",   kind:"Wallet", status:"active",
    fees:{ dep:"1.9%",         wd:"1.5%",      settle:"€0.40 / payout" },
    limits:{ min:"€5",  max:"€30,000",  daily:"€1M",   monthly:"€25M" },
    liquidity:{ walletEur:340_000,   ceilingEur:800_000,   floorEur:120_000,  minWithdrawalEur:5_000,  settlement:"T+1", payout:"€250K / day", sweepPct:80, floorAlert:true },
    geos:["UK","IT","DE","ES","BR"],
    licenses:[{ name:"MGA", ops:["dep","wd"] },{ name:"Curacao", ops:["dep","wd"] }],
    methods:["skrill","neteller"],
    health:{ status:"online",   successRate:97.6, avgLatencyMs:360, timeoutRate:0.4, lastCheck:"1m ago" },
    tech:{ endpoint:"https://www.skrill.com/app/api", timeoutMs:8000, webhook:"/webhooks/skrill" },
    notifChannels:["slack"],
    methodOverrides:[] },
  { id:"coinify",  name:"Coinify",      kind:"Crypto", status:"active",
    fees:{ dep:"0.8%",         wd:"0.6%",      settle:"network fee" },
    limits:{ min:"€20", max:"€500,000", daily:"€10M",  monthly:"€200M" },
    liquidity:{ walletEur:880_000,   ceilingEur:2_000_000, floorEur:250_000,  minWithdrawalEur:10_000, settlement:"T+0", payout:"unlimited", sweepPct:90, floorAlert:true },
    geos:["any"],
    licenses:[{ name:"Curacao", ops:["dep","wd"] },{ name:"Kahnawake", ops:["dep","wd"] },{ name:"Anjouan", ops:["dep","wd"] }],
    methods:["bitcoin","ethereum"],
    health:{ status:"online",   successRate:99.5, avgLatencyMs:240, timeoutRate:0.05, lastCheck:"15s ago" },
    tech:{ endpoint:"https://api.coinify.com/v3/trades", timeoutMs:6000, webhook:"/webhooks/coinify" },
    notifChannels:["slack","email"],
    methodOverrides:[] },
  { id:"nuvei",    name:"Nuvei",        kind:"APM",    status:"active",
    fees:{ dep:"1.5%",         wd:"1.3%",      settle:"€0.45 / payout" },
    limits:{ min:"€10", max:"€80,000",  daily:"€4M",   monthly:"€100M" },
    liquidity:{ walletEur:1_560_000, ceilingEur:2_500_000, floorEur:400_000,  minWithdrawalEur:20_000, settlement:"T+1", payout:"€900K / day", sweepPct:80, floorAlert:true },
    geos:["BR","MX","AR","CL","CO","+LATAM"],
    licenses:[{ name:"Curacao", ops:["dep","wd"] },{ name:"Anjouan", ops:["dep","wd"] }],
    methods:["visa","mastercard","paysafe"],
    health:{ status:"online",   successRate:96.8, avgLatencyMs:520, timeoutRate:1.1, lastCheck:"1m ago" },
    tech:{ endpoint:"https://secure.safecharge.com/ppp", timeoutMs:9000, webhook:"/webhooks/nuvei" },
    notifChannels:["slack","email"],
    methodOverrides:[] },
];
// Synth a 7-day wallet history per PSP so the sparkline has data. The
// last point is the live wallet; earlier points drift around it with a
// stable seed so the curve doesn't change on every render.
for (const p of PSP_PROFILES) {
  const w = p.liquidity.walletEur;
  const seed = p.id.split("").reduce((acc, c) => (acc * 31 + c.charCodeAt(0)) | 0, 7);
  const rand = (i) => {
    const x = Math.sin(seed + i * 17) * 10000;
    return x - Math.floor(x); // 0..1
  };
  const history = [];
  for (let i = 6; i >= 0; i--) {
    const drift = (rand(i) - 0.5) * 0.35; // ±17.5%
    history.push({
      ts: Date.now() - i * 86400_000,
      wallet: Math.max(0, Math.round(w * (1 + drift))),
    });
  }
  history[history.length - 1].wallet = w; // anchor today to live value
  p.liquidity.history7d = history;
}

// Expose for read-only display in other pages (Dashboard wallet liquidity widget).
window.MOCK_PSP_PROFILES = PSP_PROFILES;

const OPS_LABEL = { dep: "Deposits", wd: "Withdrawals", refund: "Refunds" };
const ALL_OPS = ["dep", "wd", "refund"];
const ALL_LICENSES = ["MGA", "UKGC", "Curacao", "Kahnawake", "Anjouan", "DGA", "ADM/AAMS"];
const ALL_METHODS_BY_ID = {
  visa:"Visa", mastercard:"Mastercard", bankwire:"Bank Wire",
  bitcoin:"Bitcoin", ethereum:"Ethereum",
  skrill:"Skrill", neteller:"Neteller", paysafe:"Paysafecard",
};
const ALL_METHOD_IDS = Object.keys(ALL_METHODS_BY_ID);
/* Channels — every supported way the alarm engine can fan out a fire.
   `targetLabel` and `targetHint` drive the destination editor so the
   operator knows what they're typing in (an email vs a webhook URL vs a
   Telegram handle). */
const NOTIF_CHANNELS = [
  { id:"email",    label:"Email",      icon:"mail",      color:"var(--p-600)",             targetLabel:"Email address",  targetHint:"ops@iwakiri.com" },
  { id:"slack",    label:"Slack",      icon:"message",   color:"#611f69",                  targetLabel:"Channel",        targetHint:"#payments-alerts" },
  { id:"telegram", label:"Telegram",   icon:"send",      color:"#229ED9",                  targetLabel:"Chat or handle", targetHint:"@iwakiri_alerts" },
  { id:"whatsapp", label:"WhatsApp",   icon:"phone",     color:"#25D366",                  targetLabel:"Phone number",   targetHint:"+39 348 5512004" },
  { id:"sms",      label:"SMS",        icon:"phone",     color:"var(--warn-600)",          targetLabel:"Phone number",   targetHint:"+39 348 5512004" },
  { id:"webhook",  label:"Webhook",    icon:"globe",     color:"var(--purple-500)",        targetLabel:"Webhook URL",    targetHint:"https://hooks.example.com/..." },
  { id:"in_app",   label:"In-app",     icon:"bell",      color:"var(--text-secondary)",    targetLabel:"Audience",       targetHint:"All operators" },
];
const NOTIF_CHANNEL_BY_ID = Object.fromEntries(NOTIF_CHANNELS.map(c => [c.id, c]));

/* Seed destinations the operator can configure once and reuse across
   alarms. Multi-select lives in the alarm editor; CRUD on this list
   lives in Settings → Alarms (Destinations sub-section). */
const NOTIF_DESTINATIONS_DEFAULTS = [
  { id:"d1",  channel:"email",    label:"Ops on-call",        target:"ops@iwakiri.com",                       enabled:true  },
  { id:"d2",  channel:"email",    label:"Compliance team",    target:"compliance@iwakiri.com",                enabled:true  },
  { id:"d3",  channel:"email",    label:"Treasury",           target:"treasury@iwakiri.com",                  enabled:true  },
  { id:"d4",  channel:"slack",    label:"#payments-alerts",   target:"#payments-alerts",                      enabled:true  },
  { id:"d5",  channel:"slack",    label:"#vip-desk",          target:"#vip-desk",                             enabled:true  },
  { id:"d6",  channel:"telegram", label:"Iwakiri Ops bot",    target:"@iwakiri_alerts",                       enabled:true  },
  { id:"d7",  channel:"whatsapp", label:"Sara · Ops manager", target:"+39 348 5512004",                       enabled:true  },
  { id:"d8",  channel:"sms",      label:"On-call rotation",   target:"+39 348 0000000",                       enabled:false },
  { id:"d9",  channel:"webhook",  label:"PagerDuty",          target:"https://events.pagerduty.com/v2/enqueue",enabled:true  },
  { id:"d10", channel:"in_app",   label:"All operators",      target:"*",                                     enabled:true  },
];

const HEALTH_TONE = {
  online:   { dot:"var(--ok-500, #10b981)",  bg:"var(--ok-50, #d1fae5)",  fg:"var(--ok-700, #065f46)",  label:"Online"   },
  degraded: { dot:"var(--warn-500, #f59e0b)",bg:"var(--warn-50, #fef3c7)",fg:"#92400e",                 label:"Degraded" },
  down:     { dot:"var(--err-500, #ef4444)", bg:"var(--err-50, #fee2e2)", fg:"var(--err-700, #991b1b)", label:"Down"     },
};

/* Cascade error → action matrix. Encodes "what do we do when this
   provider fails?". Spec-driven: timeouts and PSP-side failures
   retry; player-side failures stop. */
const ERROR_MATRIX = [
  { error:"Timeout / network",   kind:"retry", label:"Retry next provider",          why:"PSP didn't respond in time. Probably them, not the player." },
  { error:"PSP down / 5xx",      kind:"retry", label:"Retry next provider",          why:"Provider is unhealthy. Try another." },
  { error:"PSP limit reached",   kind:"retry", label:"Retry next provider",          why:"This PSP has hit its daily / monthly cap. Skip it." },
  { error:"Low liquidity",       kind:"retry", label:"Skip and retry next provider", why:"Wallet balance below min transfer. Cannot pay out." },
  { error:"Insufficient funds",  kind:"stop",  label:"Stop · player issue",          why:"The player's account / card cannot fund. Retrying won't help." },
  { error:"Fraud decline",       kind:"stop",  label:"Stop · flag for review",       why:"Risk hit. Retrying with another PSP would hide the signal." },
];

const formatEur = (n) => "€" + Math.round(n).toLocaleString();

/* Tone for the wallet bar. Considers both bounds:
   - below floor → red ("top-up needed")
   - at/over the sweep % of ceiling → amber ("idle capital, plan a sweep")
   - between → green
   The sweep % is set per-PSP (default 80, configurable in the editor). */
const liquidityHealth = (liq) => {
  const sweepFraction = (liq.sweepPct || 80) / 100;
  if (liq.walletEur < liq.floorEur)                          return { color:"var(--err-500, #ef4444)",  label:"Below floor — top-up needed",                       tone:"err"  };
  if (liq.walletEur >= liq.ceilingEur * sweepFraction)       return { color:"var(--warn-500, #f59e0b)", label:`≥ ${liq.sweepPct || 80}% of ceiling — sweep funds`, tone:"warn" };
  return                                                            { color:"var(--ok-500, #10b981)",   label:"Healthy",                                            tone:"ok"   };
};

/* SecretField — credential input that hides its value after entry (doc 2.3:
   "hidden after entry, never visible in plain text"). Once a value is stored
   and the field is not being revealed, it renders a masked summary with a
   "Change" affordance rather than the raw secret. */
const SecretField = ({ label, value, placeholder, onChange }) => {
  const [reveal, setReveal] = useState(false);
  const [editing, setEditing] = useState(!value);
  const has = !!value;
  const masked = has ? `•••• •••• ${String(value).slice(-4)}` : "";
  return (
    <div>
      <label style={{fontSize:11, fontWeight:600, color:"var(--text-secondary)", textTransform:"uppercase", letterSpacing:".05em"}}>{label}</label>
      {(!has || editing) ? (
        <div style={{display:"flex", gap:6, marginTop:4}}>
          <input type={reveal ? "text" : "password"} value={value || ""} placeholder={placeholder} autoComplete="off"
                 onChange={e=>onChange(e.target.value)}
                 style={{flex:1, padding:"9px 10px", borderRadius:8, border:"1px solid var(--border-default)", fontSize:13, fontFamily:"var(--font-mono)"}}/>
          <button type="button" className="btn btn--secondary btn--sm" onClick={()=>setReveal(r=>!r)} title={reveal ? "Hide" : "Show"} style={{flexShrink:0, opacity: reveal ? 1 : 0.6}}>
            <Icon name="eye" size={13}/>
          </button>
        </div>
      ) : (
        <div style={{display:"flex", alignItems:"center", gap:8, marginTop:4, padding:"9px 10px", borderRadius:8, border:"1px solid var(--border-subtle)", background:"var(--n-25)"}}>
          <Icon name="lock" size={12} style={{color:"var(--text-tertiary)"}}/>
          <span style={{flex:1, fontSize:12.5, fontFamily:"var(--font-mono)", color:"var(--text-secondary)"}}>{masked}</span>
          <button type="button" className="btn btn--ghost btn--sm" onClick={()=>{ setEditing(true); setReveal(false); }}>Change</button>
        </div>
      )}
    </div>
  );
};

/* CustomerioPanel — the per-skin Customer.io configuration surface.
   Master toggle (2.2) · secure credentials (2.3) · every outbound trigger
   visible with its own on/off and dynamic tunable fields (2.1).

   DEFINED here, RENDERED in CMS → Skins → Customer.io (HskCustomerIoTab),
   against skin_customerio_configs via upsert_customerio_config (070). The owner
   moved the surface there on 2026-08-16 — and the move exposed that this panel
   was rendered by NOTHING on this page: it was defined, documented, pointed at
   by the skins tab, and never mounted. The definition stays in this file (every
   top-level name is a window global, so the tab renders it directly), which is
   how the surface moves without a second copy that would drift. */
/* Section heading used inside the integration panel. Compact, uppercase. */
const CioHeading = ({ icon, children, note }) => (
  <div style={{display:"flex", alignItems:"center", gap:6, margin:"2px 0 8px"}}>
    {icon && <Icon name={icon} size={11} style={{color:"var(--text-tertiary)"}}/>}
    <span style={{fontSize:10.5, fontWeight:700, letterSpacing:".06em", textTransform:"uppercase", color:"var(--text-secondary)"}}>{children}</span>
    {note && <span style={{fontSize:10.5, color:"var(--text-tertiary)", fontWeight:500, textTransform:"none", letterSpacing:0}}>· {note}</span>}
  </div>
);

/* Read-only inbound webhook URL with copy-to-clipboard. Per skin — this is
   the endpoint the operator pastes into Customer.io; it pairs with the
   signing secret. */
const CioWebhookField = ({ url }) => {
  const [copied, setCopied] = useState(false);
  const copy = () => {
    try { navigator.clipboard && navigator.clipboard.writeText(url); } catch (_e) {}
    setCopied(true); setTimeout(() => setCopied(false), 1500);
  };
  return (
    <div>
      <label style={{fontSize:11, fontWeight:600, color:"var(--text-secondary)", textTransform:"uppercase", letterSpacing:".05em"}}>Inbound webhook URL</label>
      <div style={{display:"flex", gap:6, marginTop:4}}>
        <input readOnly value={url} onFocus={e=>e.target.select()}
               style={{flex:1, minWidth:0, padding:"9px 10px", borderRadius:8, border:"1px solid var(--border-subtle)", background:"var(--n-25)", fontSize:12.5, fontFamily:"var(--font-mono)", color:"var(--text-secondary)"}}/>
        <button type="button" className="btn btn--secondary btn--sm" onClick={copy} title="Copy" style={{flexShrink:0, color: copied ? "var(--ok-600, #15803d)" : undefined}}>
          <Icon name={copied ? "check" : "copy"} size={13}/>
        </button>
      </div>
      <div style={{fontSize:10.5, color:"var(--text-tertiary)", marginTop:4}}>Paste this into Customer.io as the webhook endpoint · pairs with the signing secret</div>
    </div>
  );
};

const CustomerioPanel = ({ cfg, setCfg, webhookId }) => {
  const on = cfg.enabled;
  const [showInbound, setShowInbound] = useState(false);
  const [q, setQ] = useState("");
  const [catFilter, setCatFilter] = useState("all");
  const [enabledOnly, setEnabledOnly] = useState(false);
  const setTrigger = (key, patch) => setCfg(c => ({ ...c, triggers: { ...c.triggers, [key]: { ...c.triggers[key], ...patch } } }));
  const setTriggerValue = (key, fieldKey, val) => setCfg(c => ({ ...c, triggers: { ...c.triggers, [key]: { ...c.triggers[key], values: { ...c.triggers[key].values, [fieldKey]: val } } } }));
  const setCred = (k, v) => setCfg(c => ({ ...c, credentials: { ...c.credentials, [k]: v } }));
  const enabledCount = CUSTOMERIO_TRIGGERS.filter(t => cfg.triggers[t.key]?.enabled).length;

  const creds = cfg.credentials || {};
  const credsMissing = !creds.siteId || !creds.apiKey || !creds.webhookSecret;
  const webhookUrl = `https://api.iwakiri.io/webhooks/customerio/${webhookId || "skin"}`;

  const filtered = CUSTOMERIO_TRIGGERS.filter(t => {
    if (catFilter !== "all" && t.cat !== catFilter) return false;
    if (enabledOnly && !cfg.triggers[t.key]?.enabled) return false;
    if (q.trim()) {
      const s = q.trim().toLowerCase();
      if (!`${t.label} ${t.purpose}`.toLowerCase().includes(s)) return false;
    }
    return true;
  });
  const bulkSet = (val) => setCfg(c => ({ ...c, triggers: { ...c.triggers, ...Object.fromEntries(filtered.map(t => [t.key, { ...c.triggers[t.key], enabled: val }])) } }));

  // One compact trigger cell — category accent + tag, name + toggle, purpose,
  // and tunable fields inline only when the trigger is on.
  const triggerCell = (t) => {
    const st = cfg.triggers[t.key];
    const cat = CUSTOMERIO_CATEGORIES[t.cat] || { label: t.cat, color: "var(--n-400)" };
    return (
      <div key={t.key} style={{border:"1px solid var(--border-subtle)", borderLeft:`3px solid ${cat.color}`, borderRadius:10, padding:"9px 11px", background: st.enabled ? "color-mix(in oklab, var(--p-500) 4%, white)" : "#fff"}}>
        <div style={{display:"flex", alignItems:"center", gap:8}}>
          <div style={{flex:1, minWidth:0}}>
            <div style={{fontSize:12, fontWeight:650, fontFamily:"var(--font-mono)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}>{t.label}</div>
            <div style={{display:"flex", alignItems:"center", gap:6, marginTop:2}}>
              <span style={{flexShrink:0, fontSize:8.5, fontWeight:700, padding:"1px 6px", borderRadius:999, textTransform:"uppercase", letterSpacing:".04em", color:cat.color, background:`color-mix(in oklab, ${cat.color} 13%, white)`}}>{cat.label}</span>
              <span title={t.purpose} style={{fontSize:10, color:"var(--text-tertiary)", whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}>{t.purpose}</span>
            </div>
          </div>
          <Toggle size="sm" value={!!st.enabled} onChange={v=>setTrigger(t.key, { enabled:v })} onLabel="" offLabel=""/>
        </div>
        {t.fields && st.enabled && (
          <div style={{display:"flex", flexWrap:"wrap", gap:8, marginTop:9, paddingTop:9, borderTop:"1px dashed var(--border-subtle)"}}>
            {t.fields.map(f => (
              <div key={f.key} style={{display:"flex", flexDirection:"column", gap:3, flex: f.type==="text" ? "1 1 100%" : "0 0 auto"}}>
                <label style={{fontSize:9.5, fontWeight:600, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".04em"}}>{f.label}</label>
                <div style={{display:"flex", alignItems:"center", gap:5}}>
                  <input
                    type={f.type === "number" ? "number" : "text"}
                    value={st.values[f.key] ?? ""}
                    min={f.min} max={f.max} placeholder={f.placeholder}
                    onChange={e=>setTriggerValue(t.key, f.key, f.type==="number" ? (e.target.value === "" ? "" : Number(e.target.value)) : e.target.value)}
                    style={{width: f.type==="text" ? "100%" : 76, padding:"6px 8px", borderRadius:6, border:"1px solid var(--border-default)", fontSize:12}}/>
                  {f.suffix && <span style={{fontSize:11.5, color:"var(--text-tertiary)", fontWeight:600}}>{f.suffix}</span>}
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    );
  };

  const filterChip = (id, label, color) => {
    const active = catFilter === id;
    return (
      <button key={id} type="button" onClick={()=>setCatFilter(id)}
              style={{display:"inline-flex", alignItems:"center", gap:5, padding:"4px 10px", borderRadius:999, cursor:"pointer", fontFamily:"inherit",
                      fontSize:11, fontWeight:600,
                      border: active ? `1px solid ${color || "var(--p-400)"}` : "1px solid var(--border-default)",
                      background: active ? `color-mix(in oklab, ${color || "var(--p-500)"} 12%, white)` : "#fff",
                      color: active ? (color || "var(--p-700)") : "var(--text-secondary)"}}>
        {color && <span style={{width:7, height:7, borderRadius:999, background:color}}/>}
        {label}
      </button>
    );
  };

  return (
    <div style={{display:"flex", flexDirection:"column", gap:14}}>
      {/* Master card — the only thing shown when the integration is off. */}
      <div style={{border:"1px solid var(--border-subtle)", borderRadius:12, padding:"13px 15px", display:"flex", alignItems:"center", gap:12,
                   background: on ? "color-mix(in oklab, var(--ok-500) 5%, white)" : "var(--n-25)"}}>
        <div style={{width:34, height:34, borderRadius:9, background:"#7c4dff", color:"#fff", display:"grid", placeItems:"center", flexShrink:0, fontWeight:800, fontSize:12.5}}>c.io</div>
        <div style={{flex:1, minWidth:0}}>
          <div style={{display:"flex", alignItems:"center", gap:8}}>
            <span style={{fontWeight:700, fontSize:13.5}}>Customer.io</span>
            {on && <span style={{fontSize:10, fontWeight:700, padding:"1px 7px", borderRadius:999, background:"color-mix(in oklab, var(--ok-500) 14%, white)", color:"var(--ok-600, #15803d)"}}>{enabledCount}/{CUSTOMERIO_TRIGGERS.length} triggers</span>}
          </div>
          <div style={{fontSize:11.5, color:"var(--text-tertiary)", marginTop:2}}>
            {on ? "Active for this skin · messaging automation & lifecycle campaigns" : "Off — no events emitted, no inbound webhooks processed. Turn on to configure."}
          </div>
        </div>
        <Toggle value={on} onChange={v=>setCfg(c=>({ ...c, enabled:v }))} onLabel="On" offLabel="Off"/>
      </div>

      {/* Everything below is hidden entirely while the integration is off. */}
      {on && (
        <div style={{display:"flex", flexDirection:"column", gap:16}}>
          {/* Warning when on but credentials are incomplete (#8) */}
          {credsMissing && (
            <div style={{display:"flex", alignItems:"center", gap:8, padding:"9px 12px", borderRadius:9, background:"var(--warn-50, #fffbeb)", border:"1px solid var(--warn-200, #fde68a)", color:"var(--warn-700, #92400e)", fontSize:11.5}}>
              <Icon name="alert" size={13}/>
              <span>Customer.io is <strong>on</strong> but credentials are incomplete — events won't reach Customer.io until Site ID, API key and signing secret are set.</span>
            </div>
          )}

          {/* Credentials + webhook URL */}
          <div>
            <CioHeading icon="lock" note="stored per skin · hidden after entry">Credentials</CioHeading>
            <div style={{display:"grid", gridTemplateColumns:"repeat(auto-fit, minmax(240px, 1fr))", gap:12}}>
              <SecretField label="Site ID"               value={creds.siteId}        placeholder="cio_site_xxxxxxxx"   onChange={v=>setCred("siteId", v)}/>
              <SecretField label="API key"               value={creds.apiKey}        placeholder="cio_api_xxxxxxxxxxxx" onChange={v=>setCred("apiKey", v)}/>
              <SecretField label="Webhook signing secret" value={creds.webhookSecret} placeholder="whsec_xxxxxxxxxxxx"   onChange={v=>setCred("webhookSecret", v)}/>
              <CioWebhookField url={webhookUrl}/>
            </div>
          </div>

          {/* Outbound triggers — unified list with search / category / bulk. */}
          <div>
            <div style={{display:"flex", alignItems:"center", justifyContent:"space-between", gap:10, flexWrap:"wrap", marginBottom:8}}>
              <CioHeading note={`Iwakiri → Customer.io · ${enabledCount}/${CUSTOMERIO_TRIGGERS.length} on`}>Outbound triggers</CioHeading>
              <div style={{display:"flex", gap:6}}>
                <button type="button" className="btn btn--secondary btn--sm" onClick={()=>bulkSet(true)} disabled={filtered.length===0}>Enable all</button>
                <button type="button" className="btn btn--secondary btn--sm" onClick={()=>bulkSet(false)} disabled={filtered.length===0}>Disable all</button>
              </div>
            </div>

            {/* Toolbar — search · category chips · enabled-only */}
            <div style={{display:"flex", alignItems:"center", gap:8, flexWrap:"wrap", marginBottom:10}}>
              <div style={{display:"flex", alignItems:"center", gap:6, flex:"1 1 200px", minWidth:160, padding:"0 10px", height:32, borderRadius:8, background:"var(--n-25)", border:"1px solid var(--border-default)"}}>
                <Icon name="search" size={12} style={{color:"var(--text-tertiary)"}}/>
                <input value={q} onChange={e=>setQ(e.target.value)} placeholder="Search triggers"
                       style={{flex:1, minWidth:0, background:"transparent", border:0, outline:"none", fontSize:12.5, color:"var(--text-primary)"}}/>
                {q && <button onClick={()=>setQ("")} title="Clear" style={{border:0, background:"transparent", cursor:"pointer", color:"var(--text-tertiary)", display:"grid", placeItems:"center"}}><Icon name="x" size={11}/></button>}
              </div>
              <button type="button" onClick={()=>setEnabledOnly(v=>!v)}
                      style={{display:"inline-flex", alignItems:"center", gap:5, padding:"5px 10px", borderRadius:8, cursor:"pointer", fontFamily:"inherit", fontSize:11.5, fontWeight:600,
                              border: enabledOnly ? "1px solid var(--p-400)" : "1px solid var(--border-default)",
                              background: enabledOnly ? "var(--p-50)" : "#fff", color: enabledOnly ? "var(--p-700)" : "var(--text-secondary)"}}>
                <Icon name="check" size={11}/> Enabled only
              </button>
            </div>
            <div style={{display:"flex", gap:6, flexWrap:"wrap", marginBottom:10}}>
              {filterChip("all", "All")}
              {Object.entries(CUSTOMERIO_CATEGORIES).map(([id, c]) => filterChip(id, c.label, c.color))}
            </div>

            {filtered.length === 0 ? (
              <div style={{padding:"16px", textAlign:"center", fontSize:12, color:"var(--text-tertiary)", border:"1px dashed var(--border-default)", borderRadius:10}}>No triggers match your filters.</div>
            ) : (
              <div style={{display:"grid", gridTemplateColumns:"repeat(auto-fill, minmax(252px, 1fr))", gap:8}}>
                {filtered.map(triggerCell)}
              </div>
            )}
          </div>

          {/* Inbound — collapsed by default; it's reference-only. */}
          <div style={{border:"1px solid var(--border-subtle)", borderRadius:10, overflow:"hidden"}}>
            <button type="button" onClick={()=>setShowInbound(s=>!s)}
                    style={{width:"100%", display:"flex", alignItems:"center", gap:8, padding:"10px 12px", background:"var(--n-25)", border:0, cursor:"pointer", fontFamily:"inherit", textAlign:"left"}}>
              <Icon name={showInbound ? "chevron_down" : "chevron_right"} size={13} style={{color:"var(--text-tertiary)"}}/>
              <span style={{fontSize:11.5, fontWeight:700, color:"var(--text-secondary)"}}>Inbound actions</span>
              <span style={{fontSize:10.5, color:"var(--text-tertiary)"}}>· {CUSTOMERIO_INBOUND.length} · Customer.io → Iwakiri · backend-handled</span>
            </button>
            {showInbound && (
              <div style={{padding:12}}>
                <div style={{display:"grid", gridTemplateColumns:"repeat(auto-fill, minmax(220px, 1fr))", gap:8}}>
                  {CUSTOMERIO_INBOUND.map(a => (
                    <div key={a.label} style={{border:"1px solid var(--border-subtle)", borderRadius:9, padding:"8px 10px"}}>
                      <div style={{display:"flex", alignItems:"center", gap:6}}>
                        <span style={{fontSize:8.5, fontWeight:700, padding:"1px 5px", borderRadius:999, textTransform:"uppercase", letterSpacing:".04em",
                                      background: a.kind==="reward" ? "color-mix(in oklab, var(--ok-500) 14%, white)" : "color-mix(in oklab, var(--info-500, #3b82f6) 14%, white)",
                                      color: a.kind==="reward" ? "var(--ok-600, #15803d)" : "var(--info-600, #2563eb)"}}>{a.kind}</span>
                        <span style={{fontSize:11.5, fontWeight:650}}>{a.label}</span>
                      </div>
                      <div style={{fontSize:10, color:"var(--text-tertiary)", marginTop:3, lineHeight:1.4}}>{a.desc}</div>
                    </div>
                  ))}
                </div>
                <div style={{marginTop:8, fontSize:10, color:"var(--text-tertiary)", lineHeight:1.5}}>
                  Every grant is guarded backend-side: <strong>re-check eligibility</strong>, <strong>respect the cooldown</strong>, and <strong>log it</strong> (expiry + campaign + audit trail).
                </div>
              </div>
            )}
          </div>
        </div>
      )}
    </div>
  );
};

/* NotificationChannelsPanel — the per-skin delivery-channel configuration
   surface. Mirrors CustomerioPanel's card language, but there's no master
   toggle (no shared upstream service) and only two channels, so it's a
   simple stacked/side-by-side pair of cards rather than a filterable list. */
const NOTIFICATION_CHANNEL_ICON = { push: "bell", inapp: "message_square" };
const NOTIFICATION_CHANNEL_COLOR = { push: "var(--info-500, #3b82f6)", inapp: "var(--p-500)" };

const NotificationChannelsPanel = ({ cfg, setCfg }) => {
  const setChannel = (key, patch) => setCfg(c => ({ ...c, channels: { ...c.channels, [key]: { ...c.channels[key], ...patch } } }));

  const channelCard = (ch) => {
    const st = cfg.channels[ch.key] || { enabled: false };
    const on = st.enabled;
    const color = NOTIFICATION_CHANNEL_COLOR[ch.key] || "var(--p-500)";

    return (
      <div key={ch.key} style={{border:"1px solid var(--border-subtle)", borderLeft:`3px solid ${color}`, borderRadius:12, padding:"13px 15px", display:"flex", alignItems:"center", gap:12, background: on ? "color-mix(in oklab, var(--ok-500) 5%, white)" : "var(--n-25)"}}>
        <div style={{width:34, height:34, borderRadius:9, background:color, color:"#fff", display:"grid", placeItems:"center", flexShrink:0}}>
          <Icon name={NOTIFICATION_CHANNEL_ICON[ch.key] || "bell"} size={16}/>
        </div>
        <div style={{flex:1, minWidth:0}}>
          <div style={{fontWeight:700, fontSize:13.5}}>{ch.label}</div>
          <div style={{fontSize:11.5, color:"var(--text-tertiary)", marginTop:2}}>{ch.desc}</div>
        </div>
        <Toggle value={on} onChange={v=>setChannel(ch.key, { enabled:v })} onLabel="On" offLabel="Off"/>
      </div>
    );
  };

  return (
    <div style={{display:"grid", gridTemplateColumns:"repeat(auto-fit, minmax(280px, 1fr))", gap:12}}>
      {NOTIFICATION_CHANNELS.map(channelCard)}
    </div>
  );
};

const BrandEditor = ({ brand, onSave, onCancel, onDelete }) => {
  const [draft, setDraft] = useState(() => ({ ...brand }));
  const update = (k, v) => setDraft(d => ({ ...d, [k]: v }));

  return (
    <div style={{position:"fixed", inset:0, background:"rgba(15,20,32,.45)", backdropFilter:"blur(4px)", zIndex:120, display:"grid", placeItems:"center"}} onClick={onCancel}>
      <div style={{width:580, maxHeight:"88vh", overflow:"auto", background:"#fff", borderRadius:16, boxShadow:"0 30px 60px -15px rgba(15,20,32,.35)"}} onClick={e=>e.stopPropagation()}>
        <div style={{padding:"16px 20px", borderBottom:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", gap:10}}>
          <div style={{width:30, height:30, borderRadius:9, background:"linear-gradient(135deg, var(--p-500), var(--p-700))", display:"grid", placeItems:"center", color:"#fff"}}><Icon name="flag" size={14}/></div>
          <div style={{flex:1}}>
            <div style={{fontWeight:700, fontSize:14.5}}>{brand.id ? "Edit skin" : "Add new skin"}</div>
            <div style={{fontSize:11.5, color:"var(--text-tertiary)"}}>{brand.id ? "Skin details and per-operator config" : "Configure a new skin tenant"}</div>
          </div>
          <button className="btn btn--ghost btn--icon btn--sm" onClick={onCancel}><Icon name="x" size={13}/></button>
        </div>

        <div style={{padding:"18px 20px", display:"flex", flexDirection:"column", gap:18}}>
          {/* Logo preview + short code */}
          <div style={{display:"flex", gap:16, alignItems:"center"}}>
            <div style={{position:"relative"}}>
              <div style={{width:72, height:72, borderRadius:16, background: draft.color || BRAND_COLOR_PRESETS[0].value, display:"grid", placeItems:"center", color:"#fff", fontWeight:800, fontSize:22, letterSpacing:".02em", boxShadow:"0 4px 10px rgba(15,20,32,.15), inset 0 1px 0 rgba(255,255,255,.15)"}}>
                {(draft.short || "??").slice(0,3)}
              </div>
              <div style={{position:"absolute", bottom:-4, right:-4, width:24, height:24, borderRadius:999, background:"#fff", border:"1px solid var(--border-default)", display:"grid", placeItems:"center", cursor:"pointer", boxShadow:"0 2px 4px rgba(15,20,32,.1)"}} title="Upload logo (placeholder)">
                <Icon name="upload" size={11}/>
              </div>
            </div>
            <div style={{flex:1, minWidth:0}}>
              <label style={{fontSize:11, fontWeight:600, color:"var(--text-secondary)", textTransform:"uppercase", letterSpacing:".05em"}}>Short code / Logo monogram</label>
              <input value={draft.short || ""} maxLength={4} onChange={e=>update("short", e.target.value.toUpperCase())} style={{width:"100%", marginTop:4, padding:"8px 10px", borderRadius:8, border:"1px solid var(--border-default)", fontSize:13, fontWeight:600, fontFamily:"var(--font-mono)", letterSpacing:".04em"}}/>
              <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:4}}>2–4 letters · Shown in avatar tiles</div>
            </div>
          </div>

          {/* Name */}
          <div>
            <label style={{fontSize:11, fontWeight:600, color:"var(--text-secondary)", textTransform:"uppercase", letterSpacing:".05em"}}>Brand name</label>
            <input value={draft.name || ""} onChange={e=>update("name", e.target.value)} style={{width:"100%", marginTop:4, padding:"9px 10px", borderRadius:8, border:"1px solid var(--border-default)", fontSize:13.5, fontWeight:600}}/>
          </div>

          {/* Currency + Market */}
          <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:14}}>
            <div>
              <label style={{fontSize:11, fontWeight:600, color:"var(--text-secondary)", textTransform:"uppercase", letterSpacing:".05em"}}>Currency</label>
              <select value={draft.currency || "EUR"} onChange={e=>update("currency", e.target.value)} style={{width:"100%", marginTop:4, padding:"9px 10px", borderRadius:8, border:"1px solid var(--border-default)", fontSize:13, background:"#fff"}}>
                {CURRENCIES.map(c => <option key={c}>{c}</option>)}
              </select>
            </div>
            <div>
              <label style={{fontSize:11, fontWeight:600, color:"var(--text-secondary)", textTransform:"uppercase", letterSpacing:".05em"}}>Market / License</label>
              <select value={draft.market || "MGA"} onChange={e=>update("market", e.target.value)} style={{width:"100%", marginTop:4, padding:"9px 10px", borderRadius:8, border:"1px solid var(--border-default)", fontSize:13, background:"#fff"}}>
                {MARKETS.map(m => <option key={m}>{m}</option>)}
              </select>
            </div>
          </div>

          {/* Color */}
          <div>
            <label style={{fontSize:11, fontWeight:600, color:"var(--text-secondary)", textTransform:"uppercase", letterSpacing:".05em"}}>Brand color</label>
            <div style={{display:"grid", gridTemplateColumns:"repeat(5, 1fr)", gap:8, marginTop:6}}>
              {BRAND_COLOR_PRESETS.map(p => {
                const selected = draft.color === p.value;
                return (
                  <button key={p.label} onClick={()=>update("color", p.value)}
                          style={{padding:0, border: selected ? "2px solid var(--p-500)" : "1px solid var(--border-default)", borderRadius:10, overflow:"hidden", cursor:"pointer", background:"#fff", boxShadow: selected ? "0 2px 6px -1px color-mix(in oklab, var(--p-500) 30%, transparent)" : "none"}}>
                    <div style={{height:28, background:p.value}}/>
                    <div style={{fontSize:10.5, fontWeight:600, padding:"4px 6px", color: selected ? "var(--p-600)" : "var(--text-secondary)"}}>{p.label}</div>
                  </button>
                );
              })}
            </div>
          </div>
        </div>

        <div style={{padding:"14px 20px", borderTop:"1px solid var(--border-subtle)", background:"var(--n-25)", display:"flex", gap:8, alignItems:"center"}}>
          {brand.id && onDelete && (
            <button onClick={()=>{ if (confirm(`Delete skin "${brand.name}"?`)) onDelete(brand.id); }}
                    style={{height:36, padding:"0 12px", borderRadius:8, border:"1px solid var(--err-200)", background:"#fff", color:"var(--err-600)", fontWeight:600, fontSize:12.5, display:"inline-flex", alignItems:"center", gap:6, cursor:"pointer"}}>
              <Icon name="trash" size={12}/> Delete
            </button>
          )}
          <div style={{flex:1}}/>
          <button onClick={onCancel} className="btn btn--secondary btn--sm">Cancel</button>
          <button onClick={()=>onSave(draft)} disabled={!draft.name || !draft.short} className="btn btn--primary btn--sm">
            <Icon name="check" size={12}/> {brand.id ? "Save changes" : "Create skin"}
          </button>
        </div>
      </div>
    </div>
  );
};

/* =========================================================================
   ProviderEditor — full editable profile for one PSP.

   Three tabs cover everything an operator can change:
   - General        · identity, status, costs, per-tx and aggregate limits, tech
   - Liquidity      · floor / ceiling / sweep % / min-withdrawable / channels +
                       per-method overrides table
   - Coverage       · supported methods, geographies, licenses × operations

   Nothing is hardcoded — every value the engine reads at runtime is bound
   to a control here. Open initialTab="liquidity" from the "Thresholds"
   button on the card; the "Edit" button opens initialTab="general".
   ========================================================================= */
const ProviderEditor = ({ psp, initialTab, destinations, onSave, onCancel, onDelete, onAddDestination, onEditDestination, onToggleDestination }) => {
  const [draft, setDraft] = useState(JSON.parse(JSON.stringify(psp)));
  const [etab, setEtab] = useState(initialTab || "general");
  const set = (path, value) => {
    setDraft(d => {
      const next = JSON.parse(JSON.stringify(d));
      const keys = path.split(".");
      let cur = next;
      for (let i = 0; i < keys.length - 1; i++) cur = cur[keys[i]];
      cur[keys[keys.length - 1]] = value;
      return next;
    });
  };
  const toggleArr = (path, item) => {
    const keys = path.split(".");
    let cur = draft;
    for (const k of keys) cur = cur[k];
    set(path, cur.includes(item) ? cur.filter(x => x !== item) : [...cur, item]);
  };

  const liq = draft.liquidity;
  const isNew = !psp.id;

  // Per-method override editor helpers
  const usedMethodIds = (draft.methodOverrides || []).map(o => o.method);
  const availableForOverride = (draft.methods || []).filter(m => !usedMethodIds.includes(m));
  const addOverride = () => {
    if (availableForOverride.length === 0) return;
    set("methodOverrides", [
      ...(draft.methodOverrides || []),
      { method: availableForOverride[0], floorEur: liq.floorEur, ceilingEur: liq.ceilingEur, sweepPct: liq.sweepPct || 80 },
    ]);
  };
  const removeOverride = (i) => {
    set("methodOverrides", draft.methodOverrides.filter((_, j) => j !== i));
  };
  const updateOverride = (i, key, value) => {
    set("methodOverrides", draft.methodOverrides.map((o, j) => j === i ? { ...o, [key]: value } : o));
  };

  return (
    <div style={{position:"fixed", inset:0, background:"rgba(15,20,32,.45)", backdropFilter:"blur(4px)", zIndex:120, display:"grid", placeItems:"center"}} onClick={onCancel}>
      <div style={{width:760, maxHeight:"90vh", overflow:"hidden", background:"#fff", borderRadius:16, boxShadow:"0 30px 60px -15px rgba(15,20,32,.35)", display:"flex", flexDirection:"column"}} onClick={e=>e.stopPropagation()}>
        {/* Header */}
        <div style={{padding:"16px 20px", borderBottom:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", gap:10}}>
          <div style={{width:32, height:32, borderRadius:9, background:"linear-gradient(135deg, var(--p-500), var(--p-700))", display:"grid", placeItems:"center", color:"#fff", fontWeight:700, fontSize:12}}>
            {(draft.name || "??").replace(/[^A-Za-z]/g,"").slice(0,2).toUpperCase()}
          </div>
          <div style={{flex:1}}>
            <div style={{fontWeight:700, fontSize:14.5}}>{isNew ? "Add provider" : `Edit ${draft.name}`}</div>
            <div style={{fontSize:11.5, color:"var(--text-tertiary)"}}>{isNew ? "Configure a new PSP profile" : "All values are editable. Saved values apply to the engine immediately."}</div>
          </div>
          <button className="btn btn--ghost btn--icon btn--sm" onClick={onCancel}><Icon name="x" size={13}/></button>
        </div>

        {/* Sub-tabs */}
        <div className="tabs" style={{padding:"0 14px", borderBottom:"1px solid var(--border-subtle)"}}>
          <button className={etab==="general"  ?"active":""} onClick={()=>setEtab("general")} style={{display:"inline-flex", alignItems:"center"}}>General<Tip>Identity of the PSP (name, kind, status), the commercial terms (fees, settlement, limits), the live health snapshot, and the technical wiring (endpoint, timeout, webhook).</Tip></button>
          <button className={etab==="liquidity"?"active":""} onClick={()=>setEtab("liquidity")} style={{display:"inline-flex", alignItems:"center"}}>Liquidity &amp; thresholds<Tip>Floor / ceiling / min-withdrawable for this PSP's wallet, plus the alarm rules that fire when balance crosses a threshold. Per-method overrides (tighter floor on a specific instrument) also live here.</Tip></button>
          <button className={etab==="coverage" ?"active":""} onClick={()=>setEtab("coverage")} style={{display:"inline-flex", alignItems:"center"}}>Coverage<Tip>Which payment methods this PSP supports, in which countries (geos), and under which operator licenses — used by the routing engine to filter incompatible PSPs.</Tip></button>
        </div>

        <div style={{padding:"18px 20px", overflow:"auto", flex:1}}>

          {etab === "general" && (
            <div style={{display:"flex", flexDirection:"column", gap:18}}>
              <Explainer compact title="What this is, in plain English"
                bullets={[
                  <><strong>Identity</strong> — name, kind (Card / Wallet / Bank / Crypto), and whether the PSP is active or paused.</>,
                  <><strong>Costs &amp; settlement</strong> — fees the PSP charges and how fast funds clear (T+0 / T+1 / T+2).</>,
                  <><strong>Limits</strong> — what the PSP itself will accept per transaction / day / month.</>,
                  <><strong>Health</strong> — live success rate, average latency, timeout rate. Used by the hybrid ranking to skip a flaky PSP automatically.</>,
                  <><strong>Tech</strong> — endpoint URL, request timeout, webhook URL the PSP calls when a transaction changes state.</>,
                ]}>
                Everything the routing engine needs to know about <strong>{draft.name || "this PSP"}</strong> on the operational side. Coverage and liquidity live in their own tabs.
              </Explainer>
              <div style={{display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap:12}}>
                <div>
                  <label className="form-label">Provider name<Tip>Display name used everywhere this PSP shows up (Routes, Transactions provider column, Liquidity alarms…). Pick the official PSP brand name.</Tip></label>
                  <input className="input input--sm" value={draft.name || ""} onChange={e=>set("name", e.target.value)}/>
                </div>
                <div>
                  <label className="form-label">Kind<Tip>The PSP family — Card (Visa / Mastercard processors), Wallet (Skrill, Neteller), Bank (Trustly, SEPA), Voucher (Paysafe), Crypto. Determines which payment methods this PSP can plausibly process.</Tip></label>
                  <select className="select input--sm" value={draft.kind} onChange={e=>set("kind", e.target.value)}>
                    {["Card","Bank","Wallet","Crypto","APM","Voucher"].map(k => <option key={k} value={k}>{k}</option>)}
                  </select>
                </div>
                <div>
                  <label className="form-label">Status</label>
                  <select className="select input--sm" value={draft.status} onChange={e=>set("status", e.target.value)}>
                    <option value="active">Active</option>
                    <option value="paused">Paused</option>
                    <option value="unavailable">Unavailable</option>
                  </select>
                </div>
              </div>

              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>Costs</div>
                <div style={{display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap:12}}>
                  <div><label className="form-label">Deposit fee</label><input className="input input--sm" value={draft.fees.dep} onChange={e=>set("fees.dep", e.target.value)} placeholder="1.4% + €0.25"/></div>
                  <div><label className="form-label">Withdrawal fee</label><input className="input input--sm" value={draft.fees.wd} onChange={e=>set("fees.wd", e.target.value)} placeholder="1.0%"/></div>
                  <div><label className="form-label">Settlement</label><input className="input input--sm" value={draft.fees.settle} onChange={e=>set("fees.settle", e.target.value)} placeholder="€0.50 / payout"/></div>
                </div>
              </div>

              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>Transaction limits</div>
                <div style={{display:"grid", gridTemplateColumns:"1fr 1fr 1fr 1fr", gap:12}}>
                  <div><label className="form-label">Per-tx min</label><input className="input input--sm" value={draft.limits.min} onChange={e=>set("limits.min", e.target.value)} placeholder="€10"/></div>
                  <div><label className="form-label">Per-tx max</label><input className="input input--sm" value={draft.limits.max} onChange={e=>set("limits.max", e.target.value)} placeholder="€50,000"/></div>
                  <div><label className="form-label">Daily cap</label><input className="input input--sm" value={draft.limits.daily} onChange={e=>set("limits.daily", e.target.value)} placeholder="€2.5M"/></div>
                  <div><label className="form-label">Monthly cap</label><input className="input input--sm" value={draft.limits.monthly} onChange={e=>set("limits.monthly", e.target.value)} placeholder="€60M"/></div>
                </div>
              </div>

              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>Settlement &amp; payout</div>
                <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:12}}>
                  <div>
                    <label className="form-label">Settlement timing</label>
                    <select className="select input--sm" value={liq.settlement} onChange={e=>set("liquidity.settlement", e.target.value)}>
                      {["T+0","T+1","T+2","T+3"].map(t => <option key={t} value={t}>{t}</option>)}
                    </select>
                  </div>
                  <div><label className="form-label">Payout capacity</label><input className="input input--sm" value={liq.payout} onChange={e=>set("liquidity.payout", e.target.value)} placeholder="€500K / day"/></div>
                </div>
              </div>

              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>Technical</div>
                <div style={{display:"grid", gridTemplateColumns:"2fr 1fr 1fr", gap:12}}>
                  <div><label className="form-label">API endpoint</label><input className="input input--sm" value={draft.tech.endpoint} onChange={e=>set("tech.endpoint", e.target.value)}/></div>
                  <div>
                    <label className="form-label">Timeout (ms)</label>
                    <input type="number" className="input input--sm" value={draft.tech.timeoutMs} onChange={e=>set("tech.timeoutMs", parseInt(e.target.value || "0", 10))} min={1000} max={60000} step={500}/>
                  </div>
                  <div><label className="form-label">Webhook path</label><input className="input input--sm" value={draft.tech.webhook} onChange={e=>set("tech.webhook", e.target.value)} placeholder="/webhooks/..."/></div>
                </div>
                <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:6}}>If no response within the timeout, the cascade engine treats it as <code>timeout / network</code> and routes to the next provider.</div>
              </div>
            </div>
          )}

          {etab === "liquidity" && (
            <div style={{display:"flex", flexDirection:"column", gap:16}}>
              <Explainer compact title="What this is, in plain English"
                bullets={[
                  <><strong>Floor</strong> — the minimum balance to keep in the wallet. Drop below it and withdrawals start failing for empty wallet. Set the floor at ~10–20% of typical daily payout volume.</>,
                  <><strong>Ceiling</strong> — the maximum we want sitting idle in this PSP. Hit it and the idle-capital alarm asks treasury to sweep funds out into the bank account.</>,
                  <><strong>Min withdrawable</strong> — the smallest amount the PSP itself accepts on a single payout. Below this the PSP rejects the request before we even try.</>,
                  <><strong>Liquidity alarms</strong> — multiple per-PSP rules (wallet near ceiling, below floor, settlement delay, decline-rate spike…) with destinations and severity. Same shape as the main Alarms.</>,
                ]}>
                Per-PSP wallet thresholds and alarm rules. Sweep / near-ceiling alerts now live in the alarm list below — add a <em>Wallet near ceiling</em> rule to fire when balance crosses a configurable % of the ceiling.
              </Explainer>

              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>Provider-level thresholds</div>
                <div style={{display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap:12}}>
                  <div>
                    <label className="form-label">Floor (€)<Tip>Minimum balance to keep in this PSP's wallet. Falling below the floor means imminent withdrawal failures — top up. Default rule of thumb: 10–20% of typical daily payout volume through this PSP.</Tip></label>
                    <input type="number" className="input input--sm" value={liq.floorEur} onChange={e=>set("liquidity.floorEur", parseInt(e.target.value || "0", 10))} min={0} step={10000}/>
                  </div>
                  <div>
                    <label className="form-label">Ceiling (€)<Tip>Maximum balance we want sitting idle in this PSP's wallet. Hit it and treasury should sweep funds out into the bank account so capital isn't locked up. Set the ceiling at roughly 2–3 days of typical payout volume.</Tip></label>
                    <input type="number" className="input input--sm" value={liq.ceilingEur} onChange={e=>set("liquidity.ceilingEur", parseInt(e.target.value || "0", 10))} min={0} step={50000}/>
                  </div>
                  <div>
                    <label className="form-label">Min withdrawable (€)<Tip>The smallest single-withdrawal amount the PSP will accept. Below this the PSP rejects the payout request — useful to validate against before we route a tiny withdrawal that can't be honored.</Tip></label>
                    <input type="number" className="input input--sm" value={liq.minWithdrawalEur} onChange={e=>set("liquidity.minWithdrawalEur", parseInt(e.target.value || "0", 10))} min={0} step={1000}/>
                  </div>
                </div>
                <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:6, lineHeight:1.5}}>
                  Sweep / near-ceiling alerts now live in <strong>Liquidity alarms</strong> below — add a <code>Wallet near ceiling</code> rule there to fire when balance hits a configurable % of the ceiling.
                </div>
              </div>

              {/* Liquidity alarms — same shape as the main Alarms page,
                  scoped to this provider's liquidity metrics. Multiple
                  rules per PSP are supported. The legacy single-toggle
                  "floorAlert" + "notifChannels" still work as the seed for
                  a default rule on first edit. */}
              {(() => {
                /* Trimmed catalog — the seven liquidity metrics that
                   matter most for treasury and routing. Long descriptions
                   so the operator never needs to guess what each one
                   measures or when it should fire. */
                const LIQ_METRICS = [
                  {
                    key:"wallet_below_floor",
                    label:"Wallet below floor",
                    unit:"amount",
                    suggestedOp:"<=",
                    suggestedThreshold: liq.floorEur,
                    desc:"The PSP's live wallet balance dropped at or below the operator-set floor. Withdrawals will start failing for empty wallet very soon. Most important top-up signal — wire treasury, ops and on-call. Suggested threshold: the configured Floor value (auto-filled above).",
                  },
                  {
                    key:"wallet_near_ceiling",
                    label:"Wallet near ceiling",
                    unit:"%",
                    suggestedOp:">=",
                    suggestedThreshold: liq.sweepPct || 80,
                    desc:"Balance has reached this percentage of the operator-set ceiling (default 80% — e.g. 80% of a 2,000,000 ceiling fires at 1,600,000 in the PSP wallet currency). Most important sweep signal — capital is starting to sit idle, treasury should pull funds out into the bank. Suggested threshold: 80%.",
                  },
                  {
                    key:"wallet_below_min_wd",
                    label:"Below minimum withdrawable",
                    unit:"amount",
                    suggestedOp:"<=",
                    suggestedThreshold: liq.minWithdrawalEur,
                    desc:"Balance dropped below the smallest single-withdrawal amount the PSP itself will accept. Any withdrawal request to this PSP will now be rejected by the PSP before we even try. Suggested threshold: the configured Min withdrawable value (auto-filled).",
                  },
                  {
                    key:"settlement_delay",
                    label:"Settlement delay",
                    unit:"hours",
                    suggestedOp:">=",
                    suggestedThreshold: 24,
                    desc:"Funds the PSP should have settled to our bank (T+0 / T+1 / T+2) are still pending after this many hours. Usually means a PSP-side hold, a banking-rail delay, or a compliance review. Open a ticket with the PSP. Suggested threshold: 24 hours past the SLA.",
                  },
                  {
                    key:"psp_down",
                    label:"PSP marked OFFLINE",
                    unit:"(event)",
                    suggestedOp:"=",
                    suggestedThreshold:"offline",
                    desc:"Fires the instant health monitoring flips this PSP to OFFLINE / DOWN. The routing engine has already started excluding it from chains. Always critical — wake the on-call to confirm the outage and notify the PSP. Suggested severity: critical.",
                  },
                  {
                    key:"psp_success_rate_below",
                    label:"PSP success rate dropped",
                    unit:"%",
                    suggestedOp:"<",
                    suggestedThreshold: 90,
                    desc:"The PSP's success rate in the rolling window dropped below this percentage. Earliest signal that the PSP is degrading — well before it actually goes OFFLINE. Consider pausing it manually if the rate keeps dropping. Suggested threshold: 90%.",
                  },
                  {
                    key:"single_tx_above",
                    label:"Single transaction ≥ amount",
                    unit:"amount",
                    suggestedOp:">=",
                    suggestedThreshold: 25_000,
                    desc:"Fires the instant a single transaction through this PSP is created with an amount at or above the threshold. Wakes the VIP desk for the very large stakes and the fraud team for anything that looks coordinated. Suggested threshold: 25,000 in the PSP wallet currency.",
                  },
                ];
                const LIQ_METRIC_BY = Object.fromEntries(LIQ_METRICS.map(m => [m.key, m]));
                const SEVS = ["low","medium","high","critical"];
                // Seed: migrate the legacy `floorAlert + notifChannels` pair
                // into a default rule the first time we open the editor.
                const seedRules = () => {
                  const out = [];
                  if (liq.floorAlert) {
                    const defaultChannels = draft.notifChannels && draft.notifChannels.length ? [...draft.notifChannels] : ["slack","email"];
                    // Best-effort: pick one global destination per default
                    // channel so the rule renders with a sensible selection.
                    const defaultDests = (destinations || [])
                      .filter(d => d.enabled && defaultChannels.includes(d.channel))
                      .reduce((acc, d) => { if (!acc.find(x => x.channel === d.channel)) acc.push(d); return acc; }, [])
                      .map(d => d.id);
                    out.push({ id:"seed-floor", name:`${draft.name || "Provider"} · wallet below floor`,
                      metric:"wallet_below_floor", op:"<=", threshold: liq.floorEur,
                      destinations: defaultDests, channels: defaultChannels,
                      severity:"high", status:"on" });
                    out.push({ id:"seed-sweep", name:`${draft.name || "Provider"} · wallet near ceiling`,
                      metric:"wallet_near_ceiling", op:">=", threshold: liq.sweepPct || 80,
                      destinations: defaultDests, channels: defaultChannels,
                      severity:"medium", status:"on" });
                  }
                  return out;
                };
                const rules = Array.isArray(draft.liquidityAlarms) ? draft.liquidityAlarms : seedRules();
                const setRules = (next) => set("liquidityAlarms", next);
                const addRule = () => {
                  const m = LIQ_METRICS[0];
                  setRules([...rules, {
                    id: "la" + Math.random().toString(36).slice(2, 7),
                    name: `${draft.name || "Provider"} · new alarm`,
                    metric: m.key, op: m.suggestedOp, threshold: m.suggestedThreshold,
                    destinations:[], channels:[], severity:"medium", status:"on",
                  }]);
                };
                const updateRule = (i, patch) => setRules(rules.map((r, j) => j === i ? { ...r, ...patch } : r));
                const removeRule = (i) => setRules(rules.filter((_, j) => j !== i));
                const toggleRuleDestination = (i, destId) => {
                  const cur = rules[i].destinations || [];
                  const next = cur.includes(destId) ? cur.filter(x => x !== destId) : [...cur, destId];
                  // Mirror picked destinations to channels for the read-only
                  // rendering in the general alarm list.
                  const derivedChannels = Array.from(new Set(next.map(id => (destinations || []).find(d => d.id === id)?.channel).filter(Boolean)));
                  updateRule(i, { destinations: next, channels: derivedChannels });
                };
                return (
                  <div>
                    <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8, display:"flex", alignItems:"center", gap:8}}>
                      <span>Liquidity alarms</span>
                      <span style={{fontWeight:500, textTransform:"none", letterSpacing:0, color:"var(--text-tertiary)"}}>— same shape as Settings → Alarms, scoped to this PSP's liquidity metrics</span>
                      <button className="btn btn--secondary btn--sm" style={{marginLeft:"auto"}} onClick={addRule}>
                        <Icon name="plus" size={11}/> Add alarm
                      </button>
                    </div>
                    {rules.length === 0 ? (
                      <div style={{padding:"12px 14px", borderRadius:8, border:"1px dashed var(--border-default)", fontSize:12.5, color:"var(--text-tertiary)"}}>
                        No liquidity alarms configured yet. Click <strong>Add alarm</strong> to create one — typical first rules are <code>wallet_below_floor</code> and <code>wallet_near_ceiling</code>.
                      </div>
                    ) : (
                      <div style={{display:"flex", flexDirection:"column", gap:8}}>
                        {rules.map((r, i) => {
                          const m = LIQ_METRIC_BY[r.metric] || LIQ_METRICS[0];
                          const sev = SEVERITY_CHIP[r.severity] || SEVERITY_CHIP.low;
                          return (
                            <div key={r.id || i} style={{padding:"10px 12px", borderRadius:10, border:"1px solid var(--border-default)", background:"#fff", display:"flex", flexDirection:"column", gap:8, opacity: r.status === "on" ? 1 : 0.65}}>
                              <div style={{display:"flex", alignItems:"center", gap:8}}>
                                <input className="input input--sm" value={r.name} onChange={e=>updateRule(i, { name:e.target.value })} placeholder="Rule name"
                                  style={{flex:1, fontWeight:600}}/>
                                <span title={r.status === "on" ? "Click to pause this alarm" : "Click to activate this alarm"}
                                  className={`chip ${r.status === "on" ? "chip--ok" : "chip--neutral"}`}
                                  style={{cursor:"pointer", fontSize:10.5}}
                                  onClick={()=>updateRule(i, { status: r.status === "on" ? "off" : "on" })}>
                                  {r.status === "on" ? "ON" : "OFF"}
                                </span>
                                <button className="btn btn--ghost btn--icon btn--sm" title="Remove" onClick={()=>removeRule(i)}><Icon name="trash" size={12}/></button>
                              </div>
                              <div style={{display:"grid", gridTemplateColumns:"2fr 0.6fr 1fr 1fr", gap:8, alignItems:"end"}}>
                                <div>
                                  <label className="form-label">Metric</label>
                                  <select className="select input--sm" value={r.metric}
                                    onChange={e => {
                                      const newM = LIQ_METRIC_BY[e.target.value];
                                      updateRule(i, {
                                        metric: e.target.value,
                                        op: r.op || (newM && newM.suggestedOp) || ">=",
                                        threshold: r.threshold || (newM && newM.suggestedThreshold) || 0,
                                      });
                                    }}>
                                    {LIQ_METRICS.map(mm => <option key={mm.key} value={mm.key}>{mm.label}</option>)}
                                  </select>
                                </div>
                                <div>
                                  <label className="form-label">Op</label>
                                  <select className="select input--sm" value={r.op} onChange={e=>updateRule(i, { op:e.target.value })}>
                                    {["<=", "<", ">", ">=", "=="].map(o => <option key={o} value={o}>{o}</option>)}
                                  </select>
                                </div>
                                <div>
                                  <label className="form-label">Threshold · {m.unit}</label>
                                  <input type="number" className="input input--sm" value={r.threshold}
                                    onChange={e => updateRule(i, { threshold: parseFloat(e.target.value || "0") })}
                                    min={0} step={m.unit === "%" ? 1 : 1000}/>
                                </div>
                                <div>
                                  <label className="form-label">Severity</label>
                                  <select className="select input--sm" value={r.severity}
                                    onChange={e=>updateRule(i, { severity:e.target.value })}
                                    style={{background:sev.bg, color:sev.fg, fontWeight:700, textTransform:"uppercase", letterSpacing:".03em"}}>
                                    {SEVS.map(s => <option key={s} value={s}>{s}</option>)}
                                  </select>
                                </div>
                              </div>
                              <div style={{fontSize:11, color:"var(--text-tertiary)", lineHeight:1.5}}>{m.desc}</div>
                              <div>
                                <div style={{display:"flex", alignItems:"center", justifyContent:"space-between", marginBottom:6}}>
                                  <span style={{fontSize:10.5, fontWeight:700, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em"}}>
                                    Destinations · send to <span style={{color:"var(--text-secondary)", fontWeight:600}}>· {(r.destinations || []).length} picked</span>
                                  </span>
                                  {onAddDestination && (
                                    <button type="button" className="btn btn--ghost btn--sm" onClick={onAddDestination}>
                                      <Icon name="plus" size={10}/> Add destination
                                    </button>
                                  )}
                                </div>
                                {(!destinations || destinations.length === 0) ? (
                                  <div style={{padding:"10px 12px", border:"1px dashed var(--border-default)", borderRadius:8, fontSize:12, color:"var(--text-tertiary)", textAlign:"center"}}>
                                    No destinations yet. Click <strong>Add destination</strong> to create one.
                                  </div>
                                ) : (
                                  <div style={{display:"flex", flexDirection:"column", gap:6}}>
                                    {NOTIF_CHANNELS.map(c => {
                                      const list = (destinations || []).filter(d => d.channel === c.id);
                                      if (list.length === 0) return null;
                                      return (
                                        <div key={c.id} style={{padding:"6px 8px", border:"1px solid var(--border-subtle)", borderRadius:8, background:"#fff"}}>
                                          <div style={{display:"flex", alignItems:"center", gap:6, marginBottom:4}}>
                                            <Icon name={c.icon} size={11} style={{color:c.color}}/>
                                            <span style={{fontSize:11.5, fontWeight:600}}>{c.label}</span>
                                            <span style={{fontSize:10.5, color:"var(--text-tertiary)"}}>{list.filter(d => (r.destinations || []).includes(d.id)).length}/{list.length}</span>
                                          </div>
                                          <div style={{display:"flex", flexWrap:"wrap", gap:4}}>
                                            {list.map(d => {
                                              const on = (r.destinations || []).includes(d.id);
                                              const disabled = !d.enabled;
                                              return (
                                                <div key={d.id} style={{display:"inline-flex", alignItems:"stretch", borderRadius:999, overflow:"hidden", border: on ? `1px solid ${c.color}` : "1px solid var(--border-default)", background: on ? `${c.color}14` : "#fff"}}>
                                                  <label style={{display:"inline-flex", alignItems:"center", gap:5, padding:"4px 4px 4px 9px", color: disabled ? "var(--text-tertiary)" : on ? c.color : "var(--text-secondary)", fontSize:11.5, fontWeight:600, cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? 0.55 : 1, textDecoration: disabled ? "line-through" : "none"}}>
                                                    <input type="checkbox" checked={on} disabled={disabled} onChange={()=>toggleRuleDestination(i, d.id)} style={{accentColor:c.color, margin:0}}/>
                                                    {d.label}
                                                    <span style={{fontSize:10, fontWeight:500, color:"var(--text-tertiary)", marginLeft:2}}>{d.target}</span>
                                                  </label>
                                                  {onToggleDestination && (
                                                    <button type="button" title={d.enabled ? "Disable destination globally" : "Enable destination globally"}
                                                      onClick={()=>onToggleDestination(d.id)}
                                                      style={{padding:"0 6px", border:"none", borderLeft:"1px solid var(--border-subtle)", background:"transparent", color:"var(--text-tertiary)", fontSize:9.5, cursor:"pointer"}}>
                                                      {d.enabled ? "on" : "off"}
                                                    </button>
                                                  )}
                                                  {onEditDestination && (
                                                    <button type="button" title="Edit destination"
                                                      onClick={()=>onEditDestination(d)}
                                                      style={{padding:"0 7px", border:"none", borderLeft:"1px solid var(--border-subtle)", background:"transparent", color:"var(--text-tertiary)", cursor:"pointer", display:"inline-grid", placeItems:"center"}}>
                                                      <Icon name="edit" size={9}/>
                                                    </button>
                                                  )}
                                                </div>
                                              );
                                            })}
                                          </div>
                                        </div>
                                      );
                                    })}
                                  </div>
                                )}
                              </div>
                            </div>
                          );
                        })}
                      </div>
                    )}
                    <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:8, lineHeight:1.5}}>
                      Liquidity alarms post to the destinations chosen on each rule. Add new destinations on the fly with the <strong>Add destination</strong> button — they immediately appear here and on the main Alarms editor. Rules continue to fire while the condition is true and de-duplicate every 5 minutes to avoid alarm storms.
                    </div>
                  </div>
                );
              })()}

              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8, display:"flex", alignItems:"center", gap:8}}>
                  Per-method overrides
                  <span style={{fontWeight:500, textTransform:"none", letterSpacing:0, color:"var(--text-tertiary)"}}>— specific method on this PSP can carry a tighter floor / ceiling than the provider defaults</span>
                </div>
                {(draft.methodOverrides || []).length === 0 ? (
                  <div style={{padding:"12px 14px", borderRadius:8, border:"1px dashed var(--border-default)", fontSize:12.5, color:"var(--text-tertiary)"}}>
                    No overrides — every supported method on this PSP uses the provider-level thresholds above.
                  </div>
                ) : (
                  <table className="paybo-v2__table" style={{opacity:1, marginTop:0}}>
                    <thead>
                      <tr>
                        <th>Method</th>
                        <th style={{width:120}}>Floor (€)</th>
                        <th style={{width:120}}>Ceiling (€)</th>
                        <th style={{width:100}}>Sweep %</th>
                        <th style={{width:40}}></th>
                      </tr>
                    </thead>
                    <tbody>
                      {draft.methodOverrides.map((o, i) => (
                        <tr key={i}>
                          <td>
                            <select className="select input--sm" value={o.method} onChange={e=>updateOverride(i, "method", e.target.value)}>
                              {(draft.methods || []).map(m => (
                                <option key={m} value={m} disabled={m !== o.method && usedMethodIds.includes(m)}>
                                  {ALL_METHODS_BY_ID[m] || m}
                                </option>
                              ))}
                            </select>
                          </td>
                          <td><input type="number" className="input input--sm" value={o.floorEur}    onChange={e=>updateOverride(i, "floorEur",    parseInt(e.target.value || "0", 10))}/></td>
                          <td><input type="number" className="input input--sm" value={o.ceilingEur}  onChange={e=>updateOverride(i, "ceilingEur",  parseInt(e.target.value || "0", 10))}/></td>
                          <td><input type="number" className="input input--sm" value={o.sweepPct}    onChange={e=>updateOverride(i, "sweepPct",    parseInt(e.target.value || "0", 10))} min={50} max={100}/></td>
                          <td style={{textAlign:"right"}}>
                            <button className="btn btn--ghost btn--icon btn--sm" title="Remove override" onClick={()=>removeOverride(i)}><Icon name="trash" size={12}/></button>
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                )}
                <button className="btn btn--secondary btn--sm" style={{marginTop:8}} onClick={addOverride} disabled={availableForOverride.length === 0}>
                  <Icon name="plus" size={11}/> Add method override
                </button>
                {availableForOverride.length === 0 && (draft.methods || []).length > 0 && (
                  <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:6}}>Every supported method already has an override. Add more methods on the Coverage tab to extend.</div>
                )}
              </div>
            </div>
          )}

          {etab === "coverage" && (
            <div style={{display:"flex", flexDirection:"column", gap:18}}>
              <Explainer compact title="What this is, in plain English"
                bullets={[
                  <><strong>Supported methods</strong> — the payment instruments this PSP can process. The routing engine refuses to chain this PSP for a method it doesn't list here.</>,
                  <><strong>Geographies</strong> — countries the PSP operates in. The route's GEO scope is intersected with this list; mismatches are excluded at runtime.</>,
                  <><strong>Licenses · operations</strong> — under which operator licenses the PSP is authorised to process payments, and which operations (deposit / withdraw / refund) each license allows.</>,
                ]}>
                What this PSP can do and where it can do it. Used by the routing engine to filter incompatible PSPs out of a chain before they're tried.
              </Explainer>
              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>Supported payment methods<Tip>Tick every method this PSP can plausibly process. The routing engine uses this list to filter PSPs out of a route's chain when the route is scoped to a method this PSP doesn't support.</Tip></div>
                <div style={{display:"flex", flexWrap:"wrap", gap:6}}>
                  {ALL_METHOD_IDS.map(m => {
                    const on = (draft.methods || []).includes(m);
                    return (
                      <label key={m} style={{display:"inline-flex", alignItems:"center", gap:6, padding:"5px 10px", borderRadius:999, border: on ? "1px solid var(--primary, #1e40af)" : "1px solid var(--border-default)", background: on ? "color-mix(in oklab, var(--primary, #1e40af) 8%, white)" : "#fff", cursor:"pointer", fontSize:12}}>
                        <input type="checkbox" checked={on} onChange={()=>toggleArr("methods", m)} style={{accentColor:"var(--primary, #1e40af)"}}/>
                        {ALL_METHODS_BY_ID[m]}
                      </label>
                    );
                  })}
                </div>
              </div>

              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>Geographies</div>
                <div style={{fontSize:11.5, color:"var(--text-tertiary)", marginBottom:6}}>Comma-separated ISO country codes, or <code>any</code> for unrestricted.</div>
                <input className="input input--sm" style={{width:"100%"}} value={(draft.geos || []).join(", ")} onChange={e=>set("geos", e.target.value.split(",").map(s => s.trim()).filter(Boolean))} placeholder="DE, IT, FR, ES, +EU"/>
              </div>

              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8, display:"flex", alignItems:"center", gap:8}}>
                  Licenses · operations
                  <span style={{fontWeight:500, textTransform:"none", letterSpacing:0, color:"var(--text-tertiary)"}}>— per license, tick the operations the PSP is authorised to perform</span>
                </div>
                <div style={{display:"flex", flexDirection:"column", gap:8}}>
                  {ALL_LICENSES.map(name => {
                    const lic = (draft.licenses || []).find(l => l.name === name);
                    const enabled = !!lic;
                    return (
                      <div key={name} style={{display:"flex", alignItems:"center", gap:10, padding:"8px 12px", borderRadius:8, border:"1px solid var(--border-subtle)", background: enabled ? "#fff" : "var(--n-25)"}}>
                        <label style={{display:"flex", alignItems:"center", gap:8, minWidth:120, cursor:"pointer", fontSize:13, fontWeight:600}}>
                          <input type="checkbox" checked={enabled} onChange={()=>{
                            if (enabled) set("licenses", draft.licenses.filter(l => l.name !== name));
                            else         set("licenses", [...(draft.licenses || []), { name, ops: ["dep","wd"] }]);
                          }} style={{accentColor:"var(--primary, #1e40af)"}}/>
                          {name}
                        </label>
                        <div style={{flex:1, display:"flex", gap:6, flexWrap:"wrap", opacity: enabled ? 1 : 0.4}}>
                          {ALL_OPS.map(op => {
                            const opOn = enabled && lic.ops.includes(op);
                            return (
                              <label key={op} style={{display:"inline-flex", alignItems:"center", gap:5, padding:"3px 9px", borderRadius:999, border:"1px solid var(--border-default)", background: opOn ? "var(--ok-50, #d1fae5)" : "#fff", color: opOn ? "var(--ok-700, #065f46)" : "var(--text-secondary)", cursor: enabled ? "pointer" : "not-allowed", fontSize:11.5, fontWeight:600}}>
                                <input type="checkbox" disabled={!enabled} checked={opOn} onChange={()=>{
                                  set("licenses", draft.licenses.map(l => l.name === name ? { ...l, ops: l.ops.includes(op) ? l.ops.filter(o => o !== op) : [...l.ops, op] } : l));
                                }} style={{accentColor:"var(--primary, #1e40af)"}}/>
                                {OPS_LABEL[op]}
                              </label>
                            );
                          })}
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
            </div>
          )}

        </div>

        {/* Footer */}
        <div style={{padding:"14px 20px", borderTop:"1px solid var(--border-subtle)", background:"var(--n-25)", display:"flex", gap:8, alignItems:"center"}}>
          {!isNew && onDelete && (
            <button onClick={()=>{ if (confirm(`Remove provider "${draft.name}"? Routes referencing it will need a new chain.`)) onDelete(psp.id); }}
                    style={{height:36, padding:"0 12px", borderRadius:8, border:"1px solid var(--err-200)", background:"#fff", color:"var(--err-600)", fontWeight:600, fontSize:12.5, display:"inline-flex", alignItems:"center", gap:6, cursor:"pointer"}}>
              <Icon name="trash" size={12}/> Remove
            </button>
          )}
          <div style={{flex:1}}/>
          <button onClick={onCancel} className="btn btn--secondary btn--sm">Cancel</button>
          <button onClick={()=>onSave(draft)} disabled={!draft.name} className="btn btn--primary btn--sm">
            <Icon name="check" size={12}/> {isNew ? "Create provider" : "Save changes"}
          </button>
        </div>
      </div>
    </div>
  );
};

/* =========================================================================
   RouteEditor — full editable route. Chain reordering via ↑/↓, optional
   weighted traffic split with per-PSP weight inputs, trigger pill multi-
   select, ranking & mode selectors. Chain is the operator's ranking; if
   weighted, traffic splits proportionally instead of strict priority.
   ========================================================================= */
/* Single source of truth for "who is making this change" — used by the
   route version-history and the cascade-override audit fields. In v1
   we map the signed-in operator to the first OP_USERS entry. When auth
   ships, swap to the live session here. */
const currentOperator = () => {
  const u = (window.MOCK && window.MOCK.OP_USERS && window.MOCK.OP_USERS[0]) || null;
  if (!u) return { id:"system", name:"System", email:"system@iwakiri.io" };
  return {
    id: u.id,
    name: u.name,
    email: `${u.name.toLowerCase().replace(/\s+/g, ".")}@iwakiri.io`,
  };
};

/* Diff two route snapshots and return human-readable change lines.
   Used by the route version-history audit trail. Kept defensive — any
   structural mismatch falls through to "Route updated". */
const diffRoute = (prev, next, brands, psps) => {
  if (!prev) return ["Route created"];
  const lines = [];
  const brandName = (id) => (brands || []).find(b => b.id === id)?.name || id || "—";
  const pspName = (id) => (psps || []).find(p => p.id === id)?.name || id;

  const simple = [
    ["brand",          "Brand scope"],
    ["method",         "Method"],
    ["currency",       "Currency"],
    ["amount",         "Amount band"],
    ["status",         "Status"],
    ["mode",           "Execution mode"],
    ["ranking",        "Ranking mode"],
    ["cascadeTimeout", "Cascade timeout"],
  ];
  for (const [k, label] of simple) {
    if (prev[k] !== next[k]) lines.push(`${label}: ${next[k]} (was: ${prev[k]})`);
  }

  const arrDiff = (a, b) => {
    const A = new Set(a || []); const B = new Set(b || []);
    const added   = [...B].filter(x => !A.has(x));
    const removed = [...A].filter(x => !B.has(x));
    return { added, removed };
  };
  const geo = arrDiff(prev.geos, next.geos);
  if (geo.added.length || geo.removed.length) {
    const bits = [];
    if (geo.added.length)   bits.push(`+${geo.added.join(", ")}`);
    if (geo.removed.length) bits.push(`−${geo.removed.join(", ")}`);
    lines.push(`GEO: ${bits.join(" · ")}`);
  }
  const lic = arrDiff(prev.licenses, next.licenses);
  if (lic.added.length || lic.removed.length) {
    const bits = [];
    if (lic.added.length)   bits.push(`+${lic.added.join(", ")}`);
    if (lic.removed.length) bits.push(`−${lic.removed.join(", ")}`);
    lines.push(`Licenses: ${bits.join(" · ")}`);
  }
  const trg = arrDiff(prev.triggers, next.triggers);
  if (trg.added.length || trg.removed.length) {
    const bits = [];
    if (trg.added.length)   bits.push(`added "${trg.added.join(", ")}"`);
    if (trg.removed.length) bits.push(`removed "${trg.removed.join(", ")}"`);
    lines.push(`Cascade triggers: ${bits.join(" · ")}`);
  }

  const chainA = (prev.chain || []).map(pspName).join(" → ");
  const chainB = (next.chain || []).map(pspName).join(" → ");
  if (chainA !== chainB) lines.push(`Provider chain: ${chainB || "(empty)"} (was: ${chainA || "(empty)"})`);

  const wPrev = (prev.weights || []).join("/"), wNext = (next.weights || []).join("/");
  if (!!prev.weighted !== !!next.weighted) {
    lines.push(`Weighted traffic split: ${next.weighted ? "enabled" : "disabled"}`);
  } else if (next.weighted && wPrev !== wNext) {
    lines.push(`Weights: ${wNext} (was: ${wPrev || "(none)"})`);
  }

  const prevF = prev.factors || {}; const nextF = next.factors || {};
  const factorKeys = ["performance","cost","liquidity","health"];
  const factorFlips = factorKeys.filter(k => !!prevF[k] !== !!nextF[k]);
  if (factorFlips.length) {
    lines.push(`Ranking factors: ${factorFlips.map(k => `${k} ${nextF[k] ? "ON" : "OFF"}`).join(" · ")}`);
  }

  const prevPolicy = JSON.stringify((prev.cascadePolicy || ERROR_MATRIX).map(x => `${x.error}:${x.kind}`));
  const nextPolicy = JSON.stringify((next.cascadePolicy || ERROR_MATRIX).map(x => `${x.error}:${x.kind}`));
  if (prevPolicy !== nextPolicy) lines.push("Cascade policy: error/action rows updated");

  return lines.length ? lines : ["Route saved (no field changes)"];
};

const ROUTE_GEO_OPTIONS = ["DE","IT","ES","FR","UK","NL","PT","SE","PL","IE","CH","AT","BE","NO","BR","TR","CA","MX","IN"];

/* ============================================================
   HYBRID RANKING — scoring engine contract
   ============================================================

   Active factors contribute EQUALLY to a PSP's hybrid score. There
   are no per-factor weight sliders — the operator decides WHICH
   factors matter on a route, not HOW MUCH.

   Score per PSP = average of active factor scores (each 0–100)

   Factor definitions:
     Performance: success_rate_last_24h (0–100)
     Cost:        100 − effective_cost_rank_percentile
                  (cheapest PSP in the chain scores 100,
                   most expensive scores 0)
     Liquidity:   100 if wallet between floor and ceiling
                  linear scale toward 0 as wallet approaches floor
                  0 if wallet below floor → PSP excluded entirely
     Health:      100 if status = ONLINE
                  50  if status = DEGRADED
                  0   if status = OFFLINE → PSP excluded entirely

   Final ranking logic:
     1. Chain order (operator-defined) is the primary sort
     2. Factors break ties and react when a higher-chain PSP is
        unhealthy
     3. If all factors are OFF → behave as Manual mode (chain order
        only, no scoring)

   Examples:
     - All 4 factors ON  → each contributes 25% to the score
     - 2 factors ON      → each contributes 50% to the score
     - 1 factor ON       → that factor is 100% of the score
     - 0 factors ON      → Manual mode, chain order is absolute
   ============================================================ */
const ROUTING_FACTORS = [
  { key:"performance", label:"Performance",
    desc:"Score = success_rate_last_24h (0–100). Higher = boosted up the chain." },
  { key:"cost",        label:"Cost",
    desc:"Score = 100 − cost-rank percentile across the chain. Cheapest PSP scores 100." },
  { key:"liquidity",   label:"Liquidity",
    desc:"100 between floor and ceiling, linear toward 0 near the floor. Below floor = PSP excluded." },
  { key:"health",      label:"Technical health",
    desc:"ONLINE=100 · DEGRADED=50 · OFFLINE excludes the PSP from routing entirely." },
];

const RouteEditor = ({ route, brands, psps, activeBrand, onSave, onCancel, onDelete }) => {
  // When the top-right brand selector is locked to a specific brand, the
  // route's scope is forced to that brand — operator can't widen it back
  // to "any" or to a different brand from inside the modal.
  const brandLocked = !!(activeBrand && !activeBrand.isAll);
  const [draft, setDraft] = useState(() => {
    const d = JSON.parse(JSON.stringify(route));
    delete d._initialTab; // UI hint, never persisted
    if (brandLocked) d.brand = activeBrand.name;
    if (!d.cascadeTimeout) d.cascadeTimeout = "30s";
    if (!Array.isArray(d.geos))     d.geos = [];
    if (!Array.isArray(d.licenses)) d.licenses = [];
    // Back-compat: drop the legacy appliesTo block entirely. The Scope
    // tab's Brand field (draft.brand) is now the single source of truth
    // for brand binding.
    delete d.appliesTo;
    if (!d.factors) d.factors = { performance:true, cost:true, liquidity:true, health:true };
    // Each route owns its cascade policy (no global / inheritance).
    // Back-compat: old routes with cascadeOverride.{enabled, matrix} get
    // their matrix lifted onto the route directly; the override toggle
    // is dropped.
    if (!d.cascadePolicy) {
      d.cascadePolicy = d.cascadeOverride?.matrix
        ? d.cascadeOverride.matrix.map(e => ({ ...e }))
        : ERROR_MATRIX.map(e => ({ ...e }));
    }
    delete d.cascadeOverride;
    return d;
  });
  // Wizard steps — Scope + Routing are merged into a single step so the
  // operator picks the payment method and the provider chain on the same
  // page. This prevents the "I just picked a method but the next step
  // shows providers that don't support it" trap.
  const STEP_KEYS = ["scope_routing", "cascading", "review"];
  const STEP_LABELS = { scope_routing:"Scope & routing", cascading:"Cascading", review:"Review & Save" };
  // _initialTab values from the route page (history / cascading / scope /
  // routing) still open the modal on a sensible step.
  const initialStep = (() => {
    const t = route?._initialTab;
    if (t === "history") return "review";
    if (t === "cascading") return "cascading";
    if (t === "scope" || t === "routing" || t === "scope_routing") return "scope_routing";
    return "scope_routing";
  })();
  const [tab, setTab] = useState(initialStep);
  const isNew = !route.id;
  const set = (k, v) => setDraft(d => ({ ...d, [k]: v }));
  const setNested = (path, v) => setDraft(d => {
    const next = { ...d };
    const parts = path.split(".");
    let cur = next;
    for (let i = 0; i < parts.length - 1; i++) { cur[parts[i]] = { ...cur[parts[i]] }; cur = cur[parts[i]]; }
    cur[parts[parts.length - 1]] = v;
    return next;
  });

  const brandOptions = brandLocked
    ? [activeBrand.name]
    : ["any", ...brands.map(b => b.name)];
  const methodOptions = Object.values(ALL_METHODS_BY_ID);
  const currencyOptions = ["any", ...CURRENCIES];

  /* Compatibility check — each PSP in the chain must declare support for
     the route's chosen method (Coverage tab on the provider profile).
     Returns the first incompatible {psp, pspName} pair or null. */
  const compatIssue = (() => {
    const m = (draft.method || "").toLowerCase();
    const targetMethodId = Object.keys(ALL_METHODS_BY_ID).find(k => (ALL_METHODS_BY_ID[k] || "").toLowerCase() === m) || m;
    for (const pspId of (draft.chain || [])) {
      const psp = psps.find(p => p.id === pspId);
      if (!psp) continue;
      const supports = (psp.methods || []).some(mid => mid === targetMethodId || (ALL_METHODS_BY_ID[mid] || "").toLowerCase() === m);
      if (!supports) return { pspName: psp.name, methodName: draft.method };
    }
    return null;
  })();

  /* GEO compatibility lint — non-blocking. Surfaces every {psp, country}
     pair where the route's GEO scope includes a country that the PSP's
     Coverage tab does not list. We allow operators to save (they may know
     something the config doesn't); the routing engine will still exclude
     mismatches at runtime. */
  const geoWarnings = (() => {
    const wantedCountries = draft.geos || [];
    if (wantedCountries.length === 0) return []; // any country = no lint
    const COUNTRY_NAME = {
      DE:"Germany", IT:"Italy", ES:"Spain", FR:"France", UK:"United Kingdom",
      NL:"Netherlands", PT:"Portugal", SE:"Sweden", PL:"Poland", IE:"Ireland",
      CH:"Switzerland", AT:"Austria", BE:"Belgium", NO:"Norway", BR:"Brazil",
      TR:"Türkiye", CA:"Canada", MX:"Mexico", IN:"India",
    };
    const out = [];
    for (const pspId of (draft.chain || [])) {
      const psp = psps.find(p => p.id === pspId);
      if (!psp) continue;
      const pspGeos = psp.geos || [];
      const pspMatchesAny = pspGeos.some(g => g.toLowerCase() === "any");
      if (pspMatchesAny) continue;
      const missing = wantedCountries.filter(c => !pspGeos.includes(c));
      if (missing.length > 0) {
        out.push({
          pspName: psp.name,
          missingCodes: missing,
          missingNames: missing.map(c => COUNTRY_NAME[c] || c),
        });
      }
    }
    return out;
  })();

  const toggleArr = (path, val) => {
    const cur = path.split(".").reduce((o, k) => o && o[k], draft) || [];
    setNested(path, cur.includes(val) ? cur.filter(x => x !== val) : [...cur, val]);
  };

  const moveChain = (i, dir) => {
    const j = i + dir;
    if (j < 0 || j >= draft.chain.length) return;
    const chain = [...draft.chain]; const weights = [...(draft.weights || [])];
    [chain[i], chain[j]] = [chain[j], chain[i]];
    if (weights.length) [weights[i], weights[j]] = [weights[j], weights[i]];
    setDraft({ ...draft, chain, weights });
  };
  const removeFromChain = (i) => {
    const chain = draft.chain.filter((_, j) => j !== i);
    const weights = (draft.weights || []).filter((_, j) => j !== i);
    setDraft({ ...draft, chain, weights });
  };
  const addToChain = (pspId) => {
    if (draft.chain.includes(pspId)) return;
    const chain = [...draft.chain, pspId];
    const weights = draft.weighted
      ? [...(draft.weights || []), Math.max(0, 100 - (draft.weights || []).reduce((a, b) => a + b, 0))]
      : draft.weights || [];
    setDraft({ ...draft, chain, weights });
  };
  const setWeight = (i, v) => {
    const weights = [...(draft.weights || [])];
    while (weights.length < draft.chain.length) weights.push(0);
    weights[i] = Math.max(0, Math.min(100, parseInt(v || "0", 10)));
    setDraft({ ...draft, weights });
  };
  const toggleWeighted = (on) => {
    if (on) {
      const eq = Math.floor(100 / Math.max(1, draft.chain.length));
      const weights = draft.chain.map((_, i) => i === draft.chain.length - 1 ? 100 - eq * (draft.chain.length - 1) : eq);
      setDraft({ ...draft, weighted:true, weights });
    } else {
      setDraft({ ...draft, weighted:false });
    }
  };
  const toggleTrigger = (t) => {
    const cur = draft.triggers || [];
    set("triggers", cur.includes(t) ? cur.filter(x => x !== t) : [...cur, t]);
  };

  const weightSum = (draft.weights || []).reduce((a, b) => a + (b || 0), 0);
  const weightWarn = draft.weighted && draft.chain.length > 0 && weightSum !== 100;
  const availableToAdd = psps.filter(p => !draft.chain.includes(p.id));

  return (
    <div style={{position:"fixed", inset:0, background:"rgba(15,20,32,.45)", backdropFilter:"blur(4px)", zIndex:120, display:"grid", placeItems:"center"}} onClick={onCancel}>
      <div style={{width:760, maxHeight:"90vh", overflow:"hidden", background:"#fff", borderRadius:16, boxShadow:"0 30px 60px -15px rgba(15,20,32,.35)", display:"flex", flexDirection:"column"}} onClick={e=>e.stopPropagation()}>
        <div style={{padding:"16px 20px", borderBottom:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", gap:10}}>
          <div style={{width:30, height:30, borderRadius:9, background:"linear-gradient(135deg, var(--p-500), var(--p-700))", display:"grid", placeItems:"center", color:"#fff"}}><Icon name="rules" size={14}/></div>
          <div style={{flex:1}}>
            <div style={{fontWeight:700, fontSize:14.5}}>{isNew ? "Add route" : "Edit route"}</div>
            <div style={{fontSize:11.5, color:"var(--text-tertiary)"}}>Routing + cascading configured together · global or per-operator</div>
          </div>
          <button className="btn btn--ghost btn--icon btn--sm" onClick={onCancel}><Icon name="x" size={13}/></button>
        </div>

        {/* 4-step wizard — Scope → Routing → Cascading → Review & Save.
            Each step gates the Next button on its own validity; only the
            final step's "Create route" / "Save changes" enables when the
            whole draft is valid. Operators can jump back to any previously
            completed step by clicking its pill. */}
        {(() => {
          const currentIdx = STEP_KEYS.indexOf(tab);
          const isStepValid = (k) => {
            if (k === "scope_routing") return !!((draft.name || "").trim() && draft.brand && draft.method && draft.currency && (draft.amount || "").toString().trim())
              && draft.chain.length > 0 && !compatIssue && (!draft.weighted || !weightWarn);
            if (k === "cascading") return (draft.triggers || []).length > 0;
            return true;
          };
          const steps = [
            ["scope_routing", "Scope & routing", "rules"],
            ["cascading",     "Cascading",       "refresh"],
            ["review",        "Review & Save",   "check"],
          ];
          return (
            <div style={{display:"flex", padding:"14px 20px", borderBottom:"1px solid var(--border-subtle)", gap:8, alignItems:"center", background:"var(--n-25)"}}>
              {steps.map(([k, lab, icon], i) => {
                const idx = i;
                const isActive = tab === k;
                const isDone = idx < currentIdx && isStepValid(k);
                const reachable = idx <= currentIdx || (steps.slice(0, idx).every(([kk]) => isStepValid(kk)));
                return (
                  <React.Fragment key={k}>
                    <button type="button" disabled={!reachable && !isNew ? false : !reachable}
                      onClick={()=>{ if (reachable) setTab(k); }}
                      style={{
                        display:"inline-flex", alignItems:"center", gap:7,
                        padding:"6px 12px", borderRadius:999,
                        border: isActive ? "1px solid var(--p-500, #1e40af)" : (isDone ? "1px solid var(--ok-300, #6ee7b7)" : "1px solid var(--border-default)"),
                        background: isActive ? "color-mix(in oklab, var(--p-500, #1e40af) 10%, white)" : (isDone ? "var(--ok-50, #d1fae5)" : "#fff"),
                        color: isActive ? "var(--p-700, #1e3a8a)" : (isDone ? "var(--ok-700, #065f46)" : "var(--text-secondary)"),
                        fontSize:12, fontWeight:700,
                        cursor: reachable ? "pointer" : "not-allowed",
                        opacity: reachable ? 1 : 0.5,
                      }}>
                      <span style={{
                        display:"inline-grid", placeItems:"center",
                        width:18, height:18, borderRadius:999,
                        background: isActive ? "var(--p-500, #1e40af)" : (isDone ? "var(--ok-500, #10b981)" : "var(--n-75)"),
                        color: (isActive || isDone) ? "#fff" : "var(--text-tertiary)",
                        fontSize:10.5, fontWeight:800,
                      }}>{isDone ? "✓" : idx + 1}</span>
                      {lab}
                    </button>
                    {idx < steps.length - 1 && (
                      <span style={{width:18, height:1, background:"var(--border-default)"}}/>
                    )}
                  </React.Fragment>
                );
              })}
            </div>
          );
        })()}

        <div style={{padding:"18px 20px", overflow:"auto", flex:1, display:"flex", flexDirection:"column", gap:18}}>

          {/* ============================================================
              STEP 1 · SCOPE & ROUTING — brand · method · currency · amount
              · GEO · license, PLUS the provider chain. Merged so picking a
              method instantly filters the provider picker below.
              ============================================================ */}
          {tab === "scope_routing" && (
            <>
              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>Identity</div>
                <div>
                  <label className="form-label">
                    Route name
                    <Tip>A short label that identifies this route in the rules table, audit log, alarms, and reports. Pick something human-readable, e.g. <em>"IT · Visa · VIP band"</em>, <em>"All brands · Bank wire · &gt;25K"</em>, or <em>"BR · Pix · Geo-restricted"</em>. Required.</Tip>
                  </label>
                  <input className="input input--sm" value={draft.name || ""} onChange={e=>set("name", e.target.value)}
                    placeholder="e.g. IT · Visa · VIP band"
                    style={{maxWidth:520}}/>
                </div>
              </div>
              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>Scope — when does this route apply?</div>
                <div style={{display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap:12}}>
                  <div>
                    <label className="form-label">
                      Brand
                      <Tip>The brand (tenant) this route applies to. Pick "any" to share the route across every brand in this PayBO instance, or a specific brand to scope it to that tenant only.</Tip>
                      {brandLocked && (
                        <span title="Locked by the top-right brand selector"
                          style={{marginLeft:6, fontSize:9, fontWeight:700, padding:"1px 6px", borderRadius:999, background:"var(--n-75)", color:"var(--text-secondary)", letterSpacing:".05em", textTransform:"uppercase"}}>
                          Locked
                        </span>
                      )}
                    </label>
                    {brandLocked ? (
                      <div className="input input--sm" style={{display:"flex", alignItems:"center", gap:6, background:"var(--n-25)", color:"var(--text-primary)", fontWeight:600, cursor:"not-allowed"}}>
                        <Icon name="lock" size={11} style={{color:"var(--text-tertiary)"}}/>
                        {activeBrand.name}
                      </div>
                    ) : (
                      <select className="select input--sm" value={draft.brand} onChange={e=>set("brand", e.target.value)}>
                        {brandOptions.map(b => <option key={b} value={b}>{b}</option>)}
                      </select>
                    )}
                  </div>
                  <div>
                    <label className="form-label">
                      Method
                      <Tip>Payment instrument this route applies to (Visa, MasterCard, Apple Pay, SEPA, Trustly, Skrill, Crypto, etc.). One route per method per scope. Provider picker below filters in real-time to PSPs that support this method.</Tip>
                    </label>
                    <select className="select input--sm" value={draft.method} onChange={e=>set("method", e.target.value)}>
                      {methodOptions.map(m => <option key={m} value={m}>{m}</option>)}
                    </select>
                  </div>
                  <div>
                    <label className="form-label">
                      Currency
                      <Tip>Transaction currency this route applies to. Pick "any" to match any currency, or a specific ISO 4217 code (EUR, USD, GBP, BRL…) to scope it tighter.</Tip>
                    </label>
                    <select className="select input--sm" value={draft.currency} onChange={e=>set("currency", e.target.value)}>
                      {currencyOptions.map(c => <option key={c} value={c}>{c}</option>)}
                    </select>
                  </div>
                </div>
              </div>

              {/* Amount band — clearer than the free-form text. Pick "any"
                  or set min / max in the route's currency. The compact band
                  string is composed back to draft.amount so existing routes
                  keep working. */}
              {(() => {
                const raw = (draft.amount || "").toString().trim();
                const isAny = raw === "" || raw.toLowerCase() === "any";
                // Parse "0 – 5,000" / "0-5000" / "0 to 5,000" / "<= 5000" / ">= 1000" into min/max.
                const parse = () => {
                  if (isAny) return { min:"", max:"" };
                  const cleaned = raw.replace(/,/g, "").replace(/[–—]/g, "-");
                  const range = cleaned.match(/^(\d+(?:\.\d+)?)\s*(?:-|to)\s*(\d+(?:\.\d+)?)$/i);
                  if (range) return { min: range[1], max: range[2] };
                  const max = cleaned.match(/^<=?\s*(\d+(?:\.\d+)?)$/);
                  if (max) return { min:"0", max: max[1] };
                  const min = cleaned.match(/^>=?\s*(\d+(?:\.\d+)?)$/);
                  if (min) return { min: min[1], max:"" };
                  return { min:"", max:"" };
                };
                const cur = parse();
                const setBand = (min, max) => {
                  if ((!min || min === "0") && !max) { set("amount", "any"); return; }
                  if (!max) { set("amount", `≥ ${min || "0"}`); return; }
                  if (!min || min === "0") { set("amount", `0 – ${max}`); return; }
                  set("amount", `${min} – ${max}`);
                };
                const symbol = draft.currency && draft.currency !== "any" ? draft.currency : "any cur.";
                return (
                  <div>
                    <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:6, display:"flex", alignItems:"center", gap:6}}>
                      Amount band — which transaction sizes this route applies to
                      <Tip>Decides which transactions in the route's currency this route matches. "Any amount" matches everything. Otherwise enter a Min and / or Max in {symbol}. Leave Max blank for "Min and above"; leave Min blank for "up to Max". The most-specific band wins when routes overlap.</Tip>
                    </div>
                    <div style={{display:"flex", alignItems:"center", gap:10, flexWrap:"wrap"}}>
                      <div style={{display:"inline-flex", gap:0, borderRadius:8, border:"1px solid var(--border-default)", overflow:"hidden", background:"#fff"}}>
                        <button type="button" onClick={()=>set("amount", "any")}
                          style={{padding:"6px 14px", border:"none", background: isAny ? "var(--p-50)" : "transparent",
                            color: isAny ? "var(--p-700, #1e3a8a)" : "var(--text-secondary)",
                            fontSize:12, fontWeight:700, cursor:"pointer"}}>Any amount</button>
                        <span style={{width:1, background:"var(--border-default)"}}/>
                        <button type="button" onClick={()=>{ if (isAny) set("amount", "0 – 5000"); }}
                          style={{padding:"6px 14px", border:"none", background: !isAny ? "var(--p-50)" : "transparent",
                            color: !isAny ? "var(--p-700, #1e3a8a)" : "var(--text-secondary)",
                            fontSize:12, fontWeight:700, cursor:"pointer"}}>Custom range</button>
                      </div>
                      {!isAny && (
                        <>
                          <div style={{display:"inline-flex", alignItems:"center", gap:6}}>
                            <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Min</label>
                            <span style={{fontSize:11, color:"var(--text-tertiary)"}}>{symbol}</span>
                            <input type="number" min="0" step="100" placeholder="0"
                              className="input input--sm" value={cur.min}
                              onChange={e=>setBand(e.target.value, cur.max)}
                              style={{width:120, fontVariantNumeric:"tabular-nums"}}/>
                          </div>
                          <span style={{color:"var(--text-tertiary)"}}>—</span>
                          <div style={{display:"inline-flex", alignItems:"center", gap:6}}>
                            <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Max</label>
                            <span style={{fontSize:11, color:"var(--text-tertiary)"}}>{symbol}</span>
                            <input type="number" min="0" step="100" placeholder="unlimited"
                              className="input input--sm" value={cur.max}
                              onChange={e=>setBand(cur.min, e.target.value)}
                              style={{width:120, fontVariantNumeric:"tabular-nums"}}/>
                          </div>
                          <span style={{fontSize:11, color:"var(--text-tertiary)"}}>
                            Reads as: <strong style={{color:"var(--text-secondary)"}}>{draft.amount}</strong>
                          </span>
                        </>
                      )}
                    </div>
                  </div>
                );
              })()}

              {/* GEO (country) scope — multi-select. Empty = any country. */}
              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:6, display:"flex", alignItems:"center", justifyContent:"space-between"}}>
                  <span>GEO — countries this route applies to
                    <Tip>Player country (resolved from IP / KYC address). Pick one or more ISO codes to scope the route; leave empty to match any country. The routing engine will exclude any PSP whose Coverage tab does not list a selected country.</Tip>
                  </span>
                  {(draft.geos || []).length === 0 && <span style={{fontSize:10, fontWeight:600, color:"var(--text-tertiary)", letterSpacing:0, textTransform:"none"}}>Empty = any country</span>}
                </div>
                <div style={{display:"flex", flexWrap:"wrap", gap:6}}>
                  {ROUTE_GEO_OPTIONS.map(code => {
                    const on = (draft.geos || []).includes(code);
                    return (
                      <button key={code} type="button" onClick={()=>toggleArr("geos", code)}
                        style={{
                          padding:"3px 9px", borderRadius:999,
                          border:"1px solid " + (on ? "var(--p-500)" : "var(--border-default)"),
                          background: on ? "var(--p-50)" : "#fff",
                          color: on ? "var(--p-700)" : "var(--text-secondary)",
                          fontSize:11, fontWeight:700, fontFamily:"var(--font-mono)", cursor:"pointer",
                        }}>{code}</button>
                    );
                  })}
                </div>
              </div>

              {/* License scope — multi-select. Empty = any license. */}
              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:6, display:"flex", alignItems:"center", justifyContent:"space-between"}}>
                  <span>License — operator licenses this route applies to
                    <Tip>The regulatory license under which the transaction is processed (MGA, UKGC, ADM, ANJ, DGOJ, AAMS, etc.). Use this when the same brand operates under multiple licenses and you need a different chain per license.</Tip>
                  </span>
                  {(draft.licenses || []).length === 0 && <span style={{fontSize:10, fontWeight:600, color:"var(--text-tertiary)", letterSpacing:0, textTransform:"none"}}>Empty = any license</span>}
                </div>
                <div style={{display:"flex", flexWrap:"wrap", gap:6}}>
                  {ALL_LICENSES.map(name => {
                    const on = (draft.licenses || []).includes(name);
                    return (
                      <button key={name} type="button" onClick={()=>toggleArr("licenses", name)}
                        style={{
                          padding:"3px 9px", borderRadius:999,
                          border:"1px solid " + (on ? "var(--p-500)" : "var(--border-default)"),
                          background: on ? "var(--p-50)" : "#fff",
                          color: on ? "var(--p-700)" : "var(--text-secondary)",
                          fontSize:11, fontWeight:700, cursor:"pointer",
                        }}>{name}</button>
                    );
                  })}
                </div>
              </div>

              {/* ===== Routing section — provider chain, ranking, mode ===== */}
              <div style={{height:1, background:"var(--border-subtle)", margin:"4px 0"}}/>

              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8, display:"flex", alignItems:"center", gap:10}}>
                  Provider chain · ranking
                  <Tip>The ordered list of PSPs the routing engine will try. Position 1 is tried first; on a cascade trigger the engine moves to position 2, and so on. Reorder with the up/down arrows.</Tip>
                  <label style={{display:"inline-flex", alignItems:"center", gap:6, marginLeft:"auto", textTransform:"none", letterSpacing:0, fontWeight:500, fontSize:12, color:"var(--text-secondary)", cursor:"pointer"}}>
                    <input type="checkbox" checked={!!draft.weighted} onChange={e=>toggleWeighted(e.target.checked)} style={{accentColor:"var(--primary, #1e40af)"}}/>
                    Weighted traffic split
                    <Tip>Split traffic by percentages instead of strict priority. Example: 60% Stripe / 30% Adyen / 10% Worldpay. Weights must total 100%. On failure the cascade still follows chain order; if a weighted PSP is excluded for health/liquidity reasons, its share is redistributed proportionally to the healthy ones.</Tip>
                  </label>
                </div>

                {draft.chain.length === 0 ? (
                  <div style={{padding:"12px 14px", borderRadius:8, border:"1px dashed var(--border-default)", fontSize:12.5, color:"var(--text-tertiary)"}}>
                    No providers in chain yet. Add at least one below.
                  </div>
                ) : (
                  <div style={{display:"flex", flexDirection:"column", gap:6}}>
                    {draft.chain.map((pid, i) => {
                      const p = psps.find(x => x.id === pid);
                      const w = (draft.weights || [])[i] || 0;
                      return (
                        <div key={pid} style={{display:"flex", alignItems:"center", gap:8, padding:"8px 10px", borderRadius:8, border:"1px solid var(--border-default)", background:"#fff"}}>
                          <span style={{display:"inline-grid", placeItems:"center", width:22, height:22, borderRadius:999, background:"var(--primary, #1e40af)", color:"#fff", fontSize:11, fontWeight:700}}>{i + 1}</span>
                          <div style={{flex:1, minWidth:0}}>
                            <div style={{fontWeight:600, fontSize:13}}>{p ? p.name : pid}</div>
                            {p && <div style={{fontSize:11, color:"var(--text-tertiary)"}}>{p.kind} · {(p.methods || []).map(m => ALL_METHODS_BY_ID[m] || m).join(" · ") || "—"}</div>}
                          </div>
                          {draft.weighted && (
                            <div style={{display:"inline-flex", alignItems:"center", gap:4}}>
                              <input type="number" min={0} max={100} step={5} value={w} onChange={e=>setWeight(i, e.target.value)}
                                     style={{width:60, padding:"4px 6px", borderRadius:6, border:"1px solid var(--border-default)", fontSize:12, textAlign:"right"}}/>
                              <span style={{fontSize:12, color:"var(--text-tertiary)"}}>%</span>
                            </div>
                          )}
                          <button className="btn btn--ghost btn--icon btn--sm" disabled={i === 0} onClick={()=>moveChain(i, -1)} title="Move up"><Icon name="arrow_up" size={11}/></button>
                          <button className="btn btn--ghost btn--icon btn--sm" disabled={i === draft.chain.length - 1} onClick={()=>moveChain(i, +1)} title="Move down"><Icon name="arrow_down" size={11}/></button>
                          <button className="btn btn--ghost btn--icon btn--sm" onClick={()=>removeFromChain(i)} title="Remove"><Icon name="trash" size={11}/></button>
                        </div>
                      );
                    })}
                  </div>
                )}

                {/* Provider picker — PSPs that do not support the chosen
                    method are disabled (greyed and unselectable) so the
                    operator can never build a chain with an incompatible
                    PSP. The compatibility check mirrors compatIssue above. */}
                {availableToAdd.length > 0 && (() => {
                  const m = (draft.method || "").toLowerCase();
                  const targetMethodId = Object.keys(ALL_METHODS_BY_ID).find(k => (ALL_METHODS_BY_ID[k] || "").toLowerCase() === m) || m;
                  const supportsMethod = (psp) =>
                    (psp.methods || []).some(mid => mid === targetMethodId || (ALL_METHODS_BY_ID[mid] || "").toLowerCase() === m);
                  const sorted = [...availableToAdd].sort((a, b) => {
                    const sa = supportsMethod(a) ? 0 : 1;
                    const sb = supportsMethod(b) ? 0 : 1;
                    return sa - sb || a.name.localeCompare(b.name);
                  });
                  const compatibleCount = sorted.filter(supportsMethod).length;
                  return (
                    <div style={{marginTop:8, display:"flex", alignItems:"center", gap:8, flexWrap:"wrap"}}>
                      <select id="route-add-psp" className="select input--sm" defaultValue="" style={{flex:1, minWidth:240}}>
                        <option value="" disabled>Add a provider…</option>
                        {sorted.map(p => {
                          const ok = supportsMethod(p);
                          return (
                            <option key={p.id} value={p.id} disabled={!ok}>
                              {p.name} · {p.kind}{ok ? "" : `  (does not support ${draft.method})`}
                            </option>
                          );
                        })}
                      </select>
                      <button className="btn btn--secondary btn--sm" onClick={()=>{
                        const sel = document.getElementById("route-add-psp");
                        if (sel && sel.value) { addToChain(sel.value); sel.value = ""; }
                      }}><Icon name="plus" size={11}/> Add</button>
                      <span style={{fontSize:11, color:"var(--text-tertiary)"}}>
                        <strong style={{color:"var(--text-secondary)"}}>{compatibleCount}</strong> of {sorted.length} support <strong style={{color:"var(--text-secondary)"}}>{draft.method}</strong>
                      </span>
                    </div>
                  );
                })()}

                {draft.weighted && (
                  <div style={{marginTop:8, padding:"8px 10px", borderRadius:6,
                               background: weightWarn ? "var(--warn-50, #fef3c7)" : "var(--ok-50, #d1fae5)",
                               color:      weightWarn ? "#92400e"                : "var(--ok-700, #065f46)",
                               fontSize:12, fontWeight:600, display:"flex", alignItems:"center", gap:8}}>
                    <Icon name={weightWarn ? "alert" : "check"} size={11}/>
                    Weights total: {weightSum}% {weightWarn && "(must equal 100%)"}
                  </div>
                )}
              </div>

              <div style={{display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap:12}}>
                <div>
                  <label className="form-label">
                    Ranking mode
                    <Tip>How the chain order is interpreted. "manual" = use the order exactly as the operator set it. "hybrid" = use the operator order as the baseline, but adjust positions based on the Ranking factors below (performance / cost / liquidity / health) when two PSPs are tied or one is unhealthy.</Tip>
                  </label>
                  <select className="select input--sm" value={draft.ranking} onChange={e=>set("ranking", e.target.value)}>
                    <option value="manual">manual</option>
                    <option value="hybrid">hybrid</option>
                  </select>
                </div>
                <div>
                  <label className="form-label">
                    Execution mode
                    <Tip>How the cascade is executed on failure. "AUTO" = retry the next PSP automatically with no human intervention. "MANUAL" = pause the transaction as To-Confirm after the first failure; an operator must approve before the cascade continues.</Tip>
                  </label>
                  <select className="select input--sm" value={draft.mode} onChange={e=>set("mode", e.target.value)}>
                    <option value="AUTO">AUTO</option>
                    <option value="MANUAL">MANUAL</option>
                  </select>
                </div>
                <div>
                  <label className="form-label">
                    Status
                    <Tip>"active" = the engine evaluates this route. "paused" = the route is kept on file but never matched; transactions fall through to the next-most-specific rule (or to "any").</Tip>
                  </label>
                  <select className="select input--sm" value={draft.status} onChange={e=>set("status", e.target.value)}>
                    <option value="active">active</option>
                    <option value="paused">paused</option>
                  </select>
                </div>
              </div>

              {/* Ranking factors — only meaningful when ranking is hybrid.
                  Manual mode keeps the chain order strictly. */}
              {draft.ranking === "hybrid" && (
                <div>
                  <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>
                    Ranking factors — hybrid score inputs
                    <Tip>Inputs to the hybrid score. Each enabled factor contributes equally to a 0–100 score per PSP. The factors only adjust the operator's chain order when two PSPs are tied or one is unhealthy — they never override the operator's hard ranking.</Tip>
                  </div>
                  <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:8}}>
                    {ROUTING_FACTORS.map(f => {
                      const on = draft.factors?.[f.key] !== false;
                      return (
                        <label key={f.key} style={{
                          display:"flex", alignItems:"flex-start", gap:10,
                          padding:"10px 12px", borderRadius:8,
                          border:"1px solid " + (on ? "color-mix(in oklab, var(--p-500) 22%, transparent)" : "var(--border-default)"),
                          background: on ? "color-mix(in oklab, var(--p-500) 4%, white)" : "#fff",
                          cursor:"pointer",
                        }}>
                          <input type="checkbox" checked={on}
                            onChange={()=>setNested(`factors.${f.key}`, !on)}
                            style={{accentColor:"var(--p-500)", marginTop:2}}/>
                          <div>
                            <div style={{fontSize:12.5, fontWeight:700, color:"var(--text-primary)"}}>{f.label}</div>
                            <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:2, lineHeight:1.45}}>{f.desc}</div>
                          </div>
                        </label>
                      );
                    })}
                  </div>
                  <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:8, lineHeight:1.5}}>
                    The chain order you set above is always honored as the operator's preference. Factors only adjust positions when two PSPs are tied or one is unhealthy — they never override the operator's hard ranking.
                  </div>
                </div>
              )}

              <div style={{fontSize:11, color:"var(--text-tertiary)", lineHeight:1.5}}>
                <strong>Strict ranking</strong> = always try PSP 1 first; cascade to 2, then 3 on failure. <strong>Weighted</strong> = split traffic by percentages (e.g. 60/30/10). On failure, cascade still uses chain order. If a PSP is unhealthy or below liquidity floor, its weight is redistributed proportionally to the remaining healthy PSPs.
              </div>
            </>
          )}

          {/* ============================================================
              TAB 3 · CASCADING — triggers + timeout + per-route policy
              override (defaults to global)
              ============================================================ */}
          {tab === "cascading" && (
            <>
              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>
                  Cascade triggers
                  <Tip>Provider-side outcomes that cause the engine to retry on the next PSP in the chain. Player-side outcomes (insufficient funds, fraud) should not be triggers — they are handled by the policy table below.</Tip>
                </div>
                <div style={{display:"flex", flexWrap:"wrap", gap:6}}>
                  {TRIGGER_OPTIONS.map(t => {
                    const on = (draft.triggers || []).includes(t);
                    const explain = TRIGGER_EXPLANATIONS[t] || "";
                    return (
                      <label key={t}
                        style={{display:"inline-flex", alignItems:"center", gap:6, padding:"4px 9px", borderRadius:999, border: on ? "1px solid var(--primary, #1e40af)" : "1px solid var(--border-default)", background: on ? "color-mix(in oklab, var(--primary, #1e40af) 8%, white)" : "#fff", cursor:"pointer", fontSize:11.5, fontFamily:"var(--font-mono)"}}>
                        <input type="checkbox" checked={on} onChange={()=>toggleTrigger(t)} style={{accentColor:"var(--primary, #1e40af)"}}/>
                        {t}
                        <span title={explain}
                          style={{display:"inline-grid", placeItems:"center", width:13, height:13, borderRadius:999, background: on ? "rgba(30,64,175,.18)" : "var(--n-75)", color: on ? "var(--primary-dark, #1e3a8a)" : "var(--text-tertiary)", fontSize:9, fontWeight:800, cursor:"help", fontFamily:"system-ui"}}>?</span>
                      </label>
                    );
                  })}
                </div>

                {/* Per-trigger explainer panel — always visible so the tech
                    team can read all triggers in one shot, not just hover one
                    at a time. */}
                <div style={{marginTop:10, padding:"10px 12px", borderRadius:8, background:"var(--n-25)", border:"1px solid var(--border-subtle)"}}>
                  <div style={{fontSize:10.5, fontWeight:700, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em", marginBottom:6}}>What each trigger means · how it's detected</div>
                  <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:6}}>
                    {TRIGGER_OPTIONS.map(t => (
                      <div key={t} style={{fontSize:11, color:"var(--text-secondary)", lineHeight:1.5}}>
                        <span style={{fontFamily:"var(--font-mono)", fontWeight:700, color:"var(--text-primary)"}}>{t}</span>
                        <span style={{color:"var(--text-tertiary)"}}> — </span>
                        {TRIGGER_EXPLANATIONS[t]}
                      </div>
                    ))}
                  </div>
                </div>

                {(() => {
                  // Cascade-timeout field — preset dropdown OR a custom
                  // number + unit input, just like the Alarm window picker.
                  const presetSet = new Set(CASCADE_TIMEOUTS);
                  const curVal = draft.cascadeTimeout || "30s";
                  const isPreset = presetSet.has(curVal);
                  const parse = (v) => {
                    const m = /^(\d+)\s*(s|sec|m|min|h|hour)?$/i.exec(String(v || ""));
                    if (!m) return { n: 30, unit: "s" };
                    const n = parseInt(m[1], 10) || 30;
                    const u = (m[2] || "s").toLowerCase();
                    const unit = u.startsWith("h") ? "h" : u.startsWith("m") ? "m" : "s";
                    return { n, unit };
                  };
                  const compose = (n, unit) => {
                    const v = Math.max(1, parseInt(n, 10) || 1);
                    return `${v}${unit}`;
                  };
                  const cur = parse(curVal);
                  return (
                    <div style={{marginTop:12, display:"flex", alignItems:"center", gap:10, padding:"8px 12px", border:"1px solid var(--border-default)", borderRadius:8, background:"var(--n-25)", flexWrap:"wrap"}}>
                      <span style={{fontSize:12, fontWeight:600, color:"var(--text-secondary)"}}>
                        Cascade if PSP doesn't respond within:
                        <Tip>Wall-clock timeout the engine waits for a PSP response before declaring a timeout and moving to the next provider. Pick a preset, or choose "Custom…" to set a precise value in seconds / minutes / hours.</Tip>
                      </span>
                      {isPreset ? (
                        <select className="select input--sm" style={{minWidth:200}}
                          value={curVal}
                          onChange={e => {
                            const v = e.target.value;
                            if (v === "__custom") {
                              set("cascadeTimeout", compose(cur.n, cur.unit));
                            } else {
                              set("cascadeTimeout", v);
                            }
                          }}>
                          {CASCADE_TIMEOUTS.map(t => (
                            <option key={t} value={t}>{t === "Never" ? "Never (don't cascade on timeout)" : t}</option>
                          ))}
                          <option value="__custom">Custom…</option>
                        </select>
                      ) : (
                        <div style={{display:"flex", gap:6, alignItems:"center"}}>
                          <input className="input input--sm" type="number" min="1" max="9999"
                            value={cur.n}
                            onChange={e => set("cascadeTimeout", compose(e.target.value, cur.unit))}
                            style={{width:80, fontVariantNumeric:"tabular-nums"}}/>
                          <select className="select input--sm"
                            value={cur.unit}
                            onChange={e => set("cascadeTimeout", compose(cur.n, e.target.value))}
                            style={{minWidth:90}}>
                            <option value="s">sec</option>
                            <option value="m">min</option>
                            <option value="h">hr</option>
                          </select>
                          <button type="button" title="Back to preset values"
                            onClick={() => set("cascadeTimeout", "30s")}
                            style={{
                              width:26, height:26, padding:0, border:"1px solid var(--border-default)",
                              borderRadius:6, background:"#fff", color:"var(--text-tertiary)",
                              cursor:"pointer", display:"grid", placeItems:"center", flexShrink:0,
                            }}>
                            <Icon name="x" size={11}/>
                          </button>
                        </div>
                      )}
                    </div>
                  );
                })()}
                <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:6}}>Picked triggers fire the cascade to the next PSP. Player-side errors (insufficient funds, fraud) stop the cascade per the policy below.</div>
              </div>

              {/* Cascade policy — every route owns its own matrix. No
                  global cascade policy lives on the page anymore: the
                  modal is the single source of truth. */}
              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>
                  Cascade policy · Error → action
                  <Tip>For every error type, decide what the engine should do: "Retry next provider" continues the cascade, "Stop" ends the attempt and surfaces the failure to the player. Player-side issues (insufficient funds, fraud) should usually Stop; provider-side issues (timeout, 5xx, low liquidity) should usually Retry.</Tip>
                </div>
                <table className="paybo-v2__table" style={{opacity:1, marginTop:0}}>
                  <thead>
                    <tr><th>Error type</th><th>Action</th><th>Why</th></tr>
                  </thead>
                  <tbody>
                    {(draft.cascadePolicy || ERROR_MATRIX).map((e, i) => (
                      <tr key={e.error}>
                        <td style={{fontWeight:600, fontSize:12}}>{e.error}</td>
                        <td>
                          <select className="select input--sm" value={e.kind}
                            onChange={ev => {
                              const next = (draft.cascadePolicy || ERROR_MATRIX).map((row, j) => j === i ? {
                                ...row,
                                kind: ev.target.value,
                                label: ev.target.value === "retry" ? "Retry next provider" : "Stop · player issue",
                              } : row);
                              set("cascadePolicy", next);
                            }}
                            style={{minWidth:140}}>
                            <option value="retry">Retry next provider</option>
                            <option value="stop">Stop</option>
                          </select>
                        </td>
                        <td style={{fontSize:11.5, color:"var(--text-tertiary)"}}>{e.why}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
                <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:8, lineHeight:1.5}}>
                  Player-side errors (insufficient funds, fraud) should usually stay on <strong>Stop</strong> — retrying just hides the signal. Provider-side errors (timeout, 5xx, low liquidity) usually want <strong>Retry next provider</strong>.
                </div>
              </div>
            </>
          )}

          {/* ============================================================
              STEP 4 · REVIEW & SAVE — summary of every choice the operator
              made in the previous steps, plus (for existing routes) the
              read-only audit trail. Restore-to-version ships in v2.
              ============================================================ */}
          {tab === "review" && (
            <>
              <div>
                <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>Review your route</div>
                <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:10, marginBottom:14}}>
                  {(() => {
                    const chain = draft.chain || [];
                    const fmtChain = chain.length === 0 ? "—" : chain.map((pid, i) => {
                      const p = psps.find(x => x.id === pid);
                      const nm = p ? p.name : pid;
                      const w = draft.weighted ? ` (${(draft.weights || [])[i] || 0}%)` : "";
                      return `${i+1}. ${nm}${w}`;
                    }).join("  ·  ");
                    const cards = [
                      ["Scope", [
                        ["Brand", draft.brand],
                        ["Method", draft.method],
                        ["Currency", draft.currency],
                        ["Amount band", draft.amount || "any"],
                        ["GEO", (draft.geos || []).length ? draft.geos.join(" · ") : "any country"],
                        ["License", (draft.licenses || []).length ? draft.licenses.join(" · ") : "any license"],
                      ]],
                      ["Routing", [
                        ["Provider chain", fmtChain],
                        ["Ranking mode", draft.ranking],
                        ["Execution mode", draft.mode],
                        ["Status", draft.status],
                        ["Weighted split", draft.weighted ? "yes" : "no"],
                      ]],
                      ["Cascading", [
                        ["Triggers", (draft.triggers || []).join(" · ") || "—"],
                        ["Timeout", draft.cascadeTimeout || "—"],
                        ["Custom policy", (() => {
                          const def = JSON.stringify(ERROR_MATRIX.map(x => `${x.error}:${x.kind}`));
                          const mine = JSON.stringify((draft.cascadePolicy || ERROR_MATRIX).map(x => `${x.error}:${x.kind}`));
                          return def === mine ? "no (default)" : "yes (override)";
                        })()],
                      ]],
                    ];
                    return cards.map(([title, rows]) => (
                      <div key={title} style={{padding:"12px 14px", border:"1px solid var(--border-default)", borderRadius:10, background:"#fff"}}>
                        <div style={{fontSize:11, fontWeight:700, textTransform:"uppercase", letterSpacing:".05em", color:"var(--p-700, #1e3a8a)", marginBottom:6}}>{title}</div>
                        {rows.map(([k, v]) => (
                          <div key={k} style={{display:"flex", gap:8, fontSize:12, padding:"2px 0", color:"var(--text-secondary)"}}>
                            <span style={{minWidth:110, color:"var(--text-tertiary)"}}>{k}</span>
                            <span style={{fontWeight:600, color:"var(--text-primary)", flex:1, wordBreak:"break-word"}}>{v || "—"}</span>
                          </div>
                        ))}
                      </div>
                    ));
                  })()}
                </div>
                <div style={{fontSize:12, color:"var(--text-secondary)", padding:"10px 12px", borderRadius:8, background:"var(--n-25)", border:"1px solid var(--border-subtle)", lineHeight:1.55}}>
                  {isNew
                    ? <>Double-check the choices above and click <strong>Create route</strong>. You can edit anything later — every save is recorded in the audit log.</>
                    : <>Review the changes and click <strong>Save changes</strong>. A new entry will be appended to the audit log with the diff and your operator id.</>}
                </div>
              </div>

              {!isNew && (
                <div>
                  <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8, marginTop:6}}>Route version history</div>
              {(() => {
                const list = (draft.history || []).slice().sort((a, b) => b.ts - a.ts);
                if (list.length === 0) {
                  return (
                    <div style={{padding:"14px 16px", border:"1px dashed var(--border-default)", borderRadius:8, fontSize:12.5, color:"var(--text-tertiary)"}}>
                      No saved versions yet. The first entry is recorded when this route is saved.
                    </div>
                  );
                }
                const fmtTs = (ts) => new Date(ts).toLocaleString("en-GB", { day:"2-digit", month:"2-digit", year:"numeric", hour:"2-digit", minute:"2-digit" }).replace(",", " ·");
                return (
                  <div style={{display:"flex", flexDirection:"column", gap:0, borderLeft:"2px solid var(--border-subtle)", paddingLeft:14}}>
                    {list.map((h, i) => (
                      <div key={`${h.ts}-${i}`} style={{position:"relative", padding:"8px 0 14px", borderBottom: i < list.length - 1 ? "1px dashed var(--border-subtle)" : "none"}}>
                        <span style={{position:"absolute", left:-21, top:13, width:10, height:10, borderRadius:999, background:"var(--p-500, #1e40af)", border:"2px solid #fff", boxShadow:"0 0 0 1px var(--border-default)"}}/>
                        <div style={{display:"flex", alignItems:"baseline", gap:8, marginBottom:4, flexWrap:"wrap"}}>
                          <span style={{fontSize:12, fontWeight:700, color:"var(--text-primary)", fontVariantNumeric:"tabular-nums"}}>{fmtTs(h.ts)}</span>
                          <span style={{fontSize:11, color:"var(--text-tertiary)"}}>·</span>
                          <span style={{fontSize:12, color:"var(--text-secondary)", fontWeight:600}}>{h.user_email || h.user_id || "system"}</span>
                        </div>
                        <ul style={{margin:0, paddingLeft:18, fontSize:12, color:"var(--text-secondary)", lineHeight:1.55}}>
                          {(h.change_summary || []).map((c, j) => (
                            <li key={j} style={{marginBottom:2}}>{c}</li>
                          ))}
                        </ul>
                      </div>
                    ))}
                  </div>
                );
              })()}
                  <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:12, lineHeight:1.5}}>
                    Each save appends a new entry with the actor, the timestamp (minute precision), the diff against the previous version, and a full JSON snapshot of the route for later restore. Read-only in v1.
                  </div>
                </div>
              )}
            </>
          )}

          {/* Inline compatibility error — always shown when present,
              regardless of the active tab, because it blocks save. */}
          {compatIssue && (
            <div style={{padding:"10px 12px", borderRadius:8, background:"var(--err-50, #fee2e2)", border:"1px solid var(--err-200, #fecaca)", display:"flex", alignItems:"center", gap:10}}>
              <Icon name="alert" size={14} style={{color:"var(--err-600, #dc2626)"}}/>
              <div style={{fontSize:12.5, color:"var(--err-700, #991b1b)", lineHeight:1.5}}>
                <strong>{compatIssue.pspName}</strong> does not support method <strong>{compatIssue.methodName}</strong>. Remove or change the PSP to save this route.
              </div>
            </div>
          )}

          {/* GEO lint — non-blocking warning. Shown on every tab. */}
          {geoWarnings.length > 0 && (
            <div style={{padding:"10px 12px", borderRadius:8, background:"var(--warn-50, #fef3c7)", border:"1px solid var(--warn-200, #fde68a)", display:"flex", alignItems:"flex-start", gap:10}}>
              <Icon name="alert" size={14} style={{color:"var(--warn-700, #92400e)", marginTop:2}}/>
              <div style={{fontSize:12.5, color:"var(--warn-700, #92400e)", lineHeight:1.5}}>
                {geoWarnings.map((w, i) => (
                  <div key={w.pspName} style={{marginBottom: i < geoWarnings.length - 1 ? 4 : 0}}>
                    <strong>{w.pspName}</strong> does not list {w.missingNames.join(", ")} in its supported geographies. Routing engine may exclude this PSP at runtime. Verify provider coverage or remove {w.missingNames.length === 1 ? w.missingNames[0] : "the unsupported countries"} from this route's scope.
                  </div>
                ))}
              </div>
            </div>
          )}
        </div>

        {(() => {
          const currentIdx = STEP_KEYS.indexOf(tab);
          const isLast = currentIdx === STEP_KEYS.length - 1;
          const isFirst = currentIdx === 0;
          const validate = (k) => {
            if (k === "scope_routing") return !!((draft.name || "").trim() && draft.brand && draft.method && draft.currency && (draft.amount || "").toString().trim())
              && draft.chain.length > 0 && !compatIssue && (!draft.weighted || !weightWarn);
            if (k === "cascading") return (draft.triggers || []).length > 0;
            return true;
          };
          const currentValid = validate(tab);
          const allValid = STEP_KEYS.slice(0, -1).every(validate);
          const stepHint = !currentValid
            ? (tab === "scope_routing"
                ? (compatIssue
                    ? "Fix the provider compatibility issue to continue."
                    : !(draft.name || "").trim()
                      ? "Give the route a name first."
                      : (!draft.brand || !draft.method || !draft.currency || !(draft.amount || "").toString().trim())
                      ? "Fill brand · method · currency · amount band, then add at least one provider."
                      : (draft.chain.length === 0
                          ? "Add at least one provider to the chain."
                          : "Weighted split must total 100%."))
            :  tab === "cascading" ? "Pick at least one cascade trigger to continue."
            :  "")
            : "";
          return (
            <div style={{padding:"14px 20px", borderTop:"1px solid var(--border-subtle)", background:"var(--n-25)", display:"flex", gap:8, alignItems:"center"}}>
              {!isNew && onDelete && (
                <button onClick={()=>{ if (confirm("Delete this route?")) onDelete(route.id); }}
                        style={{height:36, padding:"0 12px", borderRadius:8, border:"1px solid var(--err-200)", background:"#fff", color:"var(--err-600)", fontWeight:600, fontSize:12.5, display:"inline-flex", alignItems:"center", gap:6, cursor:"pointer"}}>
                  <Icon name="trash" size={12}/> Delete
                </button>
              )}
              {stepHint && (
                <span style={{fontSize:11.5, color:"var(--warn-700, #92400e)", display:"inline-flex", alignItems:"center", gap:4}}>
                  <Icon name="alert" size={11}/> {stepHint}
                </span>
              )}
              <div style={{flex:1}}/>
              <button onClick={onCancel} className="btn btn--secondary btn--sm">Cancel</button>
              {!isFirst && (
                <button onClick={()=>setTab(STEP_KEYS[currentIdx - 1])} className="btn btn--secondary btn--sm">
                  <Icon name="arrow_up" size={11} style={{transform:"rotate(-90deg)"}}/> Back
                </button>
              )}
              {!isLast && (
                <button onClick={()=>{ if (currentValid) setTab(STEP_KEYS[currentIdx + 1]); }}
                  disabled={!currentValid}
                  className="btn btn--primary btn--sm">
                  Next <Icon name="arrow_up" size={11} style={{transform:"rotate(90deg)"}}/>
                </button>
              )}
              {isLast && (
                <button onClick={()=>{ if (!allValid || compatIssue) return; onSave(draft); }}
                  disabled={!allValid || !!compatIssue}
                  className="btn btn--primary btn--sm">
                  <Icon name="check" size={12}/> {isNew ? "Create route" : "Save changes"}
                </button>
              )}
            </div>
          );
        })()}
      </div>
    </div>
  );
};

/* =========================================================================
   AlarmEditor — pick a metric (grouped, with description + source page),
   set op / threshold / window with metric-aware hints, choose severity,
   then fan-out to one or more destinations (multi-select per channel).
   ========================================================================= */
const AlarmEditor = ({ alarm, destinations, brands, brandLocked, activeBrand, onSave, onCancel, onDelete, onTestFire, onAddDestination, onEditDestination, onToggleDestination }) => {
  const [draft, setDraft] = useState(() => {
    const d = JSON.parse(JSON.stringify(alarm));
    if (!Array.isArray(d.destinations)) d.destinations = [];
    if (!Array.isArray(d.scopeBrands)) d.scopeBrands = brandLocked ? [activeBrand.name] : ["any"];
    return d;
  });
  const toggleBrand = (name) => {
    const cur = draft.scopeBrands || ["any"];
    if (name === "any") {
      setDraft(d => ({ ...d, scopeBrands: ["any"] }));
      return;
    }
    // Remove "any" sentinel as soon as a specific brand is picked.
    const base = cur.filter(x => x !== "any");
    const next = base.includes(name) ? base.filter(x => x !== name) : [...base, name];
    setDraft(d => ({ ...d, scopeBrands: next.length === 0 ? ["any"] : next }));
  };
  const isNew = !alarm.id;
  const set = (k, v) => setDraft(d => ({ ...d, [k]: v }));
  const toggleDest = (id) => {
    const cur = draft.destinations || [];
    set("destinations", cur.includes(id) ? cur.filter(x => x !== id) : [...cur, id]);
  };
  const metric = ALARM_METRIC_BY_KEY[draft.metric] || ALARM_METRICS[0];

  // Channels derived from selected destinations — kept on the alarm row
  // so the rules table can render channel chips without re-resolving.
  const derivedChannels = Array.from(new Set(
    (draft.destinations || []).map(id => destinations.find(d => d.id === id)?.channel).filter(Boolean)
  ));

  // Switching metric pulls in the suggested op/threshold/window so the
  // editor starts in a meaningful state instead of forcing the operator
  // to guess what an "approval_rate" threshold should look like.
  const pickMetric = (key) => {
    const m = ALARM_METRIC_BY_KEY[key];
    if (!m) return;
    setDraft(d => ({
      ...d,
      metric: key,
      op: d.op || m.suggestedOp,
      threshold: d.threshold || m.suggestedThreshold,
      window: d.window || m.suggestedWindow,
    }));
  };

  const previewPretty = `${metric.label} ${draft.op} ${draft.threshold || "?"}${metric.unit && metric.unit !== "(event)" ? " " + metric.unit : ""}${draft.window && draft.window !== "—" ? ` over ${draft.window}` : ""}`;

  return (
    <div style={{position:"fixed", inset:0, background:"rgba(15,20,32,.45)", backdropFilter:"blur(4px)", zIndex:120, display:"grid", placeItems:"center"}} onClick={onCancel}>
      <div style={{width:640, maxHeight:"90vh", overflow:"auto", background:"#fff", borderRadius:16, boxShadow:"0 30px 60px -15px rgba(15,20,32,.35)"}} onClick={e=>e.stopPropagation()}>
        <div style={{padding:"16px 20px", borderBottom:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", gap:10}}>
          <div style={{width:30, height:30, borderRadius:9, background:"linear-gradient(135deg, var(--p-500), var(--p-700))", display:"grid", placeItems:"center", color:"#fff"}}><Icon name="bell" size={14}/></div>
          <div style={{flex:1}}>
            <div style={{fontWeight:700, fontSize:14.5}}>{isNew ? "Add alarm" : "Edit alarm"}</div>
            <div style={{fontSize:11.5, color:"var(--text-tertiary)"}}>Metric threshold + delivery destinations</div>
          </div>
          <button className="btn btn--ghost btn--icon btn--sm" onClick={onCancel}><Icon name="x" size={13}/></button>
        </div>

        <div style={{padding:"18px 20px", display:"flex", flexDirection:"column", gap:18}}>
          <Explainer compact title="What this is, in plain English"
            bullets={[
              <><strong>Metric</strong> — what the engine measures (decline rate, queue depth, tx count, PSP wallet…). Switching the metric pre-fills sensible defaults for op / threshold / window.</>,
              <><strong>Condition</strong> — operator (&gt;, &lt;, ≥, ≤, =) + threshold + rolling window. The alarm fires when the metric crosses the threshold in that window.</>,
              <><strong>Severity</strong> — drives the alarm color and the badge in the inbox; critical-severity alarms surface as a top-bar banner until acknowledged.</>,
              <><strong>Brand scope</strong> — pick "All brands" or one or more specific brands. Useful when one brand has a different SLA than the rest.</>,
              <><strong>Destinations</strong> — where the alarm gets posted. Email / Slack / Telegram / WhatsApp / SMS / webhook / in-app. Fan-out happens in parallel.</>,
            ]}>
            An alarm is an early warning. It fires when a metric crosses a threshold (or when a single event matches a pattern) and fans out to the destinations below so the operator can act <em>before</em> the problem becomes a customer complaint.
          </Explainer>
          <div>
            <label className="form-label">Name<Tip>Short human-readable label that shows up in the rules table, in the activity log, and in the alarm-fired notifications. Pick a name that tells you what fired without needing context.</Tip></label>
            <input className="input input--sm" value={draft.name} onChange={e=>set("name", e.target.value)} placeholder="Decline rate spike"/>
          </div>

          <div>
            <div style={{fontSize:11, fontWeight:600, textTransform:"uppercase", letterSpacing:".05em", color:"var(--text-tertiary)", marginBottom:8}}>Condition</div>
            <div style={{display:"grid", gridTemplateColumns:"1fr", gap:8, marginBottom:8}}>
              <div>
                <label className="form-label">Metric</label>
                <select className="select input--sm" value={draft.metric} onChange={e=>pickMetric(e.target.value)}>
                  {Object.entries(ALARM_METRIC_GROUPS).map(([group, list]) => (
                    <optgroup key={group} label={group}>
                      {list.map(m => <option key={m.key} value={m.key}>{m.label}</option>)}
                    </optgroup>
                  ))}
                </select>
              </div>
            </div>

            {/* Metric explainer — what it is, where the value comes from. */}
            <div style={{padding:"10px 12px", border:"1px solid var(--border-default)", borderRadius:8, background:"var(--n-25)", marginBottom:10}}>
              <div style={{fontSize:12.5, color:"var(--text-secondary)", lineHeight:1.55}}>{metric.description}</div>
              <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:6, display:"flex", flexWrap:"wrap", gap:10}}>
                <span><strong style={{color:"var(--text-secondary)"}}>Unit:</strong> {metric.unit}</span>
                <span><strong style={{color:"var(--text-secondary)"}}>Source:</strong> {metric.sourcePage}</span>
              </div>
            </div>

            {metric.requiresMethod && (
              <div style={{marginBottom:10}}>
                <label className="form-label">Scope · payment method</label>
                <select className="select input--sm" value={draft.scopeMethod || "visa"} onChange={e=>set("scopeMethod", e.target.value)}>
                  {ALL_METHOD_IDS.map(id => <option key={id} value={id}>{ALL_METHODS_BY_ID[id]}</option>)}
                </select>
              </div>
            )}

            <div style={{display:"grid", gridTemplateColumns:"1fr 1.5fr 1fr", gap:8, alignItems:"end"}}>
              <div>
                <label className="form-label">Operator</label>
                <select className="select input--sm" value={draft.op} onChange={e=>set("op", e.target.value)}>
                  {ALARM_OPS.map(o => <option key={o} value={o}>{o}</option>)}
                </select>
              </div>
              <div>
                <label className="form-label">Threshold <span style={{color:"var(--text-tertiary)", fontWeight:500}}>· {metric.unit}</span></label>
                <input className="input input--sm" value={draft.threshold} onChange={e=>set("threshold", e.target.value)} placeholder={metric.suggestedThreshold}/>
              </div>
              <div>
                <label className="form-label">Window</label>
                {(() => {
                  // The window field stores a compact string like "—" | "5m"
                  // | "1h" | "24h" | "3d". When the operator picks Custom,
                  // we swap the select for a number + unit pair that writes
                  // back into the same compact format.
                  const presetSet = new Set(ALARM_WINDOWS);
                  const isPreset = presetSet.has(draft.window);
                  const parseWin = (w) => {
                    const m = /^(\d+)\s*(m|min|h|hour|d|day)?$/i.exec(String(w || ""));
                    if (!m) return { n: 5, unit: "min" };
                    const n = parseInt(m[1], 10) || 5;
                    const u = (m[2] || "min").toLowerCase();
                    const unit = u.startsWith("d") ? "day" : u.startsWith("h") ? "hour" : "min";
                    return { n, unit };
                  };
                  const composeWin = (n, unit) => {
                    const v = Math.max(1, parseInt(n, 10) || 1);
                    return v + (unit === "min" ? "m" : unit === "hour" ? "h" : "d");
                  };
                  const cur = parseWin(draft.window);
                  return isPreset ? (
                    <select className="select input--sm" value={draft.window}
                      onChange={e => {
                        const v = e.target.value;
                        if (v === "__custom") {
                          // Seed with whatever the operator had selected.
                          set("window", composeWin(cur.n, cur.unit === "min" ? "min" : cur.unit));
                        } else {
                          set("window", v);
                        }
                      }}>
                      {ALARM_WINDOWS.map(w => <option key={w} value={w}>{w}</option>)}
                      <option value="__custom">Custom…</option>
                    </select>
                  ) : (
                    <div style={{display:"flex", gap:6, alignItems:"center"}}>
                      <input className="input input--sm" type="number" min="1" max="9999"
                        value={cur.n}
                        onChange={e => set("window", composeWin(e.target.value, cur.unit))}
                        style={{flex:1, fontVariantNumeric:"tabular-nums"}}/>
                      <select className="select input--sm"
                        value={cur.unit}
                        onChange={e => set("window", composeWin(cur.n, e.target.value))}
                        style={{minWidth:80}}>
                        <option value="min">min</option>
                        <option value="hour">hr</option>
                        <option value="day">day</option>
                      </select>
                      <button type="button" title="Back to preset windows"
                        onClick={() => set("window", "1h")}
                        style={{
                          width:26, height:26, padding:0, border:"1px solid var(--border-default)",
                          borderRadius:6, background:"#fff", color:"var(--text-tertiary)",
                          cursor:"pointer", display:"grid", placeItems:"center", flexShrink:0,
                        }}>
                        <Icon name="x" size={11}/>
                      </button>
                    </div>
                  );
                })()}
              </div>
            </div>
            <div style={{marginTop:6, fontSize:11.5, color:"var(--text-tertiary)"}}>
              Will fire when: <strong style={{color:"var(--text-secondary)"}}>{previewPretty}</strong>
            </div>
          </div>

          <div>
            <label className="form-label">Severity</label>
            <div style={{display:"flex", gap:6}}>
              {ALARM_SEVERITIES.map(s => {
                const on = draft.severity === s;
                const sev = SEVERITY_CHIP[s] || SEVERITY_CHIP.low;
                return (
                  <button key={s} type="button" onClick={()=>set("severity", s)}
                          style={{padding:"5px 12px", borderRadius:999, border:`1px solid ${on ? sev.border : "var(--border-default)"}`,
                                  background: on ? sev.bg : "#fff", color: on ? sev.fg : "var(--text-secondary)",
                                  fontSize:11.5, fontWeight:700, textTransform:"uppercase", letterSpacing:".05em", cursor:"pointer"}}>
                    {s}
                  </button>
                );
              })}
            </div>
          </div>

          {/* Brand scope — alarm fires only for transactions belonging to
              one of the selected brands. "any" matches every brand and is
              the default; picking a specific brand removes the "any" chip
              automatically. When the top-right selector locks a brand the
              picker is disabled and shows only that brand. */}
          <div>
            <label className="form-label">Brand scope</label>
            <div style={{display:"flex", flexWrap:"wrap", gap:6}}>
              {brandLocked ? (
                <span style={{display:"inline-flex", alignItems:"center", gap:6, padding:"5px 12px", borderRadius:999, background:"var(--primary-soft, #eef2ff)", color:"var(--primary-dark, #1e3a8a)", fontSize:11.5, fontWeight:700, border:"1px solid color-mix(in oklab, var(--primary, #1e40af) 22%, transparent)"}}>
                  <Icon name="lock" size={10}/> {activeBrand.name}
                </span>
              ) : (
                <>
                  {(() => {
                    const anyOn = (draft.scopeBrands || []).includes("any");
                    return (
                      <button type="button" onClick={()=>toggleBrand("any")}
                        style={{padding:"5px 12px", borderRadius:999,
                          border: anyOn ? "1px solid var(--primary, #1e40af)" : "1px solid var(--border-default)",
                          background: anyOn ? "color-mix(in oklab, var(--primary, #1e40af) 8%, white)" : "#fff",
                          color: anyOn ? "var(--primary-dark, #1e3a8a)" : "var(--text-secondary)",
                          fontSize:11.5, fontWeight:700, cursor:"pointer"}}>
                        All brands
                      </button>
                    );
                  })()}
                  {(brands || []).map(b => {
                    const on = (draft.scopeBrands || []).includes(b.name);
                    return (
                      <button key={b.id} type="button" onClick={()=>toggleBrand(b.name)}
                        style={{padding:"5px 12px", borderRadius:999, display:"inline-flex", alignItems:"center", gap:6,
                          border: on ? "1px solid var(--primary, #1e40af)" : "1px solid var(--border-default)",
                          background: on ? "color-mix(in oklab, var(--primary, #1e40af) 8%, white)" : "#fff",
                          color: on ? "var(--primary-dark, #1e3a8a)" : "var(--text-secondary)",
                          fontSize:11.5, fontWeight:700, cursor:"pointer"}}>
                        <span style={{width:8, height:8, borderRadius:2, background:b.color}}/>
                        {b.name}
                      </button>
                    );
                  })}
                </>
              )}
            </div>
            <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:6}}>
              Pick <strong>All brands</strong> to fire for every tenant; pick one or more specific brands to scope it. Useful when a single brand has a different SLA or risk profile.
            </div>
          </div>

          <div>
            <div style={{display:"flex", alignItems:"center", justifyContent:"space-between", marginBottom:8}}>
              <label className="form-label" style={{margin:0}}>
                Destinations · send to
                <span title="Email / Slack / Telegram / WhatsApp / SMS / webhook / in-app. Pick one or more — fan-out happens in parallel."
                  style={{marginLeft:6, display:"inline-grid", placeItems:"center", width:14, height:14, borderRadius:999, background:"var(--n-75)", color:"var(--text-tertiary)", fontSize:9.5, fontWeight:700, cursor:"help"}}>?</span>
              </label>
              <div style={{display:"flex", alignItems:"center", gap:8}}>
                <span style={{fontSize:11, color:"var(--text-tertiary)"}}>{(draft.destinations || []).length} selected</span>
                {onAddDestination && (
                  <button type="button" className="btn btn--secondary btn--sm" onClick={onAddDestination}>
                    <Icon name="plus" size={10}/> Add destination
                  </button>
                )}
              </div>
            </div>
            {destinations.length === 0 && (
              <div style={{padding:"12px 14px", border:"1px dashed var(--border-default)", borderRadius:8, fontSize:12.5, color:"var(--text-tertiary)", textAlign:"center", marginBottom:6}}>
                No destinations yet. Click <strong>Add destination</strong> to create one.
              </div>
            )}
            {NOTIF_CHANNELS.map(c => {
              const list = destinations.filter(d => d.channel === c.id);
              if (list.length === 0) return null;
              return (
                <div key={c.id} style={{padding:"8px 10px", border:"1px solid var(--border-subtle)", borderRadius:8, marginBottom:6, background:"#fff"}}>
                  <div style={{display:"flex", alignItems:"center", gap:8, marginBottom:6}}>
                    <Icon name={c.icon} size={12} style={{color:c.color}}/>
                    <span style={{fontSize:12.5, fontWeight:600}}>{c.label}</span>
                    <span style={{fontSize:11, color:"var(--text-tertiary)"}}>{list.filter(d => (draft.destinations || []).includes(d.id)).length}/{list.length}</span>
                  </div>
                  <div style={{display:"flex", flexWrap:"wrap", gap:6}}>
                    {list.map(d => {
                      const on = (draft.destinations || []).includes(d.id);
                      const disabled = !d.enabled;
                      return (
                        <div key={d.id} style={{display:"inline-flex", alignItems:"stretch", borderRadius:999, overflow:"hidden", border: on ? `1px solid ${c.color}` : "1px solid var(--border-default)", background: on ? `${c.color}14` : "#fff"}}>
                          <label
                            style={{
                              display:"inline-flex", alignItems:"center", gap:6,
                              padding:"5px 4px 5px 10px",
                              color: disabled ? "var(--text-tertiary)" : on ? c.color : "var(--text-secondary)",
                              fontSize:12, fontWeight:600,
                              cursor: disabled ? "not-allowed" : "pointer",
                              opacity: disabled ? 0.55 : 1,
                              textDecoration: disabled ? "line-through" : "none",
                            }}>
                            <input type="checkbox" checked={on} disabled={disabled} onChange={()=>toggleDest(d.id)} style={{accentColor:c.color, margin:0}}/>
                            {d.label}
                            <span style={{fontSize:10.5, fontWeight:500, color:"var(--text-tertiary)", marginLeft:2}}>{d.target}</span>
                          </label>
                          {onToggleDestination && (
                            <button type="button" title={d.enabled ? "Disable destination globally" : "Enable destination globally"}
                              onClick={()=>onToggleDestination(d.id)}
                              style={{padding:"0 6px", border:"none", borderLeft:"1px solid var(--border-subtle)", background:"transparent", color:"var(--text-tertiary)", fontSize:10, cursor:"pointer"}}>
                              {d.enabled ? "on" : "off"}
                            </button>
                          )}
                          {onEditDestination && (
                            <button type="button" title="Edit destination"
                              onClick={()=>onEditDestination(d)}
                              style={{padding:"0 8px", border:"none", borderLeft:"1px solid var(--border-subtle)", background:"transparent", color:"var(--text-tertiary)", cursor:"pointer", display:"inline-grid", placeItems:"center"}}>
                              <Icon name="edit" size={10}/>
                            </button>
                          )}
                        </div>
                      );
                    })}
                  </div>
                </div>
              );
            })}
            <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:6}}>
              Destinations are reusable across all alarms. Toggle on/off globally with the chip; click the pencil to edit channel / target / label. Disabled destinations cannot be picked.
            </div>
          </div>

          <div>
            <label className="form-label">Status</label>
            <select className="select input--sm" value={draft.status} onChange={e=>set("status", e.target.value)}>
              <option value="on">on</option>
              <option value="off">off</option>
            </select>
          </div>
        </div>

        <div style={{padding:"14px 20px", borderTop:"1px solid var(--border-subtle)", background:"var(--n-25)", display:"flex", gap:8, alignItems:"center"}}>
          {!isNew && onDelete && (
            <button onClick={()=>{ if (confirm(`Delete alarm "${draft.name}"?`)) onDelete(alarm.id); }}
                    style={{height:36, padding:"0 12px", borderRadius:8, border:"1px solid var(--err-200)", background:"#fff", color:"var(--err-600)", fontWeight:600, fontSize:12.5, display:"inline-flex", alignItems:"center", gap:6, cursor:"pointer"}}>
              <Icon name="trash" size={12}/> Delete
            </button>
          )}
          {onTestFire && (
            <button onClick={()=>onTestFire(draft)} disabled={(draft.destinations || []).length === 0} className="btn btn--secondary btn--sm">
              <Icon name="zap" size={12}/> Test fire
            </button>
          )}
          <div style={{flex:1}}/>
          <button onClick={onCancel} className="btn btn--secondary btn--sm">Cancel</button>
          <button onClick={()=>onSave({ ...draft, channels: derivedChannels })} disabled={!draft.name || !draft.threshold || (draft.destinations || []).length === 0} className="btn btn--primary btn--sm">
            <Icon name="check" size={12}/> {isNew ? "Create alarm" : "Save changes"}
          </button>
        </div>
      </div>
    </div>
  );
};

/* =========================================================================
   DestinationEditor — small modal for creating / editing a notification
   endpoint. Channel choice drives the label + placeholder of the target
   field (Slack channel vs. email vs. webhook URL …).
   ========================================================================= */
const DestinationEditor = ({ dest, onSave, onCancel, onDelete }) => {
  const [draft, setDraft] = useState(JSON.parse(JSON.stringify(dest)));
  const isNew = !dest.id;
  const set = (k, v) => setDraft(d => ({ ...d, [k]: v }));
  const c = NOTIF_CHANNEL_BY_ID[draft.channel] || NOTIF_CHANNELS[0];
  return (
    <div style={{position:"fixed", inset:0, background:"rgba(15,20,32,.45)", backdropFilter:"blur(4px)", zIndex:130, display:"grid", placeItems:"center"}} onClick={onCancel}>
      <div style={{width:480, background:"#fff", borderRadius:16, boxShadow:"0 30px 60px -15px rgba(15,20,32,.35)"}} onClick={e=>e.stopPropagation()}>
        <div style={{padding:"16px 20px", borderBottom:"1px solid var(--border-subtle)", display:"flex", alignItems:"center", gap:10}}>
          <div style={{width:30, height:30, borderRadius:9, background:`${c.color}1a`, color:c.color, display:"grid", placeItems:"center"}}><Icon name={c.icon} size={14}/></div>
          <div style={{flex:1}}>
            <div style={{fontWeight:700, fontSize:14.5}}>{isNew ? "Add destination" : "Edit destination"}</div>
            <div style={{fontSize:11.5, color:"var(--text-tertiary)"}}>A reusable endpoint that any alarm can fan out to</div>
          </div>
          <button className="btn btn--ghost btn--icon btn--sm" onClick={onCancel}><Icon name="x" size={13}/></button>
        </div>
        <div style={{padding:"18px 20px", display:"flex", flexDirection:"column", gap:14}}>
          <Explainer compact title="What this is, in plain English">
            A destination is a reusable endpoint that <em>any</em> alarm can post to. Create one per channel (email inbox, Slack channel, Telegram chat, SMS number, webhook URL…) and pick it on as many alarms as you need. Disabling a destination here disables it across every alarm at once.
          </Explainer>
          <div>
            <label className="form-label">Channel<Tip>How the alarm is delivered. Email is best for non-urgent digests; Slack for ops chatter; Telegram / WhatsApp / SMS for after-hours; webhook for piping into your own incident system; in-app for back-office banners.</Tip></label>
            <select className="select input--sm" value={draft.channel} onChange={e=>set("channel", e.target.value)}>
              {NOTIF_CHANNELS.map(ch => <option key={ch.id} value={ch.id}>{ch.label}</option>)}
            </select>
          </div>
          <div>
            <label className="form-label">Display name<Tip>The label you'll see when picking destinations on an alarm. Something readable like "Ops on-call", "Treasury team", "Fraud Slack channel".</Tip></label>
            <input className="input input--sm" value={draft.label} onChange={e=>set("label", e.target.value)} placeholder="Ops on-call"/>
          </div>
          <div>
            <label className="form-label">{c.targetLabel}<Tip>The actual address: an email, a Slack channel id (#ops-on-call), a Telegram chat id, a phone number, a webhook URL. Format depends on the channel above.</Tip></label>
            <input className="input input--sm" value={draft.target} onChange={e=>set("target", e.target.value)} placeholder={c.targetHint}/>
          </div>
          <label style={{display:"inline-flex", alignItems:"center", gap:8, fontSize:13}}>
            <input type="checkbox" checked={draft.enabled !== false} onChange={e=>set("enabled", e.target.checked)} style={{accentColor:"var(--primary, #1e40af)"}}/>
            Enabled (alarms can pick this destination)
            <Tip>Untick to take the destination offline across every alarm without deleting it. Useful for paging silences during planned maintenance.</Tip>
          </label>
        </div>
        <div style={{padding:"14px 20px", borderTop:"1px solid var(--border-subtle)", background:"var(--n-25)", display:"flex", gap:8, alignItems:"center"}}>
          {!isNew && onDelete && (
            <button onClick={()=>{ if (confirm(`Delete destination "${draft.label}"?`)) onDelete(dest.id); }}
              style={{height:36, padding:"0 12px", borderRadius:8, border:"1px solid var(--err-200)", background:"#fff", color:"var(--err-600)", fontWeight:600, fontSize:12.5, display:"inline-flex", alignItems:"center", gap:6, cursor:"pointer"}}>
              <Icon name="trash" size={12}/> Delete
            </button>
          )}
          <div style={{flex:1}}/>
          <button onClick={onCancel} className="btn btn--secondary btn--sm">Cancel</button>
          <button onClick={()=>onSave(draft)} disabled={!draft.label || !draft.target} className="btn btn--primary btn--sm">
            <Icon name="check" size={12}/> {isNew ? "Create" : "Save"}
          </button>
        </div>
      </div>
    </div>
  );
};

/* ===== ProviderHealthPopover =====
   Pops out from a non-ONLINE status badge with the metrics that drove
   the downgrade, the last 5 errors, and the alarm rule that fired.
   Dismisses on click-outside or Escape. */
const ProviderHealthPopover = ({ psp, onClose, onAcknowledge, onOpenProvider }) => {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose && onClose(); };
    const onEsc = (e) => { if (e.key === "Escape") onClose && onClose(); };
    const t = setTimeout(() => {
      document.addEventListener("mousedown", onDoc);
      document.addEventListener("keydown", onEsc);
    }, 0);
    return () => {
      clearTimeout(t);
      document.removeEventListener("mousedown", onDoc);
      document.removeEventListener("keydown", onEsc);
    };
  }, [onClose]);
  const status = (psp.health?.status || "online").toUpperCase();
  const isDegraded = status === "DEGRADED";
  const isDown = status === "DOWN" || status === "OFFLINE";
  const headline = isDegraded ? "Why DEGRADED?" : isDown ? "Why OFFLINE?" : "Why?";
  // Seeded recent errors so demo is stable per PSP.
  const seed = (psp.id || "x").split("").reduce((a,c)=>(a*31+c.charCodeAt(0))|0, 11);
  const rand = (i) => { const x = Math.sin(seed + i*23)*10000; return x - Math.floor(x); };
  const minutesAgo = (i) => {
    const m = Math.floor(rand(i) * 25) + 1;
    const d = new Date(Date.now() - m * 60_000);
    return d.toLocaleTimeString("en-GB", { hour:"2-digit", minute:"2-digit" });
  };
  const errorCatalog = [
    "timeout (8s)", "502 Bad Gateway", "timeout (8s)",
    "declined (network)", "timeout (8s)", "503 Service Unavailable",
    "5xx burst",
  ];
  const errors = errorCatalog.slice(0, 5).map((e, i) => ({ t: minutesAgo(i), e }));
  return (
    <div ref={ref} role="dialog" aria-label="Provider health"
      style={{
        position:"absolute", zIndex:50, top:"calc(100% + 6px)", left:0, right:0,
        background:"#fff", border:"1px solid var(--border-default)", borderRadius:12,
        boxShadow:"0 24px 48px -12px rgba(15,20,32,.22), 0 0 0 1px rgba(15,20,32,.04)",
        padding:14,
      }}>
      <div style={{display:"flex", alignItems:"center", gap:8, marginBottom:10}}>
        <div style={{width:26, height:26, borderRadius:7, background:"var(--err-50, #fee2e2)", color:"var(--err-700, #991b1b)", display:"grid", placeItems:"center"}}>
          <Icon name="alert" size={12}/>
        </div>
        <div style={{flex:1, fontSize:13, fontWeight:700}}>{headline}</div>
        <button onClick={onClose}
          style={{width:22, height:22, padding:0, border:"none", borderRadius:5, background:"transparent", color:"var(--text-tertiary)", cursor:"pointer", display:"grid", placeItems:"center"}}>
          <Icon name="x" size={10}/>
        </button>
      </div>
      <div style={{display:"flex", flexDirection:"column", gap:4, fontSize:12, color:"var(--text-secondary)", lineHeight:1.55, marginBottom:10}}>
        <div>Success rate: <strong style={{color:"var(--text-primary)"}}>{(psp.health.successRate || 0).toFixed(1)}%</strong> (down from 98% 2h ago)</div>
        <div>Avg latency: <strong style={{color:"var(--text-primary)"}}>{psp.health.avgLatencyMs}ms</strong> (up ~80%)</div>
        <div>Timeout rate: <strong style={{color:"var(--text-primary)"}}>{psp.health.timeoutRate}%</strong></div>
      </div>
      <div style={{fontSize:11, fontWeight:700, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em", marginBottom:4}}>Last 5 errors</div>
      <div style={{display:"flex", flexDirection:"column", gap:3, marginBottom:10, fontSize:12, fontFamily:"var(--font-mono)"}}>
        {errors.map((x, i) => (
          <div key={i} style={{display:"flex", gap:8, color:"var(--text-secondary)"}}>
            <span style={{color:"var(--text-tertiary)", minWidth:42}}>{x.t}</span>
            <span style={{color:"var(--text-tertiary)"}}>·</span>
            <span>{x.e}</span>
          </div>
        ))}
      </div>
      <div style={{padding:"6px 10px", borderRadius:8, background:"var(--n-25)", border:"1px solid var(--border-subtle)", fontSize:11.5, color:"var(--text-secondary)", marginBottom:10}}>
        Auto-degraded by alarm rule: <strong>"5xx &gt; 2% in 10min window"</strong>
      </div>
      <div style={{display:"flex", gap:6, justifyContent:"flex-end"}}>
        <button onClick={onOpenProvider}
          style={{padding:"6px 12px", border:"1px solid var(--border-default)", borderRadius:7, background:"#fff", fontSize:12, fontWeight:600, color:"var(--text-secondary)", cursor:"pointer"}}>
          View provider
        </button>
        <button onClick={onAcknowledge}
          style={{padding:"6px 12px", border:"none", borderRadius:7, background:"var(--p-500)", color:"#fff", fontSize:12, fontWeight:700, cursor:"pointer"}}>
          Acknowledge
        </button>
      </div>
    </div>
  );
};

/* ===== LiquiditySparkline =====
   Thin 7-day SVG sparkline of wallet balance with dotted floor + ceiling
   lines and a hover tooltip showing the value + date. */
const LiquiditySparkline = ({ history, floor, ceiling }) => {
  const data = history || [];
  const [hoverIdx, setHoverIdx] = React.useState(null);
  if (data.length === 0) {
    return <div style={{fontSize:10.5, color:"var(--text-tertiary)", marginTop:8}}>Limited history — no wallet snapshots yet.</div>;
  }
  const w = 260, h = 40, pad = 4;
  const max = Math.max(ceiling || 0, ...data.map(d => d.wallet)) * 1.05;
  const x = (i) => pad + (i / Math.max(1, data.length - 1)) * (w - pad*2);
  const y = (v) => h - pad - (v / max) * (h - pad*2);
  const pts = data.map((d, i) => `${x(i)},${y(d.wallet)}`).join(" ");
  const floorY = y(floor);
  const ceilY  = y(ceiling);
  const fmt = (n) => n >= 1_000_000 ? `€${(n/1_000_000).toFixed(2)}M` : n >= 1000 ? `€${(n/1000).toFixed(0)}K` : `€${Math.round(n)}`;
  return (
    <div style={{marginTop:8, position:"relative"}}>
      <div style={{display:"flex", alignItems:"center", justifyContent:"space-between", fontSize:10, color:"var(--text-tertiary)", fontWeight:600, marginBottom:2}}>
        <span style={{textTransform:"uppercase", letterSpacing:".05em"}}>7-day wallet trend</span>
        {data.length < 7 && <span>Limited history</span>}
      </div>
      <svg viewBox={`0 0 ${w} ${h}`} width="100%" height={h}
        style={{display:"block"}}
        onMouseLeave={() => setHoverIdx(null)}>
        {/* Ceiling dashed line */}
        <line x1={pad} x2={w-pad} y1={ceilY} y2={ceilY}
          stroke="var(--text-tertiary)" strokeWidth="1" strokeDasharray="3 3" opacity=".5"/>
        {/* Floor dashed line */}
        <line x1={pad} x2={w-pad} y1={floorY} y2={floorY}
          stroke="var(--err-500, #ef4444)" strokeWidth="1" strokeDasharray="3 3" opacity=".55"/>
        {/* Area fill under line */}
        <polyline
          points={`${pad},${h - pad} ${pts} ${w-pad},${h - pad}`}
          fill="color-mix(in oklab, var(--primary, #1e40af) 12%, transparent)"
          stroke="none"/>
        {/* Line */}
        <polyline
          points={pts}
          fill="none" stroke="var(--primary, #1e40af)" strokeWidth="1.6"/>
        {/* Hit-area + dot for each point */}
        {data.map((d, i) => (
          <g key={i}>
            <circle cx={x(i)} cy={y(d.wallet)} r={2.5}
              fill={hoverIdx === i ? "var(--primary-dark, #1e3a8a)" : "var(--primary, #1e40af)"}
              stroke="#fff" strokeWidth="1"/>
            <rect x={x(i) - (w/data.length)/2} y={0} width={w/data.length} height={h}
              fill="transparent"
              onMouseEnter={() => setHoverIdx(i)}/>
          </g>
        ))}
      </svg>
      {hoverIdx != null && data[hoverIdx] && (
        <div style={{
          position:"absolute",
          left: Math.max(0, Math.min(w - 110, x(hoverIdx) * (1) - 50)),
          bottom: h + 4,
          padding:"4px 8px", borderRadius:6,
          background:"#0f172a", color:"#f1f5f9",
          fontSize:10.5, fontWeight:600, whiteSpace:"nowrap",
          pointerEvents:"none",
        }}>
          {new Date(data[hoverIdx].ts).toLocaleDateString("en-GB", { day:"2-digit", month:"short" })} · <strong>{fmt(data[hoverIdx].wallet)}</strong>
        </div>
      )}
      <div style={{display:"flex", justifyContent:"space-between", fontSize:9.5, color:"var(--text-tertiary)", marginTop:2}}>
        <span>{new Date(data[0].ts).toLocaleDateString("en-GB", { day:"2-digit", month:"short" })}</span>
        <span style={{display:"inline-flex", gap:8}}>
          <span><span style={{display:"inline-block", width:8, height:1.5, background:"var(--err-500, #ef4444)", verticalAlign:"middle", marginRight:3}}/>floor</span>
          <span><span style={{display:"inline-block", width:8, height:1.5, background:"var(--text-tertiary)", verticalAlign:"middle", marginRight:3}}/>ceiling</span>
        </span>
        <span>today</span>
      </div>
    </div>
  );
};

const SETTINGS_TAB_ROUTES = [["providers", "Providers", ""], ["routes", "Routes & cascading", "routes"], ["alarms", "Alarms", "alarms"]];
const Settings = ({ brand, brands, setBrands, activeTab: initialTab }) => {
  // Each tab gets its own URL (e.g. /payments/settings/alarms) via useUrlTab
  // (src/routes.jsx). initialTab (legacy prop, currently unused by any
  // caller) still seeds the very first render if the URL doesn't already
  // say otherwise.
  const [tab, setTab] = window.useUrlTab("/payments/settings", SETTINGS_TAB_ROUTES, initialTab === "brands" ? "providers" : (initialTab || "providers"));
  const [psps, setPsps] = useState(PSP_PROFILES);
  const [editingPsp, setEditingPsp] = useState(null); // { psp, initialTab }
  const [routes, setRoutes] = useState(ROUTES);
  const [editingRoute, setEditingRoute] = useState(null);
  const [healthPopover, setHealthPopover] = useState(null); // psp.id when open
  // Routes table filter driven by the Strategy summary cards. Null = no
  // filter; otherwise one of: "active" | "stale" | "brand_scoped" |
  // "cascade_override".
  const [routesFilter, setRoutesFilter] = useState(null);

  // Per-area filter state — added to every tab so the operator can drill in
  // even when the top-right brand selector is on "All brands". When the
  // top-right selector locks a brand, the per-area brand filter is hidden
  // and locked to that brand (auto-login behavior).
  const brandLocked = !!(brand && !brand.isAll);
  const [provBrandF, setProvBrandF] = useState("all");
  const [provStatusF, setProvStatusF] = useState("all");
  const [provMethodF, setProvMethodF] = useState("all");
  const [routeBrandF, setRouteBrandF] = useState("all");
  const [routeMethodF, setRouteMethodF] = useState("all");
  const [routeStatusF, setRouteStatusF] = useState("all");
  const [routeProviderF, setRouteProviderF] = useState("all");
  const [alarmSeverityF, setAlarmSeverityF] = useState("all");
  const [alarmStatusF, setAlarmStatusF] = useState("all");
  const [alarmGroupF, setAlarmGroupF] = useState("all");
  const [alarmProviderF, setAlarmProviderF] = useState("all");
  const [alarmBrandF, setAlarmBrandF] = useState("all");

  // Stale = no match in 14+ days OR never matched.
  const isStaleRoute = (r) => {
    if (r.lastMatchedAt == null) return true;
    return (Date.now() - r.lastMatchedAt) >= 14 * 86400_000;
  };
  const isBrandScopedRoute = (r) => r.brand && r.brand !== "any";
  // "Custom policy" = route's cascadePolicy diverges from the default
  // ERROR_MATRIX (or, for legacy data, an override flag was set).
  const isCustomCascadeRoute = (r) => {
    if (r.cascadeOverride?.enabled) return true;
    if (!r.cascadePolicy) return false;
    const def = JSON.stringify(ERROR_MATRIX.map(x => `${x.error}:${x.kind}`));
    const mine = JSON.stringify(r.cascadePolicy.map(x => `${x.error}:${x.kind}`));
    return def !== mine;
  };

  // Filter routes by the top-right brand selector before the strategy
  // filter chip kicks in. "any"-scoped routes match every brand; brand-
  // scoped routes only match when their brand name matches the selected
  // brand. When the operator picks "All brands" the routes list is
  // unfiltered. Declared first so routeStats and filteredRoutes can both
  // read it without a TDZ error.
  const brandFilteredRoutes = (() => {
    let list = routes;
    if (brandLocked) {
      list = list.filter(r => r.brand === "any" || r.brand === brand.name);
    } else if (routeBrandF !== "all") {
      list = list.filter(r => r.brand === "any" || r.brand === routeBrandF);
    }
    if (routeMethodF !== "all") list = list.filter(r => r.method === routeMethodF);
    if (routeStatusF !== "all") list = list.filter(r => r.status === routeStatusF);
    if (routeProviderF !== "all") list = list.filter(r => (r.chain || []).includes(routeProviderF));
    return list;
  })();

  const routeStats = {
    active:           brandFilteredRoutes.filter(r => r.status === "active").length,
    stale:            brandFilteredRoutes.filter(isStaleRoute).length,
    brandScoped:      brandFilteredRoutes.filter(isBrandScopedRoute).length,
    cascadeOverride:  brandFilteredRoutes.filter(isCustomCascadeRoute).length,
  };

  const filteredRoutes = (() => {
    const base = brandFilteredRoutes;
    switch (routesFilter) {
      case "active":           return base.filter(r => r.status === "active");
      case "stale":            return base.filter(isStaleRoute);
      case "brand_scoped":     return base.filter(isBrandScopedRoute);
      case "cascade_override": return base.filter(isCustomCascadeRoute);
      default:                 return base;
    }
  })();

  // Providers filter — Brand maps via routes that reference each provider:
  // when a brand is selected we only show providers in the chain of a route
  // for that brand (or "any" brand). This is the closest meaningful brand
  // scoping for a directory that is otherwise shared across the tenant.
  const filteredPsps = (() => {
    let list = psps;
    const effectiveBrand = brandLocked ? brand.name : (provBrandF === "all" ? null : provBrandF);
    if (effectiveBrand) {
      const allowed = new Set();
      routes.forEach(r => {
        if (r.brand === "any" || r.brand === effectiveBrand) (r.chain || []).forEach(pid => allowed.add(pid));
      });
      list = list.filter(p => allowed.has(p.id));
    }
    if (provStatusF !== "all") list = list.filter(p => p.status === provStatusF);
    if (provMethodF !== "all") list = list.filter(p => (p.methods || []).includes(provMethodF));
    return list;
  })();

  const [alarms, setAlarms] = useState(ALARMS);
  const [editingAlarm, setEditingAlarm] = useState(null);
  const [destinations, setDestinations] = useState(NOTIF_DESTINATIONS_DEFAULTS);
  const [editingDest, setEditingDest] = useState(null);

  // Synthesised liquidity alarms drawn from each PSP's `liquidityAlarms`
  // list — they are owned by the provider but surfaced in the general
  // alarms list as read-only entries so the operator can audit every
  // alarm in one place. Each row carries `_origin: "provider"` and a
  // `_pspId` so the row can deep-link back to the PSP editor.
  const providerAlarms = (() => {
    const out = [];
    psps.forEach(p => {
      (p.liquidityAlarms || []).forEach(r => {
        out.push({
          ...r,
          id: `psp:${p.id}:${r.id || ""}`,
          _origin: "provider",
          _pspId: p.id,
          _pspName: p.name,
          metric: `liq:${r.metric}`, // namespace so it shows under "Liquidity"
          scopeProviders: [p.id],
          scopeBrands: r.scopeBrands || ["any"],
        });
      });
    });
    return out;
  })();
  const combinedAlarms = alarms.concat(providerAlarms);

  // Alarms filter
  const filteredAlarms = (() => {
    let list = combinedAlarms;
    if (alarmSeverityF !== "all") list = list.filter(a => a.severity === alarmSeverityF);
    if (alarmStatusF   !== "all") list = list.filter(a => a.status   === alarmStatusF);
    if (alarmGroupF    !== "all") {
      list = list.filter(a => {
        if (alarmGroupF === "Liquidity & cash flow" && a._origin === "provider") return true;
        return (ALARM_METRIC_BY_KEY[a.metric]?.group) === alarmGroupF;
      });
    }
    if (alarmProviderF !== "all") list = list.filter(a => (a.scopeProviders || []).includes(alarmProviderF) || a._pspId === alarmProviderF);
    if (alarmBrandF    !== "all") list = list.filter(a => {
      const sb = a.scopeBrands || ["any"];
      return sb.includes("any") || sb.includes(alarmBrandF);
    });
    return list;
  })();
  const saveDestination = (draft) => {
    if (!draft.id) {
      const newD = { ...draft, id: "d" + Math.floor(Math.random()*100000), enabled: draft.enabled !== false };
      setDestinations([...destinations, newD]);
    } else {
      setDestinations(destinations.map(d => d.id === draft.id ? draft : d));
    }
    setEditingDest(null);
  };
  const deleteDestination = (id) => {
    setDestinations(destinations.filter(d => d.id !== id));
    setAlarms(alarms.map(a => ({ ...a, destinations: (a.destinations || []).filter(x => x !== id) })));
    setEditingDest(null);
  };
  const toggleDestinationEnabled = (id) => {
    setDestinations(destinations.map(d => d.id === id ? { ...d, enabled: !d.enabled } : d));
  };
  const testFireAlarm = (a) => {
    if (!window.PAYBO?.emitToast) return;
    const chosen = (a.destinations || []).map(id => destinations.find(d => d.id === id)).filter(Boolean);
    window.PAYBO.emitToast({
      id:`test-${Date.now()}`, tx_id:`Test fire · ${a.name || "alarm"}`, amount:0, currency:"DEMO",
      player: chosen.map(d => d.label).slice(0,3).join(", ") + (chosen.length > 3 ? ` +${chosen.length-3}` : ""),
      // "Would post" is the honest verb: nothing leaves the browser. Naming the
      // count and the destinations is the part that is real and useful.
      reason:`Nothing was sent — a real test fire needs the notification dispatcher. It would post to ${chosen.length} destination${chosen.length===1?"":"s"}.`,
    });
  };

  const savePsp = (draft) => {
    if (!draft.id) {
      const newP = { ...draft, id: draft.name.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 8) + Math.floor(Math.random() * 99) };
      setPsps([...psps, newP]);
    } else {
      setPsps(psps.map(p => p.id === draft.id ? draft : p));
    }
    setEditingPsp(null);
  };
  const deletePsp = (id) => {
    setPsps(psps.filter(p => p.id !== id));
    setEditingPsp(null);
  };

  const saveRoute = (draft) => {
    const me = currentOperator();
    const ts = Date.now();
    if (!draft.id) {
      const id = "r" + Math.random().toString(36).slice(2, 7);
      const snapshot = JSON.parse(JSON.stringify({ ...draft, id }));
      const entry = {
        ts, user_id: me.id, user_email: me.email,
        route_id: id,
        change_summary: ["Route created"],
        full_snapshot: snapshot,
      };
      const newR = { ...draft, id, history: [entry] };
      setRoutes([...routes, newR]);
    } else {
      const prev = routes.find(r => r.id === draft.id);
      const changes = diffRoute(prev, draft, brands, psps);
      // Only record an entry if there's actually a change OR an explicit
      // save with no diff (we keep "no field changes" entries off so the
      // log doesn't fill with empty rows).
      const isMeaningful = !(changes.length === 1 && changes[0] === "Route saved (no field changes)");
      const snapshot = JSON.parse(JSON.stringify({ ...draft }));
      delete snapshot.history;
      const entry = isMeaningful ? {
        ts, user_id: me.id, user_email: me.email,
        route_id: draft.id,
        change_summary: changes,
        full_snapshot: snapshot,
      } : null;
      const history = [...(prev?.history || draft.history || [])];
      if (entry) history.unshift(entry);
      const merged = { ...draft, history };
      setRoutes(routes.map(r => r.id === draft.id ? merged : r));
    }
    setEditingRoute(null);
  };
  const deleteRoute = (id) => {
    setRoutes(routes.filter(r => r.id !== id));
    setEditingRoute(null);
  };

  const saveAlarm = (draft) => {
    if (!draft.id) {
      const newA = { ...draft, id: "a" + Math.random().toString(36).slice(2, 7) };
      setAlarms([...alarms, newA]);
    } else {
      setAlarms(alarms.map(a => a.id === draft.id ? draft : a));
    }
    setEditingAlarm(null);
  };
  const deleteAlarm = (id) => {
    setAlarms(alarms.filter(a => a.id !== id));
    setEditingAlarm(null);
  };

  const blankRoute = () => ({
    name:"",
    brand:"any", method:"Visa", currency:"any", amount:"any",
    geos:[], licenses:[],
    chain:[], weighted:false, weights:[],
    triggers:["declined"], cascadeTimeout:"30s",
    factors:{ performance:true, cost:true, liquidity:true, health:true },
    cascadePolicy: ERROR_MATRIX.map(e => ({ ...e })),
    mode:"AUTO", ranking:"manual", status:"active",
    lastMatchedAt: null, volume30d: 0,
  });
  const blankAlarm = () => {
    const m = ALARM_METRICS[0];
    return {
      name:"", metric:m.key, op:m.suggestedOp, threshold:m.suggestedThreshold, window:m.suggestedWindow,
      severity:"medium", channels:[], destinations:[], status:"on",
      scopeBrands: brandLocked ? [brand.name] : ["any"],
    };
  };
  const blankDestination = () => ({ channel:"email", label:"", target:"", enabled:true });

  return (
    <div className="page">
      <div className="page__header">
        <div>
          <div className="page__title">Settings</div>
          <div className="page__subtitle">System configuration · providers, routes, alarms</div>
        </div>
      </div>

      <div className="panel" style={{overflow:"hidden"}}>
        <div className="tabs" style={{padding:"0 14px", overflowX:"auto", whiteSpace:"nowrap"}}>
          <button className={tab==="providers"?"active":""} onClick={()=>setTab("providers")}>Providers</button>
          <button className={tab==="routes"?"active":""} onClick={()=>setTab("routes")}>Routes &amp; cascading</button>
          <button className={tab==="alarms"?"active":""} onClick={()=>setTab("alarms")}>Alarms</button>
        </div>

        {tab === "providers" && (
          <div style={{padding:20}}>
            {/* Plain-English explainer for the CTO */}
            <div style={{marginBottom:18, padding:"14px 16px", borderRadius:10,
                         border:"1px solid color-mix(in oklab, var(--primary, #1e40af) 18%, transparent)",
                         background:"color-mix(in oklab, var(--primary, #1e40af) 4%, white)",
                         display:"grid", gridTemplateColumns:"32px 1fr", gap:12, alignItems:"start"}}>
              <div style={{width:32, height:32, borderRadius:8, background:"var(--primary, #1e40af)", color:"#fff",
                           display:"grid", placeItems:"center", flexShrink:0}}>
                <Icon name="info" size={14}/>
              </div>
              <div>
                <div style={{fontWeight:700, fontSize:13.5, marginBottom:4}}>What this is, in plain English</div>
                <div style={{fontSize:12.5, color:"var(--text-secondary)", lineHeight:1.55}}>
                  A <strong>Provider</strong> (PSP) is a third party that actually moves the money — Stripe, Adyen, Trustly, etc. This page is the master directory: every PSP we work with, what it costs, what it can do, where it can do it, and how much money is sitting in each one's wallet right now.
                </div>
                <ul style={{fontSize:12, color:"var(--text-secondary)", lineHeight:1.7, marginTop:8, marginBottom:0, paddingLeft:18}}>
                  <li><strong>Costs &amp; settlement</strong> — fees and how fast funds clear (T+0 / T+1 / T+2)</li>
                  <li><strong>Limits</strong> — what the PSP itself will accept per transaction / day / month</li>
                  <li><strong>Liquidity</strong> — live wallet balance with a <em>ceiling</em> (don't let too much sit idle) and a <em>floor</em> (top up before withdrawals start failing)</li>
                  <li><strong>Coverage</strong> — which countries and licenses it operates under, and which operations each license authorises</li>
                </ul>
              </div>
            </div>

            <div style={{display:"flex", alignItems:"center", justifyContent:"space-between", marginBottom:14}}>
              <div>
                <div style={{fontSize:14, fontWeight:650, color:"var(--paybo-heading, #1e3a8a)"}}>Provider directory</div>
                <div style={{fontSize:11.5, color:"var(--text-tertiary)", marginTop:2}}>{psps.filter(p=>p.status==="active").length} active · {psps.filter(p=>p.status!=="active").length} paused / unavailable</div>
              </div>
              <button className="btn btn--primary btn--sm" onClick={()=>setEditingPsp({ psp:{ name:"", kind:"Card", status:"active",
                fees:{ dep:"", wd:"", settle:"" },
                limits:{ min:"", max:"", daily:"", monthly:"" },
                liquidity:{ walletEur:0, ceilingEur:1_000_000, floorEur:100_000, minWithdrawalEur:5_000, settlement:"T+1", payout:"", sweepPct:80, floorAlert:true },
                geos:[], licenses:[], methods:[],
                health:{ status:"online", successRate:0, avgLatencyMs:0, timeoutRate:0, lastCheck:"—" },
                tech:{ endpoint:"", timeoutMs:8000, webhook:"" },
                notifChannels:["slack"], methodOverrides:[],
              }, initialTab:"general" })}>
                <Icon name="plus" size={12}/> Add provider
              </button>
            </div>

            {/* Filter row */}
            <div style={{display:"flex", gap:8, alignItems:"center", marginBottom:14, padding:"10px 12px", background:"var(--n-25)", border:"1px solid var(--border-subtle)", borderRadius:10, flexWrap:"wrap"}}>
              <Icon name="filter" size={12} style={{color:"var(--text-tertiary)"}}/>
              <span style={{fontSize:11, fontWeight:700, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em"}}>Filter</span>
              <div style={{display:"flex", alignItems:"center", gap:6}}>
                <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Brand</label>
                {brandLocked ? (
                  <span style={{display:"inline-flex", alignItems:"center", gap:5, padding:"3px 9px", borderRadius:999, background:"var(--primary-soft, #eef2ff)", color:"var(--primary-dark, #1e3a8a)", fontSize:11.5, fontWeight:600}}
                        title="Locked by top-right brand selector">
                    <Icon name="lock" size={10}/> {brand.name}
                  </span>
                ) : (
                  <select className="select input--sm" value={provBrandF} onChange={e=>setProvBrandF(e.target.value)} style={{minWidth:130}}>
                    <option value="all">All brands</option>
                    {brands.map(b => <option key={b.id} value={b.name}>{b.name}</option>)}
                  </select>
                )}
              </div>
              <div style={{display:"flex", alignItems:"center", gap:6}}>
                <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Method</label>
                <select className="select input--sm" value={provMethodF} onChange={e=>setProvMethodF(e.target.value)} style={{minWidth:130}}>
                  <option value="all">All methods</option>
                  {Object.entries(ALL_METHODS_BY_ID).map(([id, name]) => <option key={id} value={id}>{name}</option>)}
                </select>
              </div>
              <div style={{display:"flex", alignItems:"center", gap:6}}>
                <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Status</label>
                <select className="select input--sm" value={provStatusF} onChange={e=>setProvStatusF(e.target.value)} style={{minWidth:110}}>
                  <option value="all">All statuses</option>
                  <option value="active">Active</option>
                  <option value="paused">Paused</option>
                </select>
              </div>
              <div style={{flex:1}}/>
              <span style={{fontSize:11.5, color:"var(--text-tertiary)"}}>{filteredPsps.length} of {psps.length}</span>
              {(provBrandF!=="all" || provMethodF!=="all" || provStatusF!=="all") && !brandLocked && (
                <button className="btn btn--ghost btn--sm" onClick={()=>{ setProvBrandF("all"); setProvMethodF("all"); setProvStatusF("all"); }}>
                  <Icon name="x" size={10}/> Clear
                </button>
              )}
            </div>

            {filteredPsps.length === 0 && (
              <div style={{padding:"24px 16px", textAlign:"center", border:"1px dashed var(--border-default)", borderRadius:10, fontSize:12.5, color:"var(--text-tertiary)"}}>
                No providers match the current filters.
              </div>
            )}

            <div style={{display:"grid", gridTemplateColumns:"repeat(auto-fill, minmax(360px, 1fr))", gap:14}}>
              {filteredPsps.map(p => {
                const liq = p.liquidity;
                const pct = Math.min(100, Math.round((liq.walletEur / liq.ceilingEur) * 100));
                const floorPct = Math.min(100, Math.round((liq.floorEur / liq.ceilingEur) * 100));
                const sweepPct = liq.sweepPct || 80;
                const health = liquidityHealth(liq);
                const isPaused = p.status !== "active";
                const tone = HEALTH_TONE[(p.health && p.health.status) || "online"];

                return (
                  <div key={p.id} style={{
                    padding:14, borderRadius:12,
                    border: isPaused ? "1px dashed var(--border-strong, #cbd5e1)" : "1px solid var(--border-default)",
                    background: isPaused ? "var(--n-25)" : "#fff",
                    display:"flex", flexDirection:"column", gap:12,
                  }}>
                    {/* Header */}
                    <div style={{display:"flex", alignItems:"center", gap:10}}>
                      <div style={{width:36, height:36, borderRadius:8,
                                   background:"var(--primary-soft, #eef2ff)", color:"var(--primary, #1e40af)",
                                   display:"grid", placeItems:"center", fontWeight:700, fontSize:13, letterSpacing:".02em"}}>
                        {p.name.replace(/[^A-Za-z]/g, "").slice(0,2).toUpperCase()}
                      </div>
                      <div style={{flex:1, minWidth:0}}>
                        <div style={{fontWeight:700, fontSize:14}}>{p.name}</div>
                        <div style={{fontSize:11, color:"var(--text-tertiary)"}}>{p.kind} · settlement {liq.settlement}</div>
                      </div>
                      <span className={`chip ${p.status === "active" ? "chip--ok" : "chip--neutral"}`} style={{fontSize:10.5}}>{p.status}</span>
                    </div>

                    {/* Health & performance — clickable when non-ONLINE.
                        Opens a popover explaining why the PSP was degraded /
                        downgraded with the most recent errors. */}
                    {(() => {
                      const isHealthy = (p.health?.status || "online") === "online";
                      const popoverOpen = healthPopover === p.id;
                      return (
                        <div style={{position:"relative"}}>
                          <div
                            onClick={() => { if (!isHealthy) setHealthPopover(popoverOpen ? null : p.id); }}
                            title={isHealthy ? "" : "Click to see why"}
                            style={{display:"flex", alignItems:"center", gap:8, padding:"6px 10px", borderRadius:8, background:tone.bg, border:`1px solid ${tone.dot}33`,
                              cursor: isHealthy ? "default" : "pointer",
                            }}>
                            <span style={{width:8, height:8, borderRadius:999, background:tone.dot, flexShrink:0}}/>
                            <span style={{fontSize:11.5, fontWeight:700, color:tone.fg, textTransform:"uppercase", letterSpacing:".04em"}}>{tone.label}</span>
                            {!isHealthy && <Icon name="chevron_down" size={10} style={{color:tone.fg, opacity:.7}}/>}
                            <div style={{flex:1}}/>
                            <span style={{fontSize:11, color:"var(--text-secondary)"}}>Success <strong>{(p.health.successRate || 0).toFixed(1)}%</strong></span>
                            <span style={{fontSize:11, color:"var(--text-tertiary)"}}>·</span>
                            <span style={{fontSize:11, color:"var(--text-secondary)"}}>Latency <strong>{p.health.avgLatencyMs}ms</strong></span>
                            <span style={{fontSize:11, color:"var(--text-tertiary)"}}>·</span>
                            <span style={{fontSize:11, color:"var(--text-secondary)"}}>Timeout <strong>{p.health.timeoutRate}%</strong></span>
                          </div>
                          {popoverOpen && (
                            <ProviderHealthPopover
                              psp={p}
                              onClose={() => setHealthPopover(null)}
                              onAcknowledge={() => {
                                if (window.PAYBO?.emitToast) window.PAYBO.emitToast({
                                  id:`ack-${p.id}-${Date.now()}`,
                                  tx_id:`Acknowledged · ${p.name}`, amount:0, currency:"DEMO",
                                  player:"—", reason:"Status acknowledged; alarm banner cleared until next breach.",
                                });
                                setHealthPopover(null);
                              }}
                              onOpenProvider={() => { setHealthPopover(null); setEditingPsp({ psp:p, initialTab:"general" }); }}/>
                          )}
                        </div>
                      );
                    })()}

                    {/* Supported methods */}
                    {p.methods && p.methods.length > 0 && (
                      <div>
                        <div style={{fontSize:10.5, fontWeight:700, textTransform:"uppercase", color:"var(--text-tertiary)", letterSpacing:".05em", marginBottom:4}}>Supported methods</div>
                        <div style={{display:"flex", gap:4, flexWrap:"wrap"}}>
                          {p.methods.map(m => (
                            <span key={m} style={{fontSize:10.5, fontWeight:600, padding:"2px 7px", borderRadius:6, background:"var(--primary-soft, #eef2ff)", color:"var(--primary-dark, #1e3a8a)", border:"1px solid color-mix(in oklab, var(--primary, #1e40af) 18%, transparent)"}}>
                              {ALL_METHODS_BY_ID[m] || m}
                            </span>
                          ))}
                        </div>
                      </div>
                    )}

                    {/* Costs / Limits — two compact columns */}
                    <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:10, fontSize:11.5, lineHeight:1.55}}>
                      <div>
                        <div style={{fontSize:10.5, fontWeight:700, textTransform:"uppercase", color:"var(--text-tertiary)", letterSpacing:".05em", marginBottom:4}}>Costs</div>
                        <div>Deposit: <strong>{p.fees.dep || "—"}</strong></div>
                        <div>Withdrawal: <strong>{p.fees.wd || "—"}</strong></div>
                        <div>Settle: <strong>{p.fees.settle || "—"}</strong></div>
                        {(() => {
                          // Parse "1.4% + €0.25" / "1.6%" / "€0.80 flat" etc.
                          const f = p.fees?.dep || "";
                          const pctMatch = f.match(/(\d+(?:\.\d+)?)\s*%/);
                          const flatMatch = f.match(/€\s*(\d+(?:\.\d+)?)/);
                          const pct = pctMatch ? parseFloat(pctMatch[1]) / 100 : null;
                          const flat = flatMatch ? parseFloat(flatMatch[1]) : (f.toLowerCase().includes("flat") ? null : 0);
                          // Per-PSP avg ticket from the last-30d deposit data we have.
                          // PSPs in PSP_PERF carry a volume figure; we don't have
                          // tx count here, so we use a sensible default of €87.
                          const avgTicket = 87;
                          if (pct == null && flat == null) {
                            return <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:3}}>Effective at €{avgTicket}: — no data yet</div>;
                          }
                          const cost = ((pct || 0) * avgTicket) + (flat || 0);
                          return (
                            <div style={{fontSize:11, color:"var(--text-secondary)", marginTop:3}}>
                              Effective at €{avgTicket}: <strong>€{cost.toFixed(2)}</strong>
                            </div>
                          );
                        })()}
                      </div>
                      <div>
                        <div style={{fontSize:10.5, fontWeight:700, textTransform:"uppercase", color:"var(--text-tertiary)", letterSpacing:".05em", marginBottom:4}}>Limits</div>
                        <div>Per tx: <strong>{p.limits.min || "—"} – {p.limits.max || "—"}</strong></div>
                        <div>Daily: <strong>{p.limits.daily || "—"}</strong></div>
                        <div>Monthly: <strong>{p.limits.monthly || "—"}</strong></div>
                      </div>
                    </div>

                    {/* Liquidity — the new critical block */}
                    <div style={{padding:"10px 12px", borderRadius:8, background:"var(--n-25)", border:"1px solid var(--border-subtle)"}}>
                      <div style={{display:"flex", alignItems:"center", gap:6, marginBottom:6}}>
                        <div style={{fontSize:10.5, fontWeight:700, textTransform:"uppercase", color:"var(--text-tertiary)", letterSpacing:".05em"}}>Liquidity</div>
                        <span style={{fontSize:9, padding:"1px 6px", borderRadius:4, background:"var(--err-50, #fee2e2)", color:"var(--err-700, #991b1b)", fontWeight:700, letterSpacing:".05em"}}>LIVE</span>
                        <div style={{flex:1}}/>
                        <span style={{fontSize:11, fontWeight:600, color: health.color}}>● {health.label}</span>
                      </div>

                      {/* Bar with floor + sweep markers (sweep% is per-PSP) */}
                      <div style={{position:"relative", height:8, borderRadius:999, background:"var(--n-75)", overflow:"visible", marginBottom:6}}>
                        <div style={{position:"absolute", inset:0, borderRadius:999, overflow:"hidden"}}>
                          <div style={{height:"100%", width:`${pct}%`, background:health.color, transition:"width .25s"}}/>
                        </div>
                        {/* Floor marker */}
                        <div title="Floor — top-up needed below this" style={{position:"absolute", left:`${floorPct}%`, top:-3, bottom:-3, width:2, background:"var(--err-500, #ef4444)"}}/>
                        {/* Sweep marker (per-PSP %) */}
                        <div title={`${sweepPct}% of ceiling — sweep funds above this`} style={{position:"absolute", left:`${sweepPct}%`, top:-3, bottom:-3, width:2, background:"var(--warn-500, #f59e0b)"}}/>
                      </div>

                      <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:6, fontSize:11.5, lineHeight:1.5}}>
                        <div>Wallet: <strong>{formatEur(liq.walletEur)}</strong></div>
                        <div style={{textAlign:"right", color:"var(--text-tertiary)"}}>Ceiling: {formatEur(liq.ceilingEur)}</div>
                        <div style={{color:"var(--text-tertiary)"}}>Floor: {formatEur(liq.floorEur)}</div>
                        <div style={{textAlign:"right", color:"var(--text-tertiary)"}}>Min withdrawal: {formatEur(liq.minWithdrawalEur)}</div>
                      </div>

                      {/* 7-day wallet trend */}
                      <LiquiditySparkline history={liq.history7d} floor={liq.floorEur} ceiling={liq.ceilingEur}/>
                      <div style={{display:"flex", gap:8, fontSize:11, color:"var(--text-tertiary)", marginTop:6, flexWrap:"wrap"}}>
                        <span>Payout capacity: <strong>{liq.payout || "—"}</strong></span>
                        <span>·</span>
                        <span>Sweep alert at <strong>{sweepPct}%</strong> of ceiling</span>
                        {p.methodOverrides && p.methodOverrides.length > 0 && (
                          <>
                            <span>·</span>
                            <span style={{color:"var(--primary-dark, #1e3a8a)", fontWeight:600}}>{p.methodOverrides.length} method override{p.methodOverrides.length === 1 ? "" : "s"}</span>
                          </>
                        )}
                      </div>
                    </div>

                    {/* Coverage */}
                    <div>
                      <div style={{fontSize:10.5, fontWeight:700, textTransform:"uppercase", color:"var(--text-tertiary)", letterSpacing:".05em", marginBottom:4}}>Geographies</div>
                      <div style={{display:"flex", gap:4, flexWrap:"wrap"}}>
                        {p.geos.length === 0 && <span style={{fontSize:11, color:"var(--text-tertiary)"}}>—</span>}
                        {p.geos.map(g => (
                          <span key={g} style={{fontSize:10.5, fontWeight:600, padding:"2px 7px", borderRadius:999, background:"var(--n-75)", color:"var(--text-secondary)"}}>{g}</span>
                        ))}
                      </div>
                    </div>

                    {/* Licenses × operations */}
                    <div>
                      <div style={{fontSize:10.5, fontWeight:700, textTransform:"uppercase", color:"var(--text-tertiary)", letterSpacing:".05em", marginBottom:4}}>Licenses · operations</div>
                      <div style={{display:"flex", flexDirection:"column", gap:4}}>
                        {p.licenses.length === 0 && <span style={{fontSize:11, color:"var(--text-tertiary)"}}>—</span>}
                        {p.licenses.map(l => (
                          <div key={l.name} style={{display:"flex", alignItems:"center", gap:8, fontSize:11.5}}>
                            <span style={{fontWeight:600, minWidth:78}}>{l.name}</span>
                            <span style={{color:"var(--text-tertiary)"}}>·</span>
                            <span style={{display:"flex", gap:3, flexWrap:"wrap"}}>
                              {l.ops.map(op => (
                                <span key={op} style={{fontSize:10.5, fontWeight:600, padding:"1px 6px", borderRadius:4, background:"var(--ok-50, #d1fae5)", color:"var(--ok-700, #065f46)"}}>{OPS_LABEL[op] || op}</span>
                              ))}
                            </span>
                          </div>
                        ))}
                      </div>
                    </div>

                    <div style={{display:"flex", gap:8, justifyContent:"flex-end"}}>
                      <button className="btn btn--ghost btn--sm" title="Adjust floor / ceiling / sweep %" onClick={()=>setEditingPsp({ psp:p, initialTab:"liquidity" })}><Icon name="settings" size={12}/> Thresholds</button>
                      <button className="btn btn--secondary btn--sm" onClick={()=>setEditingPsp({ psp:p, initialTab:"general" })}><Icon name="edit" size={12}/> Edit</button>
                    </div>
                  </div>
                );
              })}
            </div>

            <p style={{fontSize:11.5, color:"var(--text-tertiary)", marginTop:14, lineHeight:1.6, maxWidth:780}}>
              Wallet balances refresh every 60 seconds via PSP balance APIs. Ceiling and floor breaches automatically fire alarms on the channels configured in <strong>Settings → Alarms</strong> (Slack / email / in-app), so the operator can sweep idle funds <em>before</em> capital sits unused, and top up <em>before</em> a withdrawal request gets rejected for empty wallet.
            </p>
          </div>
        )}

        {tab === "routes" && (
          <div style={{padding:20}}>
            {/* Plain-English explainer + 4 owner objectives */}
            <div style={{marginBottom:18, padding:"14px 16px", borderRadius:10,
                         border:"1px solid color-mix(in oklab, var(--primary, #1e40af) 18%, transparent)",
                         background:"color-mix(in oklab, var(--primary, #1e40af) 4%, white)",
                         display:"grid", gridTemplateColumns:"32px 1fr", gap:12, alignItems:"start"}}>
              <div style={{width:32, height:32, borderRadius:8, background:"var(--primary, #1e40af)", color:"#fff",
                           display:"grid", placeItems:"center", flexShrink:0}}>
                <Icon name="info" size={14}/>
              </div>
              <div>
                <div style={{fontWeight:700, fontSize:13.5, marginBottom:4}}>What this is, in plain English</div>
                <div style={{fontSize:12.5, color:"var(--text-secondary)", lineHeight:1.55}}>
                  Each transaction is sent to one of our providers (PSPs). <strong>Routing</strong> decides which PSP gets the transaction <em>before</em> we send it. <strong>Cascading</strong> decides what to do when a PSP fails — usually retry with the next one.
                </div>
                <div style={{display:"grid", gridTemplateColumns:"repeat(2, minmax(0, 1fr))", gap:"4px 16px", fontSize:12, color:"var(--text-secondary)", lineHeight:1.6, marginTop:8}}>
                  <div>● <strong>Maximize conversion</strong> — fewer failed deposits</div>
                  <div>● <strong>Optimize cost &amp; liquidity</strong> — prefer cheaper PSPs, avoid empty wallets</div>
                  <div>● <strong>Operational continuity</strong> — auto-failover when a PSP is down</div>
                  <div>● <strong>Operator control</strong> — manual ranking always wins over automation</div>
                </div>
              </div>
            </div>

            {/* Routing flow strip */}
            <div style={{marginBottom:18, padding:"10px 14px", borderRadius:10, border:"1px solid var(--border-subtle)", background:"#fff",
                         display:"flex", alignItems:"center", gap:10, flexWrap:"wrap", fontSize:11.5}}>
              <span style={{fontSize:10.5, fontWeight:700, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em", marginRight:4}}>Routing flow</span>
              {[
                "Receive request",
                "Identify (method · country · amount)",
                "Filter compatible PSPs",
                "Apply ranking",
                "Send to PSP",
                "✓ success / ✗ cascade",
              ].map((step, i, arr) => (
                <React.Fragment key={step}>
                  <span style={{padding:"3px 9px", borderRadius:999,
                                background: i === arr.length - 1 ? "var(--ok-50, #d1fae5)" : "var(--primary-soft, #eef2ff)",
                                color:      i === arr.length - 1 ? "var(--ok-700, #065f46)" : "var(--primary-dark, #1e3a8a)",
                                fontWeight:600}}>{step}</span>
                  {i < arr.length - 1 && <span style={{color:"var(--text-tertiary)"}}>→</span>}
                </React.Fragment>
              ))}
            </div>

            {/* Strategy summary — 4 clickable cards above the routes table.
                Each one filters the table to the matching subset. */}
            <div style={{display:"grid", gridTemplateColumns:"repeat(4, 1fr)", gap:12, marginBottom:14}}>
              {(() => {
                const cards = [
                  { key:"active",           label:"Active routes",      value:routeStats.active,          tone:"primary", desc:"Routes the engine can currently match" },
                  { key:"stale",            label:"Stale routes",       value:routeStats.stale,           tone:"neutral", desc:"No match in 14+ days" },
                  { key:"brand_scoped",     label:"Brand-scoped routes",value:routeStats.brandScoped,     tone:"primary", desc:"Not 'any' — fire only for one brand" },
                  { key:"cascade_override", label:"Custom cascade policy", value:routeStats.cascadeOverride, tone:"warn", desc:"Routes with a non-default error → action matrix" },
                ];
                const palette = {
                  primary: { bg:"#fff", border:"1px solid color-mix(in oklab, var(--primary, #1e40af) 18%, transparent)", strip:"var(--primary, #1e40af)", value:"var(--paybo-heading, #1e3a8a)" },
                  neutral: { bg:"#fff", border:"1px solid var(--border-default)",                                          strip:"var(--text-tertiary)",     value:"var(--text-primary)" },
                  warn:    { bg:"#fff", border:"1px solid var(--warn-200, #fde68a)",                                       strip:"var(--warn-500, #f59e0b)", value:"var(--warn-700, #92400e)" },
                };
                return cards.map(c => {
                  const tone = palette[c.tone];
                  const active = routesFilter === c.key;
                  return (
                    <button key={c.key} type="button"
                      onClick={() => setRoutesFilter(active ? null : c.key)}
                      style={{
                        position:"relative",
                        padding:"14px 16px", borderRadius:12,
                        background: tone.bg, border: tone.border,
                        textAlign:"left", cursor:"pointer",
                        boxShadow: active ? "0 0 0 2px " + tone.strip + ", 0 6px 16px -6px rgba(15,20,32,.15)" : "0 1px 2px rgba(15,20,32,.04)",
                        transition:"box-shadow .12s, transform .08s",
                      }}>
                      <span style={{position:"absolute", left:0, top:0, bottom:0, width:3, background:tone.strip, borderRadius:"12px 0 0 12px"}}/>
                      <div style={{fontSize:11.5, fontWeight:700, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em"}}>{c.label}</div>
                      <div style={{fontSize:28, fontWeight:700, color:tone.value, fontVariantNumeric:"tabular-nums", lineHeight:1.15, marginTop:4}}>{c.value}</div>
                      <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:2}}>{c.desc}</div>
                      {active && (
                        <span style={{position:"absolute", top:8, right:10, fontSize:9.5, fontWeight:700, padding:"2px 7px", borderRadius:999, background:tone.strip, color:"#fff", textTransform:"uppercase", letterSpacing:".05em"}}>
                          Filtering
                        </span>
                      )}
                    </button>
                  );
                });
              })()}
            </div>

            <div style={{display:"flex", alignItems:"center", justifyContent:"space-between", marginBottom:14}}>
              <div>
                <div style={{fontSize:14, fontWeight:650, color:"var(--paybo-heading, #1e3a8a)", display:"flex", alignItems:"center", gap:8, flexWrap:"wrap"}}>
                  Routes
                  {brand && !brand.isAll && (
                    <span title="Top-right brand selector is filtering the table"
                      style={{display:"inline-flex", alignItems:"center", gap:6, fontSize:11, fontWeight:600, padding:"2px 10px", borderRadius:999, background:"var(--primary-soft, #eef2ff)", color:"var(--primary-dark, #1e3a8a)", textTransform:"none", letterSpacing:0}}>
                      <Icon name="flag" size={10}/> Brand: {brand.name}
                    </span>
                  )}
                  {routesFilter && (
                    <span style={{display:"inline-flex", alignItems:"center", gap:6, fontSize:11, fontWeight:600, padding:"2px 10px", borderRadius:999, background:"var(--primary-soft, #eef2ff)", color:"var(--primary-dark, #1e3a8a)", textTransform:"none", letterSpacing:0}}>
                      Filtered · {filteredRoutes.length} of {brandFilteredRoutes.length}
                      <button type="button" onClick={() => setRoutesFilter(null)}
                        style={{width:16, height:16, padding:0, border:"none", borderRadius:999, background:"transparent", color:"var(--primary-dark, #1e3a8a)", cursor:"pointer", display:"grid", placeItems:"center"}}>
                        <Icon name="x" size={9}/>
                      </button>
                    </span>
                  )}
                </div>
                <div style={{fontSize:11.5, color:"var(--text-tertiary)", marginTop:2, maxWidth:760}}>
                  Provider chains per <strong>brand · method · currency · amount band</strong>. The chain is also the operator's <strong>ranking</strong> — first PSP gets tried first. Mode <strong>AUTO</strong> rotates automatically; <strong>MANUAL</strong> stops at To-Confirm before the next hop.
                </div>
              </div>
              <button className="btn btn--primary btn--sm" onClick={()=>setEditingRoute(blankRoute())}><Icon name="plus" size={12}/> Add route</button>
            </div>

            {/* Filter row */}
            <div style={{display:"flex", gap:8, alignItems:"center", marginBottom:14, padding:"10px 12px", background:"var(--n-25)", border:"1px solid var(--border-subtle)", borderRadius:10, flexWrap:"wrap"}}>
              <Icon name="filter" size={12} style={{color:"var(--text-tertiary)"}}/>
              <span style={{fontSize:11, fontWeight:700, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em"}}>Filter</span>
              <div style={{display:"flex", alignItems:"center", gap:6}}>
                <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Brand</label>
                {brandLocked ? (
                  <span style={{display:"inline-flex", alignItems:"center", gap:5, padding:"3px 9px", borderRadius:999, background:"var(--primary-soft, #eef2ff)", color:"var(--primary-dark, #1e3a8a)", fontSize:11.5, fontWeight:600}}
                        title="Locked by top-right brand selector">
                    <Icon name="lock" size={10}/> {brand.name}
                  </span>
                ) : (
                  <select className="select input--sm" value={routeBrandF} onChange={e=>setRouteBrandF(e.target.value)} style={{minWidth:130}}>
                    <option value="all">All brands</option>
                    <option value="any">any (shared)</option>
                    {brands.map(b => <option key={b.id} value={b.name}>{b.name}</option>)}
                  </select>
                )}
              </div>
              <div style={{display:"flex", alignItems:"center", gap:6}}>
                <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Method</label>
                <select className="select input--sm" value={routeMethodF} onChange={e=>setRouteMethodF(e.target.value)} style={{minWidth:130}}>
                  <option value="all">All methods</option>
                  {Object.values(ALL_METHODS_BY_ID).map(name => <option key={name} value={name}>{name}</option>)}
                </select>
              </div>
              <div style={{display:"flex", alignItems:"center", gap:6}}>
                <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Provider</label>
                <select className="select input--sm" value={routeProviderF} onChange={e=>setRouteProviderF(e.target.value)} style={{minWidth:140}}>
                  <option value="all">All providers</option>
                  {psps.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
                </select>
              </div>
              <div style={{display:"flex", alignItems:"center", gap:6}}>
                <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Status</label>
                <select className="select input--sm" value={routeStatusF} onChange={e=>setRouteStatusF(e.target.value)} style={{minWidth:110}}>
                  <option value="all">All statuses</option>
                  <option value="active">Active</option>
                  <option value="paused">Paused</option>
                </select>
              </div>
              <div style={{flex:1}}/>
              <span style={{fontSize:11.5, color:"var(--text-tertiary)"}}>{filteredRoutes.length} of {routes.length}</span>
              {(routeBrandF!=="all" || routeMethodF!=="all" || routeStatusF!=="all" || routeProviderF!=="all") && (
                <button className="btn btn--ghost btn--sm" onClick={()=>{ if(!brandLocked) setRouteBrandF("all"); setRouteMethodF("all"); setRouteStatusF("all"); setRouteProviderF("all"); }}>
                  <Icon name="x" size={10}/> Clear
                </button>
              )}
            </div>

            <table className="paybo-v2__table" style={{opacity:1}}>
              <thead>
                <tr>
                  <th>Scope</th>
                  <th>Provider ranking · cascade chain</th>
                  <th>Ranking</th>
                  <th>Triggers</th>
                  <th>Last matched</th>
                  <th>Volume (30d)</th>
                  <th>Mode</th>
                  <th>Status</th>
                  <th style={{width:60}}></th>
                </tr>
              </thead>
              <tbody>
                {filteredRoutes.map(rt => (
                  <tr key={rt.id}>
                    <td>
                      <div style={{fontWeight:600, fontSize:12.5}}>{rt.brand} · {rt.method}</div>
                      <div style={{fontSize:11, color:"var(--text-tertiary)", marginTop:2}}>{rt.currency} · {rt.amount}</div>
                      {(() => {
                        const brandScoped = isBrandScopedRoute(rt);
                        const matchedBrand = brandScoped ? brands.find(b => b.name === rt.brand) : null;
                        const customCascade = isCustomCascadeRoute(rt);
                        if (!brandScoped && !customCascade) return null;
                        return (
                          <div style={{display:"flex", gap:6, marginTop:6, flexWrap:"wrap"}}>
                            {brandScoped && (
                              <span style={{display:"inline-flex", alignItems:"center", gap:5, padding:"2px 8px", borderRadius:999,
                                background:"var(--primary-soft, #eef2ff)", color:"var(--primary-dark, #1e3a8a)",
                                fontSize:10.5, fontWeight:700, border:"1px solid color-mix(in oklab, var(--primary, #1e40af) 22%, transparent)"}}
                                title={`Brand-scoped: applies only to ${rt.brand}`}>
                                {matchedBrand && <span style={{width:6, height:6, borderRadius:2, background:matchedBrand.color}}/>}
                                {rt.brand}
                              </span>
                            )}
                            {customCascade && (
                              <button type="button"
                                onClick={() => setEditingRoute({ ...rt, _initialTab: "cascading" })}
                                title="This route uses a non-default Cascade policy. Click to view."
                                style={{display:"inline-flex", alignItems:"center", gap:4, padding:"2px 8px", borderRadius:999,
                                  background:"var(--warn-50, #fef3c7)", color:"var(--warn-700, #92400e)",
                                  fontSize:10.5, fontWeight:700, border:"1px solid var(--warn-200, #fde68a)",
                                  cursor:"pointer"}}>
                                <Icon name="alert" size={9}/> custom cascade
                              </button>
                            )}
                          </div>
                        );
                      })()}
                    </td>
                    <td>
                      <div style={{display:"inline-flex", alignItems:"center", gap:0, flexWrap:"wrap"}}>
                        {rt.chain.map((p, i) => {
                          const w = rt.weighted ? (rt.weights || [])[i] : null;
                          const psp = psps.find(x => x.id === p);
                          const name = psp ? psp.name : ((PROVIDERS[p] && PROVIDERS[p].name) || p);
                          return (
                            <React.Fragment key={p}>
                              {i > 0 && <span style={{margin:"0 6px", color:"var(--text-tertiary)", fontSize:11}}>{rt.weighted ? "·" : "→"}</span>}
                              <span style={{display:"inline-flex", alignItems:"center", gap:5,
                                            padding:"3px 9px 3px 6px", borderRadius:999,
                                            background:"var(--primary-soft, #eef2ff)", color:"var(--primary-dark, #1e3a8a)",
                                            fontSize:11.5, fontWeight:600,
                                            border:"1px solid color-mix(in oklab, var(--primary, #1e40af) 22%, transparent)"}}>
                                <span style={{display:"inline-grid", placeItems:"center",
                                              width:16, height:16, borderRadius:999,
                                              background:"var(--primary, #1e40af)", color:"#fff",
                                              fontSize:9.5, fontWeight:700}}>{i + 1}</span>
                                {name}
                                {rt.weighted && w != null && (
                                  <span style={{fontSize:10, fontWeight:700, padding:"1px 5px", borderRadius:4, background:"#fff", color:"var(--primary-dark, #1e3a8a)", border:"1px solid color-mix(in oklab, var(--primary, #1e40af) 22%, transparent)"}}>{w}%</span>
                                )}
                              </span>
                            </React.Fragment>
                          );
                        })}
                        {rt.weighted && (
                          <span style={{marginLeft:8, fontSize:10, fontWeight:700, padding:"2px 6px", borderRadius:4, background:"var(--ok-50, #d1fae5)", color:"var(--ok-700, #065f46)", letterSpacing:".05em"}}>WEIGHTED SPLIT</span>
                        )}
                      </div>
                    </td>
                    <td>
                      <span style={{fontSize:10.5, fontWeight:700, padding:"2px 7px", borderRadius:6, letterSpacing:".04em", textTransform:"uppercase",
                        background: rt.ranking === "hybrid" ? "color-mix(in oklab, var(--primary, #1e40af) 12%, white)" : "var(--n-75)",
                        color:      rt.ranking === "hybrid" ? "var(--primary-dark, #1e3a8a)"                              : "var(--text-secondary)"}}>
                        {rt.ranking}
                      </span>
                    </td>
                    <td>
                      <div style={{display:"flex", gap:4, flexWrap:"wrap"}}>
                        {rt.triggers.map(t => (
                          <span key={t} style={{fontSize:10.5, fontWeight:600, padding:"2px 7px", borderRadius:999, background:"var(--n-75)", color:"var(--text-secondary)", fontFamily:"var(--font-mono)"}}>{t}</span>
                        ))}
                        {rt.cascadeTimeout && rt.cascadeTimeout !== "Never" && (
                          <span style={{fontSize:10.5, fontWeight:600, padding:"2px 7px", borderRadius:999, background:"var(--n-75)", color:"var(--text-secondary)", fontFamily:"var(--font-mono)"}}
                            title={`Cascade if PSP doesn't respond within ${rt.cascadeTimeout}`}>timeout {rt.cascadeTimeout}</span>
                        )}
                      </div>
                    </td>
                    {/* Last matched — relative time, with muted styling when stale. */}
                    <td>
                      {(() => {
                        const ts = rt.lastMatchedAt;
                        if (!ts) {
                          return (
                            <span style={{display:"inline-flex", alignItems:"center", gap:5, fontSize:11.5, color:"var(--text-tertiary)"}}
                              title="No transaction has matched this route yet">
                              <Icon name="alert" size={11}/> never
                            </span>
                          );
                        }
                        const diff = Date.now() - ts;
                        const daysOld = diff / 86400_000;
                        const stale = daysOld >= 14;
                        const label =
                          diff < 60_000      ? "just now" :
                          diff < 3600_000    ? `${Math.floor(diff/60_000)} min ago` :
                          diff < 86400_000   ? `${Math.floor(diff/3600_000)} ${Math.floor(diff/3600_000)===1?"hour":"hours"} ago` :
                                               `${Math.floor(diff/86400_000)} ${Math.floor(diff/86400_000)===1?"day":"days"} ago`;
                        return (
                          <span style={{fontSize:11.5, color: stale ? "var(--warn-700, #92400e)" : "var(--text-secondary)", fontWeight: stale ? 700 : 500}}
                            title={stale ? "No match in 14+ days — route may be misconfigured" : new Date(ts).toLocaleString()}>
                            {stale && <Icon name="alert" size={10} style={{verticalAlign:"middle", marginRight:4}}/>}
                            {label}
                          </span>
                        );
                      })()}
                    </td>
                    {/* Volume share over the last 30 days. */}
                    <td>
                      {(() => {
                        const v = typeof rt.volume30d === "number" ? rt.volume30d : 0;
                        const totalRoutes = 4200; // approx 30d decisions — illustrative count for tooltips
                        const txN = Math.round((v / 100) * totalRoutes);
                        const tooltip = v < 1
                          ? `<1% of last-30-day routing decisions · ${txN} transactions`
                          : `${v}% of last-30-day routing decisions · ${txN} transactions`;
                        const isLow = v < 1;
                        return (
                          <div title={tooltip} style={{display:"flex", alignItems:"center", gap:8, minWidth:120}}>
                            <div style={{flex:1, height:6, background:"var(--n-75)", borderRadius:3, overflow:"hidden"}}>
                              <div style={{
                                height:"100%",
                                width: `${Math.max(2, v)}%`,
                                background: isLow ? "var(--text-tertiary)" : "var(--primary, #1e40af)",
                                opacity: isLow ? 0.45 : 1,
                              }}/>
                            </div>
                            <span style={{fontSize:11.5, fontWeight:600, fontVariantNumeric:"tabular-nums", minWidth:32, textAlign:"right", color: isLow ? "var(--text-tertiary)" : "var(--text-primary)"}}>
                              {isLow ? "<1%" : `${v}%`}
                            </span>
                          </div>
                        );
                      })()}
                    </td>
                    <td>
                      <span title={rt.mode === "AUTO"
                        ? "AUTO: cascade runs without human intervention. Failed transaction automatically retries on next PSP in the chain."
                        : "MANUAL: on first failure, transaction is paused as To-Confirm. An operator must approve before the cascade tries the next PSP."}
                        style={{fontSize:10.5, fontWeight:700, padding:"2px 7px", borderRadius:6, letterSpacing:".04em", cursor:"help",
                        background: rt.mode === "AUTO" ? "var(--ok-50, #d1fae5)" : "var(--warn-50, #fef3c7)",
                        color:      rt.mode === "AUTO" ? "var(--ok-700, #065f46)" : "#92400e"}}>
                        {rt.mode}
                      </span>
                    </td>
                    <td>
                      <span className={`chip ${rt.status === "active" ? "chip--ok" : "chip--neutral"}`}>{rt.status}</span>
                    </td>
                    <td style={{textAlign:"right"}}>
                      <button className="btn btn--ghost btn--icon btn--sm" title="Edit" onClick={()=>setEditingRoute(rt)}><Icon name="edit" size={13}/></button>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>

            <p style={{fontSize:11.5, color:"var(--text-tertiary)", marginTop:14, lineHeight:1.6}}>
              <strong>Resolution:</strong> most-specific scope wins. <code>any</code> matches anything not covered by a stricter rule. <strong>Manual</strong> ranking = use the chain order as-is. <strong>Hybrid</strong> ranking = blend chain order with live success rate so a temporarily-flaky PSP drops a slot, then comes back. Operator order is always respected when both options are healthy. <strong>Weighted split</strong> sends traffic by percentages instead of strict priority — when a weighted PSP is excluded for liquidity / health reasons, its share redistributes proportionally to the remaining healthy PSPs.
            </p>

          </div>
        )}

        {tab === "alarms" && (
          <div style={{padding:20}}>
            <div style={{marginBottom:18, padding:"14px 16px", borderRadius:10,
                         border:"1px solid color-mix(in oklab, var(--primary, #1e40af) 18%, transparent)",
                         background:"color-mix(in oklab, var(--primary, #1e40af) 4%, white)",
                         display:"grid", gridTemplateColumns:"32px 1fr", gap:12, alignItems:"start"}}>
              <div style={{width:32, height:32, borderRadius:8, background:"var(--primary, #1e40af)", color:"#fff",
                           display:"grid", placeItems:"center", flexShrink:0}}>
                <Icon name="info" size={14}/>
              </div>
              <div>
                <div style={{fontWeight:700, fontSize:13.5, marginBottom:4}}>What this is, in plain English</div>
                <div style={{fontSize:12.5, color:"var(--text-secondary)", lineHeight:1.55}}>
                  Alarms are early-warnings. They fire when a metric crosses a threshold or when a single event matches a pattern, and they fan out to Slack, email, or the in-app inbox so the operator can act <em>before</em> the problem becomes a customer complaint.
                </div>
                <div style={{fontSize:12, color:"var(--text-secondary)", lineHeight:1.6, marginTop:8}}>
                  Two of these alarms (<code>wallet near ceiling</code>, <code>wallet below floor</code>) are wired to the thresholds set in <strong>Settings → Providers</strong> — they fire automatically per-PSP when balance crosses the floor or hits 80% of the ceiling.
                </div>
              </div>
            </div>

            {/* ===== Alarm rules — destinations are configured inside each
                alarm modal, no longer a separate top-of-page section ===== */}
            <div style={{display:"flex", alignItems:"center", justifyContent:"space-between", marginBottom:14}}>
              <div>
                <div style={{fontSize:14, fontWeight:650, color:"var(--paybo-heading, #1e3a8a)"}}>Alarm rules</div>
                <div style={{fontSize:11.5, color:"var(--text-tertiary)", marginTop:2}}>
                  {alarms.filter(a=>a.status==="on").length} active · {alarms.filter(a=>a.status!=="on").length} paused · destinations are configured per alarm
                </div>
              </div>
              <button className="btn btn--primary btn--sm" onClick={()=>setEditingAlarm(blankAlarm())}><Icon name="plus" size={12}/> Add alarm</button>
            </div>

            {/* Filter row */}
            <div style={{display:"flex", gap:8, alignItems:"center", marginBottom:14, padding:"10px 12px", background:"var(--n-25)", border:"1px solid var(--border-subtle)", borderRadius:10, flexWrap:"wrap"}}>
              <Icon name="filter" size={12} style={{color:"var(--text-tertiary)"}}/>
              <span style={{fontSize:11, fontWeight:700, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em"}}>Filter</span>
              <div style={{display:"flex", alignItems:"center", gap:6}}>
                <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Group</label>
                <select className="select input--sm" value={alarmGroupF} onChange={e=>setAlarmGroupF(e.target.value)} style={{minWidth:140}}>
                  <option value="all">All groups</option>
                  {Object.keys(ALARM_METRIC_GROUPS).map(g => <option key={g} value={g}>{g}</option>)}
                </select>
              </div>
              <div style={{display:"flex", alignItems:"center", gap:6}}>
                <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Severity</label>
                <select className="select input--sm" value={alarmSeverityF} onChange={e=>setAlarmSeverityF(e.target.value)} style={{minWidth:120}}>
                  <option value="all">All severities</option>
                  {ALARM_SEVERITIES.map(s => <option key={s} value={s}>{s}</option>)}
                </select>
              </div>
              <div style={{display:"flex", alignItems:"center", gap:6}}>
                <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Status</label>
                <select className="select input--sm" value={alarmStatusF} onChange={e=>setAlarmStatusF(e.target.value)} style={{minWidth:100}}>
                  <option value="all">All</option>
                  <option value="on">On</option>
                  <option value="off">Off</option>
                </select>
              </div>
              <div style={{display:"flex", alignItems:"center", gap:6}}>
                <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Provider</label>
                <select className="select input--sm" value={alarmProviderF} onChange={e=>setAlarmProviderF(e.target.value)} style={{minWidth:140}}>
                  <option value="all">All providers</option>
                  {psps.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
                </select>
              </div>
              <div style={{display:"flex", alignItems:"center", gap:6}}>
                <label style={{fontSize:11, color:"var(--text-tertiary)"}}>Brand</label>
                {brandLocked ? (
                  <span style={{display:"inline-flex", alignItems:"center", gap:5, padding:"3px 9px", borderRadius:999, background:"var(--primary-soft, #eef2ff)", color:"var(--primary-dark, #1e3a8a)", fontSize:11.5, fontWeight:600}}>
                    <Icon name="lock" size={10}/> {brand.name}
                  </span>
                ) : (
                  <select className="select input--sm" value={alarmBrandF} onChange={e=>setAlarmBrandF(e.target.value)} style={{minWidth:130}}>
                    <option value="all">All brands</option>
                    {brands.map(b => <option key={b.id} value={b.name}>{b.name}</option>)}
                  </select>
                )}
              </div>
              <div style={{flex:1}}/>
              <span style={{fontSize:11.5, color:"var(--text-tertiary)"}}>{filteredAlarms.length} of {combinedAlarms.length}</span>
              {(alarmSeverityF!=="all" || alarmStatusF!=="all" || alarmGroupF!=="all" || alarmProviderF!=="all" || alarmBrandF!=="all") && (
                <button className="btn btn--ghost btn--sm" onClick={()=>{ setAlarmSeverityF("all"); setAlarmStatusF("all"); setAlarmGroupF("all"); setAlarmProviderF("all"); if(!brandLocked) setAlarmBrandF("all"); }}>
                  <Icon name="x" size={10}/> Clear
                </button>
              )}
            </div>

            <table className="paybo-v2__table" style={{opacity:1}}>
              <thead>
                <tr>
                  <th>Name</th>
                  <th>Metric</th>
                  <th>Condition</th>
                  <th>Severity</th>
                  <th>Sends to</th>
                  <th>Status</th>
                  <th style={{width:80}}></th>
                </tr>
              </thead>
              <tbody>
                {filteredAlarms.map(a => {
                  const sev = SEVERITY_CHIP[a.severity] || SEVERITY_CHIP.low;
                  const isProvider = a._origin === "provider";
                  const m = ALARM_METRIC_BY_KEY[a.metric];
                  // Both regular alarms and provider liquidity alarms now
                  // carry `destinations` ids. Fall back to raw `channels` for
                  // legacy provider rules that have not yet been migrated.
                  const dests = (a.destinations || []).map(id => destinations.find(x => x.id === id)).filter(Boolean);
                  const channelChips = isProvider && dests.length === 0
                    ? (a.channels || []).map(cid => NOTIF_CHANNEL_BY_ID[cid]).filter(Boolean)
                    : [];
                  const chosenDests = dests;
                  const metricLabel = isProvider
                    ? (a.metric || "").replace(/^liq:/, "").replace(/_/g, " ")
                    : (m ? m.label : a.metric);
                  const metricGroup = isProvider ? "Liquidity & cash flow" : (m ? m.group : "");
                  return (
                    <tr key={a.id}>
                      <td style={{fontWeight:600, fontSize:12.5}}>
                        {a.name}
                        {isProvider && (
                          <div style={{marginTop:3}}>
                            <button type="button" onClick={()=>{ const p = psps.find(x => x.id === a._pspId); if (p) setEditingPsp({ psp:p, initialTab:"liquidity" }); }}
                              title="Open the provider editor to edit this alarm"
                              style={{display:"inline-flex", alignItems:"center", gap:4, fontSize:10, fontWeight:700, padding:"1px 7px", borderRadius:999, background:"var(--primary-soft, #eef2ff)", color:"var(--primary-dark, #1e3a8a)", border:"1px solid color-mix(in oklab, var(--primary, #1e40af) 22%, transparent)", cursor:"pointer"}}>
                              <Icon name="rules" size={9}/> {a._pspName}
                            </button>
                          </div>
                        )}
                        {(a.scopeBrands && !a.scopeBrands.includes("any") && a.scopeBrands.length > 0) && (
                          <div style={{marginTop:3, display:"flex", gap:3, flexWrap:"wrap"}}>
                            {a.scopeBrands.slice(0, 3).map(bn => {
                              const bMeta = brands.find(bb => bb.name === bn);
                              return (
                                <span key={bn} style={{display:"inline-flex", alignItems:"center", gap:4, fontSize:9.5, fontWeight:700, padding:"1px 6px", borderRadius:999, background:"var(--n-75)", color:"var(--text-secondary)"}}>
                                  {bMeta && <span style={{width:5, height:5, borderRadius:1, background:bMeta.color}}/>}
                                  {bn}
                                </span>
                              );
                            })}
                            {a.scopeBrands.length > 3 && (
                              <span style={{fontSize:9.5, color:"var(--text-tertiary)"}}>+{a.scopeBrands.length - 3}</span>
                            )}
                          </div>
                        )}
                      </td>
                      <td style={{fontSize:12}}>
                        <div style={{fontWeight:600, textTransform: isProvider ? "capitalize" : "none"}}>{metricLabel}</div>
                        <div style={{fontSize:10.5, color:"var(--text-tertiary)"}}>{metricGroup}{a.scopeMethod ? ` · ${ALL_METHODS_BY_ID[a.scopeMethod] || a.scopeMethod}` : ""}</div>
                      </td>
                      <td style={{fontSize:11.5, color:"var(--text-secondary)"}}>
                        {a.op} {a.threshold}{m?.unit && m.unit !== "(event)" ? ` ${m.unit}` : ""}
                        {a.window && a.window !== "—" ? <span style={{color:"var(--text-tertiary)"}}> · over {a.window}</span> : null}
                      </td>
                      <td>
                        <span style={{fontSize:10.5, fontWeight:700, padding:"2px 8px", borderRadius:999, textTransform:"uppercase", letterSpacing:".05em", background:sev.bg, color:sev.fg, border:`1px solid ${sev.border}`}}>
                          {a.severity}
                        </span>
                      </td>
                      <td>
                        <div style={{display:"flex", gap:4, flexWrap:"wrap"}}>
                          {chosenDests.slice(0, 3).map(d => {
                            const c = NOTIF_CHANNEL_BY_ID[d.channel] || NOTIF_CHANNELS[0];
                            return (
                              <span key={d.id} title={`${c.label} — ${d.target}`}
                                style={{fontSize:10.5, fontWeight:600, padding:"2px 7px", borderRadius:999, background:`${c.color}14`, color:c.color, display:"inline-flex", alignItems:"center", gap:4}}>
                                <Icon name={c.icon} size={9}/> {d.label}
                              </span>
                            );
                          })}
                          {chosenDests.length > 3 && (
                            <span style={{fontSize:10.5, fontWeight:600, padding:"2px 7px", borderRadius:999, background:"var(--n-75)", color:"var(--text-secondary)"}}>+{chosenDests.length - 3}</span>
                          )}
                          {chosenDests.length === 0 && channelChips.length === 0 && (
                            <span style={{fontSize:10.5, color:"var(--err-600)"}}>no destination</span>
                          )}
                          {chosenDests.length === 0 && channelChips.length > 0 && channelChips.map(c => (
                            <span key={c.id} title={c.label}
                              style={{fontSize:10.5, fontWeight:600, padding:"2px 7px", borderRadius:999, background:`${c.color}14`, color:c.color, display:"inline-flex", alignItems:"center", gap:4}}>
                              <Icon name={c.icon} size={9}/> {c.label}
                            </span>
                          ))}
                        </div>
                      </td>
                      <td>
                        <span className={`chip ${a.status === "on" ? "chip--ok" : "chip--neutral"}`}>{a.status}</span>
                      </td>
                      <td style={{textAlign:"right"}}>
                        {isProvider ? (
                          <button className="btn btn--ghost btn--icon btn--sm" title="Open provider to edit"
                            onClick={()=>{ const p = psps.find(x => x.id === a._pspId); if (p) setEditingPsp({ psp:p, initialTab:"liquidity" }); }}>
                            <Icon name="external" size={13}/>
                          </button>
                        ) : (
                          <>
                            <button className="btn btn--ghost btn--icon btn--sm" title="Test fire" onClick={()=>testFireAlarm(a)}><Icon name="zap" size={13}/></button>
                            <button className="btn btn--ghost btn--icon btn--sm" title="Edit" onClick={()=>setEditingAlarm(a)}><Icon name="edit" size={13}/></button>
                          </>
                        )}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>

            <p style={{fontSize:11.5, color:"var(--text-tertiary)", marginTop:14, lineHeight:1.6}}>
              Each fired alarm is appended to the immutable Activity log. Critical-severity alarms also surface as a top-bar banner until acknowledged. The <strong>Test fire</strong> button posts a sample event to the alarm's destinations — useful when wiring a new Slack channel or webhook.
            </p>
          </div>
        )}

      </div>

      {editingPsp && <ProviderEditor psp={editingPsp.psp} initialTab={editingPsp.initialTab}
        destinations={destinations}
        onSave={savePsp} onCancel={()=>setEditingPsp(null)} onDelete={deletePsp}
        onAddDestination={()=>setEditingDest(blankDestination())}
        onEditDestination={(d)=>setEditingDest(d)}
        onToggleDestination={toggleDestinationEnabled}/>}
      {editingRoute && <RouteEditor route={editingRoute} brands={brands} psps={psps} activeBrand={brand} onSave={saveRoute} onCancel={()=>setEditingRoute(null)} onDelete={deleteRoute}/>}
      {editingAlarm && <AlarmEditor alarm={editingAlarm} destinations={destinations}
        brands={brands} brandLocked={brandLocked} activeBrand={brand}
        onSave={saveAlarm} onCancel={()=>setEditingAlarm(null)} onDelete={deleteAlarm} onTestFire={testFireAlarm}
        onAddDestination={()=>setEditingDest(blankDestination())}
        onEditDestination={(d)=>setEditingDest(d)}
        onToggleDestination={toggleDestinationEnabled}/>}
      {editingDest && <DestinationEditor dest={editingDest} onSave={saveDestination} onCancel={()=>setEditingDest(null)} onDelete={deleteDestination}/>}
    </div>
  );
};
