var _createContext_RB = React.createContext;
var _useState_RB      = React.useState;
var _useContext_RB    = React.useContext;
var _useCallback_RB   = React.useCallback;
var _useEffect_RB     = React.useEffect;
var _useRef_RB        = React.useRef;
var _useMemo_RB       = React.useMemo;

/**
 * ReportBrandingStore — the per-user `ReportBranding` record that puts a
 * Business customer's logo and words on the reports they hand to THEIR client.
 *
 * Spec: architecture/ASSESSMENT-REPORT-SPEC.md §5 (record, resolver, editor)
 * and MOULD-ASSESSMENT-MIGRATION-BRIEF.md §3 ("White-label is a report
 * concern, not an app brand" — Rozario, 2026-09-21). ADR-009 slice 4.
 *
 * Own IndexedDB key (`mould-detect-report-branding`), for the reason the
 * profile has one and then some: this record carries a **logo blob** and is
 * read only at report time. Nothing on the path to the camera should ever load
 * a 200 KB data URL it will not use, and a logo change must never rewrite the
 * scans array.
 *
 * Mechanics mirror UserProfileStore field for field — hydrate on mount, 300 ms
 * debounced persist, flush on pagehide/visibilitychange and on provider
 * unmount, a `dirtyRef` so hydration itself never writes, and persistence
 * DISABLED for the session if the read failed, so a failed read can never
 * overwrite a good stored logo with an empty record.
 *
 * Contract: nothing here throws. `setBranding` is a strict allowlist writer
 * (unknown keys dropped, `undefined` coerced, lengths capped). An unreadable
 * record degrades to "no branding", which the resolver answers with the Mould
 * Detect default — the safe direction, since the failure mode of guessing is
 * putting somebody's logo on somebody else's report.
 *
 * **The gate is not here.** `useEntitlements().whiteLabelPdf` is re-checked
 * inside the renderer by `asResolveBranding` (spec R4). This store will happily
 * hold a record for a Free user who tried the editor behind the dev flag; that
 * record simply never renders. Gating at the seam rather than the surface is
 * the whole point.
 *
 * Cloud mirror (I-08, replaces the Clerk `unsafeMetadata.mdBranding` mirror):
 * every write mirrors best-effort to the sync Lambda's `branding/current/`
 * objects, exactly as the profile does — the record as `record.json` and the
 * logo as `logo.png`, the same objects Cloud backup writes. A new device
 * therefore gets the words AND the logo back.
 *
 * File-local helpers use the `rb` prefix (no module system — every top-level
 * var is a window global, CLAUDE.md hard rule). ES5 for Babel 6.
 */

var ReportBrandingContext = _createContext_RB(null);

var RB_BRANDING_VERSION = 1;

/* §5 field caps. The copy limits are the spec's; the short ones stop a paste
   accident from becoming a header that pushes the risk badge off the page. */
var RB_NAME_MAX    = 80;
var RB_CONTACT_MAX = 120;
var RB_INTRO_MAX   = 400;
var RB_FOOTER_MAX  = 200;

/* Logo limits (§5): resized client-side to ≤ 600 px wide and ≤ 200 KB as a
   data URL. 600 px is ~2× the header's printed width at 300 dpi, which is as
   much resolution as a print path can use. */
var RB_LOGO_MAX_WIDTH = 600;
var RB_LOGO_MAX_BYTES = 200 * 1024;

/* Accepted logo types. SVG is deliberately NOT accepted: an SVG is a document
   that can carry script, and this one gets embedded in a page we print. */
var RB_LOGO_TYPES = ['image/png', 'image/jpeg', 'image/jpg'];

/** rbStr — a trimmed, capped string; never undefined, never a non-string. */
function rbStr(value, maxLen) {
    if (typeof value !== 'string') return '';
    var trimmed = value.replace(/^\s+|\s+$/g, '');
    if (maxLen && trimmed.length > maxLen) return trimmed.slice(0, maxLen);
    return trimmed;
}

/** rbNow — ISO-8601, the format every record here stamps with. */
function rbNow() { return new Date().toISOString(); }

/**
 * rbNextStamp — `updatedAt` for a write over a record stamped `prevIso`: now,
 * but never earlier than 1 ms after it (CLIENT-CONTRACT.md "Restore & conflict
 * rules" R2 — a slow clock must not make a later edit lose to an older copy).
 */
