// Opalus Investor Platform — shared data, persistence & small UI helpers
// Everything here is attached to window for cross-file (Babel) sharing.

/* ---------------------------------- format helpers ---------------------------------- */
const eur = (n) => '€' + Math.round(n).toLocaleString('en-US');
const eurC = (n) => '€' + Number(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const compact = (n) => {
  if (n >= 1e6) return '€' + (n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 2) + 'M';
  if (n >= 1e3) return '€' + (n / 1e3).toFixed(0) + 'K';
  return '€' + n;
};
const pct = (n) => n.toFixed(n % 1 === 0 ? 0 : 1) + '%';
// dd Mon yyyy (e.g. 11 Jun 2026) — used for consent/validity timestamps
const fmtDate = (d) => new Date(d).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
// dd Mon yyyy, HH:mm — consent records carry a precise timestamp
const fmtDateTime = (d) => new Date(d).toLocaleString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' });
// add whole months to a date, returning an ISO string (soph 24-month validity, etc.)
const addMonths = (d, n) => { const x = new Date(d); x.setMonth(x.getMonth() + n); return x.toISOString(); };

/* ---------------------------------- legal document versions (consent capture) ----------------------------------
   Each acceptance writes a versioned, timestamped consent record into investor.consents. Versions are
   bumped centrally here so a re-acceptance prompt can be triggered when an investor's record is stale. */
const LEGAL_DOCS = {
  terms: { label: 'Terms of Use', version: 'v3.1', updated: '14 Jan 2026' },
  privacy: { label: 'Privacy Policy', version: 'v2.4', updated: '14 Jan 2026' },
  cookies: { label: 'Cookie Policy', version: 'v1.2', updated: '2 Sep 2025' },
};

/* ---------------------------------- projects (v1 · Paris, EUR) ----------------------------------
   Equity-only per BP-002 / DM projects.offering_type{equity v1}. Every offer is ORDINARY SHARES
   in the project SPV — no loans, no interest, no LTV. `returnLow/returnHigh` is a PROJECTED equity
   return (capital growth + dividends), not a guaranteed/fixed rate. `equityOffered` is the % of the
   SPV's ordinary share capital offered to investors. `payout` describes equity returns. */
const PROJECTS = [
  {
    id: 'saint-cloud-villa',
    title: 'Saint-Cloud Garden Villa',
    location: 'Saint-Cloud, Île-de-France',
    image: 'assets/images/property-detail-hero.png',
    gallery: ['assets/images/property-detail-hero.png', 'assets/images/property-detail-1.png', 'assets/images/property-detail-2.png'],
    assetType: 'Residential', nature: 'Development', risk: 'Medium',
    returnLow: 11.5, returnHigh: 14.2, term: 24, equityOffered: 35,
    min: 1000, target: 2400000, raised: 1656000, investors: 184, status: 'Live',
    instrument: 'Ordinary shares in the project SPV', payout: 'Dividends + return of capital on exit',
    summary: 'A ground-up build of six architect-designed villas around a shared landscaped garden in the leafy western suburbs, moments from the Bois de Boulogne. Pre-sales already cover 40% of gross development value.',
    highlights: [
      'Ordinary shares in the project SPV — 35% of share capital offered',
      'Investors share pro-rata in net disposal proceeds on exit',
      'Local developer co-invests and holds the remaining equity',
    ],
  },
  {
    id: 'marais-heritage',
    title: 'Le Marais Heritage Apartments',
    location: 'Le Marais, Paris 4e',
    image: 'assets/images/property-3.png',
    gallery: ['assets/images/property-3.png', 'assets/images/property-detail-1.png', 'assets/images/property-detail-2.png'],
    assetType: 'Residential', nature: 'Renovation', risk: 'Low',
    returnLow: 9.0, returnHigh: 10.8, term: 18, equityOffered: 30,
    min: 1000, target: 1450000, raised: 1305000, investors: 226, status: 'Live',
    instrument: 'Ordinary shares in the project SPV', payout: 'Return of capital + capital growth on exit',
    summary: 'Full refurbishment of eight Haussmannian apartments in a protected 17th-century hôtel particulier, restoring period features while bringing energy performance to a modern B rating for the premium Marais rental market.',
    highlights: [
      'Prime, supply-constrained central Paris location',
      'Pro-rata equity share in net proceeds, with strong exit comparables',
      'Planning and Bâtiments de France consents already granted',
    ],
  },
  {
    id: 'batignolles-townhouses',
    title: 'Batignolles Eco-Townhouses',
    location: 'Les Batignolles, Paris 17e',
    image: 'assets/images/property-2.png',
    gallery: ['assets/images/property-2.png', 'assets/images/property-detail-2.png', 'assets/images/property-detail-1.png'],
    assetType: 'Residential', nature: 'Development', risk: 'Medium',
    returnLow: 12.0, returnHigh: 15.5, term: 30, equityOffered: 40,
    min: 1000, target: 3100000, raised: 992000, investors: 97, status: 'Live',
    instrument: 'Ordinary shares in the project SPV', payout: 'Dividends + return of capital on exit',
    summary: 'A timber-frame, low-carbon development of nine family townhouses beside the Martin Luther King park in the fast-regenerating Clichy-Batignolles eco-district.',
    highlights: [
      'Award-winning eco-district with strong price growth',
      'Fixed-price build contract limits cost overrun risk',
      'BBCA low-carbon certification targeted at completion',
    ],
  },
  {
    id: 'saint-germain-collection',
    title: 'Saint-Germain Pied-à-Terre Collection',
    location: 'Saint-Germain-des-Prés, Paris 6e',
    image: 'assets/images/property-detail-1.png',
    gallery: ['assets/images/property-detail-1.png', 'assets/images/property-3.png', 'assets/images/property-detail-hero.png'],
    assetType: 'Residential', nature: 'Renovation', risk: 'Low',
    returnLow: 8.5, returnHigh: 10.0, term: 15, equityOffered: 25,
    min: 1000, target: 980000, raised: 980000, investors: 312, status: 'Funded',
    instrument: 'Ordinary shares in the project SPV', payout: 'Return of capital + capital growth on exit',
    summary: 'Refurbishment of four compact luxury pieds-à-terre on the Rive Gauche, targeting international buyers and the high-yield short-stay market around Saint-Germain-des-Prés.',
    highlights: [
      'Fully funded — now in works phase',
      'Smallest equity stake offered on the platform at 25% of share capital',
      'Trophy address with deep international buyer demand',
    ],
  },
  {
    id: 'canal-saint-martin',
    title: 'Canal Saint-Martin Lofts',
    location: 'Canal Saint-Martin, Paris 10e',
    image: 'assets/images/property-detail-2.png',
    gallery: ['assets/images/property-detail-2.png', 'assets/images/property-detail-1.png', 'assets/images/property-3.png'],
    assetType: 'Mixed Use', nature: 'Renovation', risk: 'Medium',
    returnLow: 10.5, returnHigh: 13.0, term: 21, equityOffered: 38,
    min: 1000, target: 1850000, raised: 462500, investors: 58, status: 'Live', sophOnly: true,
    instrument: 'Ordinary shares in the project SPV', payout: 'Dividends + return of capital on exit',
    summary: 'Conversion of a former canal-side warehouse into five live-work lofts above two ground-floor retail units, in one of the most sought-after creative quarters of north-east Paris.',
    highlights: [
      'Mixed residential and retail rental income distributed as dividends',
      'Vibrant, high-footfall canal-side micro-location',
      'Developer co-invests alongside platform investors',
    ],
  },
];

/* ---------------------------------- default investor state ---------------------------------- */
const DEFAULT_STATE = {
  route: { name: 'welcome', params: {} },
  signedIn: false,
  onboarded: false,
  locale: 'en',                   // EN/FR UI language (users.preferred_locale) — BRD-9.1 multilang

  investor: {
    name: 'Claire Dubois',
    email: 'claire.dubois@email.fr',
    country: 'France',
    fiscalResidence: 'France',    // Art 16 country of fiscal residence (defaults to residence, overridable)
    countrySupported: true,       // residency eligibility
    avatar: 'assets/images/person-03.jpg',
    emailVerified: true,
    twoFA: { enabled: true, method: 'app' },   // method: 'app' | 'sms'
    kyc: 'unverified',            // unverified | pending | review | verified | rejected
    category: null,               // 'non-sophisticated' | 'sophisticated'
    sophApplication: null,        // { status: 'review'|'approved'|'rejected', validUntil }
    knowledgeTest: null,          // { passed, score, dueDate }
    netWorth: null,               // € entered in loss-bearing simulation
    lossSim: null,                // { capacity, planned, acknowledged, date, dueDate }
    riskAck: false,
    // Art 8(2) — connection to Opalus (shareholder ≥20% / manager / director / employee / controlled
    // person). When connected, the investor's investments appear on the public register, which requires
    // a prior public-disclosure consent. null until declared at onboarding; seed account has declared
    // no connection.
    restrictedPerson: { connected: false, relationship: null, disclosureConsent: false, declaredAt: '2026-01-20T09:14:00.000Z' },
    notifPrefs: { projectUpdates: true, productNews: true, marketing: false },
    password: 'password',                       // demo credential; "Create account" overwrites it
    loginLockout: { fails: 0, until: 0 },       // 5-fail / 15-min lockout (BRD-1358)
    // Versioned + timestamped consent records (T&C, Privacy, cookies) written at registration — GDPR Art 7.
    // Seed account is pre-consented to the current document versions.
    consents: {
      terms: { version: 'v3.1', acceptedAt: '2026-01-20T09:14:00.000Z' },
      privacy: { version: 'v2.4', acceptedAt: '2026-01-20T09:14:00.000Z' },
      cookies: { version: 'v1.2', acceptedAt: '2026-01-20T09:14:00.000Z' },
    },
    // Saved own-name beneficiary accounts (withdrawal destinations). status: 'verified' | 'pending'.
    bankAccounts: [
      { id: 'ba-seed', label: 'BNP Paribas — current account', iban: 'FR76 3000 4000 0312 3456 7890 143', holder: 'Claire Dubois', status: 'verified', addedAt: '2026-02-02T10:00:00.000Z' },
    ],
  },
  // wallet.balance is the AVAILABLE balance; Reserved-in-cooling-off is derived
  // from holdings whose status === 'offer' (see window.reservedTotal).
  wallet: { balance: 6200, transactions: [
    { id: 't4', type: 'Distribution', label: 'Distribution · Le Marais Heritage', amount: 122.5, date: '1 Jun 2026', kind: 'in', status: 'Completed' },
    { id: 't3', type: 'Investment released', label: 'Investment released · Saint-Cloud Garden Villa', amount: -7500, date: '22 Apr 2026', kind: 'out', status: 'Completed' },
    { id: 't2', type: 'Investment released', label: 'Investment released · Le Marais Heritage', amount: -5000, date: '7 Mar 2026', kind: 'out', status: 'Completed' },
    { id: 't1', type: 'Deposit', label: 'Deposit · SEPA transfer', amount: 18700, date: '2 Mar 2026', kind: 'in', status: 'Completed' },
  ] },
  // status: 'offer' (reflection period, reserved) | 'confirmed' (reflection ended, reserved pending project close)
  //       | 'awaiting-signature' | 'active'
  holdings: [
    { id: 'h-seed-1', projectId: 'saint-cloud-villa', invested: 7500, value: 8025, roiPct: 7.0, yieldPct: 12.85, status: 'active', kiisVersion: 3, committedAt: 0, coolingEnds: 0 },
    { id: 'h-seed-2', projectId: 'marais-heritage', invested: 5000, value: 5380, roiPct: 7.6, yieldPct: 9.8, status: 'active', kiisVersion: 2, committedAt: 0, coolingEnds: 0 },
    { id: 'h-seed-3', projectId: 'batignolles-townhouses', invested: 3000, value: 3000, roiPct: 0, yieldPct: 0, status: 'confirmed', kiisVersion: 1, committedAt: 0, coolingEnds: 0, confirmedAt: '2026-06-05T12:00:00.000Z' },
  ],
  notifications: [
    { id: 'n3', kind: 'Distribution', title: 'Distribution received', body: 'Le Marais Heritage paid €122.50 to your available balance.', date: '1 Jun 2026', read: false },
    { id: 'n2', kind: 'Regulatory', title: 'Annual loss simulation due soon', body: 'Refresh your simulation before your next investment.', date: '20 May 2026', read: false },
    { id: 'n1', kind: 'Project update', title: 'Saint-Cloud Garden Villa — works update', body: 'Foundations complete; on schedule for the next milestone.', date: '12 May 2026', read: true },
  ],
  // Art 7 complaints — free of charge; status: received | in_progress | resolved
  complaints: [],
};

/* ---------------------------------- persistence ---------------------------------- */
const STORE_KEY = 'opalus_investor_v1';
function loadState() {
  try {
    const raw = localStorage.getItem(STORE_KEY);
    if (!raw) return structuredClone(DEFAULT_STATE);
    const saved = JSON.parse(raw);
    return { ...structuredClone(DEFAULT_STATE), ...saved,
      investor: { ...DEFAULT_STATE.investor, ...(saved.investor || {}) },
      wallet: { ...DEFAULT_STATE.wallet, ...(saved.wallet || {}) } };
  } catch (e) { return structuredClone(DEFAULT_STATE); }
}
function saveState(s) { try { localStorage.setItem(STORE_KEY, JSON.stringify(s)); } catch (e) {} }
function resetState() { try { localStorage.removeItem(STORE_KEY); } catch (e) {} }

/* ---------------------------------- regulatory copy (verbatim-locked) ---------------------------------- */
const COPY = {
  art21_4: 'Based on your answers, the crowdfunding services provided through this platform may not be appropriate for you. You risk losing the entirety of the money invested.',
  art21_7: (amount) => `Your investment of ${amount} exceeds the greater of €1,000 or 5% of your net worth. Crowdfunding investments carry the risk of losing the entire amount invested and may be illiquid.`,
  capacity: (cap, date) => `This investment exceeds 10% of your simulated ability to bear losses (${cap}, simulated on ${date}). We recommend reviewing your simulation before proceeding.`,
  art22_6_pre: 'After you submit this offer you will have a 4-calendar-day reflection period during which you may withdraw it at no cost, without giving a reason, directly from your dashboard — the same way you are making it now. Your funds stay reserved in your wallet and are only released to the project when the period ends.',
  sophImmediate: 'As a sophisticated investor, no reflection period applies; your investment settles immediately on confirmation.',
  sophConsequences: 'Sophisticated investors give up protections: no appropriateness assessment, no loss-bearing simulation, no warnings on larger investments, and no 4-day reflection period — investments settle immediately and cannot be withdrawn.',
  sophStatement: 'I understand that as a sophisticated investor I will not receive the appropriateness assessment, the loss-bearing simulation, the warnings applicable to larger investments, or the 4-calendar-day reflection period, and that my investments will settle immediately and cannot be withdrawn.',
  sophVeracity: 'I confirm that the information and evidence provided are true, accurate and complete, and that I remain responsible for their truthfulness.',
  art23_13: 'We are unable to arrange this translation. We advise you to refrain from investing in this project if you cannot fully understand the Key Investment Information Sheet.',
  // Art 8(2) — public-register disclosure consent for restricted (connected) persons
  art8_2: 'As a person connected to Opalus, your participation in projects on this platform must be disclosed on our public register. The register shows that a connected person has invested, on the same terms as any other investor. By continuing, you consent to this public disclosure.',
  // Art 10(1) — safeguarding of client funds by the licensed payment service provider
  art10_1: 'Opalus never holds your money. Your funds are safeguarded in segregated client accounts at our licensed payment service provider, kept separate from Opalus’s own assets, in line with Article 10(1) of the ECSP Regulation.',
  // Art 19(2) — mandatory scheme disclaimers, surfaced at signup, on the project page and at the
  // commitment screen (BRD §8.9).
  art19_2: 'Crowdfunding services are not covered by the deposit guarantee scheme established under Directive 2014/49/EU, and securities or admitted instruments acquired through this platform are not covered by the investor compensation scheme established under Directive 97/9/EC.',
};

const ART21_7_QUESTIONS = [
  { q: 'Can you lose the entire amount you invest?', opts: ['Yes', 'No'], correct: 0,
    explain: 'Crowdfunding investments can fail entirely. You can lose everything you invest.' },
  { q: 'Is this investment covered by a deposit guarantee or investor compensation scheme?', opts: ['Yes', 'No'], correct: 1,
    explain: 'No guarantee or compensation scheme applies. If the project fails, no fund reimburses you.' },
  { q: 'Are you guaranteed to be able to sell or exit this investment at any time?', opts: ['Yes', 'No'], correct: 1,
    explain: 'There may be no market for your investment. You may be unable to exit before the project ends, if at all.' },
];

const ELIG = {
  RESIDENCY: (country) => `Only available for ${country} residents`,
  CATEGORY: 'Available to sophisticated investors only',
  STATE: 'Not currently accepting investments',
  ONBOARDING: 'Complete verification to invest',
};

/* ---------------------------------- derived helpers ---------------------------------- */
function reservedTotal(state) {
  return (state.holdings || []).filter((h) => h.status === 'offer').reduce((n, h) => n + h.invested, 0);
}
// Confirmed offers whose reflection period has ended but whose project has not yet closed/released —
// still reserved in the wallet, but no longer withdrawable. Distinct from cooling-off (Art 22(6)).
function confirmedPendingTotal(state) {
  return (state.holdings || []).filter((h) => h.status === 'confirmed').reduce((n, h) => n + h.invested, 0);
}
// Returns the consent rows for the consent-view surface, flagging records that pre-date the current
// document version (i.e. re-acceptance due).
function consentRows(state) {
  const c = (state.investor && state.investor.consents) || {};
  return ['terms', 'privacy', 'cookies'].map((key) => {
    const doc = LEGAL_DOCS[key];
    const rec = c[key] || null;
    return { key, label: doc.label, currentVersion: doc.version, updated: doc.updated,
      accepted: !!rec, version: rec ? rec.version : null, acceptedAt: rec ? rec.acceptedAt : null,
      stale: !!rec && rec.version !== doc.version };
  });
}
function investGate(state) {
  const inv = state.investor;
  if (!inv.countrySupported) return { code: 'RESIDENCY', copy: ELIG.RESIDENCY(inv.country) };
  if (inv.kyc !== 'verified' || !inv.category) return { code: 'ONBOARDING', copy: ELIG.ONBOARDING, step: 'onboarding' };
  return null;
}
function individualCap(inv) {
  return inv.category === 'sophisticated' ? Infinity : Math.max(1000, Math.round((inv.netWorth || 0) * 0.05));
}
// Per-project eligibility for the logged-in investor — returns a CMP-ELIG reason or null.
function projectEligibility(state, p) {
  const inv = state.investor;
  if (!inv.countrySupported) return { code: 'RESIDENCY', copy: ELIG.RESIDENCY(inv.country) };
  if (p.sophOnly && inv.category !== 'sophisticated') return { code: 'CATEGORY', copy: ELIG.CATEGORY };
  if (p.status === 'Funded') return { code: 'STATE', copy: ELIG.STATE };
  if (inv.kyc !== 'verified' || !inv.category) return { code: 'ONBOARDING', copy: ELIG.ONBOARDING, step: 'onboarding' };
  return null;
}

/* ---------------------------------- shared UI atoms ---------------------------------- */
const { useState, useEffect, useRef } = React;

function Eyebrow({ children, style }) {
  return <div style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-micro)', fontWeight: 600, letterSpacing: 'var(--ls-eyebrow)', textTransform: 'uppercase', color: 'var(--bronze-600)', ...style }}>{children}</div>;
}

