MediaWiki:Gadget-wix-interactive.js
Appearance
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)
- "risk-adjustment" Brittany storm confidence-level risk adjustment chart
- "grouping-funnel" 3-step IFRS 17 contract grouping walkthrough
- "csm-rollforward" Horizontal waterfall: CSM opening → movements → closing
- "building-blocks" Stacked-bar initial recognition building blocks
- "impact-sort" Binary classification: CSM vs P&L scenario sorter
- "income-builder" Sort 9 items into IFRS 17 income statement sections
- "gm-paa" Step-through comparison: General Model vs PAA
================================================================ */
( function () {
'use strict';
/* ── Dispatcher ─────────────────────────────────────────────── */
var dispatchers = {
'pool-simulator': initPoolSimulator,
'insurer-engines': initInsurerEngines,
'premium-matching': initPremiumMatching,
'reserve-sensitivity': initReserveSensitivity,
'ifrs-timeline': initIfrsTimeline,
'liability-waterfall': initLiabilityWaterfall,
'prob-weighted': initProbWeighted,
'discount-rate': initDiscountRate,
'balance-sheet': initBalanceSheet,
'risk-adjustment': initRiskAdjustment,
'grouping-funnel': initGroupingFunnel,
'csm-rollforward': initCsmRollforward,
'building-blocks': initBuildingBlocks,
'impact-sort': initImpactSort,
'income-builder': initIncomeBuilder,
'gm-paa': initGmPaa
};
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' );
}
/* ================================================================
RESERVE SENSITIVITY
Slider showing how reserve reassessments flow through to P&L
and shareholders' equity.
================================================================ */
function initReserveSensitivity( container ) {
/* ── Constants ────────────────────────────────────────────── */
var RESERVES = 90; // €bn
var EQUITY = 50; // €bn
var TAX = 0.25;
var MAX_PCT = 10; // bar scale cap
/* ── Colors (WIX tokens) ─────────────────────────────────── */
var styles = getComputedStyle( container );
var colorPos = styles.getPropertyValue( '--wix-correct' ).trim() || '#1e8449';
var colorNeg = styles.getPropertyValue( '--wix-wrong' ).trim() || '#922b21';
/* ── Formatting ──────────────────────────────────────────── */
function fmtBn( v ) {
var abs = Math.abs( v );
return abs >= 1 ? abs.toFixed( 1 ) : abs.toFixed( 2 );
}
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Slider row
var slider = wix.el( 'input', {
type: 'range', min: '-5', max: '5', value: '2', step: '0.5'
} );
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-section-label', textContent: 'Reserve reassessment' } ) );
wrapper.appendChild( wix.el( 'div', { className: 'wix-rs-slider-row' }, [
wix.el( 'span', { className: 'wix-rs-bound', textContent: '-5%' } ),
slider,
wix.el( 'span', { className: 'wix-rs-bound', textContent: '+5%' } )
] ) );
// Stat cards — row 1: P&L impact
var elPretax = wix.el( 'div', { className: 'wix-rs-card-num' } );
var elAftertax = wix.el( 'div', { className: 'wix-rs-card-num' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-rs-cards' }, [
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'Pre-tax P&L impact' } ),
elPretax
] ),
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'After-tax (25%) P&L impact' } ),
elAftertax
] )
] ) );
// Stat cards — row 2: reserves
var elNewRes = wix.el( 'div', { className: 'wix-rs-card-num' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-rs-cards' }, [
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'Reserves (original)' } ),
wix.el( 'div', { className: 'wix-rs-card-num wix-rs-card-num--muted', textContent: '\u20AC90.0bn' } )
] ),
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'Reserves (revised)' } ),
elNewRes
] )
] ) );
// Bar charts
var bar1Fill = wix.el( 'div', { className: 'wix-rs-bar-fill' } );
var bar2Fill = wix.el( 'div', { className: 'wix-rs-bar-fill' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-rs-bars' }, [
wix.el( 'div', {}, [
wix.el( 'div', { className: 'wix-rs-bar-label', textContent: 'Reserve reassessment' } ),
wix.el( 'div', { className: 'wix-rs-bar-track' }, [
wix.el( 'div', { className: 'wix-rs-bar-center' } ),
bar1Fill
] )
] ),
wix.el( 'div', {}, [
wix.el( 'div', { className: 'wix-rs-bar-label', textContent: 'Impact on shareholders\u2019 equity (\u20AC50bn)' } ),
wix.el( 'div', { className: 'wix-rs-bar-track' }, [
wix.el( 'div', { className: 'wix-rs-bar-center' } ),
bar2Fill
] )
] )
] ) );
/* ── Bar Helper ──────────────────────────────────────────── */
function setBar( el, pct, color ) {
var clamped = wix.clamp( pct, -MAX_PCT, MAX_PCT );
var w = Math.abs( clamped ) / MAX_PCT * 50;
if ( clamped >= 0 ) {
el.style.left = '50%';
el.style.width = w + '%';
el.style.borderRadius = '0 var(--wix-radius) var(--wix-radius) 0';
} else {
el.style.left = ( 50 - w ) + '%';
el.style.width = w + '%';
el.style.borderRadius = 'var(--wix-radius) 0 0 var(--wix-radius)';
}
el.style.background = color;
var label = ( pct >= 0 ? '+' : '' ) + pct.toFixed( 1 ) + '%';
el.textContent = Math.abs( pct ) >= 0.5 ? label : '';
}
/* ── Update Logic ────────────────────────────────────────── */
function update() {
var p = parseFloat( slider.value );
var delta = RESERVES * p / 100;
var pretax = -delta;
var aftertax = pretax * ( 1 - TAX );
// P&L cards
var sign;
sign = pretax >= 0 ? '+' : '-';
elPretax.textContent = sign + '\u20AC' + fmtBn( pretax ) + 'bn';
elPretax.className = 'wix-rs-card-num' + ( pretax > 0 ? ' wix-rs-card-num--pos' : pretax < 0 ? ' wix-rs-card-num--neg' : '' );
sign = aftertax >= 0 ? '+' : '-';
elAftertax.textContent = sign + '\u20AC' + fmtBn( aftertax ) + 'bn';
elAftertax.className = 'wix-rs-card-num' + ( aftertax > 0 ? ' wix-rs-card-num--pos' : aftertax < 0 ? ' wix-rs-card-num--neg' : '' );
// Revised reserves
elNewRes.textContent = '\u20AC' + ( RESERVES + delta ).toFixed( 1 ) + 'bn';
// Bars
var resPct = p;
var eqPct = aftertax / EQUITY * 100;
setBar( bar1Fill, resPct, p <= 0 ? colorPos : colorNeg );
setBar( bar2Fill, eqPct, aftertax >= 0 ? colorPos : colorNeg );
}
/* ── Event Wiring ────────────────────────────────────────── */
slider.addEventListener( 'input', update );
// Initial render
update();
}
/* ================================================================
IFRS TIMELINE
Interactive horizontal timeline of IFRS / insurance-accounting
milestones. Events are read from a data-wix-events JSON attribute.
Each event: { year, cat ("ifrs"|"ins"), title, body }
================================================================ */
function initIfrsTimeline( container ) {
/* ── Read events from data attribute ─────────────────────── */
var raw = wix.data( container, 'wix-events', '[]' );
var events;
try {
events = JSON.parse( raw );
} catch ( e ) {
return;
}
if ( !events.length ) {
return;
}
/* ── Colors (WIX tokens + category overrides) ────────────── */
var styles = getComputedStyle( container );
var colorIfrs = styles.getPropertyValue( '--wix-accent' ).trim() || '#1a5276';
var colorIns = styles.getPropertyValue( '--wix-wrong' ).trim() || '#922b21';
/* ── State ───────────────────────────────────────────────── */
var active = 0;
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Timeline track
var line = wix.el( 'div', { className: 'wix-tl-line' } );
var dotsEl = wix.el( 'div', { className: 'wix-tl-dots' } );
var track = wix.el( 'div', { className: 'wix-tl-track' }, [ line, dotsEl ] );
wrapper.appendChild( track );
// Detail card
var cardTitle = wix.el( 'div', { className: 'wix-tl-card-title' } );
var cardBody = wix.el( 'div', { className: 'wix-tl-card-body' } );
var card = wix.el( 'div', { className: 'wix-tl-card' }, [ cardTitle, cardBody ] );
wrapper.appendChild( card );
// Legend
wrapper.appendChild( wix.el( 'div', { className: 'wix-tl-legend' }, [
wix.el( 'span', { className: 'wix-tl-legend-item' }, [
wix.el( 'span', { className: 'wix-tl-legend-dot wix-tl-legend-dot--ifrs' } ),
'IFRS programme'
] ),
wix.el( 'span', { className: 'wix-tl-legend-item' }, [
wix.el( 'span', { className: 'wix-tl-legend-dot wix-tl-legend-dot--ins' } ),
'Insurance-specific'
] )
] ) );
// Navigation buttons
var btnPrev = wix.el( 'button', { className: 'wix-btn wix-btn--outline', textContent: 'Previous' } );
var btnNext = wix.el( 'button', { className: 'wix-btn wix-btn--outline', textContent: 'Next' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-tl-nav' }, [ btnPrev, btnNext ] ) );
/* ── Render ──────────────────────────────────────────────── */
function render() {
wix.empty( dotsEl );
events.forEach( function ( ev, i ) {
var dot = wix.el( 'div', { className: 'wix-tl-dot' } );
var year = wix.el( 'span', { className: 'wix-tl-year', textContent: ev.year } );
var cls = 'wix-tl-dot-wrap';
if ( ev.cat === 'ins' ) {
cls += ' wix-tl-dot-wrap--ins';
}
if ( i === active ) {
cls += ' wix-tl-dot-wrap--active';
}
var wrap = wix.el( 'div', { className: cls }, [ dot, year ] );
wrap.setAttribute( 'data-idx', String( i ) );
dotsEl.appendChild( wrap );
} );
// Card content
var ev = events[ active ];
cardTitle.textContent = ev.title;
cardBody.textContent = ev.body;
// Card accent border color
card.style.borderLeftColor = ev.cat === 'ins' ? colorIns : colorIfrs;
// Button states
btnPrev.disabled = active === 0;
btnNext.disabled = active === events.length - 1;
}
/* ── Event Wiring ────────────────────────────────────────── */
wix.on( dotsEl, 'click', '.wix-tl-dot-wrap', function ( target ) {
var idx = parseInt( target.getAttribute( 'data-idx' ), 10 );
if ( !isNaN( idx ) ) {
active = idx;
render();
}
} );
btnPrev.addEventListener( 'click', function () {
if ( active > 0 ) {
active--;
render();
}
} );
btnNext.addEventListener( 'click', function () {
if ( active < events.length - 1 ) {
active++;
render();
}
} );
// Initial render
render();
}
/* ================================================================
LIABILITY WATERFALL
Decomposes a single insurance liability number into its IFRS 17
building blocks via a waterfall chart.
Blocks are read from data-wix-blocks JSON; each entry has:
label, short, value, color, titleColor, question, body
================================================================ */
function initLiabilityWaterfall( container ) {
/* ── Read blocks from data attribute ─────────────────────── */
var raw = wix.data( container, 'wix-blocks', '[]' );
var blocks;
try {
blocks = JSON.parse( raw );
} catch ( e ) {
return;
}
if ( !blocks.length ) {
return;
}
/* ── Colors (WIX tokens) ─────────────────────────────────── */
var styles = getComputedStyle( container );
var colorText = styles.getPropertyValue( '--wix-text-muted' ).trim() || '#54595d';
var colorGrid = styles.getPropertyValue( '--wix-border-subtle' ).trim() || '#eaecf0';
/* ── Chart constants ─────────────────────────────────────── */
var MAX_VAL = 850;
var PAD = { t: 40, b: 50, l: 20, r: 20 };
/* ── State ───────────────────────────────────────────────── */
var view = 'old';
var selected = 0;
var hovered = -1;
var hitAreas = [];
var W, H;
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Toggle buttons
var btnOld = wix.el( 'button', {
className: 'wix-pm-mode-btn wix-pm-mode-btn--active',
textContent: 'Old world'
} );
var btnNew = wix.el( 'button', {
className: 'wix-pm-mode-btn',
textContent: 'IFRS 17'
} );
wrapper.appendChild( wix.el( 'div', { className: 'wix-pm-modes' }, [ btnOld, btnNew ] ) );
// Canvas
var canvas = wix.el( 'canvas', { style: { cursor: 'pointer' } } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-wf-chart' }, [ canvas ] ) );
// Detail card
var detailTitle = wix.el( 'div', { className: 'wix-wf-detail-title' } );
var detailBody = wix.el( 'div', { className: 'wix-wf-detail-body' } );
var detailCard = wix.el( 'div', { className: 'wix-wf-detail' }, [ detailTitle, detailBody ] );
wrapper.appendChild( detailCard );
/* ── Canvas Helpers ──────────────────────────────────────── */
function resize() {
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
if ( rect.width === 0 ) {
return;
}
W = rect.width;
H = 290;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.getContext( '2d' ).setTransform( dpr, 0, 0, dpr, 0, 0 );
}
function yForVal( v ) {
return PAD.t + ( 1 - v / MAX_VAL ) * ( H - PAD.t - PAD.b );
}
/* ── roundRect polyfill for older browsers ───────────────── */
function roundRect( ctx2, x, y, w, h, r ) {
if ( ctx2.roundRect ) {
ctx2.beginPath();
ctx2.roundRect( x, y, w, h, r );
return;
}
ctx2.beginPath();
ctx2.moveTo( x + r, y );
ctx2.lineTo( x + w - r, y );
ctx2.arcTo( x + w, y, x + w, y + r, r );
ctx2.lineTo( x + w, y + h - r );
ctx2.arcTo( x + w, y + h, x + w - r, y + h, r );
ctx2.lineTo( x + r, y + h );
ctx2.arcTo( x, y + h, x, y + h - r, r );
ctx2.lineTo( x, y + r );
ctx2.arcTo( x, y, x + r, y, r );
ctx2.closePath();
}
/* ── Draw ────────────────────────────────────────────────── */
function draw() {
var ctx2 = canvas.getContext( '2d' );
ctx2.clearRect( 0, 0, W, H );
hitAreas = [];
if ( view === 'old' ) {
drawOldWorld( ctx2 );
} else {
drawWaterfall( ctx2 );
}
}
function drawOldWorld( ctx2 ) {
var bw = Math.min( 160, W * 0.3 );
var x = ( W - bw ) / 2;
var top = yForVal( 800 );
var bot = yForVal( 0 );
var h = bot - top;
ctx2.fillStyle = hovered === 0 ? '#7A7A73' : '#888780';
roundRect( ctx2, x, top, bw, h, 6 );
ctx2.fill();
ctx2.fillStyle = '#fff';
ctx2.font = '500 15px system-ui, sans-serif';
ctx2.textAlign = 'center';
ctx2.textBaseline = 'middle';
ctx2.fillText( '\u20AC800m', x + bw / 2, top + h / 2 );
ctx2.fillStyle = colorText;
ctx2.font = '400 13px system-ui, sans-serif';
ctx2.fillText( 'Insurance liabilities', x + bw / 2, bot + 20 );
ctx2.fillStyle = colorGrid;
ctx2.font = '400 12px system-ui, sans-serif';
ctx2.fillText( 'What\u2019s inside?', x + bw / 2, bot + 38 );
hitAreas.push( { x: x, y: top, w: bw, h: h, idx: 0 } );
}
function drawWaterfall( ctx2 ) {
var n = blocks.length;
var usable = W - PAD.l - PAD.r;
var bw = usable / ( n + ( n - 1 ) * 0.5 );
var gap = bw * 0.5;
var running = 0;
var i, b, x, top, bot, barH, prevRunning, connY, lines, li;
for ( i = 0; i < n; i++ ) {
b = blocks[ i ];
x = PAD.l + i * ( bw + gap );
if ( i < n - 1 ) {
prevRunning = running;
running += b.value;
if ( b.value >= 0 ) {
top = yForVal( running );
bot = yForVal( prevRunning );
} else {
top = yForVal( prevRunning );
bot = yForVal( running );
}
barH = bot - top;
} else {
// Total bar: full height from 0 to total
top = yForVal( running );
bot = yForVal( 0 );
barH = bot - top;
}
// Bar
ctx2.globalAlpha = ( hovered === i || selected === i ) ? 0.85 : 1;
ctx2.fillStyle = b.color;
roundRect( ctx2, x, top, bw, barH, 4 );
ctx2.fill();
ctx2.globalAlpha = 1;
// Value label
ctx2.textAlign = 'center';
ctx2.textBaseline = 'middle';
if ( barH > 24 ) {
ctx2.fillStyle = '#fff';
ctx2.font = '500 12px system-ui, sans-serif';
ctx2.fillText( b.short, x + bw / 2, top + barH / 2 );
} else {
ctx2.fillStyle = b.color;
ctx2.font = '500 12px system-ui, sans-serif';
ctx2.fillText( b.short, x + bw / 2, top - 10 );
}
// Dashed connector to previous bar
if ( i > 0 && i < n - 1 ) {
connY = b.value >= 0 ? yForVal( running - b.value ) : yForVal( running );
ctx2.strokeStyle = colorGrid;
ctx2.setLineDash( [ 3, 3 ] );
ctx2.lineWidth = 1;
ctx2.beginPath();
ctx2.moveTo( PAD.l + ( i - 1 ) * ( bw + gap ) + bw, connY );
ctx2.lineTo( x, connY );
ctx2.stroke();
ctx2.setLineDash( [] );
}
// Connector from last component to total
if ( i === n - 2 ) {
connY = yForVal( running );
ctx2.strokeStyle = colorGrid;
ctx2.setLineDash( [ 3, 3 ] );
ctx2.lineWidth = 1;
ctx2.beginPath();
ctx2.moveTo( x + bw, connY );
ctx2.lineTo( PAD.l + ( n - 1 ) * ( bw + gap ), connY );
ctx2.stroke();
ctx2.setLineDash( [] );
}
// X-axis label (supports \n line breaks)
ctx2.fillStyle = colorText;
ctx2.font = '400 11px system-ui, sans-serif';
ctx2.textAlign = 'center';
ctx2.textBaseline = 'top';
lines = b.label.split( '\n' );
for ( li = 0; li < lines.length; li++ ) {
ctx2.fillText( lines[ li ], x + bw / 2, bot + 10 + li * 14 );
}
hitAreas.push( { x: x, y: top, w: bw, h: barH, idx: i } );
}
}
/* ── Detail Card ─────────────────────────────────────────── */
function renderDetail() {
if ( view === 'old' ) {
detailTitle.textContent = 'One opaque number';
detailTitle.style.color = '#444441';
detailBody.textContent = 'Under the old rules, this single figure hides everything: expected claims, time value adjustments, uncertainty buffers, and unearned profit. No way to tell what drives it or how it might change.';
return;
}
var b = blocks[ selected ];
detailTitle.textContent = b.question;
detailTitle.style.color = b.titleColor || '';
detailBody.textContent = b.body;
}
/* ── Hit Testing ─────────────────────────────────────────── */
function hitTest( e ) {
var rect = canvas.getBoundingClientRect();
var mx = e.clientX - rect.left;
var my = e.clientY - rect.top;
var i, a;
for ( i = hitAreas.length - 1; i >= 0; i-- ) {
a = hitAreas[ i ];
if ( mx >= a.x && mx <= a.x + a.w && my >= a.y && my <= a.y + a.h ) {
return a.idx;
}
}
return -1;
}
/* ── View Toggle ─────────────────────────────────────────── */
function setView( v ) {
view = v;
selected = 0;
hovered = -1;
if ( v === 'old' ) {
btnOld.classList.add( 'wix-pm-mode-btn--active' );
btnNew.classList.remove( 'wix-pm-mode-btn--active' );
} else {
btnNew.classList.add( 'wix-pm-mode-btn--active' );
btnOld.classList.remove( 'wix-pm-mode-btn--active' );
}
draw();
renderDetail();
}
/* ── Event Wiring ────────────────────────────────────────── */
btnOld.addEventListener( 'click', function () { setView( 'old' ); } );
btnNew.addEventListener( 'click', function () { setView( 'new' ); } );
canvas.addEventListener( 'mousemove', function ( e ) {
var h = hitTest( e );
if ( h !== hovered ) {
hovered = h;
canvas.style.cursor = h >= 0 ? 'pointer' : 'default';
draw();
}
} );
canvas.addEventListener( 'mouseleave', function () {
hovered = -1;
draw();
} );
canvas.addEventListener( 'click', function ( e ) {
var h = hitTest( e );
if ( h >= 0 ) {
selected = h;
renderDetail();
draw();
}
} );
window.addEventListener( 'resize', function () {
resize();
draw();
} );
// Initial render
resize();
draw();
renderDetail();
}
/* ================================================================
PROB-WEIGHTED
Probability-weighted estimate calculator.
Two scenarios (quiet year / major flood) with an adjustable
flood probability slider.
================================================================ */
function initProbWeighted( container ) {
/* ── Constants ────────────────────────────────────────────── */
var QUIET = 8; // €m
var FLOOD = 40; // €m
/* ── Colors ──────────────────────────────────────────────── */
var COLOR_QUIET_BG = '#d4e6f1';
var COLOR_QUIET_FG = '#0C447C';
var COLOR_FLOOD_BG = '#fadbd8';
var COLOR_FLOOD_FG = '#791F1F';
var COLOR_RESULT_BG = '#d4e6f1';
var COLOR_RESULT_FG = '#1a5276';
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Slider row
var slider = wix.el( 'input', {
type: 'range', min: '0', max: '50', value: '10', step: '1'
} );
var sliderVal = wix.el( 'span', { className: 'wix-eng-slider-val', textContent: '10%' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-eng-slider-row' }, [
wix.el( 'div', { className: 'wix-eng-slider-label', textContent: 'Flood probability' } ),
slider,
sliderVal
] ) );
// Scenario cards row
var elQuietVal = wix.el( 'div', { className: 'wix-pw-scene-val', style: { color: COLOR_QUIET_FG }, textContent: '\u20AC8m' } );
var elFloodVal = wix.el( 'div', { className: 'wix-pw-scene-val', style: { color: COLOR_FLOOD_FG }, textContent: '\u20AC40m' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-pw-calc' }, [
wix.el( 'div', { className: 'wix-pw-scene', style: { background: COLOR_QUIET_BG } }, [
wix.el( 'div', { className: 'wix-pw-scene-title', style: { color: COLOR_QUIET_FG }, textContent: 'Quiet year' } ),
elQuietVal
] ),
wix.el( 'div', { className: 'wix-pw-op', textContent: '+' } ),
wix.el( 'div', { className: 'wix-pw-scene', style: { background: COLOR_FLOOD_BG } }, [
wix.el( 'div', { className: 'wix-pw-scene-title', style: { color: COLOR_FLOOD_FG }, textContent: 'Major flood' } ),
elFloodVal
] )
] ) );
// Probability row (x multipliers)
var elQuietPct = wix.el( 'div', { className: 'wix-pw-prob-num', style: { color: COLOR_QUIET_FG }, textContent: '90%' } );
var elFloodPct = wix.el( 'div', { className: 'wix-pw-prob-num', style: { color: COLOR_FLOOD_FG }, textContent: '10%' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-pw-prob-row' }, [
wix.el( 'div', { style: { textAlign: 'center' } }, [
wix.el( 'div', { className: 'wix-pw-mult-x', textContent: '\u00D7' } ),
wix.el( 'div', { className: 'wix-pw-prob', style: { background: COLOR_QUIET_BG } }, [ elQuietPct ] )
] ),
wix.el( 'div' ),
wix.el( 'div', { style: { textAlign: 'center' } }, [
wix.el( 'div', { className: 'wix-pw-mult-x', textContent: '\u00D7' } ),
wix.el( 'div', { className: 'wix-pw-prob', style: { background: COLOR_FLOOD_BG } }, [ elFloodPct ] )
] )
] ) );
// Equals sign
wrapper.appendChild( wix.el( 'div', { className: 'wix-pw-eq' }, [
wix.el( 'span', { className: 'wix-pw-eq-sign', textContent: '=' } )
] ) );
// Result card
var elWeighted = wix.el( 'div', { className: 'wix-pw-result-num' } );
var elVs = wix.el( 'div', { className: 'wix-pw-result-vs' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-pw-result' }, [
wix.el( 'div', { className: 'wix-pw-result-label', textContent: 'Probability-weighted estimate' } ),
elWeighted,
elVs
] ) );
/* ── Update Logic ────────────────────────────────────────── */
function update() {
var fp = parseInt( slider.value, 10 );
var qp = 100 - fp;
sliderVal.textContent = fp + '%';
elQuietPct.textContent = qp + '%';
elFloodPct.textContent = fp + '%';
var w = ( qp / 100 ) * QUIET + ( fp / 100 ) * FLOOD;
var wR = Math.round( w * 10 ) / 10;
elWeighted.textContent = '\u20AC' + wR.toFixed( 1 ) + 'm';
var mostLikely = qp >= fp ? QUIET : FLOOD;
elVs.textContent = 'vs. \u20AC' + mostLikely + 'm most likely outcome';
}
/* ── Event Wiring ────────────────────────────────────────── */
slider.addEventListener( 'input', update );
// Initial render
update();
}
/* ================================================================
DISCOUNT RATE
Discount-rate dashboard: shows how discounting reduces a
future claim to present value, with year-by-year unwinding.
================================================================ */
function initDiscountRate( container ) {
/* ── Constants ────────────────────────────────────────────── */
var CLAIM = 100000;
var YEARS = 5;
/* ── Colors (WIX tokens) ─────────────────────────────────── */
var styles = getComputedStyle( container );
var colorPV = styles.getPropertyValue( '--wix-accent' ).trim() || '#1a5276';
var colorAccr = styles.getPropertyValue( '--wix-correct' ).trim() || '#1e8449';
var colorGrid = styles.getPropertyValue( '--wix-border-subtle' ).trim() || '#eaecf0';
var colorAxis = styles.getPropertyValue( '--wix-text-muted' ).trim() || '#54595d';
var colorNom = styles.getPropertyValue( '--wix-border' ).trim() || '#c8ccd1';
/* ── Helpers ─────────────────────────────────────────────── */
function pv( fv, r, t ) {
return fv / Math.pow( 1 + r, t );
}
function fmt( v ) {
return '\u20AC' + wix.formatNumber( Math.round( v ) );
}
function fmtK( v ) {
return '\u20AC' + Math.round( v / 1000 ) + 'k';
}
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Slider row (reuse insurer-engines pattern)
var slider = wix.el( 'input', {
type: 'range', min: '0', max: '6', step: '0.1', value: '3'
} );
var sliderVal = 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: 'Discount rate' } ),
slider,
sliderVal
] ) );
// Stat cards (3-column grid, reuse reserve-sensitivity pattern)
var elPV = wix.el( 'div', { className: 'wix-rs-card-num' } );
var elOver = wix.el( 'div', { className: 'wix-rs-card-num' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-dr-cards' }, [
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'Claim (nominal)' } ),
wix.el( 'div', { className: 'wix-rs-card-num wix-rs-card-num--muted', textContent: '\u20AC100,000' } )
] ),
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'Present value today' } ),
elPV
] ),
wix.el( 'div', { className: 'wix-rs-card' }, [
wix.el( 'div', { className: 'wix-rs-card-label', textContent: 'Overstatement if no discount' } ),
elOver
] )
] ) );
// Chart + table box (same style as stat cards)
var canvas = wix.el( 'canvas' );
var tableEl = wix.el( 'div', { className: 'wix-dr-table' } );
var chartBox = wix.el( 'div', { className: 'wix-rs-card wix-dr-box' }, [
wix.el( 'div', { className: 'wix-eng-section-label', textContent: 'Liability unwinding \u2014 year by year' } ),
wix.el( 'div', { className: 'wix-dr-chart' }, [ canvas ] ),
wix.el( 'div', { className: 'wix-pm-legend' }, [
wix.el( 'span', { className: 'wix-pm-legend-item' }, [
wix.el( 'span', { className: 'wix-pm-swatch', style: { background: colorPV } } ),
'Present value'
] ),
wix.el( 'span', { className: 'wix-pm-legend-item' }, [
wix.el( 'span', { className: 'wix-pm-swatch', style: { background: colorAccr } } ),
'Annual accretion'
] ),
wix.el( 'span', { className: 'wix-pm-legend-item' }, [
wix.el( 'span', { className: 'wix-dr-nom-swatch' } ),
'Nominal (\u20AC100,000)'
] )
] ),
tableEl
] );
wrapper.appendChild( chartBox );
/* ── Chart Drawing ───────────────────────────────────────── */
function drawChart( pvs, accretions ) {
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: 12, b: 28, l: 50, r: 12 };
var cw = w - pad.l - pad.r;
var ch = h - pad.t - pad.b;
var yMax = 105000;
var n = YEARS + 1;
ctx.clearRect( 0, 0, w, h );
// Y helper
function yPos( v ) {
return pad.t + ch - ( ch * v / yMax );
}
function xPos( i ) {
return pad.l + ( cw * i / ( n - 1 ) );
}
// Grid + 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;
var i, y;
for ( i = 0; i <= gridSteps; i++ ) {
var gv = yMax * i / gridSteps;
y = yPos( gv );
ctx.beginPath();
ctx.moveTo( pad.l, y );
ctx.lineTo( pad.l + cw, y );
ctx.stroke();
ctx.fillText( fmtK( gv ), pad.l - 6, y + 4 );
}
// X labels
ctx.textAlign = 'center';
for ( i = 0; i < n; i++ ) {
ctx.fillText( 'Year ' + i, xPos( i ), h - 6 );
}
// Nominal dashed line
ctx.strokeStyle = colorNom;
ctx.lineWidth = 1;
ctx.setLineDash( [ 4, 4 ] );
ctx.beginPath();
ctx.moveTo( pad.l, yPos( CLAIM ) );
ctx.lineTo( pad.l + cw, yPos( CLAIM ) );
ctx.stroke();
ctx.setLineDash( [] );
// Accretion bars
var barWidth = Math.min( 30, cw / n * 0.45 );
for ( i = 0; i < n; i++ ) {
if ( accretions[ i ] <= 0 ) {
continue;
}
var bx = xPos( i ) - barWidth / 2;
var by = yPos( accretions[ i ] );
var bh = yPos( 0 ) - by;
ctx.fillStyle = colorAccr + '66';
ctx.fillRect( bx, by, barWidth, bh );
ctx.strokeStyle = colorAccr;
ctx.lineWidth = 1;
ctx.strokeRect( bx, by, barWidth, bh );
}
// PV line
ctx.beginPath();
ctx.strokeStyle = colorPV;
ctx.lineWidth = 2;
for ( i = 0; i < n; i++ ) {
var px = xPos( i );
var py = yPos( pvs[ i ] );
if ( i === 0 ) {
ctx.moveTo( px, py );
} else {
ctx.lineTo( px, py );
}
}
ctx.stroke();
// PV dots
for ( i = 0; i < n; i++ ) {
ctx.beginPath();
ctx.arc( xPos( i ), yPos( pvs[ i ] ), 4, 0, Math.PI * 2 );
ctx.fillStyle = colorPV;
ctx.fill();
}
}
/* ── Table ───────────────────────────────────────────────── */
function buildTable( pvs, accretions ) {
wix.empty( tableEl );
var thead = wix.el( 'tr', {}, [
wix.el( 'th', { textContent: 'Year' } ),
wix.el( 'th', { textContent: 'Remaining' } ),
wix.el( 'th', { textContent: 'Present value' } ),
wix.el( 'th', { textContent: 'Accretion' } )
] );
var table = wix.el( 'table', { className: 'wix-dr-tbl' }, [ thead ] );
for ( var yr = 0; yr <= YEARS; yr++ ) {
table.appendChild( wix.el( 'tr', {}, [
wix.el( 'td', { textContent: 'Year ' + yr } ),
wix.el( 'td', { textContent: ( YEARS - yr ) + ' yr' } ),
wix.el( 'td', { textContent: fmt( pvs[ yr ] ) } ),
wix.el( 'td', { textContent: yr === 0 ? '\u2014' : fmt( accretions[ yr ] ) } )
] ) );
}
tableEl.appendChild( table );
}
/* ── Update ──────────────────────────────────────────────── */
function update() {
var r = parseFloat( slider.value ) / 100;
sliderVal.textContent = slider.value + '%';
var todayPV = pv( CLAIM, r, YEARS );
elPV.textContent = fmt( todayPV );
elOver.textContent = fmt( CLAIM - todayPV );
var pvs = [];
var accretions = [];
for ( var yr = 0; yr <= YEARS; yr++ ) {
var remaining = YEARS - yr;
var val = pv( CLAIM, r, remaining );
pvs.push( Math.round( val ) );
accretions.push( yr === 0 ? 0 : Math.round( val - pv( CLAIM, r, remaining + 1 ) ) );
}
drawChart( pvs, accretions );
buildTable( pvs, accretions );
}
/* ── Event Wiring ────────────────────────────────────────── */
slider.addEventListener( 'input', update );
window.addEventListener( 'resize', function () {
update();
} );
// Initial render
update();
}
/* ================================================================
BALANCE SHEET
IFRS 17 balance sheet: two-column stacked bar (assets vs
liabilities) with click-to-explore detail card and brackets.
Blocks read from data-wix-blocks JSON attribute.
================================================================ */
function initBalanceSheet( container ) {
/* ── Read blocks from data attribute ─────────────────────── */
var raw = wix.data( container, 'wix-blocks', '[]' );
var blocks;
try {
blocks = JSON.parse( raw );
} catch ( e ) {
return;
}
if ( !blocks.length ) {
return;
}
var TOTAL = blocks[ 0 ].val;
/* ── Colors (WIX tokens for brackets/axis) ───────────────── */
var styles = getComputedStyle( container );
var colorAxis = styles.getPropertyValue( '--wix-text-muted' ).trim() || '#54595d';
var colorLine = styles.getPropertyValue( '--wix-border' ).trim() || '#c8ccd1';
/* ── State ───────────────────────────────────────────────── */
var selected = 0;
var hitAreas = [];
var W, H;
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
// Canvas
var canvas = wix.el( 'canvas', { style: { cursor: 'pointer' } } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-bs-chart' }, [ canvas ] ) );
// Detail card
var detailTitle = wix.el( 'div', { className: 'wix-wf-detail-title' } );
var detailBody = wix.el( 'div', { className: 'wix-wf-detail-body' } );
var detailCard = wix.el( 'div', { className: 'wix-wf-detail' }, [ detailTitle, detailBody ] );
wrapper.appendChild( detailCard );
/* ── Canvas Helpers ──────────────────────────────────────── */
function resize() {
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
if ( rect.width === 0 ) {
return;
}
W = rect.width;
H = 400;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.getContext( '2d' ).setTransform( dpr, 0, 0, dpr, 0, 0 );
}
/* roundRect polyfill */
function roundRect( ctx2, x, y, w, h, r ) {
if ( ctx2.roundRect ) {
ctx2.beginPath();
ctx2.roundRect( x, y, w, h, r );
return;
}
ctx2.beginPath();
ctx2.moveTo( x + r, y );
ctx2.lineTo( x + w - r, y );
ctx2.arcTo( x + w, y, x + w, y + r, r );
ctx2.lineTo( x + w, y + h - r );
ctx2.arcTo( x + w, y + h, x + w - r, y + h, r );
ctx2.lineTo( x + r, y + h );
ctx2.arcTo( x, y + h, x, y + h - r, r );
ctx2.lineTo( x, y + r );
ctx2.arcTo( x, y, x + r, y, r );
ctx2.closePath();
}
/* Bracket with 1 or 2 line label */
function drawBracket( ctx2, x, y1, y2, label1, label2 ) {
var mid = ( y1 + y2 ) / 2;
ctx2.strokeStyle = colorLine;
ctx2.lineWidth = 1;
ctx2.beginPath();
ctx2.moveTo( x, y1 );
ctx2.lineTo( x + 6, y1 );
ctx2.lineTo( x + 6, y2 );
ctx2.lineTo( x, y2 );
ctx2.stroke();
ctx2.fillStyle = colorAxis;
ctx2.font = '400 11px system-ui, sans-serif';
ctx2.textAlign = 'left';
ctx2.textBaseline = 'middle';
if ( label2 ) {
ctx2.fillText( label1, x + 12, mid - 7 );
ctx2.fillText( label2, x + 12, mid + 7 );
} else {
ctx2.fillText( label1, x + 12, mid );
}
}
/* ── Draw ────────────────────────────────────────────────── */
function draw() {
var ctx2 = canvas.getContext( '2d' );
ctx2.clearRect( 0, 0, W, H );
hitAreas = [];
var padTop = 30;
var padBot = 10;
var bracketZone = 150;
var colGap = 12;
var colW = Math.min( 180, ( W - bracketZone - colGap ) / 2 );
var chartH = H - padTop - padBot;
var totalW = colW * 2 + colGap + bracketZone;
var padL = Math.max( 10, ( W - totalW ) / 2 );
var xA = padL;
var xL = padL + colW + colGap;
// Column headers
ctx2.font = '400 13px system-ui, sans-serif';
ctx2.textAlign = 'center';
ctx2.fillStyle = colorAxis;
ctx2.fillText( 'Assets', xA + colW / 2, padTop - 10 );
ctx2.fillText( 'Liabilities', xL + colW / 2, padTop - 10 );
// ── Assets column (single block) ──
var aBlock = blocks[ 0 ];
var isSA = selected === 0;
ctx2.fillStyle = isSA ? aBlock.hColor : aBlock.color;
roundRect( ctx2, xA, padTop, colW, chartH, 4 );
ctx2.fill();
ctx2.fillStyle = aBlock.textColor;
ctx2.font = '500 15px system-ui, sans-serif';
ctx2.textAlign = 'center';
ctx2.textBaseline = 'middle';
ctx2.fillText( aBlock.label, xA + colW / 2, padTop + chartH / 2 - 14 );
ctx2.fillStyle = aBlock.subColor;
ctx2.font = '400 12px system-ui, sans-serif';
ctx2.fillText( aBlock.sub, xA + colW / 2, padTop + chartH / 2 + 4 );
ctx2.fillText( '\u20AC' + aBlock.val + 'm', xA + colW / 2, padTop + chartH / 2 + 22 );
hitAreas.push( { x: xA, y: padTop, w: colW, h: chartH, idx: 0 } );
// ── Liabilities column (stacked segments) ──
var liab = blocks.slice( 1 );
var segGap = 3;
var totalGap = ( liab.length - 1 ) * segGap;
var availH = chartH - totalGap;
var y = padTop;
var yPositions = [];
var i, b, bH, isSel, midY;
for ( i = 0; i < liab.length; i++ ) {
b = liab[ i ];
bH = ( b.val / TOTAL ) * availH;
isSel = selected === i + 1;
ctx2.fillStyle = isSel ? b.hColor : b.color;
roundRect( ctx2, xL, y, colW, bH, 4 );
ctx2.fill();
midY = y + bH / 2;
ctx2.textAlign = 'center';
ctx2.textBaseline = 'middle';
if ( bH > 60 ) {
ctx2.fillStyle = b.textColor;
ctx2.font = '500 14px system-ui, sans-serif';
ctx2.fillText( b.label, xL + colW / 2, midY - 10 );
ctx2.fillStyle = b.subColor;
ctx2.font = '400 11px system-ui, sans-serif';
ctx2.fillText( b.sub, xL + colW / 2, midY + 6 );
ctx2.fillText( '\u20AC' + b.val + 'm', xL + colW / 2, midY + 22 );
} else if ( bH > 35 ) {
ctx2.fillStyle = b.textColor;
ctx2.font = '500 14px system-ui, sans-serif';
ctx2.fillText( b.label, xL + colW / 2, midY - 6 );
ctx2.fillStyle = b.subColor;
ctx2.font = '400 11px system-ui, sans-serif';
ctx2.fillText( '\u20AC' + b.val + 'm', xL + colW / 2, midY + 10 );
} else {
ctx2.fillStyle = b.textColor;
ctx2.font = '500 14px system-ui, sans-serif';
ctx2.fillText( b.label + ' \u20AC' + b.val + 'm', xL + colW / 2, midY );
}
hitAreas.push( { x: xL, y: y, w: colW, h: bH, idx: i + 1 } );
yPositions.push( { top: y, bot: y + bH } );
y += bH + segGap;
}
// ── Brackets ──
var bx1 = xL + colW + 10;
if ( yPositions.length >= 1 ) {
drawBracket( ctx2, bx1, yPositions[ 0 ].top + 2, yPositions[ 0 ].bot - 2, 'Firm\u2019s funds' );
}
if ( yPositions.length >= 4 ) {
drawBracket( ctx2, bx1, yPositions[ 1 ].top + 2, yPositions[ 3 ].bot - 2, 'Technical', 'provisions' );
var bx2 = bx1 + 72;
drawBracket( ctx2, bx2, yPositions[ 2 ].top + 2, yPositions[ 3 ].bot - 2, 'Fulfilment', 'cash flows' );
}
}
/* ── Detail Card ─────────────────────────────────────────── */
function renderDetail() {
var b = blocks[ selected ];
detailTitle.textContent = b.question;
detailTitle.style.color = b.textColor;
detailBody.textContent = b.body;
}
/* ── Hit Testing ─────────────────────────────────────────── */
function hitTest( e ) {
var rect = canvas.getBoundingClientRect();
var mx = e.clientX - rect.left;
var my = e.clientY - rect.top;
for ( var i = hitAreas.length - 1; i >= 0; i-- ) {
var a = hitAreas[ i ];
if ( mx >= a.x && mx <= a.x + a.w && my >= a.y && my <= a.y + a.h ) {
return a.idx;
}
}
return -1;
}
/* ── Event Wiring ────────────────────────────────────────── */
canvas.addEventListener( 'mousemove', function ( e ) {
canvas.style.cursor = hitTest( e ) >= 0 ? 'pointer' : 'default';
} );
canvas.addEventListener( 'click', function ( e ) {
var h = hitTest( e );
if ( h >= 0 ) {
selected = h;
draw();
renderDetail();
}
} );
window.addEventListener( 'resize', function () {
resize();
draw();
} );
// Initial render
resize();
draw();
renderDetail();
}
/* ================================================================
RISK ADJUSTMENT — data-wix-module="risk-adjustment"
Brittany storm: lognormal claim distribution with interactive
confidence slider. Shows best estimate, percentile threshold,
and the risk adjustment gap on a PDF chart.
================================================================ */
function initRiskAdjustment( container ) {
/* ── Constants ────────────────────────────────────────────── */
var MU = 1.7118;
var SIG = 0.40;
var MEAN = 6.0;
var XMIN = 1, XMAX = 18, STEPS = 400;
/* ── Math helpers ────────────────────────────────────────── */
function lognormPDF( x ) {
if ( x <= 0 ) return 0;
var lx = Math.log( x );
return Math.exp( -0.5 * Math.pow( ( lx - MU ) / SIG, 2 ) ) /
( x * SIG * Math.sqrt( 2 * Math.PI ) );
}
function ratApprox( t ) {
var c = [ 2.515517, 0.802853, 0.010328 ];
var d = [ 1.432788, 0.189269, 0.001308 ];
return t - ( c[0] + c[1]*t + c[2]*t*t ) /
( 1 + d[0]*t + d[1]*t*t + d[2]*t*t*t );
}
function normInv( p ) {
if ( p <= 0 ) return -Infinity;
if ( p >= 1 ) return Infinity;
if ( p < 0.5 ) return -ratApprox( Math.sqrt( -2 * Math.log( p ) ) );
if ( p > 0.5 ) return ratApprox( Math.sqrt( -2 * Math.log( 1 - p ) ) );
return 0;
}
function lognormCDFInv( p ) {
return Math.exp( MU + SIG * normInv( p ) );
}
/* ── Chart colors (from WIX tokens) ──────────────────────── */
var styles = getComputedStyle( container );
var colorAccent = styles.getPropertyValue( '--wix-accent' ).trim() || '#1a5276';
var colorWarn = styles.getPropertyValue( '--wix-wrong' ).trim() || '#922b21';
var colorGrid = styles.getPropertyValue( '--wix-border-subtle' ).trim() || '#eaecf0';
var colorAxis = styles.getPropertyValue( '--wix-text-muted' ).trim() || '#54595d';
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var slider = wix.el( 'input', { type: 'range', min: '50', max: '95', value: '75', step: '1' } );
var sliderVal = wix.el( 'span', { className: 'wix-ra-slider-val', textContent: '75%' } );
var elBE = wix.el( 'div', { className: 'wix-ra-stat-val', textContent: '\u20AC6.0m' } );
var elPct = wix.el( 'div', { className: 'wix-ra-stat-val', textContent: '\u20AC7.2m' } );
var elRA = wix.el( 'div', { className: 'wix-ra-stat-val wix-ra-stat-val--accent', textContent: '\u20AC1.2m' } );
var canvas = wix.el( 'canvas' );
var explain = wix.el( 'p', { className: 'wix-ra-explain' } );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
/* Slider row */
wrapper.appendChild( wix.el( 'div', { className: 'wix-ra-slider-row' }, [
wix.el( 'span', { className: 'wix-ra-slider-label', textContent: 'Confidence level' } ),
slider,
sliderVal
] ) );
/* Stats row */
wrapper.appendChild( wix.el( 'div', { className: 'wix-ra-stats' }, [
wix.el( 'div', { className: 'wix-ra-stat' }, [
wix.el( 'div', { className: 'wix-ra-stat-label', textContent: 'Best estimate' } ),
elBE
] ),
wix.el( 'div', { className: 'wix-ra-stat' }, [
wix.el( 'div', { className: 'wix-ra-stat-label', textContent: 'Covered up to' } ),
elPct
] ),
wix.el( 'div', { className: 'wix-ra-stat' }, [
wix.el( 'div', { className: 'wix-ra-stat-label', textContent: 'Risk adjustment' } ),
elRA
] )
] ) );
/* Chart */
wrapper.appendChild( wix.el( 'div', { className: 'wix-ra-chart' }, [ canvas ] ) );
/* Legend */
function swatch( color ) {
return wix.el( 'span', { className: 'wix-ra-legend-swatch', style: { background: color } } );
}
wrapper.appendChild( wix.el( 'div', { className: 'wix-ra-legend' }, [
wix.el( 'span', { className: 'wix-ra-legend-item' }, [
swatch( colorAccent + '40' ), 'Claim distribution'
] ),
wix.el( 'span', { className: 'wix-ra-legend-item' }, [
swatch( colorAccent ), 'Best estimate (mean)'
] ),
wix.el( 'span', { className: 'wix-ra-legend-item' }, [
swatch( colorWarn ), 'Confidence threshold'
] )
] ) );
/* Explanation */
wrapper.appendChild( explain );
/* ── Drawing ─────────────────────────────────────────────── */
function draw() {
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, H = rect.height;
var pad = { t: 16, r: 16, b: 36, l: 44 };
var cw = W - pad.l - pad.r;
var ch = H - pad.t - pad.b;
ctx.clearRect( 0, 0, W, H );
/* Compute PDF curve */
var xs = [], ys = [], ymax = 0;
for ( var i = 0; i <= STEPS; i++ ) {
var x = XMIN + ( XMAX - XMIN ) * i / STEPS;
var y = lognormPDF( x );
xs.push( x ); ys.push( y );
if ( y > ymax ) ymax = y;
}
ymax *= 1.1;
function tx( x ) { return pad.l + ( x - XMIN ) / ( XMAX - XMIN ) * cw; }
function ty( y ) { return pad.t + ch - ( y / ymax ) * ch; }
var conf = parseInt( slider.value, 10 );
var pctX = lognormCDFInv( conf / 100 );
/* Axes */
ctx.strokeStyle = colorGrid;
ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.moveTo( pad.l, pad.t + ch ); ctx.lineTo( pad.l + cw, pad.t + ch ); ctx.stroke();
ctx.beginPath(); ctx.moveTo( pad.l, pad.t ); ctx.lineTo( pad.l, pad.t + ch ); ctx.stroke();
/* X-axis labels */
ctx.font = '11px sans-serif';
ctx.fillStyle = colorAxis;
ctx.textAlign = 'center';
for ( var v = 2; v <= 16; v += 2 ) {
var xp = tx( v );
ctx.fillText( '\u20AC' + v + 'm', xp, pad.t + ch + 20 );
ctx.strokeStyle = colorGrid;
ctx.beginPath(); ctx.moveTo( xp, pad.t + ch ); ctx.lineTo( xp, pad.t + ch + 4 ); ctx.stroke();
}
/* Shaded area under curve up to percentile */
ctx.beginPath();
ctx.moveTo( tx( xs[0] ), ty( 0 ) );
for ( i = 0; i <= STEPS; i++ ) {
if ( xs[i] <= pctX ) ctx.lineTo( tx( xs[i] ), ty( ys[i] ) );
}
var clipIdx = -1;
for ( i = 0; i < xs.length; i++ ) {
if ( xs[i] > pctX ) { clipIdx = i; break; }
}
if ( clipIdx > 0 ) {
var frac = ( pctX - xs[clipIdx-1] ) / ( xs[clipIdx] - xs[clipIdx-1] );
var yClip = ys[clipIdx-1] + frac * ( ys[clipIdx] - ys[clipIdx-1] );
ctx.lineTo( tx( pctX ), ty( yClip ) );
}
ctx.lineTo( tx( pctX ), ty( 0 ) );
ctx.closePath();
ctx.fillStyle = colorAccent + '30';
ctx.fill();
/* Full PDF curve */
ctx.beginPath();
ctx.moveTo( tx( xs[0] ), ty( ys[0] ) );
for ( i = 1; i <= STEPS; i++ ) ctx.lineTo( tx( xs[i] ), ty( ys[i] ) );
ctx.strokeStyle = colorAccent;
ctx.lineWidth = 2;
ctx.stroke();
/* Best-estimate (mean) dashed line */
ctx.setLineDash( [ 5, 4 ] );
ctx.lineWidth = 1.5;
ctx.strokeStyle = colorAccent;
ctx.beginPath();
ctx.moveTo( tx( MEAN ), ty( 0 ) );
ctx.lineTo( tx( MEAN ), ty( lognormPDF( MEAN ) ) );
ctx.stroke();
/* Percentile dashed line */
ctx.strokeStyle = colorWarn;
ctx.beginPath();
ctx.moveTo( tx( pctX ), ty( 0 ) );
ctx.lineTo( tx( pctX ), ty( lognormPDF( pctX ) ) );
ctx.stroke();
ctx.setLineDash( [] );
/* Risk-adjustment bracket */
var raStart = tx( MEAN ), raEnd = tx( pctX );
var arrowY = ty( 0 ) - 18;
if ( raEnd - raStart > 20 ) {
ctx.strokeStyle = colorWarn;
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo( raStart, arrowY ); ctx.lineTo( raEnd, arrowY ); ctx.stroke();
ctx.beginPath(); ctx.moveTo( raStart, arrowY - 4 ); ctx.lineTo( raStart, arrowY + 4 ); ctx.stroke();
ctx.beginPath(); ctx.moveTo( raEnd, arrowY - 4 ); ctx.lineTo( raEnd, arrowY + 4 ); ctx.stroke();
ctx.fillStyle = colorWarn;
ctx.font = '500 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText( 'RA: \u20AC' + ( pctX - MEAN ).toFixed( 1 ) + 'm',
( raStart + raEnd ) / 2, arrowY - 6 );
}
/* Update stat cards */
var ra = pctX - MEAN;
sliderVal.textContent = conf + '%';
elPct.textContent = '\u20AC' + pctX.toFixed( 1 ) + 'm';
elRA.textContent = '\u20AC' + ra.toFixed( 1 ) + 'm';
/* Update explanation */
explain.textContent = conf === 50
? 'At the 50% level the threshold equals the best estimate, so the risk adjustment is zero \u2014 the insurer holds no buffer for uncertainty at all.'
: 'At the ' + conf + '% confidence level, the insurer holds enough to cover outcomes up to \u20AC' + pctX.toFixed( 1 ) + 'm. The risk adjustment of \u20AC' + ra.toFixed( 1 ) + 'm is the gap between that threshold and the \u20AC6.0m best estimate \u2014 the price AXA pays for bearing the uncertainty on 3,000 Brittany homes.';
}
/* ── Events ──────────────────────────────────────────────── */
slider.addEventListener( 'input', draw );
window.addEventListener( 'resize', draw );
draw();
}
/* ================================================================
GROUPING FUNNEL — data-wix-module="grouping-funnel"
3-step walkthrough: portfolio → profitability → annual cohort.
================================================================ */
function initGroupingFunnel( container ) {
/* ── Step data ───────────────────────────────────────────── */
var labels = [
'<b>Step 1 \u2014 Portfolio:</b> group contracts with similar risks managed together',
'<b>Step 2 \u2014 Profitability:</b> separate onerous from profitable at initial recognition',
'<b>Step 3 \u2014 Annual cohort:</b> contracts issued more than 12\u00A0months apart cannot mix'
];
var counts = [
'AXA writes <b>5,000</b> home insurance contracts in coastal Brittany \u2014 plus a separate motor book in Spain',
'<b>3 groups</b> within the Brittany portfolio based on expected profitability',
'<b>6 measurement groups</b> \u2014 each profitability bucket split into 2025 and 2026 cohorts'
];
/* ── State ───────────────────────────────────────────────── */
var step = 0;
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
/* Progress steps */
var stepEls = [];
for ( var i = 0; i < 3; i++ ) {
stepEls.push( wix.el( 'div', { className: 'wix-gf-step' } ) );
}
wrapper.appendChild( wix.el( 'div', { className: 'wix-gf-steps' }, stepEls ) );
/* Label */
var labelEl = wix.el( 'p', { className: 'wix-gf-label' } );
wrapper.appendChild( labelEl );
/* Visual area */
var visualEl = wix.el( 'div', { className: 'wix-gf-visual' } );
wrapper.appendChild( visualEl );
/* Count box */
var countEl = wix.el( 'div', { className: 'wix-gf-count' } );
wrapper.appendChild( countEl );
/* Nav buttons */
var btnPrev = wix.el( 'button', { className: 'wix-btn wix-btn--outline', innerHTML: '\u25C2 Previous' } );
var btnNext = wix.el( 'button', { className: 'wix-btn', innerHTML: 'Next \u25B8' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-gf-nav' }, [ btnPrev, btnNext ] ) );
/* ── Helpers ─────────────────────────────────────────────── */
function makeSeg( flex, bg, text, textColor, extra ) {
var s = wix.el( 'div', { className: 'wix-gf-seg' } );
s.style.flex = flex;
s.style.background = bg;
s.style.color = textColor;
s.textContent = text;
if ( extra ) {
Object.keys( extra ).forEach( function ( k ) { s.style[k] = extra[k]; } );
}
return s;
}
function makeBarRow( label, segments, groupLabel ) {
var bar = wix.el( 'div', { className: 'wix-gf-bar' }, segments );
var children = [
wix.el( 'p', { className: 'wix-gf-bar-label', textContent: label } ),
bar
];
if ( groupLabel ) {
children.push( wix.el( 'p', { className: 'wix-gf-group-label', textContent: groupLabel } ) );
}
return wix.el( 'div', { className: 'wix-gf-bar-row' }, children );
}
/* ── Render ──────────────────────────────────────────────── */
function render() {
/* Progress dots */
for ( var i = 0; i < 3; i++ ) {
stepEls[i].className = 'wix-gf-step' + ( i <= step ? ' wix-gf-step--done' : '' );
}
/* Label + count */
labelEl.innerHTML = labels[step];
countEl.innerHTML = counts[step];
/* Nav state */
btnPrev.disabled = step === 0;
btnNext.disabled = step === 2;
/* Visual */
wix.empty( visualEl );
if ( step === 0 ) {
visualEl.appendChild( makeBarRow(
'Home insurance \u2014 Brittany, France',
[ makeSeg( 5000, '#EEEDFE', '5,000 contracts', '#3C3489' ) ],
'Portfolio 1 \u2014 same weather risks, same underwriting team'
) );
visualEl.appendChild( wix.el( 'div', { style: { height: '8px' } } ) );
visualEl.appendChild( makeBarRow(
'Motor third-party liability \u2014 Spain',
[ makeSeg( 3000, '#F1EFE8', '3,000 contracts', '#5F5E5A' ) ],
'Portfolio 2 \u2014 different risk drivers, different team'
) );
}
if ( step === 1 ) {
visualEl.appendChild( makeBarRow(
'Home insurance \u2014 Brittany, France',
[
makeSeg( 4200, '#E1F5EE', '4,200 profitable', '#085041' ),
makeSeg( 500, '#FAEEDA', '500 borderline', '#633806' ),
makeSeg( 300, '#FCEBEB', '300 onerous', '#791F1F' )
],
null
) );
/* Annotation row */
var annotData = [
{ flex: 4200, text: 'CSM stores profit', color: '#0F6E56' },
{ flex: 500, text: 'Monitor', color: '#854F0B' },
{ flex: 300, text: 'Loss recognised', color: '#A32D2D' }
];
var annotChildren = [];
annotData.forEach( function ( a ) {
var item = wix.el( 'div', {
className: 'wix-gf-annot-item',
textContent: a.text
} );
item.style.flex = a.flex;
item.style.color = a.color;
return annotChildren.push( item );
} );
visualEl.appendChild( wix.el( 'div', { className: 'wix-gf-annot' }, annotChildren ) );
visualEl.appendChild( wix.el( 'div', { style: { height: '12px' } } ) );
visualEl.appendChild( makeBarRow(
'Motor third-party liability \u2014 Spain',
[ makeSeg( 3000, '#F1EFE8', 'Separate portfolio \u2014 own profitability split', '#5F5E5A' ) ],
null
) );
}
if ( step === 2 ) {
visualEl.appendChild( wix.el( 'p', { className: 'wix-gf-bar-label', textContent: 'Home insurance \u2014 Brittany, France' } ) );
var rows = [
{ n: 4200, label: 'Profitable', bg: '#E1F5EE', tc: '#085041', sub: '#0F6E56' },
{ n: 500, label: 'Borderline', bg: '#FAEEDA', tc: '#633806', sub: '#854F0B' },
{ n: 300, label: 'Onerous', bg: '#FCEBEB', tc: '#791F1F', sub: '#A32D2D' }
];
var maxN = 4200;
rows.forEach( function ( r ) {
var a = Math.round( r.n * 0.55 );
var b = r.n - a;
var cohortBar = wix.el( 'div', { className: 'wix-gf-cohort-bar' } );
cohortBar.style.maxWidth = Math.round( r.n / maxN * 100 ) + '%';
cohortBar.appendChild( makeSeg( a, r.bg, a.toLocaleString() + ' (2025)', r.tc,
{ height: '36px', border: '0.5px solid ' + r.sub } ) );
cohortBar.appendChild( makeSeg( b, r.bg, b.toLocaleString() + ' (2026)', r.tc,
{ height: '36px', opacity: '0.6', border: '0.5px solid ' + r.sub } ) );
visualEl.appendChild( wix.el( 'div', { className: 'wix-gf-cohort-row' }, [
wix.el( 'span', { className: 'wix-gf-cohort-label', textContent: r.label } ),
cohortBar
] ) );
} );
}
}
/* ── Events ──────────────────────────────────────────────── */
btnPrev.addEventListener( 'click', function () {
if ( step > 0 ) { step--; render(); }
} );
btnNext.addEventListener( 'click', function () {
if ( step < 2 ) { step++; render(); }
} );
render();
}
/* ================================================================
CSM ROLLFORWARD — data-wix-module="csm-rollforward"
Horizontal waterfall chart: FY24 opening balance through six
movement items to FY25 closing balance. Sliders let the user
adjust each component; click any bar for an explanation.
================================================================ */
function initCsmRollforward( container ) {
/* ── Item definitions ────────────────────────────────────── */
var ITEMS = [
{ key: 'nb', label: 'New business CSM', val: 2199, min: 0, max: 5000, step: 50, color: '#1D9E75',
explain: 'The CSM recognised at inception of new insurance contracts written during the year. It represents the present value of future unearned profit that the insurer expects to earn from these new policies. A higher figure signals strong commercial momentum.' },
{ key: 'roi', label: 'Return on inforce', val: 1328, min: 0, max: 3000, step: 50, color: '#378ADD',
explain: 'Sometimes called the "unwind of discount rate", this is the interest accretion on the opening CSM balance. Because future cash flows were discounted at inception, the passage of time increases their present value. Think of it as the CSM earning a return simply by getting one year closer to settlement.' },
{ key: 'rel', label: 'CSM release', val: -2954, min: -5000, max: 0, step: 50, color: '#D85A30',
explain: 'The portion of CSM released to the income statement as profit in the period. Under IFRS 17, profit is recognised as services are delivered to policyholders \u2014 so this is the primary mechanism that turns the CSM balance into reported earnings. It is always negative in the rollforward because it reduces the remaining stock of unearned profit.' },
{ key: 'eco', label: 'Economic variance', val: 594, min: -2000, max: 2000, step: 50, color: '#534AB7',
explain: 'Changes in the CSM caused by movements in financial assumptions \u2014 interest rates, equity markets, credit spreads, and foreign exchange. Under the Variable Fee Approach (VFA), changes in the insurer\u2019s share of underlying asset returns adjust the CSM rather than hitting P&L directly. Positive values mean financial conditions improved for the insurer.' },
{ key: 'opv', label: 'Operating variance', val: -316, min: -2000, max: 2000, step: 50, color: '#D4537E',
explain: 'Changes in the CSM driven by updates to non-financial (operating) assumptions \u2014 mortality, morbidity, lapse rates, and expenses. When actual experience or updated projections differ from what was assumed at inception, the CSM absorbs the difference. Negative means experience or updated assumptions were worse than expected.' },
{ key: 'oth', label: 'Other', val: -1451, min: -3000, max: 1000, step: 50, color: '#888780',
explain: 'A catch-all for items such as foreign exchange translation effects on non-euro subsidiaries, scope changes (acquisitions or disposals of portfolios), model or methodology changes, and any other adjustments that don\u2019t fit neatly into the categories above.' }
];
var FY24 = 33853;
var TOTAL_COLOR = '#3266ad';
var TOTALS_EXPLAIN = 'The opening (FY24) and closing (FY25) CSM balance represents the total stock of unearned profit the insurer expects to recognise in future periods. The rollforward reconciles how the balance moved from one year-end to the next through the components shown above.';
/* ── Live values ─────────────────────────────────────────── */
var vals = {};
ITEMS.forEach( function ( it ) { vals[it.key] = it.val; } );
/* ── Formatting ──────────────────────────────────────────── */
function fmt( v ) {
return ( v < 0 ? '-' : '' ) + '\u20AC' + Math.abs( Math.round( v ) ).toLocaleString() + 'm';
}
/* ── Chart colors (grid / axis) ──────────────────────────── */
var styles = getComputedStyle( container );
var colorGrid = styles.getPropertyValue( '--wix-border-subtle' ).trim() || '#eaecf0';
var colorAxis = styles.getPropertyValue( '--wix-text-muted' ).trim() || '#54595d';
var colorText = styles.getPropertyValue( '--wix-text' ).trim() || '#202122';
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
/* Title */
wrapper.appendChild( wix.el( 'div', { className: 'wix-sim-title', textContent: 'AXA CSM rollforward FY24\u201325 (\u20AC\u00A0million)' } ) );
/* Slider grid */
var sliderGrid = wix.el( 'div', { className: 'wix-csm-sliders' } );
var sliderRefs = {};
var valRefs = {};
ITEMS.forEach( function ( it ) {
var sl = wix.el( 'input', { type: 'range', min: String( it.min ), max: String( it.max ),
step: String( it.step ), value: String( it.val ) } );
var vl = wix.el( 'span', { className: 'wix-csm-slider-val', textContent: it.val.toLocaleString() } );
sliderRefs[it.key] = sl;
valRefs[it.key] = vl;
sliderGrid.appendChild( wix.el( 'div', { className: 'wix-csm-slider-group' }, [
wix.el( 'label', { textContent: it.label } ),
wix.el( 'div', { className: 'wix-csm-slider-row' }, [ sl, vl ] )
] ) );
} );
wrapper.appendChild( sliderGrid );
/* Chart */
var canvas = wix.el( 'canvas' );
var chartWrap = wix.el( 'div', { className: 'wix-csm-chart' }, [ canvas ] );
wrapper.appendChild( chartWrap );
/* Explainer */
var exDot = wix.el( 'span', { className: 'wix-csm-ex-dot' } );
var exTitle = wix.el( 'span', { className: 'wix-csm-ex-title' } );
var exVal = wix.el( 'span', { className: 'wix-csm-ex-val' } );
var exBody = wix.el( 'p', { className: 'wix-csm-ex-body' } );
var explainer = wix.el( 'div', { className: 'wix-csm-explainer wix-csm-explainer--hidden' }, [
wix.el( 'div', { className: 'wix-csm-ex-head' }, [ exDot, exTitle, exVal ] ),
exBody
] );
wrapper.appendChild( explainer );
/* Hint */
var hint = wix.el( 'p', { className: 'wix-csm-hint', textContent: 'Click any bar for an explanation of that component.' } );
wrapper.appendChild( hint );
/* ── Waterfall data ──────────────────────────────────────── */
function getRows() {
var rows = [];
rows.push( { label: 'FY24', val: FY24, color: TOTAL_COLOR, isTotal: true } );
var running = FY24;
ITEMS.forEach( function ( it ) {
var v = vals[it.key];
rows.push( { label: it.label, val: v, color: it.color, base: v >= 0 ? running : running + v } );
running += v;
} );
rows.push( { label: 'FY25', val: running, color: TOTAL_COLOR, isTotal: true } );
return rows;
}
/* ── Hit regions for click ───────────────────────────────── */
var hitBoxes = [];
/* ── Drawing ─────────────────────────────────────────────── */
function draw() {
var rows = getRows();
var numBars = rows.length;
var dpr = window.devicePixelRatio || 1;
var rect = chartWrap.getBoundingClientRect();
var W = rect.width;
if ( W === 0 ) return;
var BAR_H = 38;
var GAP = 10;
var totalH = numBars * ( BAR_H + GAP ) + 40;
chartWrap.style.height = totalH + 'px';
canvas.width = W * dpr;
canvas.height = totalH * dpr;
var ctx = canvas.getContext( '2d' );
ctx.scale( dpr, dpr );
ctx.clearRect( 0, 0, W, totalH );
/* Compute axis range */
var allVals = rows.map( function ( r ) {
if ( r.isTotal ) return r.val;
return r.base;
} );
allVals = allVals.concat( rows.map( function ( r ) {
if ( r.isTotal ) return r.val;
return r.base + Math.abs( r.val );
} ) );
var dataMax = Math.max.apply( null, allVals ) * 1.12;
var dataMin = 0;
var padL = 130, padR = 16, padT = 6, padB = 30;
var cw = W - padL - padR;
function tx( v ) { return padL + ( v - dataMin ) / ( dataMax - dataMin ) * cw; }
/* Grid lines */
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
var step = 5000;
if ( dataMax > 50000 ) step = 10000;
if ( dataMax < 15000 ) step = 2000;
for ( var g = 0; g <= dataMax; g += step ) {
var gx = tx( g );
ctx.strokeStyle = colorGrid;
ctx.lineWidth = 0.5;
ctx.beginPath(); ctx.moveTo( gx, padT ); ctx.lineTo( gx, totalH - padB ); ctx.stroke();
ctx.fillStyle = colorAxis;
ctx.fillText( fmt( g ), gx, totalH - padB + 16 );
}
/* Bars */
hitBoxes = [];
rows.forEach( function ( r, i ) {
var y = padT + i * ( BAR_H + GAP );
var barBase = r.isTotal ? 0 : r.base;
var barVal = r.isTotal ? r.val : Math.abs( r.val );
var x0 = tx( barBase );
var x1 = tx( barBase + barVal );
var bw = Math.max( x1 - x0, 2 );
/* Row label */
ctx.fillStyle = colorText;
ctx.font = '12px sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText( r.label, padL - 10, y + BAR_H / 2 );
/* Bar */
ctx.fillStyle = r.color;
ctx.beginPath();
ctx.roundRect( x0, y, bw, BAR_H, 3 );
ctx.fill();
/* Value label on bar */
var valText = fmt( r.isTotal ? r.val : r.val );
ctx.fillStyle = '#fff';
ctx.font = '500 11px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if ( bw > 60 ) {
ctx.fillText( valText, x0 + bw / 2, y + BAR_H / 2 );
} else {
ctx.fillStyle = r.color;
ctx.textAlign = 'left';
ctx.fillText( valText, x0 + bw + 4, y + BAR_H / 2 );
}
/* Connector line to next row */
if ( !r.isTotal && i < rows.length - 1 ) {
var endX = tx( barBase + ( r.val >= 0 ? Math.abs( r.val ) : 0 ) );
ctx.strokeStyle = colorGrid;
ctx.lineWidth = 1;
ctx.setLineDash( [ 3, 3 ] );
ctx.beginPath();
ctx.moveTo( endX, y + BAR_H );
ctx.lineTo( endX, y + BAR_H + GAP );
ctx.stroke();
ctx.setLineDash( [] );
}
/* Hit box */
hitBoxes.push( { x: x0, y: y, w: bw, h: BAR_H, idx: i } );
} );
}
/* ── Click → explainer ───────────────────────────────────── */
canvas.addEventListener( 'click', function ( e ) {
var rect = canvas.getBoundingClientRect();
var dpr = window.devicePixelRatio || 1;
var mx = ( e.clientX - rect.left );
var my = ( e.clientY - rect.top );
for ( var i = 0; i < hitBoxes.length; i++ ) {
var b = hitBoxes[i];
if ( mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h ) {
var idx = b.idx;
if ( idx === 0 || idx === ITEMS.length + 1 ) {
exDot.style.background = TOTAL_COLOR;
exTitle.textContent = idx === 0 ? 'FY24 opening balance' : 'FY25 closing balance';
exVal.textContent = idx === 0 ? fmt( FY24 ) : fmt( getRows()[ITEMS.length + 1].val );
exBody.textContent = TOTALS_EXPLAIN;
} else {
var item = ITEMS[idx - 1];
exDot.style.background = item.color;
exTitle.textContent = item.label;
exVal.textContent = fmt( vals[item.key] );
exBody.textContent = item.explain;
}
explainer.className = 'wix-csm-explainer';
hint.style.display = 'none';
return;
}
}
} );
/* ── Slider events ───────────────────────────────────────── */
ITEMS.forEach( function ( it ) {
sliderRefs[it.key].addEventListener( 'input', function () {
vals[it.key] = Number( sliderRefs[it.key].value );
valRefs[it.key].textContent = vals[it.key].toLocaleString();
draw();
} );
} );
/* ── Resize ──────────────────────────────────────────────── */
window.addEventListener( 'resize', draw );
draw();
}
/* ================================================================
BUILDING BLOCKS — data-wix-module="building-blocks"
Stacked-bar visualisation of IFRS 17 initial recognition.
Two sliders control outflows and risk adjustment; the chart
shows inflows vs outflows+RA+CSM and flips between profitable
and onerous states with a day-one loss bar.
================================================================ */
function initBuildingBlocks( container ) {
/* ── Constants ────────────────────────────────────────────── */
var INFLOWS = 3000000;
var MAX_BAR = 260;
var C_INFLOW = '#2b6cb0';
var C_OUTFLOW = '#c05621';
var C_RA = '#9b59b6';
var C_CSM = '#27ae60';
var C_LOSS = '#e74c3c';
/* ── Formatting ──────────────────────────────────────────── */
function fmt( v ) {
return '\u20AC' + Math.round( v ).toLocaleString();
}
function fmtShort( v ) {
if ( Math.abs( v ) >= 1000000 ) return '\u20AC' + ( v / 1000000 ).toFixed( 2 ) + 'M';
if ( Math.abs( v ) >= 1000 ) return '\u20AC' + ( v / 1000 ).toFixed( 0 ) + 'K';
return '\u20AC' + v;
}
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
/* Title */
wrapper.appendChild( wix.el( 'div', { className: 'wix-sim-title', textContent: 'Building Block Stacker' } ) );
/* Status banner */
var bannerIcon = wix.el( 'span' );
var bannerText = wix.el( 'span' );
var banner = wix.el( 'div', { className: 'wix-bb-banner wix-bb-banner--profit' }, [
bannerIcon, bannerText
] );
wrapper.appendChild( banner );
/* Chart area */
function makeSeg( color ) {
var valEl = wix.el( 'span', { className: 'wix-bb-seg-val' } );
var seg = wix.el( 'div', { className: 'wix-bb-seg', style: { background: color, height: '0px' } }, [ valEl ] );
return { seg: seg, valEl: valEl };
}
function makeGroup( labelHTML, stack, extra ) {
var label = wix.el( 'span', { className: 'wix-bb-bar-label', innerHTML: labelHTML } );
var grp = wix.el( 'div', { className: 'wix-bb-group' }, [ stack, label ] );
if ( extra ) Object.keys( extra ).forEach( function ( k ) { grp.style[k] = extra[k]; } );
return grp;
}
function makeOp( text, extra ) {
var op = wix.el( 'span', { className: 'wix-bb-op', textContent: text } );
if ( extra ) Object.keys( extra ).forEach( function ( k ) { op.style[k] = extra[k]; } );
return op;
}
/* Inflow bar */
var inflow = makeSeg( C_INFLOW );
var stackInflow = wix.el( 'div', { className: 'wix-bb-stack' }, [ inflow.seg ] );
/* Outflow + RA + CSM stacked bar */
var csmSeg = makeSeg( C_CSM );
var raSeg = makeSeg( C_RA );
var outflowSeg = makeSeg( C_OUTFLOW );
var stackRight = wix.el( 'div', { className: 'wix-bb-stack' }, [
csmSeg.seg, raSeg.seg, outflowSeg.seg
] );
/* Result bar */
var resultSeg = makeSeg( C_CSM );
var resultLabel = wix.el( 'span', { className: 'wix-bb-bar-label' } );
var stackResult = wix.el( 'div', { className: 'wix-bb-stack' }, [ resultSeg.seg ] );
/* Loss bar */
var lossSeg = makeSeg( C_LOSS );
var stackLoss = wix.el( 'div', { className: 'wix-bb-stack' }, [ lossSeg.seg ] );
var lossOp = makeOp( '+', { visibility: 'hidden' } );
var lossGroup = makeGroup( 'Day-one<br>loss', stackLoss, { visibility: 'hidden' } );
var chartRow = wix.el( 'div', { className: 'wix-bb-row' }, [
wix.el( 'span', { className: 'wix-bb-zero', textContent: '\u20AC0' } ),
makeGroup( 'Inflows<br>(Premiums)', stackInflow ),
makeOp( 'vs' ),
makeGroup( 'Outflows + RA<br>+ CSM', stackRight ),
makeOp( '=' ),
wix.el( 'div', { className: 'wix-bb-group' }, [ stackResult, resultLabel ] ),
lossOp,
lossGroup
] );
/* Legend */
function legendItem( color, text ) {
return wix.el( 'span', { className: 'wix-bb-legend-item' }, [
wix.el( 'span', { className: 'wix-bb-legend-swatch', style: { background: color } } ),
text
] );
}
var legend = wix.el( 'div', { className: 'wix-bb-legend' }, [
legendItem( C_INFLOW, 'PV of inflows' ),
legendItem( C_OUTFLOW, 'PV of outflows' ),
legendItem( C_RA, 'Risk adjustment' ),
legendItem( C_CSM, 'CSM' ),
legendItem( C_LOSS, 'Loss' )
] );
wrapper.appendChild( wix.el( 'div', { className: 'wix-bb-chart' }, [ chartRow, legend ] ) );
/* Slider controls */
var slOutflow = wix.el( 'input', { type: 'range', min: '1500000', max: '3800000', step: '10000', value: '2600000' } );
var svOutflow = wix.el( 'span', { className: 'wix-bb-slider-val' } );
var slRa = wix.el( 'input', { type: 'range', min: '0', max: '500000', step: '5000', value: '120000' } );
var svRa = wix.el( 'span', { className: 'wix-bb-slider-val' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-bb-controls' }, [
wix.el( 'div', { className: 'wix-bb-controls-title', textContent: 'Adjust the assumptions' } ),
wix.el( 'div', { className: 'wix-bb-slider-row' }, [
wix.el( 'span', { className: 'wix-bb-slider-label' }, [
wix.el( 'span', { className: 'wix-bb-slider-dot', style: { background: C_OUTFLOW } } ),
'PV of outflows'
] ),
slOutflow, svOutflow
] ),
wix.el( 'div', { className: 'wix-bb-slider-row' }, [
wix.el( 'span', { className: 'wix-bb-slider-label' }, [
wix.el( 'span', { className: 'wix-bb-slider-dot', style: { background: C_RA } } ),
'Risk adjustment'
] ),
slRa, svRa
] )
] ) );
/* Reset button */
var btnReset = wix.el( 'button', { className: 'wix-btn wix-btn--outline', textContent: '\u21BA Reset to Belgian example' } );
wrapper.appendChild( wix.el( 'div', { style: { marginBottom: '1.25rem' } }, [ btnReset ] ) );
/* Insight */
var insightEl = wix.el( 'div', { className: 'wix-bb-insight' } );
wrapper.appendChild( insightEl );
/* ── Update logic ────────────────────────────────────────── */
function update() {
var outflow = Number( slOutflow.value );
var ra = Number( slRa.value );
var netFC = outflow - INFLOWS;
var fulfilment = netFC + ra;
var csm = Math.max( 0, -fulfilment );
var loss = Math.max( 0, fulfilment );
var isOnerous = fulfilment > 0;
/* Slider readouts */
svOutflow.textContent = fmt( outflow );
svRa.textContent = fmt( ra );
/* Bar heights */
var leftTotal = INFLOWS;
var rightTotal = outflow + ra + csm;
var maxVal = Math.max( leftTotal, rightTotal, 1 );
inflow.seg.style.height = ( INFLOWS / maxVal * MAX_BAR ) + 'px';
inflow.valEl.textContent = fmtShort( INFLOWS );
outflowSeg.seg.style.height = ( outflow / maxVal * MAX_BAR ) + 'px';
outflowSeg.valEl.textContent = fmtShort( outflow );
raSeg.seg.style.height = ( ra / maxVal * MAX_BAR ) + 'px';
raSeg.valEl.textContent = fmtShort( ra );
var hCsm = ( csm / maxVal * MAX_BAR );
csmSeg.seg.style.height = hCsm + 'px';
csmSeg.valEl.textContent = csm > 0 ? fmtShort( csm ) : '';
csmSeg.seg.style.display = csm > 0 ? 'block' : 'none';
/* Result bar */
resultSeg.seg.style.height = '28px';
resultSeg.valEl.textContent = '\u20AC0';
if ( !isOnerous ) {
resultSeg.seg.style.background = C_CSM;
resultLabel.innerHTML = 'No day-one<br>profit';
} else {
resultSeg.seg.style.background = C_LOSS;
resultLabel.innerHTML = 'CSM<br>= \u20AC0';
}
/* Loss bar */
if ( isOnerous ) {
lossGroup.style.visibility = 'visible';
lossOp.style.visibility = 'visible';
lossSeg.seg.style.height = Math.max( loss / maxVal * MAX_BAR, 18 ) + 'px';
lossSeg.valEl.textContent = fmtShort( loss );
} else {
lossGroup.style.visibility = 'hidden';
lossOp.style.visibility = 'hidden';
lossSeg.seg.style.height = '0px';
}
/* Banner */
if ( !isOnerous ) {
banner.className = 'wix-bb-banner wix-bb-banner--profit';
bannerIcon.textContent = '\u2705';
bannerText.innerHTML = 'Profitable group \u2014 CSM of <strong>' + fmt( csm ) + '</strong> absorbs the expected gain.';
} else {
banner.className = 'wix-bb-banner wix-bb-banner--onerous';
bannerIcon.textContent = '\uD83D\uDEA8';
bannerText.innerHTML = 'Onerous group \u2014 CSM is zero. Day-one loss of <strong>' + fmt( loss ) + '</strong> hits the income statement.';
}
/* Insight */
if ( !isOnerous ) {
insightEl.innerHTML =
'<strong>How it works:</strong> The present value of future cash flows (outflows minus inflows) is <strong>' + fmt( netFC ) + '</strong>. ' +
'Adding the risk adjustment of <strong>' + fmt( ra ) + '</strong> gives fulfilment cash flows of <strong>' + fmt( fulfilment ) + '</strong>. ' +
'Since this is negative (inflows exceed outflows + RA), the group is profitable. ' +
'The <span class="wix-bb-hl-csm">CSM is set to ' + fmt( csm ) + '</span> to absorb this gain, so no profit appears on day one. ' +
'That unearned profit will be released gradually as coverage is provided.';
} else {
insightEl.innerHTML =
'<strong>How it works:</strong> The present value of future cash flows (outflows minus inflows) is <strong>' + fmt( netFC ) + '</strong>. ' +
'Adding the risk adjustment of <strong>' + fmt( ra ) + '</strong> gives fulfilment cash flows of <strong>' + fmt( fulfilment ) + '</strong>. ' +
'Since this is positive (outflows + RA exceed inflows), there is no profit to store. ' +
'The CSM cannot go negative, so it is <span class="wix-bb-hl-loss">floored at zero</span>. ' +
'The <span class="wix-bb-hl-loss">' + fmt( loss ) + ' shortfall is recognised as a loss</span> in the income statement immediately \u2014 the principle of prudence demands early warning.';
}
}
/* ── Events ──────────────────────────────────────────────── */
slOutflow.addEventListener( 'input', update );
slRa.addEventListener( 'input', update );
btnReset.addEventListener( 'click', function () {
slOutflow.value = '2600000';
slRa.value = '120000';
update();
} );
update();
}
/* ================================================================
IMPACT SORT — data-wix-module="impact-sort"
Binary classification exercise: does the scenario adjust the CSM
or hit P&L directly? Six shuffled cards, score tracking, and a
final results screen.
================================================================ */
function initImpactSort( container ) {
/* ── Scenario data ───────────────────────────────────────── */
var SCENARIOS = [
{ icon: '\uD83D\uDC94',
text: 'AXA France whole-of-life portfolio: a new longevity study shows policyholders are living <strong>2\u00A0years longer</strong> than assumed \u2014 expected death claims over the remaining coverage period fall by \u20AC12M.',
answer: 'csm',
correctHead: '\u2705 Correct \u2014 adjusts the CSM.',
correctBody: 'This change relates to mortality expected over the <em>remaining</em> coverage period \u2014 service the insurer has <strong>not yet delivered</strong>. Because it concerns future service, the \u20AC12M favourable change increases the CSM, and will be released as profit gradually as coverage continues.',
wrongHead: '\u2717 Not quite \u2014 this one adjusts the CSM.',
wrongBody: 'Ask the key question: has the service already been provided? No \u2014 these are death claims expected <em>in the future</em>, over the remaining life of the policies. Because it concerns future service, the \u20AC12M favourable change increases the CSM rather than appearing in P&L immediately.' },
{ icon: '\uD83D\uDCB0',
text: 'AXA Belgium unit-linked savings contracts: revised data shows policyholder lapse rates will <strong>rise by 15%</strong> over the remaining contract term, reducing expected future fee income by \u20AC8M.',
answer: 'csm',
correctHead: '\u2705 Correct \u2014 adjusts the CSM.',
correctBody: 'Unfavourable changes about future service are absorbed by the CSM, just like favourable ones. The CSM decreases by \u20AC8M because fewer policyholders will stay to generate fees. The CSM acts as a buffer in both directions \u2014 as long as it stays at or above zero.',
wrongHead: '\u2717 Not quite \u2014 this one adjusts the CSM.',
wrongBody: 'The change relates to lapses that have <em>not yet happened</em> \u2014 it\u2019s about future service. The fees lost are fees the insurer has not yet earned. Unfavourable changes about the future are absorbed by the CSM (decreasing it by \u20AC8M). The key: the service hasn\u2019t been provided yet.' },
{ icon: '\uD83C\uDFE5',
text: 'AXA Germany group disability portfolio: a large employer reports a <strong>cluster of 40 new disability claims</strong> during the current reporting quarter, all arising from a factory closure this year.',
answer: 'pl',
correctHead: '\u2705 Correct \u2014 hits P&L directly.',
correctBody: 'The insurer was providing disability coverage when these claims were incurred \u2014 the service has <strong>already been delivered</strong> for this period. Since this relates to current service, the full amount goes straight to insurance service expenses in the income statement. The CSM cannot absorb it.',
wrongHead: '\u2717 Not quite \u2014 this one hits P&L directly.',
wrongBody: 'Ask the key question: has the service already been provided? Yes \u2014 the disabilities occurred during the current quarter while the insurer was on risk. This is current-period service, so the cost goes straight to insurance service expenses. The CSM only absorbs changes about <em>future</em> service.' },
{ icon: '\uD83D\uDCC5',
text: 'AXA Switzerland individual annuity book: a <strong>prior-year reserve for an annuitant who died in 2023</strong> is re-estimated upward because final medical and estate costs were higher than provisioned.',
answer: 'pl',
correctHead: '\u2705 Correct \u2014 hits P&L directly.',
correctBody: 'This relates to <strong>past service</strong> \u2014 the annuitant died in a prior period and the claim event is long settled. Any re-estimate of an already-incurred claim bypasses the CSM entirely and flows directly into insurance service expenses. The CSM only represents profit on service <em>still to come</em>.',
wrongHead: '\u2717 Not quite \u2014 this one hits P&L directly.',
wrongBody: 'The annuitant died in 2023 \u2014 the service has long since been provided. Re-estimates of already-incurred claims relate to past service and go straight to the income statement. Remember: the CSM represents profit on service <em>still to come</em>, not service already delivered.' },
{ icon: '\uD83E\uDE7A',
text: 'AXA Japan critical illness portfolio: following a screening campaign, the insurer estimates <strong>200 cancer diagnoses occurred during H1</strong> but won\u2019t be reported until H2. The full IBNR provision is booked.',
answer: 'pl',
correctHead: '\u2705 Correct \u2014 hits P&L directly.',
correctBody: 'The diagnoses are <strong>incurred</strong> in H1, even if not yet reported. Once incurred, the liability relates to current service \u2014 the insurer was providing critical illness cover when the diseases arose. Any estimates of these claims go straight to the income statement. IBNR doesn\u2019t change this: incurred means the service has been provided.',
wrongHead: '\u2717 Not quite \u2014 this one hits P&L directly.',
wrongBody: 'The cancers were diagnosed during H1 \u2014 the insurer was on risk when they occurred. Even though the claims are IBNR (incurred but not yet reported), they are still <em>incurred</em>, meaning the service has been provided. All incurred claim estimates bypass the CSM and go to P&L.' },
{ icon: '\uD83D\uDC94',
text: 'AXA France whole-of-life portfolio: a pandemic scenario causes expected mortality claims to <strong>rise so sharply that the CSM would go below zero</strong>. The change relates entirely to future service.',
answer: 'both',
correctHead: '\u2705 Correct \u2014 but there\u2019s a twist.',
wrongHead: '\u2717 It\u2019s actually a trick question \u2014 both answers are partially right.',
bothBody: 'This change relates to future service, so it <em>starts</em> by adjusting the CSM. But the CSM cannot go negative. The portion that brings the CSM to zero is absorbed normally; the <strong>excess</strong> spills over into P&L as an immediate loss. The group becomes onerous and a loss component is established. This is the one scenario where the answer is \u201Cboth\u201D \u2014 CSM first, then P&L for the overflow.' }
];
/* ── State ───────────────────────────────────────────────── */
var order = shuffle( makeRange( SCENARIOS.length ) );
var current = 0;
var correct = 0;
var wrong = 0;
var answered = false;
function makeRange( n ) { var a = []; for ( var i = 0; i < n; i++ ) a.push( i ); return a; }
function shuffle( arr ) {
for ( var i = arr.length - 1; i > 0; i-- ) {
var j = Math.floor( Math.random() * ( i + 1 ) );
var t = arr[i]; arr[i] = arr[j]; arr[j] = t;
}
return arr;
}
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
/* Title */
wrapper.appendChild( wix.el( 'div', { className: 'wix-sim-title', textContent: 'Where Does the Impact Land?' } ) );
/* Decision rule */
wrapper.appendChild( wix.el( 'div', { className: 'wix-is-rule', innerHTML: '<strong>The key question:</strong> \u201CHas the service already been provided?\u201D If yes \u2192 P&L. If no \u2192 CSM.' } ) );
/* Progress bar */
var progFill = wix.el( 'div', { className: 'wix-progress-fill', style: { width: '0%' } } );
var progText = wix.el( 'span', { className: 'wix-is-badge', style: { border: 'none', padding: '0', background: 'none', color: '' } } );
var progBar = wix.el( 'div', { className: 'wix-progress-wrap', style: { marginBottom: '1rem' } }, [
wix.el( 'div', { className: 'wix-progress-track' }, [ progFill ] ),
progText
] );
wrapper.appendChild( progBar );
/* Score badges */
var badgeCorrect = wix.el( 'span', { className: 'wix-is-badge wix-is-badge--correct' } );
var badgeWrong = wix.el( 'span', { className: 'wix-is-badge wix-is-badge--wrong' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-is-score-row' }, [ badgeCorrect, badgeWrong ] ) );
/* Card stage */
var cardStage = wix.el( 'div' );
wrapper.appendChild( cardStage );
/* Choice buttons */
var btnCsm = wix.el( 'button', { className: 'wix-is-choice wix-is-choice--csm' }, [
wix.el( 'span', { className: 'wix-is-choice-icon', textContent: '\uD83D\uDD04' } ),
wix.el( 'span', { className: 'wix-is-choice-title', textContent: 'Adjusts the CSM' } ),
wix.el( 'span', { className: 'wix-is-choice-sub', textContent: 'Future service \u2014 profit recognised later' } )
] );
var btnPl = wix.el( 'button', { className: 'wix-is-choice wix-is-choice--pl' }, [
wix.el( 'span', { className: 'wix-is-choice-icon', textContent: '\uD83D\uDCC9' } ),
wix.el( 'span', { className: 'wix-is-choice-title', textContent: 'Hits P&L directly' } ),
wix.el( 'span', { className: 'wix-is-choice-sub', textContent: 'Current or past service \u2014 recognised now' } )
] );
var choicesEl = wix.el( 'div', { className: 'wix-is-choices' }, [ btnCsm, btnPl ] );
wrapper.appendChild( choicesEl );
/* Feedback */
var fbHead = wix.el( 'div', { className: 'wix-is-fb-head' } );
var fbBody = wix.el( 'div' );
var btnNext = wix.el( 'button', { className: 'wix-btn', textContent: 'Next scenario \u25B8', style: { marginTop: '0.6rem' } } );
var feedbackEl = wix.el( 'div', { className: 'wix-is-feedback wix-hidden' }, [ fbHead, fbBody, btnNext ] );
wrapper.appendChild( feedbackEl );
/* Final screen */
var finalIcon = wix.el( 'div', { className: 'wix-is-final-icon' } );
var finalTitle = wix.el( 'div', { className: 'wix-is-final-title' } );
var finalBody = wix.el( 'div', { className: 'wix-is-final-body' } );
var btnRestart = wix.el( 'button', { className: 'wix-btn wix-btn--outline', textContent: '\u21BA Try again' } );
var finalEl = wix.el( 'div', { className: 'wix-is-final wix-hidden' }, [ finalIcon, finalTitle, finalBody, btnRestart ] );
wrapper.appendChild( finalEl );
/* ── Render helpers ──────────────────────────────────────── */
function updateScores() {
badgeCorrect.textContent = '\u2713 ' + correct + ' correct';
badgeWrong.textContent = '\u2717 ' + wrong + ' wrong';
}
function renderCard() {
feedbackEl.className = 'wix-is-feedback wix-hidden';
answered = false;
if ( current >= order.length ) {
showFinal();
return;
}
var s = SCENARIOS[order[current]];
wix.empty( cardStage );
var card = wix.el( 'div', { className: 'wix-is-card' }, [
wix.el( 'div', { className: 'wix-is-card-num', textContent: 'Scenario ' + ( current + 1 ) + ' of ' + order.length } ),
wix.el( 'span', { className: 'wix-is-card-icon', textContent: s.icon } ),
wix.el( 'div', { className: 'wix-is-card-text', innerHTML: s.text } )
] );
cardStage.appendChild( card );
progFill.style.width = ( current / order.length * 100 ) + '%';
progText.textContent = current + ' / ' + order.length;
btnCsm.disabled = false;
btnPl.disabled = false;
choicesEl.className = 'wix-is-choices';
updateScores();
}
function handleAnswer( chosen ) {
if ( answered ) return;
answered = true;
var s = SCENARIOS[order[current]];
var isCorrect = s.answer === 'both' ? true : chosen === s.answer;
if ( isCorrect ) correct++; else wrong++;
updateScores();
/* Card border */
var card = cardStage.firstChild;
card.className = 'wix-is-card ' + ( isCorrect ? 'wix-is-card--correct' : 'wix-is-card--wrong' );
/* Feedback */
if ( s.answer === 'both' ) {
feedbackEl.className = 'wix-is-feedback wix-is-feedback--correct';
fbHead.textContent = chosen === 'csm' ? s.correctHead : s.wrongHead;
fbBody.innerHTML = s.bothBody;
} else if ( isCorrect ) {
feedbackEl.className = 'wix-is-feedback wix-is-feedback--correct';
fbHead.textContent = s.correctHead;
fbBody.innerHTML = s.correctBody;
} else {
feedbackEl.className = 'wix-is-feedback wix-is-feedback--wrong';
fbHead.textContent = s.wrongHead;
fbBody.innerHTML = s.wrongBody;
}
btnCsm.disabled = true;
btnPl.disabled = true;
btnNext.textContent = current >= order.length - 1 ? 'See results \u25B8' : 'Next scenario \u25B8';
}
function showFinal() {
wix.empty( cardStage );
choicesEl.className = 'wix-is-choices wix-hidden';
feedbackEl.className = 'wix-is-feedback wix-hidden';
progFill.style.width = '100%';
progText.textContent = order.length + ' / ' + order.length;
var pct = Math.round( correct / order.length * 100 );
finalIcon.textContent = pct === 100 ? '\uD83C\uDFC6' : pct >= 67 ? '\uD83D\uDC4F' : '\uD83D\uDCDA';
finalTitle.textContent = pct === 100 ? 'Perfect score!' : pct >= 67 ? 'Well done!' : 'Keep practising!';
finalBody.innerHTML = 'You got <strong>' + correct + ' out of ' + order.length + '</strong> correct (' + pct + '%).<br><br>' +
( pct === 100
? 'You\u2019ve nailed the future vs.\u00A0current/past service distinction \u2014 the key question IFRS\u00A017 asks at every re-estimate.'
: 'Remember the key question: <strong>\u201CHas the service already been provided?\u201D</strong> If yes, the change bypasses the CSM and goes straight to P&L. If no, it adjusts the CSM.' );
finalEl.className = 'wix-is-final';
}
function restart() {
order = shuffle( makeRange( SCENARIOS.length ) );
current = 0;
correct = 0;
wrong = 0;
finalEl.className = 'wix-is-final wix-hidden';
choicesEl.className = 'wix-is-choices';
renderCard();
}
/* ── Events ──────────────────────────────────────────────── */
btnCsm.addEventListener( 'click', function () { handleAnswer( 'csm' ); } );
btnPl.addEventListener( 'click', function () { handleAnswer( 'pl' ); } );
btnNext.addEventListener( 'click', function () { current++; renderCard(); } );
btnRestart.addEventListener( 'click', restart );
renderCard();
}
/* ================================================================
INCOME BUILDER — data-wix-module="income-builder"
Sort 9 line items into the correct section of the IFRS 17
income statement: Revenue, Service Expenses, or Finance.
Items accumulate as chips in the statement skeleton.
================================================================ */
function initIncomeBuilder( container ) {
/* ── Item data ───────────────────────────────────────────── */
var ITEMS = [
{ id: 'csm-release', icon: '\uD83C\uDF81', label: 'CSM release',
desc: 'Share of unearned profit recognised as coverage is provided',
zone: 'revenue',
okHead: '\u2705 Correct \u2014 Insurance Revenue.',
okBody: 'The CSM release represents profit earned by delivering coverage during the period. It is one of the three components that build insurance revenue \u2014 alongside expected claims costs and the RA release.',
noHead: '\u2717 This belongs in Insurance Revenue.',
noBody: 'The CSM release is the share of unearned profit recognised as the insurer delivers coverage. Revenue under IFRS 17 is built from three releases: expected claims costs, the RA release, and the CSM release. All three go to revenue.' },
{ id: 'ra-release', icon: '\uD83D\uDEE1\uFE0F', label: 'Risk adjustment release',
desc: 'Reduction in RA as risk is borne over time',
zone: 'revenue',
okHead: '\u2705 Correct \u2014 Insurance Revenue.',
okBody: 'Bearing risk is part of the service the insurer provides. As each period passes and uncertainty diminishes, the released portion of the RA flows into insurance revenue \u2014 it represents compensation for risk the insurer has now borne.',
noHead: '\u2717 This belongs in Insurance Revenue.',
noBody: 'The RA release reflects risk the insurer has already borne \u2014 that\u2019s a service delivered. Under IFRS 17, it is one of the three components of insurance revenue, not an expense or a finance item.' },
{ id: 'claims-alloc', icon: '\uD83D\uDCCB', label: 'Expected claims & expenses allocated to period',
desc: 'Portion of estimated claims/expenses matching coverage provided (e.g.\u00A0the Lyon home contract at 6\u00A0months)',
zone: 'revenue',
okHead: '\u2705 Correct \u2014 Insurance Revenue.',
okBody: 'This is often the most surprising one. Under IFRS 17, the portion of expected claims cost allocated to the period is a component of revenue, not an expense. Think of the Lyon home contract: at six months, half the expected cost is released into revenue. The logic is that revenue measures the <em>value of service delivered</em>, and the expected claims cost is part of that value. Actual claims incurred go to expenses.',
noHead: '\u2717 This actually belongs in Insurance Revenue.',
noBody: 'Under IFRS 17, revenue is not premiums \u2014 it\u2019s the value of coverage delivered. That value includes the expected claims cost allocated to the period (like the Lyon home contract at 6\u00A0months), plus the RA release and CSM release. The <em>expected</em> cost is a revenue component; <em>actual</em> incurred claims go to expenses.' },
{ id: 'claims-incurred', icon: '\u26C8\uFE0F', label: 'Claims incurred',
desc: 'Actual claim costs from events that occurred (e.g.\u00A0hailstorm damaging 200 cars in Bavaria)',
zone: 'expenses',
okHead: '\u2705 Correct \u2014 Insurance Service Expenses.',
okBody: 'When the Bavaria hailstorm damages 200 cars, the cost of those actual claims is an insurance service expense. This is the cost of events that have happened \u2014 current or past service \u2014 hitting the income statement directly.',
noHead: '\u2717 This belongs in Insurance Service Expenses.',
noBody: 'Claims incurred, like the Bavaria hailstorm damaging 200 cars, are the actual costs of insured events. These go to insurance service expenses \u2014 they\u2019re the cost side of underwriting, paired against revenue to produce the insurance service result.' },
{ id: 'est-change-past', icon: '\uD83D\uDD27', label: 'Changes in estimates for current/past service',
desc: 'Re-estimates of claims already incurred or coverage already provided',
zone: 'expenses',
okHead: '\u2705 Correct \u2014 Insurance Service Expenses.',
okBody: 'When estimates change for service already delivered \u2014 like revised repair costs on claims already incurred \u2014 the adjustment bypasses the CSM and goes straight to insurance service expenses. Only changes about <em>future</em> service adjust the CSM.',
noHead: '\u2717 This belongs in Insurance Service Expenses.',
noBody: 'Changes in estimates for current or past service relate to coverage already provided. They cannot go through the CSM (which is about future profit) and they aren\u2019t a finance effect. They go directly to insurance service expenses in the income statement.' },
{ id: 'onerous-loss', icon: '\uD83D\uDEA8', label: 'Losses on onerous groups',
desc: 'Initial losses at recognition and subsequent deterioration of onerous groups',
zone: 'expenses',
okHead: '\u2705 Correct \u2014 Insurance Service Expenses.',
okBody: 'Losses on onerous contracts \u2014 whether recognised at day one or from subsequent deterioration \u2014 are part of insurance service expenses. They reflect the cost of underwriting contracts that are expected to be unprofitable, which is fundamentally a service-related cost.',
noHead: '\u2717 This belongs in Insurance Service Expenses.',
noBody: 'Onerous group losses are an underwriting cost, not a finance effect. Whether it\u2019s a day-one loss or subsequent deterioration, these sit within insurance service expenses \u2014 they tell you about pricing and claims management, not market conditions.' },
{ id: 'acq-costs', icon: '\uD83E\uDD1D', label: 'Acquisition costs amortised',
desc: 'Commissions to brokers (e.g.\u00A0in Belgium) and distribution costs (e.g.\u00A0in Spain), spread over the period',
zone: 'expenses',
okHead: '\u2705 Correct \u2014 Insurance Service Expenses.',
okBody: 'Under IFRS 17, acquisition costs \u2014 like commissions paid to Belgian brokers or Spanish distribution costs \u2014 are amortised and included within insurance service expenses. They\u2019re not shown as a separate deduction; this keeps the insurance service result self-contained.',
noHead: '\u2717 This belongs in Insurance Service Expenses.',
noBody: 'Acquisition costs (Belgian broker commissions, Spanish distribution costs) are amortised within insurance service expenses under IFRS 17 \u2014 not shown separately. The insurance service result captures the <em>full</em> cost of acquiring, servicing, and settling contracts.' },
{ id: 'discount-unwind', icon: '\u23F3', label: 'Unwinding of the discount',
desc: 'Liability grows as future payments get closer in time (accretion of interest)',
zone: 'finance',
okHead: '\u2705 Correct \u2014 Insurance Finance Income / Expense.',
okBody: 'The discount unwind is a financing effect, not an underwriting cost. As future claim payments draw closer, the present value of the liability rises \u2014 this accretion of interest belongs in insurance finance income or expense, keeping the service result clean.',
noHead: '\u2717 This belongs in Insurance Finance Income / Expense.',
noBody: 'A common mistake! The discount unwind feels like a cost of doing business, but IFRS 17 treats it as a financing effect. It goes to insurance finance income or expense \u2014 <em>not</em> to the service result. This separation is deliberate: it keeps underwriting profitability free from time-value-of-money effects.' },
{ id: 'rate-change', icon: '\uD83D\uDCC9', label: 'Effect of discount rate changes',
desc: 'Gains or losses when market interest rates move between reporting dates (e.g.\u00A0rates drop for AXA Germany long-tail liability contracts)',
zone: 'finance',
okHead: '\u2705 Correct \u2014 Insurance Finance Income / Expense.',
okBody: 'When interest rates drop \u2014 as in the AXA Germany long-tail liability example \u2014 the present value of future claims increases, creating a financial expense. This has nothing to do with underwriting quality, so it sits in insurance finance income or expense. The insurer can also choose the OCI option to keep rate volatility out of reported profit entirely.',
noHead: '\u2717 This belongs in Insurance Finance Income / Expense.',
noBody: 'Discount rate changes (like the AXA Germany long-tail scenario where rates drop sharply) are a market-driven effect, not underwriting performance. IFRS 17 places them in insurance finance income or expense to keep the service result undistorted. The insurer may also elect the OCI option to smooth this further.' }
];
/* ── State ───────────────────────────────────────────────── */
var order = shuffle( makeRange( ITEMS.length ) );
var cur = 0, ok = 0, no = 0, answered = false;
var placed = { revenue: [], expenses: [], finance: [] };
function makeRange( n ) { var a = []; for ( var i = 0; i < n; i++ ) a.push( i ); return a; }
function shuffle( arr ) {
for ( var i = arr.length - 1; i > 0; i-- ) {
var j = Math.floor( Math.random() * ( i + 1 ) );
var t = arr[i]; arr[i] = arr[j]; arr[j] = t;
}
return arr;
}
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
/* Title */
wrapper.appendChild( wix.el( 'div', { className: 'wix-sim-title', textContent: 'Build the Income Statement' } ) );
/* Progress */
var progFill = wix.el( 'div', { className: 'wix-progress-fill', style: { width: '0%' } } );
var progText = wix.el( 'span', { style: { fontSize: '0.78em', fontWeight: '600', color: 'var(--wix-text-muted)', whiteSpace: 'nowrap' } } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-progress-wrap', style: { marginBottom: '1rem' } }, [
wix.el( 'div', { className: 'wix-progress-track' }, [ progFill ] ),
progText
] ) );
/* Score badges */
var badgeOk = wix.el( 'span', { className: 'wix-is-badge wix-is-badge--correct' } );
var badgeNo = wix.el( 'span', { className: 'wix-is-badge wix-is-badge--wrong' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-is-score-row' }, [ badgeOk, badgeNo ] ) );
/* Tile stage */
var tileStage = wix.el( 'div', { style: { minHeight: '60px', marginBottom: '1rem' } } );
wrapper.appendChild( tileStage );
/* Choice buttons */
var btnRev = wix.el( 'button', { className: 'wix-ib-choice wix-ib-choice--rev', innerHTML: 'Insurance<br>Revenue' } );
var btnExp = wix.el( 'button', { className: 'wix-ib-choice wix-ib-choice--exp', innerHTML: 'Service<br>Expenses' } );
var btnFin = wix.el( 'button', { className: 'wix-ib-choice wix-ib-choice--fin', innerHTML: 'Finance<br>Inc/Exp' } );
var choicesEl = wix.el( 'div', { className: 'wix-ib-choices' }, [ btnRev, btnExp, btnFin ] );
wrapper.appendChild( choicesEl );
/* Income statement skeleton */
function makeSection( dotColor, titleColor, titleText, zoneKey ) {
var countEl = wix.el( 'span', { className: 'wix-ib-sec-count', textContent: '0 items' } );
var dropEl = wix.el( 'div', { className: 'wix-ib-drop' }, [
wix.el( 'span', { className: 'wix-ib-drop-ph', textContent: 'Items will appear here' } )
] );
var section = wix.el( 'div', { className: 'wix-ib-section' }, [
wix.el( 'div', { className: 'wix-ib-sec-head' }, [
wix.el( 'span', { className: 'wix-ib-sec-dot', style: { background: dotColor } } ),
wix.el( 'span', { className: 'wix-ib-sec-title', style: { color: titleColor }, textContent: titleText } ),
countEl
] ),
dropEl
] );
return { section: section, dropEl: dropEl, countEl: countEl };
}
var revSec = makeSection( 'var(--ib-rev)', 'var(--ib-rev)', 'Insurance Revenue', 'revenue' );
var expSec = makeSection( 'var(--ib-exp)', 'var(--ib-exp)', 'Insurance Service Expenses', 'expenses' );
var finSec = makeSection( 'var(--ib-fin)', 'var(--ib-fin)', 'Insurance Finance Income / Expense', 'finance' );
var resultRow = wix.el( 'div', { className: 'wix-ib-result' }, [
wix.el( 'span', { className: 'wix-ib-result-label', textContent: '= Insurance Service Result' } ),
wix.el( 'span', { className: 'wix-ib-result-eq', textContent: 'Revenue \u2212 Expenses' } )
] );
wrapper.appendChild( wix.el( 'div', { className: 'wix-ib-stmt' }, [
revSec.section, expSec.section, resultRow, finSec.section
] ) );
/* Feedback */
var fbHead = wix.el( 'div', { className: 'wix-is-fb-head' } );
var fbBody = wix.el( 'div' );
var btnNext = wix.el( 'button', { className: 'wix-btn', textContent: 'Next item \u25B8', style: { marginTop: '0.5rem' } } );
var feedbackEl = wix.el( 'div', { className: 'wix-is-feedback wix-hidden' }, [ fbHead, fbBody, btnNext ] );
wrapper.appendChild( feedbackEl );
/* Final screen */
var finalIcon = wix.el( 'div', { className: 'wix-is-final-icon' } );
var finalTitle = wix.el( 'div', { className: 'wix-is-final-title' } );
var finalBody = wix.el( 'div', { className: 'wix-is-final-body' } );
var btnRestart = wix.el( 'button', { className: 'wix-btn wix-btn--outline', textContent: '\u21BA Try again' } );
var finalEl = wix.el( 'div', { className: 'wix-is-final wix-hidden' }, [ finalIcon, finalTitle, finalBody, btnRestart ] );
wrapper.appendChild( finalEl );
/* ── Helpers ─────────────────────────────────────────────── */
var ZONE_MAP = {
revenue: { sec: revSec, chipCls: 'wix-ib-chip--rev' },
expenses: { sec: expSec, chipCls: 'wix-ib-chip--exp' },
finance: { sec: finSec, chipCls: 'wix-ib-chip--fin' }
};
function updateScores() {
badgeOk.textContent = '\u2713 ' + ok;
badgeNo.textContent = '\u2717 ' + no;
}
function updateCounts() {
revSec.countEl.textContent = placed.revenue.length + ' item' + ( placed.revenue.length !== 1 ? 's' : '' );
expSec.countEl.textContent = placed.expenses.length + ' item' + ( placed.expenses.length !== 1 ? 's' : '' );
finSec.countEl.textContent = placed.finance.length + ' item' + ( placed.finance.length !== 1 ? 's' : '' );
}
function toggleBtns( on ) {
btnRev.disabled = !on;
btnExp.disabled = !on;
btnFin.disabled = !on;
}
/* ── Render ──────────────────────────────────────────────── */
function renderTile() {
feedbackEl.className = 'wix-is-feedback wix-hidden';
answered = false;
if ( cur >= order.length ) { showFinal(); return; }
var it = ITEMS[order[cur]];
wix.empty( tileStage );
tileStage.appendChild( wix.el( 'div', { className: 'wix-ib-tile' }, [
wix.el( 'span', { textContent: it.icon } ),
wix.el( 'span', { textContent: it.label } )
] ) );
tileStage.appendChild( wix.el( 'div', { className: 'wix-ib-tile-desc', textContent: it.desc } ) );
progFill.style.width = ( cur / order.length * 100 ) + '%';
progText.textContent = cur + ' / ' + order.length;
toggleBtns( true );
choicesEl.className = 'wix-ib-choices';
updateScores();
}
function handleAnswer( chosen ) {
if ( answered ) return;
answered = true;
toggleBtns( false );
var it = ITEMS[order[cur]];
var isCorrect = chosen === it.zone;
if ( isCorrect ) ok++; else no++;
updateScores();
/* Place chip in correct zone (always into the right section) */
var target = ZONE_MAP[it.zone];
var ph = target.sec.dropEl.querySelector( '.wix-ib-drop-ph' );
if ( ph ) ph.parentNode.removeChild( ph );
var chip = wix.el( 'span', { className: 'wix-ib-chip ' + target.chipCls, textContent: it.icon + ' ' + it.label } );
target.sec.dropEl.appendChild( chip );
placed[it.zone].push( it.id );
updateCounts();
/* Hide tile */
wix.empty( tileStage );
/* Feedback */
if ( isCorrect ) {
feedbackEl.className = 'wix-is-feedback wix-is-feedback--correct';
fbHead.textContent = it.okHead;
fbBody.innerHTML = it.okBody;
} else {
feedbackEl.className = 'wix-is-feedback wix-is-feedback--wrong';
fbHead.textContent = it.noHead;
fbBody.innerHTML = it.noBody;
}
btnNext.textContent = cur >= order.length - 1 ? 'See results \u25B8' : 'Next item \u25B8';
}
function showFinal() {
wix.empty( tileStage );
choicesEl.className = 'wix-ib-choices wix-hidden';
feedbackEl.className = 'wix-is-feedback wix-hidden';
progFill.style.width = '100%';
progText.textContent = order.length + ' / ' + order.length;
var pct = Math.round( ok / order.length * 100 );
finalIcon.textContent = pct === 100 ? '\uD83C\uDFC6' : pct >= 67 ? '\uD83D\uDC4F' : '\uD83D\uDCDA';
finalTitle.textContent = pct === 100 ? 'Perfect \u2014 you built it!' : pct >= 67 ? 'Almost there!' : 'Keep practising!';
finalBody.innerHTML = 'You placed <strong>' + ok + ' of ' + order.length + '</strong> items correctly (' + pct + '%).<br><br>' +
'The completed income statement above is now your reference. Notice how the <strong>Insurance Service Result</strong> sits between revenue and expenses \u2014 purely underwriting \u2014 while finance effects live below, keeping the two stories separate.';
finalEl.className = 'wix-is-final';
}
function restart() {
order = shuffle( makeRange( ITEMS.length ) );
cur = 0; ok = 0; no = 0; answered = false;
placed.revenue = []; placed.expenses = []; placed.finance = [];
/* Clear chips, restore placeholders */
[ revSec, expSec, finSec ].forEach( function ( s ) {
wix.empty( s.dropEl );
s.dropEl.appendChild( wix.el( 'span', { className: 'wix-ib-drop-ph', textContent: 'Items will appear here' } ) );
} );
updateCounts();
finalEl.className = 'wix-is-final wix-hidden';
choicesEl.className = 'wix-ib-choices';
renderTile();
}
/* ── Events ──────────────────────────────────────────────── */
btnRev.addEventListener( 'click', function () { handleAnswer( 'revenue' ); } );
btnExp.addEventListener( 'click', function () { handleAnswer( 'expenses' ); } );
btnFin.addEventListener( 'click', function () { handleAnswer( 'finance' ); } );
btnNext.addEventListener( 'click', function () { cur++; renderTile(); } );
btnRestart.addEventListener( 'click', restart );
renderTile();
}
/* ================================================================
GM vs PAA / VFA STEPPER — data-wix-module="gm-paa"
Side-by-side step-through. Reads optional data-wix-config JSON
to switch between PAA mode (kept/skipped) and VFA mode
(same/modified). Falls back to PAA defaults when no config is
present.
================================================================ */
function initGmPaa( container ) {
/* ── Read config (or use PAA defaults) ───────────────────── */
var raw = container.getAttribute( 'data-wix-config' );
var cfg;
try { cfg = raw ? JSON.parse( raw ) : {}; } catch ( e ) { cfg = {}; }
var mode = cfg.mode || 'paa'; // 'paa' | 'vfa'
/* ── Default PAA steps ───────────────────────────────────── */
var PAA_STEPS = [
{ label: 'Collect premiums and deduct acquisition costs', type: 'same',
explain: 'Both paths start identically. The insurer collects premiums from the German motor group and deducts acquisition costs paid.' },
{ label: 'Project all future cash flows for remaining coverage', type: 'skipped',
explain: 'The General Model requires probability-weighted projections of every future cash flow. The PAA skips this \u2014 the premium itself serves as the proxy for expected service.' },
{ label: 'Discount projected cash flows to present value', type: 'skipped',
explain: 'Because the PAA doesn\u2019t project future cash flows for remaining coverage, there is nothing to discount. For a one-year contract the time-value difference is immaterial.' },
{ label: 'Calculate a risk adjustment on remaining coverage', type: 'skipped',
explain: 'The General Model quantifies compensation for bearing uncertainty about future cash flows. The PAA does not require this on the Liability for Remaining Coverage \u2014 another layer of actuarial judgment avoided.' },
{ label: 'Derive and track a Contractual Service Margin (CSM)', type: 'skipped',
explain: 'The heart of the simplification. The General Model calculates the CSM as the residual between fulfilment cash flows and premiums, then tracks it over time. The PAA has no CSM \u2014 the premium-based liability replaces the entire mechanism.' },
{ label: 'Release the CSM via coverage units each quarter', type: 'skipped',
explain: 'Under the General Model the insurer defines coverage units and allocates the CSM to determine revenue. Under the PAA, proportional time passage does the same job \u2014 each quarter, one quarter of the liability is released as insurance revenue.' },
{ label: 'Release remaining coverage as insurance revenue over time', type: 'same',
explain: 'Both approaches recognise insurance revenue as coverage is delivered. The PAA simply releases the premium-based liability proportionally over the coverage period \u2014 same result, far fewer calculations.' },
{ label: 'When a claim occurs, measure the Liability for Incurred Claims using fulfilment cash flows', type: 'same',
explain: 'Once a claim event happens, both paths converge. The insurer estimates the present value of future payments using probability-weighted cash flows. The PAA simplifies the \u201Cpromise outstanding\u201D side, not the \u201Cclaims already happened\u201D side.' },
{ label: 'Discount the Liability for Incurred Claims to present value', type: 'same',
explain: 'Both the General Model and PAA require discounting on incurred claims. The simplification only applies to the Liability for Remaining Coverage.' },
{ label: 'Add a risk adjustment to the Liability for Incurred Claims', type: 'same',
explain: 'The PAA removes the risk adjustment only on the remaining coverage side. For incurred claims, the insurer still quantifies uncertainty. Both models are identical here.' },
{ label: 'Apply the onerous contract test \u2014 recognise any loss immediately', type: 'same',
explain: 'If the group is expected to generate a loss, both approaches require immediate recognition. The PAA is a simplification, not an escape from prudent accounting.' },
{ label: 'Follow IFRS 17 grouping rules (portfolios, profitability groups, annual cohorts)', type: 'same',
explain: 'Grouping requirements are unchanged. Portfolios, profitability groups, and annual cohorts apply in exactly the same way under the PAA as under the General Model.' }
];
/* ── Default VFA steps ───────────────────────────────────── */
var VFA_STEPS = [
{ label: 'Calculate fulfilment cash flows at initial recognition', type: 'same',
explain: 'Both approaches start identically. The insurer projects probability-weighted future cash flows for the Belgian with-profits portfolio, including policyholder benefits, expenses, and premiums.' },
{ label: 'Discount cash flows to present value', type: 'same',
explain: 'Both approaches discount projected cash flows. For direct participating contracts the cash flows depend on the underlying items, but the discounting mechanics are the same at inception.' },
{ label: 'Add a risk adjustment for non-financial risk', type: 'same',
explain: 'Both approaches require the insurer to quantify the compensation it demands for bearing non-financial uncertainty (e.g. mortality, lapse risk). Identical treatment.' },
{ label: 'Derive the Contractual Service Margin (CSM) as the residual', type: 'same',
explain: 'Both approaches derive the CSM the same way at inception: premiums minus fulfilment cash flows. The CSM represents unearned profit to be released over the coverage period.' },
{ label: 'When non-financial assumptions change for future service \u2192 adjust the CSM', type: 'same',
explain: 'Both approaches route changes in non-financial estimates (e.g. revised mortality or lapse expectations) relating to future service through the CSM. No difference here.' },
{ label: 'When non-financial assumptions change for current or past service \u2192 recognise in profit or loss', type: 'same',
explain: 'Experience adjustments and changes relating to current or past service go straight to profit or loss under both approaches. The VFA does not change this rule.' },
{ label: 'When value of underlying items rises \u2192 insurer\u2019s share (\u20AC2.5M on a \u20AC25M gain) goes to income statement or OCI', type: 'modified',
explain: 'This is where the VFA diverges. Equity markets rise, the \u20AC500M pool gains \u20AC25M. Policyholders receive 90% (\u20AC22.5M). Under the General Model, the insurer\u2019s 10% share (\u20AC2.5M) flows to the income statement or OCI \u2014 creating immediate profit. Under the VFA, that \u20AC2.5M adjusts the CSM upward instead, to be released as insurance revenue over future periods. This reflects the economics of a variable fee.' },
{ label: 'When value of underlying items falls \u2192 insurer\u2019s share (\u20AC3M on a \u20AC30M drop) goes to income statement or OCI', type: 'modified',
explain: 'The same logic applies in reverse. Markets fall, the pool drops \u20AC30M. Policyholders absorb \u20AC27M (90%). Under the General Model, the insurer\u2019s \u20AC3M loss hits the income statement immediately \u2014 creating visible volatility even though the long-term fee structure is unchanged. Under the VFA, the \u20AC3M reduces the CSM instead. As long as the CSM remains positive, the insurer simply recognises less profit in future periods.' },
{ label: 'When discount rates or other financial assumptions change \u2192 adjust income statement or OCI', type: 'modified',
explain: 'Another key modification. Under the General Model, changes in financial assumptions (like discount rates) flow to the income statement or OCI. Under the VFA, the insurer\u2019s share of these changes is routed through the CSM as well \u2014 preventing artificial volatility that doesn\u2019t reflect the fee-based economics.' },
{ label: 'Release the CSM as insurance revenue over the coverage period', type: 'same',
explain: 'Both approaches release the CSM as insurance revenue over time using coverage units. The difference is that under the VFA, the CSM absorbs more movements (financial changes), so the revenue pattern is smoother and more reflective of the insurer\u2019s true variable fee.' },
{ label: 'Apply the onerous contract test \u2014 recognise any loss immediately', type: 'same',
explain: 'If the CSM is exhausted and the group becomes loss-making, both approaches require immediate loss recognition in profit or loss. The VFA does not shield the insurer from genuine losses.' }
];
/* ── Resolve config ──────────────────────────────────────── */
var STEPS = cfg.steps || ( mode === 'vfa' ? VFA_STEPS : PAA_STEPS );
var title = cfg.title || ( mode === 'vfa' ? 'General Model vs VFA \u2014 Side by Side' : 'General Model vs PAA \u2014 Side by Side' );
var scenario = cfg.scenario || ( mode === 'vfa'
? '<strong>Scenario:</strong> A with-profits savings portfolio in Belgium. Underlying items worth \u20AC500M. Policyholders receive 90% of returns; AXA retains 10% as a variable fee for managing assets and bearing insurance risk.'
: '<strong>Scenario:</strong> A group of one-year motor insurance contracts in Germany, measured under both approaches.' );
var colLabel = cfg.colLabel || ( mode === 'vfa' ? 'VFA' : 'PAA (shortcut)' );
var footnote = cfg.footnote || ( mode === 'vfa'
? 'The VFA modifies how financial changes are treated \u2014 everything else stays the same as the General Model.'
: 'The PAA simplifies the Liability for Remaining Coverage side \u2014 not the Liability for Incurred Claims side.' );
/* Labels for PAA mode counters vs VFA mode counters */
var isPaa = mode !== 'vfa';
var lbl1 = isPaa ? 'Steps kept' : 'Steps identical';
var lbl2 = isPaa ? 'Steps skipped' : 'Steps modified by VFA';
/* VFA class on container for colour override */
if ( mode === 'vfa' ) container.className += ' wix-gp--vfa';
/* ── State ───────────────────────────────────────────────── */
var current = -1;
/* ── Build UI ────────────────────────────────────────────── */
wix.empty( container );
var wrapper = wix.el( 'div', { className: 'wix-eng-wrapper' } );
container.appendChild( wrapper );
wrapper.appendChild( wix.el( 'div', { className: 'wix-sim-title', textContent: title } ) );
wrapper.appendChild( wix.el( 'div', { className: 'wix-gp-scenario', innerHTML: scenario } ) );
/* Table */
var thead = wix.el( 'div', { className: 'wix-gp-thead' }, [
wix.el( 'span', { className: 'wix-gp-th', textContent: 'Step' } ),
wix.el( 'span', { className: 'wix-gp-th wix-gp-th--gm', textContent: 'General Model' } ),
wix.el( 'span', { className: 'wix-gp-th wix-gp-th--alt', textContent: colLabel } )
] );
var rowEls = [];
var gmMarks = [];
var altMarks = [];
STEPS.forEach( function ( s, i ) {
var gm = wix.el( 'div', { className: 'wix-gp-mark' } );
var alt = wix.el( 'div', { className: 'wix-gp-mark' } );
gmMarks.push( gm );
altMarks.push( alt );
rowEls.push( wix.el( 'div', { className: 'wix-gp-row' }, [
wix.el( 'div', { className: 'wix-gp-row-label' }, [
wix.el( 'span', { className: 'wix-gp-num', textContent: String( i + 1 ) } ),
wix.el( 'span', { textContent: s.label } )
] ),
gm,
alt
] ) );
} );
wrapper.appendChild( wix.el( 'div', { className: 'wix-gp-table' }, [ thead, wix.el( 'div', {}, rowEls ) ] ) );
/* Explanation box */
var eStep = wix.el( 'div', { className: 'wix-gp-explain-step' } );
var eBody = wix.el( 'div', { className: 'wix-gp-explain-body', innerHTML: 'Press <strong>Next</strong> to begin.' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-gp-explain' }, [ eStep, eBody ] ) );
/* Counters (2 boxes — no "effort saved" for VFA mode) */
var ctA = wix.el( 'div', { className: 'wix-gp-cbox-num wix-gp-cbox-num--kept', textContent: '0' } );
var ctB = wix.el( 'div', { className: isPaa ? 'wix-gp-cbox-num wix-gp-cbox-num--skip' : 'wix-gp-cbox-num wix-gp-cbox-num--pct', textContent: '0' } );
var ctPct = isPaa ? wix.el( 'div', { className: 'wix-gp-cbox-num wix-gp-cbox-num--pct', textContent: '\u2014' } ) : null;
var counterChildren = [
wix.el( 'div', { className: 'wix-gp-cbox' }, [ ctA, wix.el( 'div', { className: 'wix-gp-cbox-lbl', textContent: lbl1 } ) ] ),
wix.el( 'div', { className: 'wix-gp-cbox' }, [ ctB, wix.el( 'div', { className: 'wix-gp-cbox-lbl', textContent: lbl2 } ) ] )
];
if ( ctPct ) counterChildren.push(
wix.el( 'div', { className: 'wix-gp-cbox' }, [ ctPct, wix.el( 'div', { className: 'wix-gp-cbox-lbl', textContent: 'Effort saved' } ) ] )
);
wrapper.appendChild( wix.el( 'div', { className: 'wix-gp-counters' }, counterChildren ) );
/* Controls */
var btnReset = wix.el( 'button', { className: 'wix-btn wix-btn--outline', textContent: 'Reset' } );
var btnNext = wix.el( 'button', { className: 'wix-btn', textContent: 'Next \u2192' } );
wrapper.appendChild( wix.el( 'div', { className: 'wix-gp-controls' }, [ btnReset, btnNext ] ) );
wrapper.appendChild( wix.el( 'p', { className: 'wix-gp-footnote', textContent: footnote } ) );
/* ── Update ──────────────────────────────────────────────── */
function update() {
var a = 0, b = 0;
STEPS.forEach( function ( s, i ) {
var isSame = s.type === 'same' || ( isPaa && s.type !== 'skipped' );
if ( i <= current ) {
rowEls[i].className = 'wix-gp-row wix-gp-row--revealed' + ( i === current ? ' wix-gp-row--current' : '' );
gmMarks[i].className = 'wix-gp-mark wix-gp-mark--yes';
gmMarks[i].textContent = '\u2714';
if ( isSame ) {
altMarks[i].className = 'wix-gp-mark wix-gp-mark--yes';
altMarks[i].textContent = '\u2714';
a++;
} else if ( s.type === 'modified' ) {
altMarks[i].className = 'wix-gp-mark wix-gp-mark--mod';
altMarks[i].textContent = '\u2260 CSM';
b++;
} else {
altMarks[i].className = 'wix-gp-mark wix-gp-mark--no';
altMarks[i].textContent = '\u2718';
b++;
}
} else {
rowEls[i].className = 'wix-gp-row';
gmMarks[i].className = 'wix-gp-mark';
gmMarks[i].textContent = '';
altMarks[i].className = 'wix-gp-mark';
altMarks[i].textContent = '';
}
} );
/* Explanation */
if ( current >= 0 ) {
var s = STEPS[current];
var tag;
if ( isPaa ) tag = ( s.type === 'skipped' ? 'Skipped by PAA' : 'Kept by PAA' );
else tag = ( s.type === 'modified' ? 'Modified by VFA' : 'Identical' );
eStep.textContent = 'Step ' + ( current + 1 ) + ' of ' + STEPS.length + ' \u00B7 ' + tag;
eBody.innerHTML = s.explain;
} else {
eStep.textContent = '';
eBody.innerHTML = 'Press <strong>Next</strong> to begin.';
}
/* Counters */
ctA.textContent = a;
ctB.textContent = b;
if ( ctPct ) {
var total = a + b;
ctPct.textContent = total > 0 ? Math.round( b / STEPS.length * 100 ) + '%' : '\u2014';
}
/* Button */
if ( current >= STEPS.length - 1 ) {
btnNext.disabled = true;
btnNext.textContent = 'Complete \u2713';
} else {
btnNext.disabled = false;
btnNext.textContent = 'Next \u2192';
}
}
/* ── Events ──────────────────────────────────────────────── */
btnNext.addEventListener( 'click', function () {
if ( current < STEPS.length - 1 ) { current++; update(); }
} );
btnReset.addEventListener( 'click', function () {
current = -1; update();
} );
update();
}
}() );