function rbNextStamp(prevIso) {
    var now = Date.now();
    var prev = (typeof prevIso === 'string') ? Date.parse(prevIso) : NaN;
    return new Date((isFinite(prev) && prev >= now) ? prev + 1 : now).toISOString();
}

/**
 * rbDataUrl — a logo data URL we are willing to store and print, or null.
 * Anything that is not a PNG/JPEG data URL of an acceptable size is treated as
 * absent rather than repaired: a half-understood image in a paid deliverable is
 * worse than no image.
 */
function rbDataUrl(value) {
    if (typeof value !== 'string') return null;
    if (!/^data:image\/(png|jpeg|jpg);base64,/.test(value)) return null;
    if (value.length > RB_LOGO_MAX_BYTES * 1.5) return null; // base64 is ~4/3 of the bytes
    return value;
}

/** rbDefaultBranding — the empty record. Never shared by reference. */
function rbDefaultBranding() {
    return {
        v: RB_BRANDING_VERSION,
        businessName: '',
        logoDataUrl: null,
        contactLine: '',
        introCopy: '',
        footerCopy: '',
        // Reserved by §5 and deliberately not editable yet: an accent colour
        // that a customer picks can land on any of the four band colours and
        // break the legibility rule. It exists in the shape so turning it on
        // later is a feature, not a migration.
        accent: null,
        updatedAt: null
    };
}

/**
 * rbNormaliseBranding — THE allowlist writer (§5). Builds the record field by
 * field: unknown keys dropped, `undefined` coerced, lengths capped, the logo
 * validated. JSON primitives only, so it rides the future opaque S3 sync and a
 * Swift `Codable` port unchanged.
 */
function rbNormaliseBranding(raw) {
    var out = rbDefaultBranding();
    if (!raw || typeof raw !== 'object') return out;
    out.businessName = rbStr(raw.businessName, RB_NAME_MAX);
    out.logoDataUrl  = rbDataUrl(raw.logoDataUrl);
    out.contactLine  = rbStr(raw.contactLine, RB_CONTACT_MAX);
    out.introCopy    = rbStr(raw.introCopy, RB_INTRO_MAX);
    out.footerCopy   = rbStr(raw.footerCopy, RB_FOOTER_MAX);
    out.accent       = null;
    out.updatedAt    = rbStr(raw.updatedAt, 40) || null;
    return out;
}

/**
 * rbMigrateBranding — the migration hook. Switch on the stored `v`; today
 * 1 → 1 is the identity normalisation. An absent, corrupt or future-versioned
 * record becomes the empty default rather than throwing — and the empty
 * default is what the resolver answers with the Mould Detect brand, so the
 * failure mode is "our logo", never "someone else's".
 */
function rbMigrateBranding(raw) {
    if (!raw || typeof raw !== 'object') return rbDefaultBranding();
    var v = (typeof raw.v === 'number') ? raw.v : 0;
    switch (v) {
        case 1:
            return rbNormaliseBranding(raw);
        default:
            return rbDefaultBranding();
    }
}

/** rbHasBranding — is there anything here worth resolving? (The resolver asks
 *  the same question of businessName; this is the editor's version.) */
function rbHasBranding(record) {
    if (!record) return false;
    return !!(rbStr(record.businessName) || record.logoDataUrl ||
              rbStr(record.contactLine) || rbStr(record.introCopy) || rbStr(record.footerCopy));
}

/**
 * rbMirrorToCloud — fire-and-forget mirror of the record (logo included) to the
 * sync Lambda's `branding/current/` objects (SyncService.mirror; same envelope
 * and R1–R10 rules as Cloud backup). Works for both AUTH_PROVIDER values. No
 * await, no state, never throws; signed out ⇒ nothing.
 */
function rbMirrorToCloud(auth, record) {
    try {
        if (!auth || !auth.isSignedIn || !record) return;
        if (typeof SyncService === 'undefined' || typeof SyncService.mirror !== 'function') return;
        SyncService.mirror('branding', record, { getToken: auth.getToken });
    } catch (err) {
        console.warn('ReportBrandingStore: cloud branding mirror threw (ignored):', err);
    }
}