function RiskBadge({ level, size }) {
  const { Badge } = window.OpalusDesignSystem_1dce4b;
  const tone = level === 'Low' ? 'success' : level === 'High' ? 'danger' : 'risk';
  return <Badge tone={tone} size={size || 'sm'} dot>{level} Risk</Badge>;
}

function StatusBadge({ status, size }) {
  const { Badge } = window.OpalusDesignSystem_1dce4b;
  const map = { Live: 'bronzeSolid', Funded: 'funded', Completed: 'navy', 'Under Review': 'neutral' };
  return <Badge tone={map[status] || 'neutral'} size={size || 'sm'}>{status}</Badge>;
}

// horizontal step progress used across onboarding & invest flows
function Stepper({ steps, current }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 0, width: '100%' }}>
      {steps.map((label, i) => {
        const done = i < current, active = i === current;
        return (
          <React.Fragment key={label}>
            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, flex: '0 0 auto' }}>
              <span style={{ width: 30, height: 30, borderRadius: '50%', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                fontFamily: 'var(--font-body)', fontSize: 13, fontWeight: 600,
                background: done ? 'var(--bronze-600)' : active ? 'var(--navy-700)' : 'var(--gray-200)',
                color: done || active ? '#fff' : 'var(--gray-500)',
                boxShadow: active ? '0 0 0 4px var(--focus-ring)' : 'none', transition: 'all .2s var(--ease-out)' }}>
                {done ? '✓' : i + 1}
              </span>
              <span className="opx-step-label" style={{ fontFamily: 'var(--font-body)', fontSize: 11.5, fontWeight: active ? 600 : 400, color: active ? 'var(--ink)' : 'var(--text-muted)', whiteSpace: 'nowrap' }}>{label}</span>
            </div>
            {i < steps.length - 1 && <span style={{ flex: 1, height: 2, background: i < current ? 'var(--bronze-400)' : 'var(--gray-200)', margin: '0 8px', marginBottom: 22 }} />}
          </React.Fragment>
        );
      })}
    </div>
  );
}

