Jump to content

MediaWiki:Gadget-wix-interactive.js

From Insurer Brain
Revision as of 16:47, 6 April 2026 by Wikilah admin (talk | contribs)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* ================================================================
   WIX-INTERACTIVE.JS — Wiki Interactive Experience: Complex Widgets
   ================================================================
   Depends on: ext.gadget.wix-core  (window.wix must exist)

   Dispatches by data-wix-module value. Each value maps to an
   initializer function that builds the widget DOM and wires logic.

   Supported modules:
   - "pool-simulator"    Risk pooling comparison (Bordeaux vs Normandy)
   - "insurer-engines"   Two-engine profit simulator (underwriting + investment)
   ================================================================ */

( function () {
  'use strict';

  /* ── Dispatcher ─────────────────────────────────────────────── */

  var dispatchers = {
    'pool-simulator':    initPoolSimulator,
    'insurer-engines':   initInsurerEngines,
    'premium-matching':  initPremiumMatching
  };

  mw.hook( 'wikipage.content' ).add( function () {
    Object.keys( dispatchers ).forEach( function ( moduleType ) {
      wix.initModules( moduleType ).forEach( dispatchers[ moduleType ] );
    } );
  } );


  /* ================================================================
     POOL SIMULATOR
     Compares self-insured ("alone") vs pooled risk over 25 years.
     ================================================================ */

  function initPoolSimulator( container ) {

    /* ── Constants ────────────────────────────────────────────── */

    var YEARS       = 25;
    var PROB        = 0.02;
    var REPAIR      = 15000;
    var POOL_FEE    = 200;
    var START       = 20000;
    var ANNUAL_SAVE = 2000;

    /* ── Chart colors (match WIX tokens) ─────────────────────── */

    var styles     = getComputedStyle( container );
    var colorAlone = styles.getPropertyValue( '--wix-wrong' ).trim() || '#922b21';
    var colorPool  = styles.getPropertyValue( '--wix-correct' ).trim() || '#1e8449';
    var colorGrid  = styles.getPropertyValue( '--wix-border-subtle' ).trim() || '#eaecf0';
    var colorAxis  = styles.getPropertyValue( '--wix-text-muted' ).trim() || '#54595d';

    /* ── State ────────────────────────────────────────────────── */

    var year = 0;
    var playing = false;
    var timer = null;
    var aloneSavings = START;
    var poolSavings  = START;
    var aloneSpent   = 0;
    var poolSpent    = 0;
    var aloneData    = [ START ];
    var poolData     = [ START ];
    var hitYears     = {};

    /* ── Formatting ───────────────────────────────────────────── */

    function fmt( n ) {
      return '\u20AC' + wix.formatNumber( Math.round( n ) );
    }

    function speedMs() {
      var speeds = [ 800, 500, 300, 150, 80 ];
      return speeds[ parseInt( speedSlider.value, 10 ) - 1 ] || 300;
    }

    /* ── Build UI ─────────────────────────────────────────────── */

    wix.empty( container );

    // Dynamic elements (need references for updates)
    var elAloneSavings = wix.el( 'div', { className: 'wix-sim-stat-val', textContent: fmt( START ) } );
    var elAloneSpent   = wix.el( 'div', { className: 'wix-sim-stat-val', textContent: fmt( 0 ) } );
    var elPoolSavings  = wix.el( 'div', { className: 'wix-sim-stat-val', textContent: fmt( START ) } );
    var elPoolSpent    = wix.el( 'div', { className: 'wix-sim-stat-val', textContent: fmt( 0 ) } );

    var canvasAlone = wix.el( 'canvas' );
    var canvasPool  = wix.el( 'canvas' );

    var elAloneLog = wix.el( 'div', { className: 'wix-sim-log wix-sim-log--safe', textContent: 'Ready to simulate' } );
    var elPoolLog  = wix.el( 'div', { className: 'wix-sim-log wix-sim-log--safe', textContent: 'Ready to simulate' } );

    // Alone panel
    var alonePanel = wix.el( 'div', { className: 'wix-sim-panel wix-sim-panel--alone' }, [
      wix.el( 'div', { className: 'wix-sim-label wix-sim-label--alone', textContent: 'Going it alone' } ),
      wix.el( 'div', { className: 'wix-sim-title', textContent: 'Bordeaux homeowner' } ),
      wix.el( 'div', { className: 'wix-sim-stats' }, [
        wix.el( 'div', { className: 'wix-sim-stat' }, [
          wix.el( 'div', { className: 'wix-sim-stat-label', textContent: 'Savings' } ),
          elAloneSavings
        ] ),
        wix.el( 'div', { className: 'wix-sim-stat' }, [
          wix.el( 'div', { className: 'wix-sim-stat-label', textContent: 'Total spent on hail' } ),
          elAloneSpent
        ] )
      ] ),
      wix.el( 'div', { className: 'wix-sim-chart' }, [ canvasAlone ] ),
      elAloneLog
    ] );

    // Pool panel
    var poolPanel = wix.el( 'div', { className: 'wix-sim-panel wix-sim-panel--pool' }, [
      wix.el( 'div', { className: 'wix-sim-label wix-sim-label--pool', textContent: 'In the Normandy pool' } ),
      wix.el( 'div', { className: 'wix-sim-title', textContent: '1 of 1,000 homeowners' } ),
      wix.el( 'div', { className: 'wix-sim-stats' }, [
        wix.el( 'div', { className: 'wix-sim-stat' }, [
          wix.el( 'div', { className: 'wix-sim-stat-label', textContent: 'Savings' } ),
          elPoolSavings
        ] ),
        wix.el( 'div', { className: 'wix-sim-stat' }, [
          wix.el( 'div', { className: 'wix-sim-stat-label', textContent: 'Total paid to pool' } ),
          elPoolSpent
        ] )
      ] ),
      wix.el( 'div', { className: 'wix-sim-chart' }, [ canvasPool ] ),
      elPoolLog
    ] );

    // Outer wrapper card
    var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
    container.appendChild( wrapper );

    // Panels grid
    wrapper.appendChild( wix.el( 'div', { className: 'wix-sim-panels' }, [
      alonePanel,
      poolPanel
    ] ) );

    // Controls
    var btnStep = wix.el( 'button', { className: 'wix-btn wix-btn--outline', textContent: 'Step 1 year' } );
    var btnPlay = wix.el( 'button', { className: 'wix-btn', textContent: 'Play', style: { minWidth: '4.5rem' } } );
    var btnReset = wix.el( 'button', { className: 'wix-btn wix-btn--outline', textContent: 'Reset' } );
    var elYear = wix.el( 'span', { className: 'wix-sim-year', textContent: 'Year 0 / ' + YEARS } );
    var speedSlider = wix.el( 'input', { type: 'range', min: '1', max: '5', value: '3', step: '1' } );
    var speedLabel = wix.el( 'label', { className: 'wix-sim-speed' }, [
      'Speed ',
      speedSlider
    ] );

    wrapper.appendChild( wix.el( 'div', { className: 'wix-sim-controls' }, [
      btnStep, btnPlay, btnReset, elYear, speedLabel
    ] ) );

    // Summary (hidden until simulation ends)
    var summaryEl = wix.el( 'div', { className: 'wix-sim-summary wix-hidden' } );
    wrapper.appendChild( summaryEl );


    /* ── Chart Drawing ────────────────────────────────────────── */

    function drawChart( canvas, data, dots, lineColor ) {
      var ctx = canvas.getContext( '2d' );
      var dpr = window.devicePixelRatio || 1;
      var rect = canvas.getBoundingClientRect();

      if ( rect.width === 0 || rect.height === 0 ) {
        return;
      }

      canvas.width  = rect.width * dpr;
      canvas.height = rect.height * dpr;
      ctx.scale( dpr, dpr );

      var w = rect.width;
      var h = rect.height;
      var pad = { t: 8, b: 24, l: 50, r: 12 };
      var cw = w - pad.l - pad.r;
      var ch = h - pad.t - pad.b;

      ctx.clearRect( 0, 0, w, h );

      // Value range
      var allVals = data.slice();
      var maxVal = Math.max( START + ANNUAL_SAVE * YEARS, Math.max.apply( null, allVals ) ) * 1.05;
      var minVal = Math.min( 0, Math.min.apply( null, allVals ) );
      var range  = maxVal - minVal || 1;

      // Grid lines + Y-axis labels
      ctx.strokeStyle = colorGrid;
      ctx.lineWidth = 0.5;
      ctx.fillStyle = colorAxis;
      ctx.font = '11px system-ui, sans-serif';
      ctx.textAlign = 'right';

      var i, y, x, yr;
      for ( i = 0; i <= 4; i++ ) {
        y = pad.t + ch - ( ch * i / 4 );
        ctx.beginPath();
        ctx.moveTo( pad.l, y );
        ctx.lineTo( pad.l + cw, y );
        ctx.stroke();
        ctx.fillText( fmt( minVal + range * i / 4 ), pad.l - 6, y + 4 );
      }

      // X-axis labels
      ctx.textAlign = 'center';
      ctx.fillStyle = colorAxis;
      for ( yr = 0; yr <= YEARS; yr += 5 ) {
        x = pad.l + ( cw * yr / YEARS );
        ctx.fillText( String( yr ), x, h - 4 );
      }

      if ( data.length < 2 ) {
        return;
      }

      // Line
      ctx.beginPath();
      ctx.lineWidth = 2;
      ctx.strokeStyle = lineColor;
      for ( i = 0; i < data.length; i++ ) {
        x = pad.l + ( cw * i / YEARS );
        y = pad.t + ch - ( ch * ( data[ i ] - minVal ) / range );
        if ( i === 0 ) {
          ctx.moveTo( x, y );
        } else {
          ctx.lineTo( x, y );
        }
      }
      ctx.stroke();

      // Gradient fill
      var lastIdx = data.length - 1;
      var grad = ctx.createLinearGradient( 0, pad.t, 0, pad.t + ch );
      grad.addColorStop( 0, lineColor + '18' );
      grad.addColorStop( 1, lineColor + '02' );
      ctx.beginPath();
      for ( i = 0; i < data.length; i++ ) {
        x = pad.l + ( cw * i / YEARS );
        y = pad.t + ch - ( ch * ( data[ i ] - minVal ) / range );
        if ( i === 0 ) {
          ctx.moveTo( x, y );
        } else {
          ctx.lineTo( x, y );
        }
      }
      ctx.lineTo( pad.l + ( cw * lastIdx / YEARS ), pad.t + ch );
      ctx.lineTo( pad.l, pad.t + ch );
      ctx.closePath();
      ctx.fillStyle = grad;
      ctx.fill();

      // Hit year dots
      Object.keys( dots ).forEach( function ( hy ) {
        hy = parseInt( hy, 10 );
        if ( hy >= data.length ) {
          return;
        }
        x = pad.l + ( cw * hy / YEARS );
        y = pad.t + ch - ( ch * ( data[ hy ] - minVal ) / range );
        ctx.beginPath();
        ctx.arc( x, y, 4, 0, Math.PI * 2 );
        ctx.fillStyle = lineColor;
        ctx.fill();
      } );

      // Current position dot (ring)
      var cx = pad.l + ( cw * lastIdx / YEARS );
      var cy = pad.t + ch - ( ch * ( data[ lastIdx ] - minVal ) / range );
      ctx.beginPath();
      ctx.arc( cx, cy, 5, 0, Math.PI * 2 );
      ctx.fillStyle = lineColor;
      ctx.fill();
      ctx.beginPath();
      ctx.arc( cx, cy, 3, 0, Math.PI * 2 );
      ctx.fillStyle = '#fff';
      ctx.fill();
    }


    /* ── Update UI ────────────────────────────────────────────── */

    function updateUI() {
      elAloneSavings.textContent = fmt( aloneSavings );
      elAloneSpent.textContent   = fmt( aloneSpent );
      elPoolSavings.textContent  = fmt( poolSavings );
      elPoolSpent.textContent    = fmt( poolSpent );
      elYear.textContent         = 'Year ' + year + ' / ' + YEARS;

      drawChart( canvasAlone, aloneData, hitYears, colorAlone );
      drawChart( canvasPool,  poolData,  {},       colorPool );
    }


    /* ── Simulation Logic ─────────────────────────────────────── */

    function stepYear() {
      if ( year >= YEARS ) {
        stop();
        return;
      }
      year++;

      aloneSavings += ANNUAL_SAVE;
      poolSavings  += ANNUAL_SAVE;

      // Hailstorm?
      var hit = Math.random() < PROB;

      if ( hit ) {
        aloneSavings -= REPAIR;
        aloneSpent   += REPAIR;
        hitYears[ year ] = true;
        elAloneLog.textContent = 'Year ' + year + ': Hailstorm strikes! -\u20AC15,000 in repairs';
        elAloneLog.className   = 'wix-sim-log wix-sim-log--hit';
      } else {
        elAloneLog.textContent = 'Year ' + year + ': No hailstorm';
        elAloneLog.className   = 'wix-sim-log wix-sim-log--safe';
      }

      // Pool fee (always paid)
      poolSavings -= POOL_FEE;
      poolSpent   += POOL_FEE;

      if ( hit ) {
        elPoolLog.textContent = 'Year ' + year + ': Hailstorm strikes \u2014 pool covers the repair. You paid \u20AC200';
        elPoolLog.className   = 'wix-sim-log wix-sim-log--safe';
      } else {
        elPoolLog.textContent = 'Year ' + year + ': No hailstorm. Your pool contribution: \u20AC200';
        elPoolLog.className   = 'wix-sim-log wix-sim-log--safe';
      }

      aloneData.push( aloneSavings );
      poolData.push( poolSavings );

      updateUI();

      if ( year >= YEARS ) {
        stop();
        showSummary();
      }
    }

    function stop() {
      playing = false;
      clearInterval( timer );
      timer = null;
      btnPlay.textContent = 'Play';
    }

    function reset() {
      stop();
      year         = 0;
      aloneSavings = START;
      poolSavings  = START;
      aloneSpent   = 0;
      poolSpent    = 0;
      aloneData    = [ START ];
      poolData     = [ START ];
      hitYears     = {};

      elAloneLog.textContent = 'Ready to simulate';
      elAloneLog.className   = 'wix-sim-log wix-sim-log--safe';
      elPoolLog.textContent  = 'Ready to simulate';
      elPoolLog.className    = 'wix-sim-log wix-sim-log--safe';

      summaryEl.classList.add( 'wix-hidden' );
      wix.empty( summaryEl );

      updateUI();
    }

    function showSummary() {
      wix.empty( summaryEl );
      summaryEl.classList.remove( 'wix-hidden' );
      summaryEl.classList.add( 'wix-animate-in' );

      var hitsCount = Object.keys( hitYears ).length;

      summaryEl.appendChild( wix.el( 'div', { className: 'wix-sim-summary-title', textContent: 'After ' + YEARS + ' years' } ) );

      var grid = wix.el( 'div', { className: 'wix-sim-summary-grid' }, [
        wix.el( 'div', { className: 'wix-sim-summary-item' }, [
          wix.el( 'div', { className: 'wix-sim-summary-heading wix-sim-summary-heading--alone', textContent: 'Going it alone' } ),
          wix.el( 'div', {}, [ 'Hailstorms suffered: ' + hitsCount ] ),
          wix.el( 'div', {}, [ 'Total repair cost: ' + fmt( aloneSpent ) ] ),
          wix.el( 'div', {}, [ 'Final savings: ' + fmt( aloneSavings ) ] )
        ] ),
        wix.el( 'div', { className: 'wix-sim-summary-item' }, [
          wix.el( 'div', { className: 'wix-sim-summary-heading wix-sim-summary-heading--pool', textContent: 'In the pool' } ),
          wix.el( 'div', {}, [ 'Hailstorms suffered: ' + hitsCount + ' (same weather)' ] ),
          wix.el( 'div', {}, [ 'Total pool contributions: ' + fmt( poolSpent ) ] ),
          wix.el( 'div', {}, [ 'Final savings: ' + fmt( poolSavings ) ] )
        ] )
      ] );

      summaryEl.appendChild( grid );
    }


    /* ── Event Wiring ─────────────────────────────────────────── */

    btnStep.addEventListener( 'click', function () {
      stop();
      stepYear();
    } );

    btnPlay.addEventListener( 'click', function () {
      if ( year >= YEARS ) {
        reset();
      }
      if ( playing ) {
        stop();
        return;
      }
      playing = true;
      btnPlay.textContent = 'Pause';
      timer = setInterval( stepYear, speedMs() );
    } );

    btnReset.addEventListener( 'click', reset );

    speedSlider.addEventListener( 'input', function () {
      if ( playing ) {
        clearInterval( timer );
        timer = setInterval( stepYear, speedMs() );
      }
    } );


    /* ── Initial Render + Resize ──────────────────────────────── */

    updateUI();

    window.addEventListener( 'resize', function () {
      drawChart( canvasAlone, aloneData, hitYears, colorAlone );
      drawChart( canvasPool,  poolData,  {},       colorPool );
    } );
  }



  /* ================================================================
     INSURER ENGINES
     Two-engine profit simulator: underwriting + investment income.
     ================================================================ */

  function initInsurerEngines( container ) {

    /* ── Constants ────────────────────────────────────────────── */

    var PREMIUMS = 5000000;
    var EXPENSES = 1200000;

    /* ── Colors (match WIX tokens) ───────────────────────────── */

    var styles   = getComputedStyle( container );
    var colorPos = styles.getPropertyValue( '--wix-correct' ).trim() || '#1e8449';
    var colorNeg = styles.getPropertyValue( '--wix-wrong' ).trim() || '#922b21';
    var colorInv = styles.getPropertyValue( '--wix-accent' ).trim() || '#1a5276';

    /* ── Formatting ───────────────────────────────────────────── */

    function fmt( v ) {
      var sign = v < 0 ? '-' : '';
      var abs = Math.abs( v );
      if ( abs >= 1000000 ) {
        return sign + '\u20AC' + ( abs / 1000000 ).toFixed( 2 ) + 'M';
      }
      return sign + '\u20AC' + wix.formatNumber( Math.round( abs ) );
    }

    /* ── Build UI ─────────────────────────────────────────────── */

    wix.empty( container );

    // Outer wrapper — single card containing the entire widget
    var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
    container.appendChild( wrapper );

    // Fixed info row
    wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-fixed' }, [
      wix.el( 'div', {}, [ 'Policies: ', wix.el( 'span', { textContent: '10,000' } ) ] ),
      wix.el( 'div', {}, [ 'Avg premium: ', wix.el( 'span', { textContent: '\u20AC500' } ) ] ),
      wix.el( 'div', {}, [ 'Total premiums: ', wix.el( 'span', { textContent: '\u20AC5,000,000' } ) ] ),
      wix.el( 'div', {}, [ 'Expenses: ', wix.el( 'span', { textContent: '\u20AC1,200,000' } ) ] )
    ] ) );

    // Scenario strip
    wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-section-label', textContent: 'Claims scenario' } ) );

    var scenarios = [
      { label: 'Mild year',    sub: '\u20AC2.8M', claims: 2800000 },
      { label: 'Expected',     sub: '\u20AC3.2M', claims: 3200000 },
      { label: 'Harsh winter', sub: '\u20AC3.6M', claims: 3600000 }
    ];

    var scenarioBtns = [];
    var strip = wix.el( 'div', { className: 'wix-eng-scenario-strip' } );

    scenarios.forEach( function ( s, idx ) {
      var cls = 'wix-eng-scenario-btn';
      if ( idx === 1 ) {
        cls += ' wix-eng-scenario-btn--active';
      }
      var btn = wix.el( 'button', {
        className: cls,
        'data-claims': String( s.claims )
      }, [
        s.label,
        wix.el( 'br' ),
        s.sub
      ] );
      scenarioBtns.push( btn );
      strip.appendChild( btn );
    } );

    wrapper.appendChild( strip );

    // Sliders
    var claimsSlider = wix.el( 'input', {
      type: 'range', min: '2400000', max: '4200000', step: '50000', value: '3200000'
    } );
    var claimsVal = wix.el( 'div', { className: 'wix-eng-slider-val', textContent: '\u20AC3.20M' } );

    wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-slider-row' }, [
      wix.el( 'div', { className: 'wix-eng-slider-label', textContent: 'Actual claims' } ),
      claimsSlider,
      claimsVal
    ] ) );

    var investSlider = wix.el( 'input', {
      type: 'range', min: '0', max: '8', step: '0.1', value: '3'
    } );
    var investVal = wix.el( 'div', { className: 'wix-eng-slider-val', textContent: '3.0%' } );

    wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-slider-row' }, [
      wix.el( 'div', { className: 'wix-eng-slider-label', textContent: 'Annual return on float' } ),
      investSlider,
      investVal
    ] ) );

    // Divider
    wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-divider' } ) );

    // Engine 1: Underwriting
    var uwValEl = wix.el( 'div', { className: 'wix-eng-val', textContent: '\u20AC600,000' } );
    var uwDetail = wix.el( 'div', { className: 'wix-eng-detail', textContent: '\u20AC5,000,000 premiums \u2212 \u20AC1,200,000 expenses \u2212 \u20AC3,200,000 claims' } );
    var uwBar = wix.el( 'div', { className: 'wix-eng-bar' } );

    wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-card' }, [
      wix.el( 'div', { className: 'wix-eng-head' }, [
        wix.el( 'div', { className: 'wix-eng-title', textContent: 'Engine 1: underwriting' } ),
        uwValEl
      ] ),
      uwDetail,
      wix.el( 'div', { className: 'wix-eng-bar-track' }, [ uwBar ] )
    ] ) );

    // Combined ratio
    var crValEl = wix.el( 'div', { className: 'wix-eng-cr-val', textContent: '88.0%' } );
    var crSub = wix.el( 'div', { className: 'wix-eng-cr-sub', textContent: 'Below 100% \u2014 underwriting profit' } );

    wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-cr' }, [
      wix.el( 'div', { className: 'wix-eng-cr-label', textContent: 'Combined ratio' } ),
      crValEl,
      crSub
    ] ) );

    // Engine 2: Investment
    var invValEl = wix.el( 'div', { className: 'wix-eng-val', textContent: '\u20AC75,000' } );
    var invDetail = wix.el( 'div', { className: 'wix-eng-detail' } );
    var invBar = wix.el( 'div', { className: 'wix-eng-bar' } );

    wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-card' }, [
      wix.el( 'div', { className: 'wix-eng-head' }, [
        wix.el( 'div', { className: 'wix-eng-title', textContent: 'Engine 2: investment' } ),
        invValEl
      ] ),
      invDetail,
      wix.el( 'div', { className: 'wix-eng-bar-track' }, [ invBar ] )
    ] ) );

    // Divider
    wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-divider' } ) );

    // Total profit
    var totalValEl = wix.el( 'div', { className: 'wix-eng-total-val', textContent: '\u20AC675,000' } );
    var totalSub = wix.el( 'div', { className: 'wix-eng-total-sub', textContent: 'Underwriting + investment, before tax' } );

    wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-total' }, [
      wix.el( 'div', { className: 'wix-eng-total-head' }, [
        wix.el( 'div', { className: 'wix-eng-total-label', textContent: 'Total pre-tax profit' } ),
        totalValEl
      ] ),
      totalSub
    ] ) );

    // Callout
    var callout = wix.el( 'div', { className: 'wix-eng-callout wix-eng-callout--profit', textContent: 'Both engines are contributing to profit.' } );
    wrapper.appendChild( callout );


    /* ── Update Logic ─────────────────────────────────────────── */

    function update() {
      var claims = parseInt( claimsSlider.value, 10 );
      var rate = parseFloat( investSlider.value ) / 100;
      var uw = PREMIUMS - EXPENSES - claims;
      var avgFloat = Math.round( PREMIUMS * 0.5 );
      var inv = Math.round( avgFloat * rate );
      var total = uw + inv;
      var cr = ( claims + EXPENSES ) / PREMIUMS * 100;

      // Slider displays
      claimsVal.textContent = '\u20AC' + ( claims / 1000000 ).toFixed( 2 ) + 'M';
      investVal.textContent = ( rate * 100 ).toFixed( 1 ) + '%';

      // Engine 1: underwriting
      var uwColor = uw >= 0 ? colorPos : colorNeg;
      uwValEl.textContent = fmt( uw );
      uwValEl.style.color = uwColor;
      uwDetail.textContent = '\u20AC5,000,000 premiums \u2212 \u20AC1,200,000 expenses \u2212 \u20AC' + wix.formatNumber( claims ) + ' claims';
      uwBar.style.width = Math.min( 100, Math.abs( uw ) / 1000000 * 100 ) + '%';
      uwBar.style.background = uwColor;

      // Combined ratio
      crValEl.textContent = cr.toFixed( 1 ) + '%';
      crValEl.style.color = cr > 100 ? colorNeg : colorPos;
      if ( cr > 100 ) {
        crSub.textContent = 'Above 100% \u2014 underwriting loss';
      } else if ( cr === 100 ) {
        crSub.textContent = 'Exactly 100% \u2014 break-even';
      } else {
        crSub.textContent = 'Below 100% \u2014 underwriting profit';
      }

      // Engine 2: investment
      invValEl.textContent = fmt( inv );
      invValEl.style.color = colorInv;
      invDetail.textContent = 'The insurer collects \u20AC5M upfront and pays claims gradually. On average, \u20AC' +
        wix.formatNumber( avgFloat ) + ' sits invested during the year, earning ' + ( rate * 100 ).toFixed( 1 ) + '%.';
      invBar.style.width = Math.min( 100, inv / 200000 * 100 ) + '%';
      invBar.style.background = colorInv;

      // Total
      totalValEl.textContent = fmt( total );
      totalValEl.style.color = total >= 0 ? colorPos : colorNeg;

      // Callout
      if ( uw < 0 && total >= 0 ) {
        callout.className = 'wix-eng-callout wix-eng-callout--profit';
        callout.textContent = 'The combined ratio exceeds 100%, so underwriting alone is losing money. But investment income more than covers the gap \u2014 the insurer is still profitable overall.';
      } else if ( uw < 0 && total < 0 ) {
        callout.className = 'wix-eng-callout wix-eng-callout--loss';
        callout.textContent = 'Investment income cannot offset the underwriting loss. The insurer is losing money overall.';
      } else {
        callout.className = 'wix-eng-callout wix-eng-callout--profit';
        callout.textContent = 'Both engines are contributing to profit.';
      }

      // Sync scenario buttons
      scenarioBtns.forEach( function ( btn ) {
        if ( parseInt( btn.getAttribute( 'data-claims' ), 10 ) === claims ) {
          btn.classList.add( 'wix-eng-scenario-btn--active' );
        } else {
          btn.classList.remove( 'wix-eng-scenario-btn--active' );
        }
      } );
    }


    /* ── Event Wiring ─────────────────────────────────────────── */

    claimsSlider.addEventListener( 'input', update );
    investSlider.addEventListener( 'input', update );

    wix.on( strip, 'click', '.wix-eng-scenario-btn', function ( btn ) {
      claimsSlider.value = btn.getAttribute( 'data-claims' );
      update();
    } );

    // Initial render
    update();
  }


  /* ================================================================
     PREMIUM MATCHING
     Demonstrates the matching principle: front-loaded vs spread
     revenue recognition for a Madrid insurer.
     ================================================================ */

  function initPremiumMatching( container ) {

    /* ── Constants ────────────────────────────────────────────── */

    var MONTHS   = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                     'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
    var PREMIUM  = 1200;
    var MONTHLY_EXPENSE = 80;
    var EXPENSES = [];
    var i;
    for ( i = 0; i < 12; i++ ) {
      EXPENSES.push( MONTHLY_EXPENSE );
    }

    /* ── Colors (WIX tokens) ─────────────────────────────────── */

    var styles      = getComputedStyle( container );
    var colorRev    = styles.getPropertyValue( '--wix-correct' ).trim() || '#1e8449';
    var colorExp    = styles.getPropertyValue( '--wix-wrong' ).trim()   || '#922b21';
    var colorProfit = styles.getPropertyValue( '--wix-accent' ).trim()  || '#1a5276';
    var colorGrid   = styles.getPropertyValue( '--wix-border-subtle' ).trim() || '#eaecf0';
    var colorAxis   = styles.getPropertyValue( '--wix-text-muted' ).trim()    || '#54595d';

    /* ── State ───────────────────────────────────────────────── */

    var mode    = 'frontload';
    var revenue = [ PREMIUM, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];

    /* ── Formatting ──────────────────────────────────────────── */

    function fmt( n ) {
      return '\u20AC' + wix.formatNumber( Math.round( n ) );
    }

    /* ── Build UI ────────────────────────────────────────────── */

    wix.empty( container );

    // Outer wrapper card
    var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
    container.appendChild( wrapper );

    // Mode buttons
    var btnFrontload = wix.el( 'button', {
      className: 'wix-pm-mode-btn wix-pm-mode-btn--active',
      textContent: 'All in January'
    } );
    var btnMatched = wix.el( 'button', {
      className: 'wix-pm-mode-btn',
      textContent: 'Spread evenly (matched)'
    } );

    wrapper.appendChild( wix.el( 'div', { className: 'wix-pm-modes' }, [
      btnFrontload,
      btnMatched
    ] ) );

    // Stat cards
    var elRevenue = wix.el( 'div', { className: 'wix-pm-stat-val', textContent: fmt( PREMIUM ) } );
    var elExpense = wix.el( 'div', { className: 'wix-pm-stat-val', textContent: fmt( MONTHLY_EXPENSE * 12 ) } );
    var elProfit  = wix.el( 'div', { className: 'wix-pm-stat-val wix-pm-stat-val--profit', textContent: fmt( PREMIUM - MONTHLY_EXPENSE * 12 ) } );

    wrapper.appendChild( wix.el( 'div', { className: 'wix-pm-stats' }, [
      wix.el( 'div', { className: 'wix-pm-stat' }, [
        wix.el( 'div', { className: 'wix-pm-stat-label', textContent: 'Annual revenue' } ),
        elRevenue
      ] ),
      wix.el( 'div', { className: 'wix-pm-stat' }, [
        wix.el( 'div', { className: 'wix-pm-stat-label', textContent: 'Annual expenses' } ),
        elExpense
      ] ),
      wix.el( 'div', { className: 'wix-pm-stat' }, [
        wix.el( 'div', { className: 'wix-pm-stat-label', textContent: 'Annual profit' } ),
        elProfit
      ] )
    ] ) );

    // Chart canvas
    var canvas = wix.el( 'canvas' );
    wrapper.appendChild( wix.el( 'div', { className: 'wix-pm-chart' }, [ canvas ] ) );

    // Legend
    wrapper.appendChild( wix.el( 'div', { className: 'wix-pm-legend' }, [
      wix.el( 'span', { className: 'wix-pm-legend-item' }, [
        wix.el( 'span', { className: 'wix-pm-swatch wix-pm-swatch--rev' } ),
        'Revenue'
      ] ),
      wix.el( 'span', { className: 'wix-pm-legend-item' }, [
        wix.el( 'span', { className: 'wix-pm-swatch wix-pm-swatch--exp' } ),
        'Expenses'
      ] ),
      wix.el( 'span', { className: 'wix-pm-legend-item' }, [
        wix.el( 'span', { className: 'wix-pm-swatch wix-pm-swatch--profit' } ),
        'Profit / loss'
      ] )
    ] ) );

    // Insight callout
    var insightEl = wix.el( 'div', { className: 'wix-eng-callout wix-eng-callout--profit' } );
    wrapper.appendChild( insightEl );


    /* ── Chart Drawing (vanilla canvas) ──────────────────────── */

    function drawChart() {
      var profit = [];
      for ( var idx = 0; idx < 12; idx++ ) {
        profit.push( revenue[ idx ] - EXPENSES[ idx ] );
      }

      var ctx = canvas.getContext( '2d' );
      var dpr = window.devicePixelRatio || 1;
      var rect = canvas.getBoundingClientRect();

      if ( rect.width === 0 || rect.height === 0 ) {
        return;
      }

      canvas.width  = rect.width * dpr;
      canvas.height = rect.height * dpr;
      ctx.scale( dpr, dpr );

      var w = rect.width;
      var h = rect.height;
      var pad = { t: 16, b: 28, l: 48, r: 12 };
      var cw = w - pad.l - pad.r;
      var ch = h - pad.t - pad.b;

      ctx.clearRect( 0, 0, w, h );

      // Value range
      var allVals = revenue.concat( EXPENSES ).concat( profit );
      var yMax = Math.max.apply( null, allVals ) + 100;
      var yMin = Math.min.apply( null, allVals ) - 60;
      var range = yMax - yMin || 1;

      // Y-axis helper
      function yPos( v ) {
        return pad.t + ch - ( ch * ( v - yMin ) / range );
      }

      // Grid lines + Y labels
      ctx.strokeStyle = colorGrid;
      ctx.lineWidth = 0.5;
      ctx.fillStyle = colorAxis;
      ctx.font = '11px system-ui, sans-serif';
      ctx.textAlign = 'right';

      var gridSteps = 5;
      for ( i = 0; i <= gridSteps; i++ ) {
        var gv = yMin + range * i / gridSteps;
        var gy = yPos( gv );
        ctx.beginPath();
        ctx.moveTo( pad.l, gy );
        ctx.lineTo( pad.l + cw, gy );
        ctx.stroke();
        ctx.fillText( '\u20AC' + Math.round( gv ), pad.l - 6, gy + 4 );
      }

      // Zero line (if visible)
      if ( yMin < 0 && yMax > 0 ) {
        var zy = yPos( 0 );
        ctx.strokeStyle = colorAxis;
        ctx.lineWidth = 0.8;
        ctx.beginPath();
        ctx.moveTo( pad.l, zy );
        ctx.lineTo( pad.l + cw, zy );
        ctx.stroke();
        ctx.strokeStyle = colorGrid;
        ctx.lineWidth = 0.5;
      }

      // Bar drawing
      var groupWidth = cw / 12;
      var barWidth   = Math.max( 2, ( groupWidth - 8 ) / 3 );
      var gap        = 2;

      for ( idx = 0; idx < 12; idx++ ) {
        var gx = pad.l + groupWidth * idx + ( groupWidth - ( barWidth * 3 + gap * 2 ) ) / 2;
        var zeroY = yPos( 0 );

        // Revenue bar
        var ry = yPos( revenue[ idx ] );
        ctx.fillStyle = colorRev;
        ctx.fillRect( gx, Math.min( ry, zeroY ), barWidth, Math.abs( ry - zeroY ) || 1 );

        // Expense bar
        var ey = yPos( EXPENSES[ idx ] );
        ctx.fillStyle = colorExp;
        ctx.fillRect( gx + barWidth + gap, Math.min( ey, zeroY ), barWidth, Math.abs( ey - zeroY ) || 1 );

        // Profit / loss bar (single color to avoid confusion with expenses)
        var pv = profit[ idx ];
        var py = yPos( pv );
        ctx.fillStyle = colorProfit;
        ctx.fillRect( gx + ( barWidth + gap ) * 2, Math.min( py, zeroY ), barWidth, Math.abs( py - zeroY ) || 1 );

        // X label
        ctx.fillStyle = colorAxis;
        ctx.textAlign = 'center';
        ctx.font = '11px system-ui, sans-serif';
        ctx.fillText( MONTHS[ idx ], pad.l + groupWidth * idx + groupWidth / 2, h - 6 );
      }
    }


    /* ── Update ──────────────────────────────────────────────── */

    function update() {
      drawChart();

      if ( mode === 'frontload' ) {
        insightEl.className = 'wix-eng-callout wix-eng-callout--loss';
        insightEl.textContent = 'January shows \u20AC1,120 profit while every other month shows an \u20AC80 loss \u2014 yet the underlying economics are perfectly steady. The annual profit is still \u20AC240 either way, but the monthly picture is wildly misleading. This is the distortion the matching principle prevents.';
      } else {
        insightEl.className = 'wix-eng-callout wix-eng-callout--profit';
        insightEl.textContent = 'Revenue and expenses are aligned in every month: \u20AC100 of revenue minus \u20AC80 of expenses gives a steady \u20AC20 profit. The monthly picture now faithfully reflects the economics of the contract. This is proper matching.';
      }
    }

    function setMode( m ) {
      mode = m;
      if ( m === 'frontload' ) {
        revenue = [ PREMIUM, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
        btnFrontload.classList.add( 'wix-pm-mode-btn--active' );
        btnMatched.classList.remove( 'wix-pm-mode-btn--active' );
      } else {
        revenue = [];
        for ( var j = 0; j < 12; j++ ) {
          revenue.push( 100 );
        }
        btnMatched.classList.add( 'wix-pm-mode-btn--active' );
        btnFrontload.classList.remove( 'wix-pm-mode-btn--active' );
      }
      update();
    }


    /* ── Event Wiring ────────────────────────────────────────── */

    btnFrontload.addEventListener( 'click', function () {
      setMode( 'frontload' );
    } );

    btnMatched.addEventListener( 'click', function () {
      setMode( 'matched' );
    } );

    window.addEventListener( 'resize', drawChart );


    /* ── Initial Render ──────────────────────────────────────── */

    setMode( 'frontload' );
  }

}() );