/**
 * rbDeleteCloudLogo — the user removed the logo (board Q-13, contract 1.0.4):
 * queue the delete of `branding/current/logo.png` (SyncService.deleteBrandingLogo,
 * the photo-delete queue). Called signed in or not: signed out it stays queued
 * and goes with the next mirror or backup. `removedAt` is the removal's own
 * `updatedAt`, so a logo set again later cancels the queued delete. Never
 * throws, never awaited.
 */
function rbDeleteCloudLogo(auth, removedAt) {
    try {
        if (typeof SyncService === 'undefined' || typeof SyncService.deleteBrandingLogo !== 'function') return;
        SyncService.deleteBrandingLogo({ removedAt: removedAt, getToken: auth && auth.getToken });
    } catch (err) {
        console.warn('ReportBrandingStore: cloud logo delete threw (ignored):', err);
    }
}

/**
 * rbResizeLogo — a picked file as a data URL the record will accept, resized
 * on the client (§5): ≤ 600 px wide, ≤ 200 KB.
 *
 * PNG first, because a logo is usually a PNG with transparency and flattening
 * it onto white would put a white slab on a coloured letterhead. If the PNG
 * comes back over budget the fallback is JPEG at descending quality — a
 * transparent logo flattened onto white still prints correctly on the report's
 * white ground, and an over-budget record is refused outright rather than
 * stored and silently dropped by the allowlist later.
 *
 * Resolves `{dataUrl, width, height, bytes, type}`, or REJECTS with a message
 * the sheet shows verbatim. It never throws synchronously.
 */
function rbResizeLogo(file) {
    return new Promise(function (resolve, reject) {
        if (!file) { reject(new Error('No file was chosen.')); return; }
        var type = String(file.type || '').toLowerCase();
        var accepted = false;
        for (var i = 0; i < RB_LOGO_TYPES.length; i++) if (RB_LOGO_TYPES[i] === type) accepted = true;
        if (!accepted) { reject(new Error('Logos must be a PNG or JPEG image.')); return; }

        var reader = new FileReader();
        reader.onerror = function () { reject(new Error('That file could not be read.')); };
        reader.onload = function () {
            var img = new Image();
            img.onerror = function () {
                img.onload = img.onerror = null;
                reject(new Error('That file is not an image we can read.'));
            };
            img.onload = function () {
                img.onload = img.onerror = null;
                try {
                    var w = img.width || 1;
                    var h = img.height || 1;
                    if (w > RB_LOGO_MAX_WIDTH) {
                        h = Math.max(1, Math.round(h * RB_LOGO_MAX_WIDTH / w));
                        w = RB_LOGO_MAX_WIDTH;
                    }
                    var canvas = document.createElement('canvas');
                    canvas.width = w;
                    canvas.height = h;
                    var ctx = canvas.getContext('2d');
                    ctx.drawImage(img, 0, 0, w, h);

                    var out = canvas.toDataURL('image/png');
                    var outType = 'image/png';
                    var qualities = [0.9, 0.75, 0.6];
                    for (var q = 0; q < qualities.length && out.length > RB_LOGO_MAX_BYTES; q++) {
                        // Flatten onto white before the JPEG pass: JPEG has no
                        // alpha, and an unflattened transparent area encodes as
                        // black, which would turn a logo into a silhouette.
                        var flat = document.createElement('canvas');
                        flat.width = w; flat.height = h;
                        var fctx = flat.getContext('2d');
                        fctx.fillStyle = '#ffffff';
                        fctx.fillRect(0, 0, w, h);
                        fctx.drawImage(canvas, 0, 0);
                        out = flat.toDataURL('image/jpeg', qualities[q]);
                        outType = 'image/jpeg';
                        flat.width = 0; flat.height = 0;
                    }

                    ctx.clearRect(0, 0, w, h);
                    canvas.width = 0;
                    canvas.height = 0;

                    if (out.length > RB_LOGO_MAX_BYTES) {
                        reject(new Error('That logo is still over 200 KB after resizing. Try a simpler image.'));
                        return;
                    }
                    resolve({ dataUrl: out, width: w, height: h, bytes: out.length, type: outType });
                } catch (err) {
                    reject(new Error('That logo could not be resized.'));
                }
            };
            img.src = reader.result;
        };
        try {
            reader.readAsDataURL(file);
        } catch (err) {
            reject(new Error('That file could not be read.'));
        }
    });
}