// labelled field wrapper
function Field({ label, hint, children }) {
  return (
    <label style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
      <span style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-micro)', fontWeight: 600, letterSpacing: 'var(--ls-eyebrow)', textTransform: 'uppercase', color: 'var(--gray-600)' }}>{label}</span>
      {children}
      {hint && <span style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>{hint}</span>}
    </label>
  );
}

// little info note / callout box
function Note({ tone = 'cream', icon, title, children, style }) {
  const palette = {
    cream: { bg: 'var(--cream-100)', bd: 'var(--bronze-300)', fg: 'var(--bronze-700)' },
    info: { bg: 'var(--info-50)', bd: 'var(--info-400)', fg: 'var(--info-600)' },
    warn: { bg: 'var(--warn-50)', bd: '#e7c98a', fg: 'var(--warn-600)' },
    danger: { bg: 'var(--danger-50)', bd: '#f0b4b6', fg: 'var(--danger-600)' },
    success: { bg: 'var(--success-50)', bd: '#a8e3b6', fg: 'var(--success-600)' },
  }[tone];
  return (
    <div style={{ display: 'flex', gap: 12, background: palette.bg, border: `1px solid ${palette.bd}`, borderRadius: 'var(--radius-md)', padding: '14px 16px', ...style }}>
      {icon && <span style={{ color: palette.fg, flex: '0 0 auto', marginTop: 1 }}>{icon}</span>}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
        {title && <span style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-sm)', fontWeight: 600, color: palette.fg }}>{title}</span>}
        <span style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-xs)', lineHeight: 1.55, color: 'var(--text-body)' }}>{children}</span>
      </div>
    </div>
  );
}

