Files
nur-falah-prevention/src/lib/NominationRegistry.svelte
T
wmj a0c70e1411 Add estate agent delegation: real accounts, multi-tenant backend, RLS-enforced roles
Per explicit product direction: the app assumed a single user (head of
family) with everything in per-device localStorage. There was no way for
a family to delegate estate management to an agent (relative or
professional) without literally handing over the device. This required
real backend infrastructure, not a UI addition — added Supabase
(Postgres + Auth) as a multi-tenant backend.

Schema (nf_ prefixed to stay isolated from other tables in the reused
"Falah OS demo" project): nf_families, nf_family_members (role: owner/
agent, status: invited/active), and family-scoped versions of every
estate table — nf_assets, nf_trusted_contacts, nf_hibah_gifts,
nf_waqf_designations/nf_waqf_beneficiaries, nf_nominations,
nf_attestors, nf_death_triggers.

Permission model, enforced by RLS at the database level (not just
hidden in the UI): an agent can do everything an owner can — add/edit
assets, draft Hibah/Waqf/Nominations, set up the Death Trigger — except
fire it. nf_death_triggers' UPDATE/INSERT policies use a WITH CHECK that
only allows triggered=true when the caller has role='owner' on that
family. A professional agent can be invited to multiple families and
switches between them from their own dashboard.

New: auth.js, family.js, db.js, AuthScreen.svelte, FamilySwitcher.svelte,
FamilyManagement.svelte (new "Family" tab: invite agents, see members,
switch families). AssetRegistry, HibahTracker, FamilyWaqfDesignator,
NominationRegistry, CoverageDashboard, and DeathTrigger all migrated
from storage.js (localStorage) to db.js (Supabase), scoped to the
active family_id. App.svelte now gates on auth + family selection
before showing the main tab shell.

Three real bugs found and fixed via testing against the live backend
(not caught by the old localStorage-based suites, which had no
cross-client concurrency to expose them):
- RLS gap: pending-invite lookup joins nf_families(name), but the
  invitee isn't a family member yet, so the join was silently dropped —
  added a policy letting a pending invitee see just the family name.
- Attestor row race: lazy "create on first blur" could double-fire from
  two different code paths, creating duplicate rows and confirming the
  wrong one. Fixed by eagerly creating attestor rows on first load so
  every row always has a real id — no more create-or-update ambiguity.
- Out-of-order async clobber: three death-trigger setup fields each
  fired a full-snapshot upsert on every input; whichever request
  *finished* last (not fired last) won, silently reverting the other
  two fields to stale values. Fixed with per-field partial updates
  (updateDeathTriggerField) that can't clobber columns they don't touch.

e2e-family-agent.cjs: full owner/agent flow against the live Supabase
backend — invite, accept, shared live data, agent blocked from firing
the trigger (button stays disabled and a direct RLS-level attempt would
also fail), owner successfully fires it. 12/12 passing.
e2e-smoke-authed.cjs: post-auth-gate sweep confirming every existing tab
still renders and its info panel still opens under the new sign-in
requirement. 24/24 passing, zero console errors.

Known follow-up, not done here: the eight pre-auth E2E suites
(e2e-uat.cjs, e2e-fastpath.cjs, e2e-trust.cjs, e2e-business.cjs,
e2e-digital-vehicle.cjs, e2e-property.cjs, e2e-other.cjs, e2e-info.cjs)
assume an anonymous landing page and need a sign-in prelude added
before they're valid again — their detailed assertions were re-verified
functionally via the smoke test and manual review, not by running them
as-is.
2026-08-13 21:07:23 +08:00

327 lines
23 KiB
Svelte