var ReportBrandingProvider = function (props) {
    var brandingState = _useState_RB(rbDefaultBranding);
    var branding = brandingState[0];
    var setBrandingState = brandingState[1];

    var hydratedState = _useState_RB(false);
    var hydrated = hydratedState[0];
    var setHydrated = hydratedState[1];

    var failedState = _useState_RB(false);
    var hydrationFailed = failedState[0];
    var setHydrationFailed = failedState[1];

    var savedState = _useState_RB(0);
    var savedAt = savedState[0];
    var setSavedAt = savedState[1];

    var auth = useAuth();
    var authRef = _useRef_RB(auth);
    authRef.current = auth;

    // Gates, both of them, for the same reasons UserProfileStore has both:
    // never persist before the stored record has been read, and never persist
    // the hydration result back over itself.
    var hydratedRef = _useRef_RB(false);
    var dirtyRef = _useRef_RB(false);
    var latestRef = _useRef_RB(branding);
    latestRef.current = branding;

    _useEffect_RB(function () {
        StorageService.loadReportBranding().then(function (raw) {
            if (raw) setBrandingState(rbMigrateBranding(raw));
            hydratedRef.current = true;
            setHydrated(true);
        })['catch'](function (err) {
            // Deliberately do NOT mark hydratedRef: persisting the empty default
            // after a failed read would delete the customer's logo.
            console.error('ReportBrandingStore hydration failed — branding persistence disabled for this session:', err);
            setHydrationFailed(true);
            setHydrated(true);
        });
    }, []);

    var debounceRef = _useRef_RB(null);

    _useEffect_RB(function () {
        if (!hydratedRef.current || !dirtyRef.current) return;
        if (debounceRef.current) clearTimeout(debounceRef.current);
        debounceRef.current = setTimeout(function () {
            debounceRef.current = null;
            StorageService.saveReportBranding(branding).then(function () {
                setSavedAt(Date.now());
            })['catch'](function (err) {
                console.error('Failed to persist report branding:', err);
            });
        }, 300);
        return function () {
            if (debounceRef.current) clearTimeout(debounceRef.current);
        };
    }, [branding]);

    var flushPendingWrite = _useCallback_RB(function () {
        if (!debounceRef.current) return;
        clearTimeout(debounceRef.current);
        debounceRef.current = null;
        StorageService.saveReportBranding(latestRef.current)['catch'](function (err) {
            console.error('Failed to flush report branding on page hide:', err);
        });
    }, []);

    _useEffect_RB(function () {
        var onVisibilityChange = function () {
            if (document.visibilityState === 'hidden') flushPendingWrite();
        };
        window.addEventListener('pagehide', flushPendingWrite);
        document.addEventListener('visibilitychange', onVisibilityChange);
        return function () {
            window.removeEventListener('pagehide', flushPendingWrite);
            document.removeEventListener('visibilitychange', onVisibilityChange);
            flushPendingWrite();
        };
    }, [flushPendingWrite]);

    /**
     * setBranding(patch) — the ONLY writer. Builds the record field by field
     * through rbNormaliseBranding from `latestRef` (not a setState updater), so
     * the normalised record can be RETURNED synchronously — the editor's live
     * header preview reads it on the same tick.
     *
     * `logoDataUrl: null` in a patch clears the logo; omitting the key keeps
     * it. That asymmetry is deliberate and is why "Remove" passes an explicit
     * null rather than an empty string.
     */
    var setBranding = _useCallback_RB(function (patch) {
        var prev = latestRef.current || rbDefaultBranding();
        var merged = Object.assign({}, prev, patch || {}, { updatedAt: rbNextStamp(prev.updatedAt) });
        var next = rbNormaliseBranding(merged);
        if (window.MetricsService) MetricsService.track('branding_saved', { hasLogo: !!next.logoDataUrl }); // product metrics §2 — whether a logo exists, never the business name or the image
        latestRef.current = next;
        dirtyRef.current = true;
        setBrandingState(next);
        // After the local write, never awaited: IndexedDB is the read path, the
        // mirror only lets the words outlive this browser.
        rbMirrorToCloud(authRef.current, next);
        if (prev.logoDataUrl && !next.logoDataUrl) rbDeleteCloudLogo(authRef.current, next.updatedAt);
        return next;
    }, []);

    /** clearBranding — back to the Mould Detect default, logo included. */
    var clearBranding = _useCallback_RB(function () {
        var hadLogo = !!(latestRef.current && latestRef.current.logoDataUrl);
        var next = rbDefaultBranding();
        next.updatedAt = rbNextStamp(latestRef.current && latestRef.current.updatedAt);
        latestRef.current = next;
        dirtyRef.current = true;
        setBrandingState(next);
        rbMirrorToCloud(authRef.current, next);
        if (hadLogo) rbDeleteCloudLogo(authRef.current, next.updatedAt);
        return next;
    }, []);

    /**
     * Cloud adoption (I-08) — once per signed-in session, after a SUCCESSFUL
     * hydration: nothing stored here (never written, never cleared) ⇒ pull
     * `branding/current/` through the importer (R3 decides; a logo comes too);
     * otherwise mirror what is here, so a first sign-in gets a cloud copy
     * without waiting for an edit (skip-unchanged makes it free when the
     * cloud already has it).
     */
    var cloudAdoptedRef = _useRef_RB(false);

    _useEffect_RB(function () {
        if (!hydrated || hydrationFailed || cloudAdoptedRef.current) return;
        if (!auth || !auth.isSignedIn) return;
        if (typeof SyncService === 'undefined' || typeof SyncService.pull !== 'function') return;
        cloudAdoptedRef.current = true;
        var current = latestRef.current;
        if (rbHasBranding(current) || (current && current.updatedAt)) rbMirrorToCloud(auth, current);
        else SyncService.pull('branding', { getToken: auth.getToken });
    }, [hydrated, hydrationFailed, auth]);

    /**
     * importBranding(record) — the ONLY way a cloud restore reaches this store
     * (review F1). One record, so the service's rule applied once: the remote
     * wins only when its `updatedAt` is STRICTLY newer. Normalised on the way in
     * (`rbMigrateBranding`), so a backup written by another client — or a
     * future version — can never put an unvalidated logo in front of a customer.
     *
     * Deliberately does NOT mirror to the cloud: this is someone else's device's
     * record arriving, not a thing the user just typed here.
     *
     * Returns `{ added, replaced, kept }`, or null when hydration failed.
     */
    var importBranding = _useCallback_RB(function (record) {
        if (!hydratedRef.current) return null;
        if (!record || typeof record !== 'object') return { added: 0, replaced: 0, kept: 0 };
        var next = rbMigrateBranding(record);
        var prev = latestRef.current || null;
        var had = rbHasBranding(prev);
        // A CLEARED record is still a record (contract R1): "Remove branding"
        // stamps `updatedAt`, so an older remote copy must not bring the old
        // business name back. Only a never-written default (no stamp) yields.
        var comparable = (had || (prev && prev.updatedAt)) ? prev : null;
        if (!window.syRemoteWins('branding', comparable, next)) return { added: 0, replaced: 0, kept: 1 };
        latestRef.current = next;
        dirtyRef.current = true;
        setBrandingState(next);
        return { added: had ? 0 : 1, replaced: had ? 1 : 0, kept: 0 };
    }, []);

    _useEffect_RB(function () {
        if (typeof SyncService === 'undefined' || !SyncService.registerImporter) return;
        return SyncService.registerImporter('branding', importBranding);
    }, [importBranding]);

    var contextValue = _useMemo_RB(function () {
        return {
            branding: branding,
            hydrated: hydrated,
            hydrationFailed: hydrationFailed,
            hasBranding: rbHasBranding(branding),
            savedAt: savedAt,
            setBranding: setBranding,
            clearBranding: clearBranding,
            resizeLogo: rbResizeLogo,
            // --- slice 6: cloud restore ---
            importBranding: importBranding
        };
    }, [branding, hydrated, hydrationFailed, savedAt, setBranding, clearBranding, importBranding]);

    return React.createElement(ReportBrandingContext.Provider, { value: contextValue }, props.children);
};