// checkbox / acknowledgement row used across regulatory gates
function AckBox({ checked, onChange, children, style }) {
  return (
    <label style={{ display: 'flex', gap: 11, alignItems: 'flex-start', cursor: 'pointer', ...style }}>
      <span style={{ width: 20, height: 20, flex: '0 0 auto', marginTop: 1, borderRadius: 5, border: `1.5px solid ${checked ? 'var(--bronze-600)' : 'var(--border-strong)'}`, background: checked ? 'var(--bronze-600)' : '#fff', color: '#fff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 12 }} onClick={(e) => { e.preventDefault(); onChange(!checked); }}>{checked && '✓'}</span>
      <span style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-xs)', color: 'var(--text-body)', lineHeight: 1.5 }} onClick={(e) => { e.preventDefault(); onChange(!checked); }}>{children}</span>
    </label>
  );
}

// Art 10(1) safekeeping / PSP-segregation disclosure — surfaced at deposit and at the invest/commit
// journey points. A lock-led navy callout consistent with the platform's safeguarding messaging.
function Art10Disclosure({ style }) {
  const { IconlyRegularLightLock } = window.OpalusDesignSystem_1dce4b;
  return (
    <div style={{ display: 'flex', gap: 13, alignItems: 'flex-start', background: 'var(--surface-inset)', border: '1px solid var(--border-faint)', borderRadius: 'var(--radius-md)', padding: '14px 16px', ...style }}>
      <span style={{ width: 32, height: 32, borderRadius: 'var(--radius-md)', background: 'var(--navy-950)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: '0 0 auto' }}>
        <IconlyRegularLightLock style={{ width: 17, height: 17, color: 'var(--bronze-400)' }} />
      </span>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
        <span style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-sm)', fontWeight: 600, color: 'var(--ink)' }}>Your money is safeguarded</span>
        <span style={{ fontFamily: 'var(--font-body)', fontSize: 'var(--fs-xs)', lineHeight: 1.55, color: 'var(--text-body)' }}>{COPY.art10_1}</span>
      </div>
    </div>
  );
}

Object.assign(window, {
  OPALUS_PROJECTS: PROJECTS, OPALUS_DEFAULT_STATE: DEFAULT_STATE,
  opalusLoad: loadState, opalusSave: saveState, opalusReset: resetState,
  eur, eurC, compact, pct, opalusFmtDate: fmtDate, opalusFmtDateTimeShort: fmtDateTime, opalusAddMonths: addMonths,
  OPALUS_COPY: COPY, ART21_7_QUESTIONS, OPALUS_ELIG: ELIG, OPALUS_LEGAL_DOCS: LEGAL_DOCS,
  reservedTotal, confirmedPendingTotal, consentRows, investGate, individualCap, projectEligibility,
  Eyebrow, RiskBadge, StatusBadge, Stepper, Field, Note, AckBox, Art10Disclosure,
});
