// v4/shared.jsx — shared theme + chrome for the brutalist site.
window.v4 = {
bg: '#f2f0e9',
paper: '#ffffff',
ink: '#0d0d0d',
inkSoft: '#2a2a2a',
inkMuted: '#5e5e5e',
inkDim: '#9e9e9e',
line: '#0d0d0d',
lineSoft: '#d6d4cc',
yellow: '#f2e600',
red: '#e83d2c',
sans: '"Geist", "Helvetica Neue", Helvetica, Arial, sans-serif',
mono: '"Geist Mono", "JetBrains Mono", "IBM Plex Mono", ui-monospace, monospace',
};
const v4 = window.v4;
const V4_CLOCKS = [
['AKL', 'Pacific/Auckland'], ['LON', 'Europe/London'],
['NYC', 'America/New_York'], ['HKG', 'Asia/Hong_Kong'],
];
function v4ClockString() {
const now = new Date();
return V4_CLOCKS.map(([code, tz]) => {
try {
const t = new Intl.DateTimeFormat('en-GB', { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12: false }).format(now);
return `${code} ${t}`;
} catch (e) { return code; }
}).join(' · ');
}
function V4UtilityBar({ live = '● Chapter 01 free · No card · No email' }) {
const [clock, setClock] = React.useState(v4ClockString);
React.useEffect(() => {
const id = setInterval(() => setClock(v4ClockString()), 30000);
return () => clearInterval(id);
}, []);
return (
{live}
EN / 中文
{clock}
);
}
function V4Nav({ current }) {
const items = [
['Curriculum', 'chapter.html'],
['Tools', 'tools.html'],
['Calculator', 'bridge.html'],
['Cases', 'article.html'],
['Pricing', 'pricing.html'],
];
const access = window.useVCAccess ? window.useVCAccess() : null;
const cta = access && access.product ? ['Library →', 'library.html'] : ['Start Free ↗', 'landing.html#cta'];
return (
);
}
function V4Footer() {
return (
<>
The newsletter · launching soon
One case.
Every Tuesday.
SME valuation cases, opened up. The broker’s number, the buyer’s number, and the three judgment calls in between. Sign-up opens with the first issue.
>
);
}
/* ── Contact: the one place the public email address is set (footer, terms page, library, pricing) ── */
window.VC_CONTACT_EMAIL = 'hello@keelvalue.com';
/* ── Seller name on the site: empty on purpose. Michael 2026-10-26: the site does not show the company behind
Keelvalue (same as Greenwake Isle). Stripe holds the legal entity; customers see "Keelvalue". If ever filled,
the terms page shows it; nothing depends on it. ── */
window.VC_SELLER = '';
/* ── Pricing: single source of truth (landing, pricing page, chapters, lessons, tools) ── */
window.VC_TIERS = [
{ id: 'free', name: 'Free', price: '0', per: '', sub: 'No card · no email',
perks: ['Chapter 01, in full', 'The first lesson of every chapter', 'All 8 tools as CSV and print-ready pages', 'The earnings-bridge calculator, on screen'],
cta: 'Start reading →', href: 'chapter-01.html' },
{ id: 'deal', name: 'Deal Pack', price: '249', per: 'one payment', sub: 'For a live deal · 90 days',
perks: ['Every lesson, for 90 days', '4 Excel workbooks with live formulas', 'Earnings-bridge exports: one-page PDF + Excel'],
cta: 'Get the Deal Pack →', href: 'pricing.html#deal' },
{ id: 'full', name: 'Complete', price: '490', per: 'one payment', sub: 'No expiry',
perks: ['Every lesson, no expiry', 'All 6 Excel workbooks', 'Earnings-bridge exports: one-page PDF + Excel', 'Chapter quizzes and completion records'],
cta: 'Get Complete →', href: 'pricing.html#full' },
];
window.VC_WORKBOOKS = [
{ id: 'bridge', cover: 'earnings-bridge', t: 'Earnings Bridge', d: 'Reported to transferable EBITDA, line by line, with a value range.', tier: 'deal' },
{ id: 'qofe', cover: 'qofe-checklist', t: 'Quality of Earnings Checklist', d: 'All 47 items, classification drop-downs and running totals.', tier: 'deal' },
{ id: 'multiples', cover: 'multiples-scorecard', t: 'Multiples Quality Scorecard', d: 'Twelve factors, weights and a ±50% cap, to a concluded multiple.', tier: 'deal' },
{ id: 'earnout', cover: 'earnout-structuring', t: 'Earn-out Structuring', d: 'Close, earn-out and escrow split, and the present value of the contingent part.', tier: 'deal' },
{ id: 'wacc', cover: 'nz-wacc-buildup', t: 'NZ WACC Build-up', d: 'Risk-free, ERP, size and specific risk, debt and weights, with a source and date per input.', tier: 'full' },
{ id: 'compset', cover: 'comparable-set-audit', t: 'Comparable-Set Audit', d: 'Six tests per comparable, and the median multiple of the ones that pass.', tier: 'full' },
];
/** React hook over window.VCAccess (access.js): re-renders when an access code is redeemed or cleared. */
window.useVCAccess = function useVCAccess() {
const get = () => (window.VCAccess ? window.VCAccess.state() : { product: null });
const [st, setSt] = React.useState(get);
React.useEffect(() => {
const h = () => setSt(get());
window.addEventListener('vc-access', h);
if (window.VCAccess) window.VCAccess.ready.then(h);
return () => window.removeEventListener('vc-access', h);
}, []);
return st;
};
/** "Two ways in" block: what the paid tiers actually contain. */
function V4PaidOffer({ kicker = 'What unlocks the rest', title = <>Two ways in.> }) {
const tiers = window.VC_TIERS.filter(t => t.id !== 'free');
return (
);
}
/** Access-code entry. Verifies the signature in this browser (access.js); nothing is sent anywhere. */
function V4CodeForm({ dark = false, onDone }) {
const access = window.useVCAccess();
const [code, setCode] = React.useState('');
const [msg, setMsg] = React.useState(null);
const [busy, setBusy] = React.useState(false);
const fg = dark ? v4.bg : v4.ink, bg = dark ? v4.ink : v4.paper;
const submit = async (e) => {
e.preventDefault();
if (!code.trim() || !window.VCAccess) return;
setBusy(true); setMsg(null);
const r = await window.VCAccess.redeem(code);
setBusy(false);
if (r.ok) { setCode(''); setMsg({ ok: true, t: 'Unlocked in this browser.' }); if (onDone) onDone(); }
else setMsg({ ok: false, t: r.reason });
};
return (
);
}
/** "What one line of judgment is worth" — value arithmetic from the site's own teaching case №047.
* Shows the size of one judgment call; it is not a promise about anyone's deal. No fear framing. */
const VC_VALUE_LINE = { amount: 225000, ask: 4.5, paid: 2.1 }; // article.html: rent line −$225k; ask $5.4M on $1.2M = 4.5×; buyer paid 2.1×
function V4ValueMath({ compact = false }) {
const { amount, ask, paid } = VC_VALUE_LINE;
const fmt = (n) => '$' + Math.round(n).toLocaleString('en-NZ');
const atAsk = amount * ask, atPaid = amount * paid;
const million = (n) => '$' + (n / 1e6).toFixed(2) + 'M';
if (compact) return (
What one line is worth
In teaching case №047, the related-party rent line took {fmt(amount)} off EBITDA. At the {ask}× asking multiple that one line is about {million(atAsk)} of price. Illustrative: it shows what a judgment call is worth, not what your deal will be.
See the arithmetic →
);
const term = (big, label, hl) => (
{big}
{label}
);
const op = (c) => {c};
return (
§ What one line of judgment is worth · teaching case №047
{term('−' + fmt(amount), 'One EBITDA line: related-party rent')}
{op('×')}
{term(ask + '×', 'The multiple in the asking price')}
{op('=')}
{term('≈ ' + million(atAsk), `${fmt(amount)} × ${ask} = ${fmt(atAsk)} of price`, true)}
In the case, the company rented its warehouse from the owners’ family at about 40% of the market rate. A new owner pays market rent, so bringing the rent to market took {fmt(amount)} a year off EBITDA. The asking price was $5.4M on $1.2M of reported EBITDA, which is {ask}×. At that multiple, this single line accounts for about {million(atAsk)} of the price.
The multiple matters as much as the line. At the {paid}× the buyer finally paid, the same line is about {fmt(Math.round(atPaid / 10000) * 10000)}. That is the point of showing the arithmetic: one well-defended line changes the price by a multiple of itself, in either direction. Chapter III is about finding and defending lines like this one.
Case №047 is an illustrative teaching case, not a real transaction. The arithmetic shows what one judgment call can be worth on that file. It says nothing about what any other business is worth, and it is not a promise of any result.
);
}
Object.assign(window, { V4UtilityBar, V4Nav, V4Footer, V4PaidOffer, V4CodeForm, V4ValueMath });