/**
 * useReportBranding — context hook. Like useUserProfile and useAssessments it
 * does NOT throw when the provider is missing: a tree mounted without it
 * degrades to "no branding", which resolves to the Mould Detect default.
 */
var useReportBranding = function () {
    var ctx = _useContext_RB(ReportBrandingContext);
    if (ctx) return ctx;
    return {
        branding: rbDefaultBranding(),
        hydrated: false,
        hydrationFailed: true,
        hasBranding: false,
        savedAt: 0,
        setBranding: function () { return null; },
        clearBranding: function () { return null; },
        resizeLogo: rbResizeLogo,
        importBranding: function () { return null; }
    };
};

/**
 * useReportBrandingEnabled — the ONE place the editor's enabled/disabled state
 * is decided (§5): `useEntitlements().whiteLabelPdf` OR the
 * `REPORT_BRANDING_PREVIEW` dev flag.
 *
 * This is the EDITOR's gate, not the RENDERER's. The renderer re-checks for
 * itself inside `asResolveBranding` (spec R4) — which is why this hook returns
 * a `brandingPreview` field for the page to pass along rather than letting the
 * page decide what the report shows.
 *
 * Slice 5 made `REPORT_BRANDING_PREVIEW` **redundant**: the newer
 * `ENTITLEMENTS_PREVIEW_TIER` flag set to business, professional or enterprise
 * arrives here as a real `ent.whiteLabelPdf === true`, so both this gate and the
 * resolver's seam open through the ordinary entitlement path rather than
 * through an override. The old key is kept — it still works, and something may
 * yet be pointing at it — but the tier preview is the one to reach for, because
 * it exercises the seam the way a paying customer will.
 */
