Jump to content

MediaWiki:Gadget-wix-interactive.js: Difference between revisions

From Insurer Brain
Content deleted Content added
Created page with "/* ================================================================ 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 (Borde..."
 
No edit summary
Line 8: Line 8:


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


Line 17: Line 18:


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


Line 433: Line 435:
drawChart( canvasPool, poolData, {}, colorPool );
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 );

// Fixed info row
container.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
container.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 );
} );

container.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' } );

container.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%' } );

container.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
container.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' } );

container.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' } );

container.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' } );

container.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
container.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' } );

container.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.' } );
container.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();
}
}



Revision as of 01:36, 1 April 2026

/* ================================================================
   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
  };

  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
    ] );

    // Panels grid
    container.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
    ] );

    container.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' } );
    container.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 );

    // Fixed info row
    container.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
    container.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 );
    } );

    container.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' } );

    container.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%' } );

    container.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
    container.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' } );

    container.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' } );

    container.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' } );

    container.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
    container.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' } );

    container.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.' } );
    container.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();
  }

}() );