<script>
// Third fast-path channel alongside Hibah and Waqf. Some assets (EPF, Takaful/
// insurance, bank accounts, land) have no practical way to be fully gifted or
// waqf'd away during life — but several of them have their own legally-direct
// nomination mechanism that already bypasses probate BY LAW, independent of
// Faraid: EPF nomination (EPF Act 1991 s.51 pays the nominee directly), Takaful/
// insurance nomination as trust (Insurance Act 1996 s.166 / Takaful equivalent),
// bank death-mandate or joint-tenancy-with-survivorship. Land and business
// interests generally have no such channel and need a pre-funded trust/nominee
// holding structure instead — a real legal step this app can prepare paperwork
// for but cannot execute on its own.
import { onMount } from 'svelte';
import { activeFamilyId } from './family.js';
import { listAssets, listNominations, addNomination as addNominationDb, removeNomination as removeNominationDb } from './db.js';
import { suggestedChannel } from './nonprobate.js';
import Disclaimer from './Disclaimer.svelte';
import InfoPanel from './InfoPanel.svelte';
let familyId = $state(null);
activeFamilyId.subscribe(v => familyId = v);
let assets = $state([]);
const CHANNEL_TYPES = [
{ value: 'epf', label: 'EPF nomination', note: 'Pays the nominee directly under EPF Act 1991 s.51 — bypasses probate by law.' },
{ value: 'takaful', label: 'Takaful / insurance nomination', note: 'Nomination as trust under Insurance Act 1996 s.166 (or Takaful equivalent) — pays the named beneficiary directly.' },
{ value: 'bank-mandate', label: 'Bank death mandate / joint account', note: 'Ask the bank for a death-benefit nomination, or hold as joint account with survivorship.' },
{ value: 'trust', label: 'Pre-funded trust / nominee holding', note: 'For land or illiquid assets with no nomination bypass — requires setting up a real trust or nominee entity now, while alive.' },
{ value: 'business-continuity', label: 'Business continuity instrument', note: 'A business dies with its structure by default: a sole proprietorship dies with the owner, a partnership dissolves on any partner\'s death unless the agreement says otherwise, and company shares fall into probate like any other asset unless a buy-sell or shareholder agreement already routes them.' },
{ value: 'digital-custody', label: 'Digital custody plan', note: 'A single seed phrase known only to you is not a fast path — it is a total-loss risk if you die without sharing it. Use the exchange\'s own beneficiary feature where one exists, or a multi-sig / key-escrow arrangement with your executor for self-custody wallets.' }
];
const CUSTODY_TYPES = [
{ value: 'exchange-beneficiary', label: 'Custodial exchange beneficiary feature', warning: null },
{ value: 'multisig', label: 'Self-custody multi-sig wallet', warning: 'Multi-sig only works if the co-signers are set up and able to act now — an executor who is a co-signer can release funds without waiting for probate, but only if the wallet was actually configured this way while you were alive.' },
{ value: 'key-escrow', label: 'Key-escrow with executor', warning: 'Never store the actual seed phrase or private key in this app or any single record your executor can\'t independently verify. This field should reference WHERE the key is escrowed (e.g. a sealed instruction with a lawyer, a hardware device with split-knowledge access) — not the key itself.' }
];
const BUSINESS_STRUCTURES = [
{ value: 'sole-prop', label: 'Sole proprietorship', warning: 'A sole proprietorship legally dies with you — there is no entity left to transfer. The only fast-path options are converting it to a company with a shareholder agreement, or dedicating it as a waqf-owned enterprise now, while alive.' },
{ value: 'partnership', label: 'Partnership', warning: null },
{ value: 'company', label: 'Company (shares)', warning: null }
];
const BUSINESS_INSTRUMENTS = [
{ value: 'partnership-continuation', label: 'Partnership continuation clause', note: 'Prevents automatic dissolution on a partner\'s death — but does not by itself transfer the deceased partner\'s share; pair it with a buy-sell agreement for that.' },
{ value: 'shareholder-buy-sell', label: 'Shareholder agreement with buy-sell provision', note: 'Pre-agreed, often Takaful-funded, automatic buyout of the deceased\'s shares by co-shareholders at a pre-set valuation — the standard fast-path for company shares.' },
{ value: 'waqf-enterprise', label: 'Dedicate as waqf-owned enterprise', note: 'Use the Family Waqf Designator for this instead — a waqf-owned enterprise survives you and serves your purpose in perpetuity, same as any other waqf corpus.' }
];
const emptyForm = () => ({ linkedAssetId: '', type: 'epf', institution: '', nomineeeName: '', referenceNumber: '', trusteeName: '', successorTrustee: '', trustBeneficiaries: '', businessStructure: 'company', businessInstrument: 'shareholder-buy-sell', successorOwner: '', buySellTerms: '', custodyType: 'exchange-beneficiary', platform: '', keyHolderName: '', accessInstructionsRef: '' });
let nominations = $state([]);
let form = $state(emptyForm());
async function refresh() {
if (!familyId) return;
assets = await listAssets(familyId);
nominations = await listNominations(familyId);
}
onMount(refresh);
$effect(() => { familyId; refresh(); });
async function addNomination() {
if (!form.linkedAssetId) return;
if (form.type === 'trust') {
if (!form.trusteeName) return;
} else if (form.type === 'business-continuity') {
if (form.businessInstrument === 'waqf-enterprise') return; // redirect only, nothing to save here
if (!form.successorOwner) return;
} else if (form.type === 'digital-custody') {
if (!form.keyHolderName) return;
} else if (!form.nomineeeName) {
return;
}
await addNominationDb(familyId, form);
form = emptyForm();
await refresh();
}
function exportBusinessContinuity(n) {
const asset = assets.find(a => a.id === n.linkedAssetId);
const structureLabel = BUSINESS_STRUCTURES.find(s => s.value === n.businessStructure)?.label;
const instrumentLabel = BUSINESS_INSTRUMENTS.find(i => i.value === n.businessInstrument)?.label;
const lines = [
'BUSINESS CONTINUITY INSTRUMENT — DRAFTING AID',
`Generated: ${new Date().toISOString().slice(0, 10)}`,
`Business: ${asset ? asset.description : '(asset removed)'}`,
`Structure: ${structureLabel}`,
`Instrument: ${instrumentLabel}`,
`Successor / buyer: ${n.successorOwner}`,
`Terms: ${n.buySellTerms || '________________________'}`,
'',
n.businessInstrument === 'shareholder-buy-sell'
? 'ACTION: this shares are transferred per a pre-agreed buy-sell provision in the\nshareholder agreement, often funded by a Takaful/insurance payout timed to the\nbuyout value. Once the agreement is executed and (ideally) funded, the transfer\nat death is a private buyout between shareholders — not a probate filing.'
: 'ACTION: a partnership continuation clause prevents automatic dissolution on\ndeath, but the deceased partner\'s share itself still needs a buy-sell provision\nor a will-directed transfer to actually move — pair this with a buy-sell\nagreement for full coverage.',
'',
'ACTION REQUIRED: this document is a drafting aid only. A lawyer must draft the',
'actual shareholder/partnership agreement, and it must be signed and (for a',
'buy-sell) ideally funded by a Takaful/insurance policy while all parties are',
'alive for this to have legal effect and actually bypass probate.',
'',
'DISCLAIMER: Not a fatwa. Not legal advice. Drafting aid only.'
];
const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'business-continuity-draft.txt'; a.click();
URL.revokeObjectURL(url);
}
function exportDigitalCustody(n) {
const asset = assets.find(a => a.id === n.linkedAssetId);
const custodyLabel = CUSTODY_TYPES.find(c => c.value === n.custodyType)?.label;
const lines = [
'DIGITAL CUSTODY PLAN — DRAFTING AID',
`Generated: ${new Date().toISOString().slice(0, 10)}`,
`Asset: ${asset ? asset.description : '(asset removed)'}`,
`Platform / wallet: ${n.platform || '________________________'}`,
`Custody type: ${custodyLabel}`,
`Key holder / executor: ${n.keyHolderName}`,
`Access instructions reference: ${n.accessInstructionsRef || '________________________'}`,
'',
n.custodyType === 'exchange-beneficiary'
? 'ACTION: File the death claim directly with the exchange/custodial platform using\nits own beneficiary/inheritance feature. This is a platform process, not a\nprobate filing — confirm the feature is actually activated on the account now,\nwhile you\'re alive, not assumed to exist.'
: n.custodyType === 'multisig'
? 'ACTION: The named co-signer(s) release funds per the wallet\'s multi-sig\nconfiguration once death is confirmed. This only works if the multi-sig was\nactually set up with a living co-signer — a single-key wallet has no fast path\nat all.'
: 'ACTION: Retrieve the escrowed access instructions from wherever they are held\n(named above) and follow them to access the wallet. This app does not, and must\nnot, store the seed phrase or private key itself.',
'',
'WARNING: never enter an actual seed phrase, private key, or password into this',
'app or any single document. This drafting aid should only ever reference WHERE',
'access is arranged, never the credential itself.',
'',
'DISCLAIMER: Not a fatwa. Not legal advice. Drafting aid only.'
];
const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'digital-custody-plan-draft.txt'; a.click();
URL.revokeObjectURL(url);
}
function exportTrustDeed(n) {
const asset = assets.find(a => a.id === n.linkedAssetId);
const lines = [
'TRUST / NOMINEE HOLDING SETUP — DRAFTING AID',
`Generated: ${new Date().toISOString().slice(0, 10)}`,
`Asset: ${asset ? `${asset.type}${asset.description}` : '(asset removed)'}`,
`Trustee: ${n.trusteeName}`,
`Successor trustee: ${n.successorTrustee || '________________________'}`,
`Beneficiaries: ${n.trustBeneficiaries || '________________________'}`,
'',
'PURPOSE: this asset has no nomination-based probate bypass available (unlike EPF or',
'Takaful). A pre-funded trust or nominee holding structure — set up and title-transferred',
'NOW, while the settlor is alive — is the only way to move it out of the probate-bound',
'estate. Once the trustee holds legal title on the beneficiaries\' behalf, distribution at',
'death is an internal trustee record update, not a Land Office / court filing.',
'',
'ACTION REQUIRED: this document is a drafting aid only. It does not itself create a trust',
'or transfer title. A licensed trust company, Amanah Raya-equivalent body, or lawyer must',
'draft the actual trust deed and complete the title transfer at the Land Office (or',
'equivalent registry) for this to have legal effect and actually bypass probate.',
'',
'DISCLAIMER: Not a fatwa. Not legal advice. Drafting aid only.'
];
const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'trust-setup-draft.txt'; a.click();
URL.revokeObjectURL(url);
}
async function remove(id) {
await removeNominationDb(id);
await refresh();
}
const selectedAsset = $derived(assets.find(a => a.id === form.linkedAssetId));
const suggestion = $derived(selectedAsset ? suggestedChannel(selectedAsset.type) : null);
</script>
<div class="module">
<div class="module-header">
<h2>Nomination Registry</h2>
<InfoPanel
title="Nomination Registry"
what="Some things can't be simply gifted away (like your EPF/retirement savings, insurance, or a business) — but many of them have their own built-in shortcut that pays your chosen person directly, without needing a court at all. This tab is where you record which shortcut covers which asset."
how="Pick an asset from your registry, and the app suggests the right shortcut for that type — EPF/Takaful nomination for retirement and insurance money, a bank mandate for cash, a trust for land, a business continuity instrument for a company, or a digital custody plan for crypto. Fill in the fields for that shortcut and save it — then remember to actually file the real nomination with that institution too, this app only keeps a record."
fields={[
{ label: 'Asset', hint: 'Pick from your Asset Registry — the app will suggest the right channel automatically.' },
{ label: 'Channel type', hint: 'EPF, Takaful/insurance, bank mandate, trust, business continuity, or digital custody — pick what fits.' },
{ label: 'Extra fields', hint: 'Change depending on the channel — e.g. a trustee for a trust, or a nominee for EPF/Takaful.' }
]}
/>
</div>
<div class="fast-path-note">Third fast-path channel. EPF and Takaful/insurance nominations pay the nominee directly by law — no Faraid/probate involvement at all. Bank mandates and trust/nominee holdings need setup now, while alive, but also bypass the court queue once in place.</div>
<p class="sub">Log which non-probate channel covers each asset that can't be fully gifted (Hibah) or dedicated (Waqf).</p>
<div class="form-card">
<label class="field"><span>Asset</span>
<select bind:value={form.linkedAssetId}>
<option value="">Select from Asset Registry…</option>
{#each assets as a}<option value={a.id}>{a.type} {a.description}</option>{/each}
</select>
</label>
{#if suggestion}
<p class="suggestion">Suggested channel for this asset type: <strong>{suggestion.label}</strong>. {suggestion.note}</p>
{/if}
<label class="field"><span>Channel type</span>
<select bind:value={form.type}>
{#each CHANNEL_TYPES as c}<option value={c.value}>{c.label}</option>{/each}
</select>
</label>
{#if form.type === 'trust'}
<div class="trust-fields">
<p class="trust-note">Land and other illiquid assets have no legal nomination bypass — a trust needs a trustee, not just a "nominee", and title must actually be transferred to them while you're alive for this to work.</p>
<label class="field"><span>Trustee</span><input type="text" bind:value={form.trusteeName} /></label>
<label class="field"><span>Successor trustee</span><input type="text" bind:value={form.successorTrustee} /></label>
<label class="field"><span>Beneficiaries</span><input type="text" bind:value={form.trustBeneficiaries} placeholder="e.g. equal split among children" /></label>
</div>
{:else if form.type === 'business-continuity'}
<div class="trust-fields">
<label class="field"><span>Business structure</span>
<select bind:value={form.businessStructure}>
{#each BUSINESS_STRUCTURES as s}<option value={s.value}>{s.label}</option>{/each}
</select>
</label>
{#if BUSINESS_STRUCTURES.find(s => s.value === form.businessStructure)?.warning}
<p class="business-warning">{BUSINESS_STRUCTURES.find(s => s.value === form.businessStructure).warning}</p>
{/if}
<label class="field"><span>Continuity instrument</span>
<select bind:value={form.businessInstrument}>
{#each BUSINESS_INSTRUMENTS as i}<option value={i.value}>{i.label}</option>{/each}
</select>
</label>
<p class="trust-note">{BUSINESS_INSTRUMENTS.find(i => i.value === form.businessInstrument)?.note}</p>
{#if form.businessInstrument === 'waqf-enterprise'}
<p class="business-warning">Go to the Family Waqf Designator tab and select this business as the corpus asset — nothing to save here.</p>
{:else}
<label class="field"><span>{form.businessInstrument === 'shareholder-buy-sell' ? 'Buying co-shareholder(s)' : 'Successor partner'}</span><input type="text" bind:value={form.successorOwner} /></label>
<label class="field"><span>Terms</span><input type="text" bind:value={form.buySellTerms} placeholder="e.g. valuation method, Takaful-funded buyout" /></label>
{/if}
</div>
{:else if form.type === 'digital-custody'}
<div class="trust-fields">
<label class="field"><span>Platform / wallet</span><input type="text" bind:value={form.platform} placeholder="e.g. Coinbase, Ledger self-custody" /></label>
<label class="field"><span>Custody type</span>
<select bind:value={form.custodyType}>
{#each CUSTODY_TYPES as c}<option value={c.value}>{c.label}</option>{/each}
</select>
</label>
{#if CUSTODY_TYPES.find(c => c.value === form.custodyType)?.warning}
<p class="business-warning">{CUSTODY_TYPES.find(c => c.value === form.custodyType).warning}</p>
{/if}
<label class="field"><span>Key holder / executor</span><input type="text" bind:value={form.keyHolderName} /></label>
<label class="field"><span>Access instructions reference (never the actual key)</span><input type="text" bind:value={form.accessInstructionsRef} placeholder="e.g. sealed letter with lawyer X, ref #123" /></label>
</div>
{:else}
<label class="field"><span>Institution</span><input type="text" bind:value={form.institution} placeholder="e.g. KWSP, Bank Islam, Takaful Ikhlas" /></label>
<label class="field"><span>Nominee name</span><input type="text" bind:value={form.nomineeeName} /></label>
<label class="field"><span>Reference / policy / account number</span><input type="text" bind:value={form.referenceNumber} /></label>
{/if}
<button class="btn-primary" disabled={form.type === 'business-continuity' && form.businessInstrument === 'waqf-enterprise'} onclick={addNomination}>Add {form.type === 'trust' ? 'trust setup' : form.type === 'business-continuity' ? 'business continuity instrument' : form.type === 'digital-custody' ? 'digital custody plan' : 'nomination'}</button>
</div>
{#each nominations as n (n.id)}
{@const asset = assets.find(a => a.id === n.linkedAssetId)}
{@const channelInfo = CHANNEL_TYPES.find(c => c.value === n.type)}
<div class="nomination-row">
<div class="nomination-info">
<strong>{asset ? asset.description : '(asset removed)'}</strong>
<span class="muted">{channelInfo?.label}{!['trust', 'business-continuity', 'digital-custody'].includes(n.type) ? ` · ${n.institution || '—'}` : ''}</span>
{#if n.type === 'trust'}
<span class="muted">Trustee: {n.trusteeName} · Successor: {n.successorTrustee || '—'}</span>
<span class="muted">Beneficiaries: {n.trustBeneficiaries || '—'}</span>
{:else if n.type === 'business-continuity'}
<span class="muted">{BUSINESS_INSTRUMENTS.find(i => i.value === n.businessInstrument)?.label}</span>
<span class="muted">Successor: {n.successorOwner} · Terms: {n.buySellTerms || '—'}</span>
{:else if n.type === 'digital-custody'}
<span class="muted">{CUSTODY_TYPES.find(c => c.value === n.custodyType)?.label} · {n.platform || '—'}</span>
<span class="muted">Key holder: {n.keyHolderName}</span>
{:else}
<span class="muted">Nominee: {n.nomineeeName} · Ref: {n.referenceNumber || '—'}</span>
{/if}
</div>
<div class="row-actions">
{#if n.type === 'trust'}<button class="export-btn" onclick={() => exportTrustDeed(n)}>Export</button>{/if}
{#if n.type === 'business-continuity'}<button class="export-btn" onclick={() => exportBusinessContinuity(n)}>Export</button>{/if}
{#if n.type === 'digital-custody'}<button class="export-btn" onclick={() => exportDigitalCustody(n)}>Export</button>{/if}
<button onclick={() => remove(n.id)}>✕</button>
</div>
</div>
{:else}
<p class="empty">No nominations logged yet.</p>
{/each}
<Disclaimer text="This registry records your nomination — it does not submit it. You must still file the actual nomination form with EPF, the Takaful/insurance provider, or the bank for it to take legal effect." />
</div>
<style>
.module { padding: 4px 0 40px; }
.module-header { display: flex; align-items: center; margin-bottom: 4px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 0; }
.fast-path-note { font-size: 11.5px; color: #2ECC71; background: rgba(46,204,113,0.08); border: 1px solid rgba(46,204,113,0.25); border-radius: 10px; padding: 10px 12px; margin: 10px 0 12px; line-height: 1.5; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; }
.form-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 16px; }
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.field span { font-size: 12px; color: #B8B2A6; }
.field select, .field input { background: rgba(255,255,255,0.05); border: 1px solid rgba(201,168,76,0.2); border-radius: 8px; padding: 10px 12px; color: #E8E4DC; font-size: 14px; }
.suggestion { font-size: 11.5px; color: #C9A84C; background: rgba(201,168,76,0.06); border-radius: 8px; padding: 8px 10px; margin-bottom: 12px; line-height: 1.5; }
.btn-primary { width: 100%; padding: 12px; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; background: #C9A84C; color: #070A0D; }
.nomination-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 12px 0; border-bottom: 1px solid rgba(255,255,255,0.06); }
.nomination-info { display: flex; flex-direction: column; gap: 2px; font-size: 13px; color: #E8E4DC; }
.muted { color: #8A8478; font-size: 11.5px; }
.nomination-row button { background: none; border: none; color: #8A8478; cursor: pointer; }
.empty { font-size: 13px; color: #8A8478; text-align: center; padding: 20px 0; }
.trust-fields { background: rgba(201,168,76,0.05); border-radius: 10px; padding: 12px; margin-bottom: 12px; }
.trust-note { font-size: 11.5px; color: #C9A84C; line-height: 1.5; margin-bottom: 10px; }
.row-actions { display: flex; gap: 8px; align-items: center; }
.export-btn { background: rgba(46,204,113,0.15); color: #2ECC71; border: none; border-radius: 8px; padding: 6px 10px; font-size: 11px; cursor: pointer; font-weight: 600; }
.business-warning { font-size: 11.5px; color: #EF4444; background: rgba(239,68,68,0.08); border-radius: 8px; padding: 8px 10px; margin-bottom: 12px; line-height: 1.5; }
</style>