var useReportBrandingEnabled = function () {
    var ent = useEntitlements();
    var preview = useFeatureFlag('REPORT_BRANDING_PREVIEW');
    return {
        entitled: !!ent.whiteLabelPdf,
        brandingPreview: !!preview,
        enabled: !!ent.whiteLabelPdf || !!preview
    };
};

/* §5 — the line shown below Business. Kept as a constant because the report
   spec quotes it and the tests assert it. */
var RB_LOCKED_COPY = 'Add your logo and details to reports from the Business plan';

var RB_INPUT_CLS = 'w-full bg-surface border-none rounded-xl text-sm p-3 shadow-soft ring-1 ring-sage/20 focus:ring-2 focus:ring-primary font-semibold text-forest placeholder:text-sage/60 placeholder:font-medium disabled:opacity-60';
var RB_LABEL_CLS = 'text-[10px] font-black text-sage uppercase tracking-[0.15em] ml-1';

/**
 * ReportBrandingSheet — the editor (§5).
 *
 * A bottom sheet over the report, because that is where branding is decided:
 * the inspector is looking at the document they are about to hand over. The
 * live header preview at the top is the same block `asResolveBranding` feeds
 * the real header, rendered through the same resolver — so what it shows is
 * what prints, including the fact that below Business it shows Mould Detect.
 *
 * Below Business (and with the preview flag off) every field is VISIBLE and
 * disabled with §5's line. Hiding them would make the capability invisible to
 * the customer who is deciding whether to pay for it.
 */
