// 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 (
{/* Brand mark (images/brand/, 2026-10-26): at icon sizes it is always the yellow V on an ink square. */}
keelvalue
SME Valuation · Curriculum & Tools
{items.map(([label, href]) => ( {label} ))} {cta[0]}
); } 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.

Read case №047 free →
Until then: Chapter 01 and all 8 tools are free on the site.
© 2026 KEELVALUE Terms Refunds Privacy {window.VC_CONTACT_EMAIL ? {window.VC_CONTACT_EMAIL} : Contact} { e.preventDefault(); window.scrollTo({ top: 0, behavior: 'smooth' }); }} style={{ color: '#888', textDecoration: 'none' }}>↑ TO TOP
); } /* ── 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 (
§ {kicker}

{title}

{tiers.map((t, i) => (
{t.sub}
{t.name} NZD {t.price} · {t.per}
{t.perks.map(p =>
+ {p}
)}
{t.cta}
))}
); } /** 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 (