var ReportBrandingSheet = function (props) {
    var store = useReportBranding();
    var gate = useReportBrandingEnabled();
    var branding = store.branding;
    var setBranding = store.setBranding;

    var errorState = _useState_RB('');
    var error = errorState[0];
    var setError = errorState[1];

    var busyState = _useState_RB(false);
    var busy = busyState[0];
    var setBusy = busyState[1];

    var fileRef = _useRef_RB(null);

    var disabled = !gate.enabled;

    // The preview runs through the REAL resolver with the REAL gate, so an
    // unentitled user sees exactly what their report will show — Mould Detect.
    var preview = asResolveBranding(
        { whiteLabelPdf: gate.entitled, brandingPreview: gate.brandingPreview },
        branding
    );

    var onPickLogo = function (e) {
        var file = e.target.files && e.target.files[0];
        // Clear the input so picking the SAME file twice still fires a change.
        e.target.value = '';
        if (!file) return;
        setError('');
        setBusy(true);
        store.resizeLogo(file).then(function (out) {
            setBusy(false);
            setBranding({ logoDataUrl: out.dataUrl });
        })['catch'](function (err) {
            setBusy(false);
            setError((err && err.message) || 'That logo could not be used.');
        });
    };

    var textField = function (label, testid, value, key, max, placeholder, multiline) {
        var remaining = max - (value || '').length;
        return (
            <div className="flex flex-col gap-1">
                <div className="flex items-center justify-between gap-2">
                    <label className={RB_LABEL_CLS}>{label}</label>
                    {max >= 200 && (
                        <span className="text-[9px] font-bold text-forest/60">{remaining} left</span>
                    )}
                </div>
                {multiline ? (
                    <textarea
                        data-testid={testid}
                        rows={3}
                        maxLength={max}
                        disabled={disabled}
                        className={RB_INPUT_CLS}
                        placeholder={placeholder}
                        value={value}
                        onChange={function (e) { var p = {}; p[key] = e.target.value; setBranding(p); }}
                    />
                ) : (
                    <input
                        type="text"
                        data-testid={testid}
                        maxLength={max}
                        disabled={disabled}
                        className={RB_INPUT_CLS}
                        placeholder={placeholder}
                        value={value}
                        onChange={function (e) { var p = {}; p[key] = e.target.value; setBranding(p); }}
                    />
                )}
            </div>
        );
    };

    return (
        <div
            data-testid="report-branding-sheet"
            /* Above the bottom-nav island (z-50), which is mounted outside the
               routes and would otherwise float over the sheet's own controls. */
            className="fixed inset-0 z-[60] flex items-end justify-center bg-forest/40 backdrop-blur-sm"
            onClick={function (e) { if (e.target === e.currentTarget && props.onClose) props.onClose(); }}
        >
            <div className="w-full max-w-md bg-background-light rounded-t-2xl shadow-2xl max-h-[88dvh] overflow-y-auto">
                <div className="sticky top-0 bg-background-light/95 backdrop-blur-md px-5 pt-5 pb-3 flex items-center justify-between gap-3 z-10">
                    <div className="min-w-0">
                        <h2 className="text-base font-extrabold text-forest">Report branding</h2>
                        <p className="text-[11px] font-semibold text-muted">Your logo and words on the reports you hand over</p>
                    </div>
                    <button
                        data-testid="branding-close"
                        onClick={function () { if (props.onClose) props.onClose(); }}
                        className="w-9 h-9 rounded-full bg-surface shadow-soft text-forest flex items-center justify-center shrink-0 btn-nature"
                        aria-label="Close"
                    >
                        <span className="material-symbols-outlined text-lg">close</span>
                    </button>
                </div>

                <div className="px-5 pb-8 space-y-4">
                    {/* Live header preview — the resolver's own output. */}
                    <div className="bio-bg bio-bg-10 rounded-xl p-4 shadow-soft border border-stone-100/50 space-y-2">
                        <span className={RB_LABEL_CLS}>Header preview</span>
                        <div data-testid="branding-preview" className="bg-surface rounded-xl p-4 shadow-soft flex items-center gap-3">
                            {preview.logo ? (
                                <img
                                    data-testid="branding-preview-logo"
                                    src={preview.logo}
                                    alt={preview.name}
                                    className="h-10 w-auto max-w-[140px] object-contain shrink-0"
                                />
                            ) : (
                                <span data-testid="branding-preview-name" className="text-base font-extrabold text-forest">{preview.name}</span>
                            )}
                            <div className="min-w-0">
                                {preview.logo && (
                                    <p className="text-xs font-extrabold text-forest truncate">{preview.name}</p>
                                )}
                                {preview.contact && (
                                    <p className="text-[11px] font-semibold text-muted truncate">{preview.contact}</p>
                                )}
                            </div>
                        </div>
                        {!gate.enabled && (
                            <p data-testid="branding-locked" className="text-[11px] font-bold text-forest bg-[#d4a373]/25 rounded-xl px-3 py-2">
                                {RB_LOCKED_COPY}
                            </p>
                        )}
                    </div>

                    {/* Logo */}
                    <div className="bio-bg bio-bg-20 rounded-xl p-4 shadow-soft border border-stone-100/50 space-y-3">
                        <span className={RB_LABEL_CLS}>Logo</span>
                        <p className="text-[11px] font-medium text-forest/70 leading-relaxed">
                            PNG or JPEG. Resized here to 600 px wide and kept under 200 KB, so it prints sharp and the
                            record stays small. With cloud backup on, a copy is kept with your account; removing the logo here removes that copy too.
                        </p>
                        {branding.logoDataUrl && (
                            <div className="bg-surface rounded-xl p-3 shadow-soft flex items-center gap-3">
                                <img
                                    data-testid="branding-logo-preview"
                                    src={branding.logoDataUrl}
                                    alt="Your logo"
                                    className="h-12 w-auto max-w-[160px] object-contain"
                                />
                                <button
                                    data-testid="branding-logo-remove"
                                    disabled={disabled}
                                    onClick={function () { setError(''); setBranding({ logoDataUrl: null }); }}
                                    className="ml-auto text-[10px] font-black text-sage uppercase tracking-widest btn-nature disabled:opacity-40"
                                >
                                    Remove
                                </button>
                            </div>
                        )}
                        <input
                            ref={fileRef}
                            type="file"
                            accept="image/png,image/jpeg"
                            data-testid="branding-logo-input"
                            className="hidden"
                            onChange={onPickLogo}
                        />
                        <button
                            data-testid="branding-logo-pick"
                            disabled={disabled || busy}
                            onClick={function () { if (fileRef.current) fileRef.current.click(); }}
                            className="w-full bg-forest text-white font-extrabold py-3 rounded-xl shadow-clay flex items-center justify-center gap-2 btn-nature hover:brightness-110 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
                        >
                            <span className="material-symbols-outlined text-lg">image</span>
                            {busy ? 'Resizing…' : (branding.logoDataUrl ? 'Replace logo' : 'Choose a logo')}
                        </button>
                        {error && (
                            <p data-testid="branding-logo-error" className="text-[11px] font-bold text-forest bg-warning-light rounded-xl px-3 py-2">{error}</p>
                        )}
                    </div>

                    {/* Words */}
                    <div className="bio-bg bio-bg-30 rounded-xl p-4 shadow-soft border border-stone-100/50 space-y-3">
                        {textField('Business name', 'branding-name', branding.businessName, 'businessName', RB_NAME_MAX, 'e.g. Riteflo Mould Services', false)}
                        {textField('Contact line', 'branding-contact', branding.contactLine, 'contactLine', RB_CONTACT_MAX, 'e.g. 0400 000 000 · hello@example.com', false)}
                        {textField('Intro paragraph', 'branding-intro', branding.introCopy, 'introCopy', RB_INTRO_MAX, 'A short paragraph that opens every report', true)}
                        {textField('Footer line', 'branding-footer', branding.footerCopy, 'footerCopy', RB_FOOTER_MAX, 'Defaults to your business name', true)}
                        <p className="text-[11px] font-medium text-forest/70 leading-relaxed">
                            Every report also carries “{AR_POWERED_BY}” in the footer.
                        </p>
                    </div>

                    <button
                        data-testid="branding-done"
                        onClick={function () { if (props.onClose) props.onClose(); }}
                        className="w-full bg-forest text-white font-extrabold py-4 rounded-xl shadow-clay btn-nature hover:brightness-110 transition-all"
                    >
                        Done
                    </button>
                </div>
            </div>
        </div>
    );
};

window.ReportBrandingProvider = ReportBrandingProvider;
window.useReportBranding = useReportBranding;
window.useReportBrandingEnabled = useReportBrandingEnabled;
window.ReportBrandingSheet = ReportBrandingSheet;
window.rbNormaliseBranding = rbNormaliseBranding;
window.rbMigrateBranding = rbMigrateBranding;
window.rbDefaultBranding = rbDefaultBranding;
window.rbResizeLogo = rbResizeLogo;
window.RB_LOCKED_COPY = RB_LOCKED_COPY;
window.RB_INTRO_MAX = RB_INTRO_MAX;
window.RB_FOOTER_MAX = RB_FOOTER_MAX;
window.RB_LOGO_MAX_WIDTH = RB_LOGO_MAX_WIDTH;
window.RB_LOGO_MAX_BYTES = RB_LOGO_MAX_BYTES;
