<?php
/**
 * Plugin Name: Contractor Roof Estimate Calculator
 * Plugin URI: https://quietcitydesign.com/
 * Description: Configurable homeowner roof replacement range estimator with printable results, lead capture, saved submissions, and email delivery.
 * Version: 5.1.7.4
 * Author: Quiet City Design
 * Author URI: https://quietcitydesign.com/
 * Text Domain: rfec
 */

if (!defined('ABSPATH')) exit;

define('RFEC_VERSION', '5.1.7.4');
define('RFEC_DIR', plugin_dir_path(__FILE__));
define('RFEC_URL', plugin_dir_url(__FILE__));
define('RFEC_QCD_ENDPOINT', 'https://quietcitydesign.com/wp-json/qcd/v1/estimator-event');
require_once RFEC_DIR.'includes/class-estimator-core.php';
require_once RFEC_DIR.'includes/class-admin-ui.php';
require_once RFEC_DIR.'includes/class-qcd-client.php';
require_once RFEC_DIR.'includes/class-email-renderer.php';

final class RFEC_Plugin {
    private static $instance;
    private $option;
    private $admin_ui;

    public static function instance() {
        return self::$instance ?: (self::$instance = new self());
    }

    private function __construct() {
        $this->option = RFEC_Estimator_Core::option_name();
        $this->admin_ui = new RFEC_Admin_UI($this->option, 'rfec');
        add_action('init', [$this, 'register_submission_type']);
        add_action('admin_menu', [$this, 'admin_menu']);
        add_action('admin_init', [$this, 'register_settings']);
        add_action('admin_init', [$this, 'maybe_upgrade']);
        add_action('admin_enqueue_scripts', [$this, 'admin_assets']);
        add_action('all_admin_notices', [$this, 'estimates_page_promotion']);
        add_action('admin_footer', [$this, 'toolkit_menu_target']);
        add_action('wp_dashboard_setup', [$this, 'dashboard_widget']);
        add_action('admin_post_rfec_qcd_optin', [$this, 'handle_qcd_optin']);
        add_action('admin_post_rfec_dismiss_milestone', [$this, 'dismiss_milestone']);
        add_action('rfec_retry_milestone_event', [$this, 'retry_milestone_event']);
        add_action('wp_enqueue_scripts', [$this, 'register_assets']);
        add_shortcode(RFEC_Estimator_Core::manifest('shortcode'), [$this, 'shortcode']);
        add_action('wp_ajax_rfec_submit', [$this, 'submit']);
        add_action('wp_ajax_nopriv_rfec_submit', [$this, 'submit']);
        add_filter('manage_'.RFEC_Estimator_Core::post_type().'_posts_columns', [$this, 'columns']);
        add_action('manage_'.RFEC_Estimator_Core::post_type().'_posts_custom_column', [$this, 'column_value'], 10, 2);
        add_action('add_meta_boxes', [$this, 'meta_boxes']);
    }

    public static function presets() {
        return [
            'CA' => [
                'market' => 'CA', 'currency' => 'CAD', 'currency_symbol' => '$', 'unit' => 'sq ft',
                'material_rates' => ['architectural_asphalt' => 9.25, 'premium_asphalt' => 11.50, 'metal_shingles' => 14.00, 'standing_seam' => 17.50, 'cedar' => 16.00, 'slate' => 25.00, 'flat_membrane' => 13.00, 'custom_1' => 0, 'custom_2' => 0, 'custom_3' => 0],
                'addon_rates' => ['vent' => 225, 'skylight' => 650, 'chimney' => 900],
                'tearoff_per_sqft' => 1.65, 'underlayment_per_sqft' => 0.75, 'permit_allowance' => 450,
                'flashing_allowance' => 850,
                'pitch_factors' => ['low'=>.95,'standard'=>1,'steep'=>1.18,'very_steep'=>1.35],
                'profile_factors' => ['standard'=>1,'premium'=>1.12,'designer'=>1.25],
                'stories_factors' => ['one'=>1,'two'=>1.08,'three_plus'=>1.18],
                'complexity_factors' => ['simple'=>.95,'average'=>1,'complex'=>1.15,'very_complex'=>1.30],
                'access_factors' => ['open'=>1,'limited'=>1.08,'hand_carry'=>1.16],
                'waste_percent' => 12, 'minimum_job' => 6500,
                'category_ranges' => ['system'=>10,'removal'=>15,'protection'=>20,'penetrations'=>25],
            ],
            'US' => [
                'market' => 'US', 'currency' => 'USD', 'currency_symbol' => '$', 'unit' => 'sq ft',
                'material_rates' => ['architectural_asphalt' => 7.00, 'premium_asphalt' => 9.00, 'metal_shingles' => 11.50, 'standing_seam' => 15.00, 'cedar' => 13.00, 'slate' => 22.00, 'flat_membrane' => 10.50, 'custom_1' => 0, 'custom_2' => 0, 'custom_3' => 0],
                'addon_rates' => ['vent' => 175, 'skylight' => 500, 'chimney' => 700],
                'tearoff_per_sqft' => 1.25, 'underlayment_per_sqft' => 0.60, 'permit_allowance' => 300,
                'flashing_allowance' => 650,
                'pitch_factors' => ['low'=>.95,'standard'=>1,'steep'=>1.18,'very_steep'=>1.35],
                'profile_factors' => ['standard'=>1,'premium'=>1.12,'designer'=>1.25],
                'stories_factors' => ['one'=>1,'two'=>1.08,'three_plus'=>1.18],
                'complexity_factors' => ['simple'=>.95,'average'=>1,'complex'=>1.15,'very_complex'=>1.30],
                'access_factors' => ['open'=>1,'limited'=>1.08,'hand_carry'=>1.16],
                'waste_percent' => 12, 'minimum_job' => 5000,
                'category_ranges' => ['system'=>10,'removal'=>15,'protection'=>20,'penetrations'=>25],
            ],
        ];
    }

    public function settings() {
        $defaults = array_merge(self::presets()['CA'], [
            'contractor_email' => get_option('admin_email'), 'booking_url' => '', 'solar_handling_note' => '',
            'contractor_name' => get_bloginfo('name'), 'accent_color' => '#1d5dbf', 'language' => 'en_CA',
            'form_heading' => 'Save and email your estimate',
            'cta_text' => 'We’ll send you a copy and share your project details with '.get_bloginfo('name').' for follow-up.',
            'submit_button_text' => 'Schedule a free on-site visit',
            'lead_email_intro' => '<p>Thank you for taking the time to tell us about your roofing project. This estimate provides a ballpark planning range. An on-site inspection is needed to confirm the project scope and final price.</p>',
            'lead_email_schedule' => '<p>To get an accurate estimate, schedule a no-obligation site inspection.</p>',
            'lead_email_signature' => '<p>Thank you,<br><strong>{contractor_name}</strong></p>',
            'lead_email_show_schedule_button' => 1,
            'lead_email_show_logo' => 0,
            'lead_email_logo_id' => 0,
            'include_tax' => 0, 'tax_rate' => 0, 'tax_label' => 'Applicable sales tax',
            'material_enabled' => array_merge(array_fill_keys(array_keys($this->materials()), 1), array_fill_keys(array_keys($this->custom_materials()), 0)),
            'material_images' => array_fill_keys(array_keys(array_merge($this->materials(), $this->custom_materials())), 0),
            'material_names' => array_fill_keys(array_keys($this->custom_materials()), ''),
            'complexity_names' => $this->default_complexity_options(),
            'complexity_images' => array_fill_keys(array_keys($this->default_complexity_options()), 0),
        ]);
        $saved = get_option($this->option, []);
        $out = wp_parse_args($saved, $defaults);
        $out['language'] = in_array($saved['language'] ?? '', ['en_CA','en_US'], true) ? $saved['language'] : (($out['market'] ?? 'CA') === 'US' ? 'en_US' : 'en_CA');
        $out['material_rates'] = wp_parse_args($saved['material_rates'] ?? [], $defaults['material_rates']);
        $out['addon_rates'] = wp_parse_args($saved['addon_rates'] ?? [], $defaults['addon_rates']);
        $out['category_ranges'] = wp_parse_args($saved['category_ranges'] ?? [], $defaults['category_ranges']);
        $out['material_enabled'] = wp_parse_args($saved['material_enabled'] ?? [], $defaults['material_enabled']);
        $out['material_images'] = wp_parse_args($saved['material_images'] ?? [], $defaults['material_images']);
        $out['material_names'] = wp_parse_args($saved['material_names'] ?? [], $defaults['material_names']);
        $out['complexity_names'] = wp_parse_args($saved['complexity_names'] ?? [], $defaults['complexity_names']);
        $out['complexity_images'] = wp_parse_args($saved['complexity_images'] ?? [], $defaults['complexity_images']);
        foreach (['pitch_factors','profile_factors','stories_factors','complexity_factors','access_factors'] as $group) $out[$group] = wp_parse_args($saved[$group] ?? [], $defaults[$group]);
        $out['accent_color'] = sanitize_hex_color($out['accent_color']) ?: '';
        $out['form_heading'] = trim((string)$out['form_heading']) ?: 'Save and email your estimate';
        $out['submit_button_text'] = trim((string)$out['submit_button_text']) ?: 'Schedule a free on-site visit';
        foreach (['lead_email_intro','lead_email_schedule','lead_email_signature'] as $key) $out[$key] = wp_kses_post($out[$key]);
        return $out;
    }

    public function register_submission_type() {
        register_post_type(RFEC_Estimator_Core::post_type(), [
            'labels' => ['name' => 'Roof Estimates', 'singular_name' => 'Roof Estimate', 'menu_name' => 'Roof Estimates', 'add_new' => '', 'add_new_item' => ''],
            'public' => false, 'show_ui' => true, 'show_in_menu' => true, 'menu_icon' => 'dashicons-clipboard',
            'supports' => ['title'], 'capability_type' => 'post', 'map_meta_cap' => true,
            'capabilities' => ['create_posts' => 'do_not_allow'],
        ]);
    }

    public function admin_menu() {
        add_submenu_page('edit.php?post_type=rfec_submission', 'Calculator Settings', 'Settings', 'manage_options', 'rfec-settings', [$this, 'settings_page']);
        global $submenu;
        $parent = 'edit.php?post_type=rfec_submission';
        $submenu[$parent][] = ['Lead Toolkit', 'manage_options', $this->qcd_url('admin-menu')];
    }

    public function toolkit_menu_target() {
        ?><script>document.addEventListener('DOMContentLoaded',function(){var link=document.querySelector('#adminmenu a[href*="utm_content=admin-menu"]');if(link){link.target='_blank';link.rel='noopener noreferrer';}});</script><?php
    }

    public function register_settings() {
        register_setting('rfec_group', $this->option, ['sanitize_callback' => [$this, 'sanitize_settings']]);
    }

    public function sanitize_settings($input) {
        $preset_key = in_array($input['market'] ?? 'CA', ['CA', 'US'], true) ? $input['market'] : 'CA';
        $preset = self::presets()[$preset_key];
        $out = [];
        foreach (['market','language','currency','currency_symbol','unit','contractor_name','form_heading','submit_button_text','tax_label','solar_handling_note'] as $key) $out[$key] = sanitize_text_field($input[$key] ?? $preset[$key] ?? '');
        $out['language'] = in_array($out['language'], ['en_CA','en_US'], true) ? $out['language'] : ($preset_key === 'US' ? 'en_US' : 'en_CA');
        $out['form_heading'] = trim($out['form_heading']) ?: 'Save and email your estimate';
        $out['submit_button_text'] = trim($out['submit_button_text']) ?: 'Schedule a free on-site visit';
        $out['accent_color'] = sanitize_hex_color($input['accent_color'] ?? '') ?: '';
        $out['cta_text'] = sanitize_textarea_field($input['cta_text'] ?? '');
        foreach (['lead_email_intro','lead_email_schedule','lead_email_signature'] as $key) $out[$key] = wp_kses_post($input[$key] ?? '');
        $out['lead_email_show_schedule_button'] = empty($input['lead_email_show_schedule_button']) ? 0 : 1;
        $out['lead_email_show_logo'] = empty($input['lead_email_show_logo']) ? 0 : 1;
        $out['lead_email_logo_id'] = absint($input['lead_email_logo_id'] ?? 0);
        $out['contractor_email'] = sanitize_email($input['contractor_email'] ?? get_option('admin_email'));
        $out['booking_url'] = esc_url_raw($input['booking_url'] ?? '');
        $out['include_tax'] = empty($input['include_tax']) ? 0 : 1;
        $out['tax_rate'] = min(100, max(0, (float)($input['tax_rate'] ?? 0)));
        $out['tax_label'] = trim($out['tax_label']) ?: 'Applicable sales tax';
        foreach (['tearoff_per_sqft','underlayment_per_sqft','permit_allowance','flashing_allowance','waste_percent','minimum_job'] as $key) $out[$key] = max(0, (float)($input[$key] ?? $preset[$key]));
        foreach ($preset['material_rates'] as $key => $value) $out['material_rates'][$key] = max(0, (float)($input['material_rates'][$key] ?? $value));
        $enabled_count = 0;
        foreach ($preset['material_rates'] as $key => $value) {
            $out['material_enabled'][$key] = empty($input['material_enabled'][$key]) ? 0 : 1;
            $out['material_images'][$key] = absint($input['material_images'][$key] ?? 0);
            if (isset($this->custom_materials()[$key])) {
                $out['material_names'][$key] = sanitize_text_field($input['material_names'][$key] ?? '');
                if ($out['material_enabled'][$key] && (!$out['material_names'][$key] || $out['material_rates'][$key] <= 0)) {
                    $out['material_enabled'][$key] = 0;
                    add_settings_error($this->option, 'rfec_'.$key.'_incomplete', 'A custom roof type was left disabled because it needs both a name and a price greater than zero.', 'warning');
                }
            }
            $enabled_count += $out['material_enabled'][$key];
        }
        if (!$enabled_count) {
            add_settings_error($this->option, 'rfec_material_required', 'At least one roof type must remain enabled. All roof types were restored.', 'error');
            foreach ($preset['material_rates'] as $key => $value) $out['material_enabled'][$key] = isset($this->custom_materials()[$key]) ? 0 : 1;
        }
        foreach ($preset['addon_rates'] as $key => $value) $out['addon_rates'][$key] = max(0, (float)($input['addon_rates'][$key] ?? $value));
        foreach ($preset['category_ranges'] as $key=>$value) $out['category_ranges'][$key]=min(100,max(0,(float)($input['category_ranges'][$key]??$value)));
        foreach (['pitch_factors','profile_factors','stories_factors','complexity_factors','access_factors'] as $group) foreach ($preset[$group] as $key=>$value) $out[$group][$key]=max(0,(float)($input[$group][$key]??$value));
        foreach ($this->default_complexity_options() as $key=>$label) {
            $out['complexity_names'][$key] = sanitize_text_field($input['complexity_names'][$key] ?? $label) ?: $label;
            $out['complexity_images'][$key] = absint($input['complexity_images'][$key] ?? 0);
        }
        return $out;
    }

    public function maybe_upgrade() {
        $installed = get_option('rfec_version', '1.0.0');
        if (version_compare($installed, RFEC_VERSION, '>=')) return;
        $saved = get_option($this->option, []);
        if (version_compare($installed, '1.8.0', '<')) {
            $triggered = get_option('rfec_triggered_milestones', []);
            $count = $this->estimate_count();
            foreach ($this->milestones_up_to($count) as $milestone) {
                if (empty($triggered[$milestone])) {
                    $triggered[$milestone] = ['status' => 'historical', 'timestamp' => current_time('mysql')];
                }
            }
            update_option('rfec_triggered_milestones', $triggered, false);
        }
        update_option('rfec_version', RFEC_VERSION);
    }

    public function settings_page() {
        if (!current_user_can('manage_options')) return;
        $s = $this->settings();
        $materials = array_merge($this->materials(), $this->custom_materials());
        ?>
        <div class="wrap rfec-admin">
        <form id="rfec-settings-form" method="post" action="options.php" data-rfec-settings-form><?php settings_fields('rfec_group'); ?>
        <div class="rfec-settings-header"><div><h1>Roof Calculator Settings</h1><p>Configure the calculator below, then add <code>[roof_estimate_calculator]</code> to any page.</p></div></div>
        <nav class="nav-tab-wrapper rfec-settings-tabs" aria-label="Calculator settings sections">
            <button type="button" class="nav-tab nav-tab-active" data-rfec-tab="welcome" aria-selected="true">Start Here</button>
            <button type="button" class="nav-tab" data-rfec-tab="business" aria-selected="false">Business Info</button>
            <button type="button" class="nav-tab" data-rfec-tab="form" aria-selected="false">Market Form</button>
            <button type="button" class="nav-tab" data-rfec-tab="products" aria-selected="false">Product Configuration</button>
            <button type="button" class="nav-tab" data-rfec-tab="multipliers" aria-selected="false">Adjustments</button>
            <button type="button" class="nav-tab" data-rfec-tab="emails" aria-selected="false">Email Notifications</button>
            <button type="button" class="nav-tab" data-rfec-tab="test" aria-selected="false">Test Estimate Form</button>
        </nav>

        <section class="rfec-settings-panel" data-rfec-panel="welcome">
        <h2>Start Here</h2>
        <div class="rfec-welcome-grid">
            <div class="rfec-welcome-card">
                <h3>Access Calculator Training</h3>
                <p>Get practical setup training, calculator improvements and contractor onboarding tactics from Quiet City Design. These updates are designed to help you turn more estimate requests into real sales conversations.</p>
                <?php $this->qcd_optin_panel(true); ?>
            </div>
            <div class="rfec-welcome-card">
                <h3>Quick Setup Checklist</h3>
                <ol>
                    <li>Add your contractor name, lead email and booking link.</li>
                    <li>Review the roof types, base prices and product configuration.</li>
                    <li>Confirm the allowances and adjustments match how you price work.</li>
                    <li>Add <code>[roof_estimate_calculator]</code> to the page where visitors should request an estimate.</li>
                    <li>Save your settings, then open the <button type="button" class="button-link" data-rfec-open-tab="test">Test Estimate Form</button> to check pricing without saving a lead.</li>
                </ol>
                <div class="rfec-toolkit-callout">
                    <h3>Need the follow-up system too?</h3>
                    <p>The Lead System Toolkit helps contractors reply faster, catch missed calls and keep every customer conversation in one inbox.</p>
                    <p><a class="button button-secondary" href="<?php echo esc_url($this->qcd_toolkit_url('settings-start-here', 'the-toolkit/')); ?>" target="_blank" rel="noopener noreferrer">See The Lead System Toolkit</a></p>
                </div>
            </div>
        </div>
        </section>

        <section class="rfec-settings-panel" data-rfec-panel="business" hidden>
        <h2>Business Info</h2>
        <p class="rfec-tab-intro">Tell the calculator who should receive new leads and where visitors can schedule an on-site visit. These details are also used in estimate emails and follow-up actions.</p>
        <table class="form-table"><tbody>
        <?php $this->text_row('Contractor name', 'contractor_name', $s['contractor_name']); ?>
        <?php $this->text_row('Lead notification email', 'contractor_email', $s['contractor_email'], 'email', 'New estimate requests and homeowner details are sent to this address.'); ?>
        <?php $this->text_row('On-site booking link', 'booking_url', $s['booking_url'], 'url', 'After submitting the final form, the homeowner is sent to this scheduling page.'); ?>
        <?php $this->text_row('Solar project handling note', 'solar_handling_note', $s['solar_handling_note'], 'text', 'When a homeowner reports solar panels, this short description is added to the contractor notification. Solar does not affect calculator pricing.'); ?>
        </tbody></table>
        <?php $this->settings_save_actions(); ?>
        </section>

        <section class="rfec-settings-panel" data-rfec-panel="form" hidden>
        <h2>Market Form</h2>
        <p class="rfec-tab-intro">Set the market format, lead-capture wording and colour used throughout the public calculator. Pricing is configured separately under Product Configuration and Adjustments.</p>
        <table class="form-table"><tbody>
        <?php $this->select_row('Pricing preset', 'market', $s['market'], ['CA'=>'Canada','US'=>'United States'], 'Choose the market that best matches your business. Review all rates before publishing because local costs vary.'); ?>
        <?php $this->select_row('Language & region', 'language', $s['language'], ['en_CA'=>'English (Canada)','en_US'=>'English (United States)'], 'Controls regional spelling and address terminology throughout the estimator, emails and printouts.'); ?>
        <?php $this->text_row('Currency code', 'currency', $s['currency'], 'text', 'For example, CAD or USD.'); ?>
        <?php $this->text_row('Currency symbol', 'currency_symbol', $s['currency_symbol'], 'text', 'For example, $.'); ?>
        <?php $this->text_row('Measurement unit', 'unit', $s['unit'], 'text', 'Shown beside roof areas. The calculator uses square feet.'); ?>
        <?php $this->text_row('Final form heading', 'form_heading', $s['form_heading'], 'text', 'Shown above the final contact fields.'); ?>
        <?php $this->textarea_row('Final form CTA', 'cta_text', $s['cta_text'], 'Shown between the heading and visitor contact fields.'); ?>
        <?php $this->text_row('Final form button text', 'submit_button_text', $s['submit_button_text'], 'text', 'This button saves and emails the estimate, then opens the booking link.'); ?>
        <?php $this->color_row('Calculator accent colour', 'accent_color', $s['accent_color'], 'Used for progress bars, primary buttons, selections, checkboxes, outlines and links. Clear the override to use the theme’s primary colour.'); ?>
        </tbody></table>
        <?php $this->settings_save_actions(); ?>
        </section>

        <section class="rfec-settings-panel" data-rfec-panel="products" hidden>
        <h2>Product Configuration</h2>
        <p class="rfec-tab-intro">Choose the roof types you sell, set their installed base rates, then configure roof-specific allowances. Base rates should include standard materials and labour for a straightforward installation.</p>
        <h2>Roof type configuration</h2><p>Manage each product in one row. The three custom options are disabled by default and only appear in the calculator when they have been enabled and given both a name and a price. A placeholder is used when no custom image is selected.</p>
        <table class="widefat striped rfec-material-settings"><thead><tr><th>Roof type</th><th>Available</th><th>Installed price per sq ft</th><th>Option image</th></tr></thead><tbody>
        <?php foreach ($materials as $key=>$label):
            $is_custom = isset($this->custom_materials()[$key]);
            $image_id = absint($s['material_images'][$key] ?? 0);
            $custom_image = $image_id ? wp_get_attachment_image_url($image_id, 'medium') : '';
            $fallback = $is_custom ? RFEC_URL.'assets/roofs/custom-placeholder.svg' : RFEC_URL.'assets/roofs/'.$key.'.jpeg';
            $preview = $custom_image ?: $fallback; ?>
            <tr data-rfec-image-row>
                <th scope="row"><?php if ($is_custom): ?><label class="screen-reader-text" for="rfec-<?php echo esc_attr($key); ?>-name"><?php echo esc_html($label); ?> name</label><input class="regular-text" id="rfec-<?php echo esc_attr($key); ?>-name" type="text" name="<?php echo esc_attr($this->field_name('material_names['.$key.']')); ?>" value="<?php echo esc_attr($s['material_names'][$key] ?? ''); ?>" placeholder="<?php echo esc_attr($label); ?>"><?php else: echo esc_html($label); endif; ?></th>
                <td><label><input type="checkbox" name="<?php echo esc_attr($this->field_name('material_enabled['.$key.']')); ?>" value="1" <?php checked(!empty($s['material_enabled'][$key])); ?>> Enabled</label></td>
                <td><label class="screen-reader-text" for="rfec-<?php echo esc_attr($key); ?>-price">Installed price per sq ft</label><input id="rfec-<?php echo esc_attr($key); ?>-price" type="number" min="0" step="0.01" name="<?php echo esc_attr($this->field_name('material_rates['.$key.']')); ?>" value="<?php echo esc_attr($s['material_rates'][$key]); ?>"></td>
                <td><div class="rfec-admin-image"><img src="<?php echo esc_url($preview); ?>" alt=""><div><input type="hidden" data-rfec-image-id name="<?php echo esc_attr($this->field_name('material_images['.$key.']')); ?>" value="<?php echo esc_attr($image_id); ?>"><input type="hidden" data-rfec-fallback value="<?php echo esc_url($fallback); ?>"><button type="button" class="button" data-rfec-select-image>Select or upload image</button> <button type="button" class="button-link-delete" data-rfec-remove-image <?php echo $image_id ? '' : 'hidden'; ?>>Remove custom image</button></div></div></td>
            </tr>
        <?php endforeach; ?>
        </tbody></table>
        <h2>Allowances</h2><table class="form-table"><tbody>
        <tr><th scope="row">Sales tax</th><td><label><input type="checkbox" name="<?php echo esc_attr($this->field_name('include_tax')); ?>" value="1" <?php checked(!empty($s['include_tax'])); ?>> Include sales tax in the estimate range and line items</label><p class="description">Leave this unchecked to show estimates before tax.</p></td></tr>
        <?php $this->text_row('Tax label', 'tax_label', $s['tax_label'], 'text', 'For example: HST, GST, QST or State sales tax.'); ?>
        <?php $this->number_row('Sales tax rate, %', 'tax_rate', $s['tax_rate'], '0.01'); ?>
        <?php $this->number_row('Roof vent, each', 'addon_rates[vent]', $s['addon_rates']['vent']); ?>
        <?php $this->number_row('Skylight allowance, each', 'addon_rates[skylight]', $s['addon_rates']['skylight']); ?>
        <?php $this->number_row('Chimney allowance, each', 'addon_rates[chimney]', $s['addon_rates']['chimney']); ?>
        <?php
        $this->number_row('One-layer removal and disposal, per sq ft', 'tearoff_per_sqft', $s['tearoff_per_sqft'], '0.01');
        $this->number_row('Underlayment / ice protection, per sq ft', 'underlayment_per_sqft', $s['underlayment_per_sqft'], '0.01');
        foreach (['permit_allowance'=>'Permit handling allowance','flashing_allowance'=>'Complex-roof flashing allowance','waste_percent'=>'Base hidden waste allowance, %','minimum_job'=>'Minimum project charge'] as $key=>$label) $this->number_row($label, $key, $s[$key], '1');
        ?>
        </tbody></table>
        <h2>Estimate category ranges</h2><div class="rfec-calculation-note"><p>These percentages communicate uncertainty before an on-site inspection. Enter <strong>10</strong> to display a category from 10% below to 10% above its calculated midpoint. Permit and minimum-project allowances stay fixed.</p><p><strong>Example:</strong> a $10,000 Roofing system midpoint with a 10% range displays as $9,000–$11,000.</p></div>
        <table class="widefat striped" style="max-width:760px"><thead><tr><th>Estimate category</th><th>Range, plus/minus %</th><th>How it is factored</th></tr></thead><tbody>
        <?php foreach (['system'=>['Roofing system','Materials, hidden waste, pitch, stories, shape and access'],'removal'=>['Removal and disposal','One-layer tear-off and site cleanup'],'protection'=>['Roof protection','Underlayment, ice protection and flashing'],'penetrations'=>['Roof penetrations','Vents, skylights, chimneys and ventilation upgrade']] as $key=>$row): ?>
        <tr><th scope="row"><?php echo esc_html($row[0]); ?></th><td><span aria-hidden="true" style="font-weight:600;margin-right:5px">±</span><input type="number" min="0" max="100" step="1" style="width:90px" name="<?php echo esc_attr($this->field_name('category_ranges['.$key.']')); ?>" value="<?php echo esc_attr($s['category_ranges'][$key]); ?>"> %</td><td><?php echo esc_html($row[1]); ?></td></tr>
        <?php endforeach; ?>
        </tbody></table>
        <?php $this->settings_save_actions(); ?>
        </section>

        <section class="rfec-settings-panel" data-rfec-panel="multipliers" hidden>
        <h2>Adjustments</h2>
        <div class="rfec-calculation-note"><p>These groups match the choices homeowners make in the calculator. Only one option from each group applies, but selections from different groups work together and compound.</p><p><strong>How multipliers are calculated:</strong> A multiplier of 1.00 makes no change. A multiplier of 1.15 adds 15%, while 0.85 reduces that part of the estimate by 15%. Multipliers compound when more than one applies. For example: <code>1.18 x 1.25 = 1.475</code>, which creates a combined increase of 47.5%.</p></div>
        <h2>Roof complexity configuration</h2><p>Edit the homeowner-facing label and price multiplier for each roof shape. The built-in illustration is used until you select a custom image from the Media Library.</p>
        <table class="widefat striped rfec-material-settings rfec-complexity-settings" style="margin-bottom:24px"><thead><tr><th>Roof shape label</th><th>Price multiplier</th><th>Option image</th></tr></thead><tbody>
        <?php foreach ($this->default_complexity_options() as $key=>$default_label):
            $image_id = absint($s['complexity_images'][$key] ?? 0);
            $custom_image = $image_id ? wp_get_attachment_image_url($image_id, 'medium') : '';
            $fallback = RFEC_URL.'assets/complexity/'.$key.'.jpeg';
            $preview = $custom_image ?: $fallback; ?>
            <tr data-rfec-image-row>
                <th scope="row"><label class="screen-reader-text" for="rfec-complexity-<?php echo esc_attr($key); ?>-name"><?php echo esc_html($default_label); ?> label</label><input class="regular-text" style="padding-left:10px" id="rfec-complexity-<?php echo esc_attr($key); ?>-name" type="text" name="<?php echo esc_attr($this->field_name('complexity_names['.$key.']')); ?>" value="<?php echo esc_attr($s['complexity_names'][$key] ?? $default_label); ?>"></th>
                <td><label class="screen-reader-text" for="rfec-complexity-<?php echo esc_attr($key); ?>-factor">Price multiplier</label><input id="rfec-complexity-<?php echo esc_attr($key); ?>-factor" type="number" min="0" step="0.01" name="<?php echo esc_attr($this->field_name('complexity_factors['.$key.']')); ?>" value="<?php echo esc_attr($s['complexity_factors'][$key]); ?>"></td>
                <td><div class="rfec-admin-image"><img src="<?php echo esc_url($preview); ?>" alt=""><div><input type="hidden" data-rfec-image-id name="<?php echo esc_attr($this->field_name('complexity_images['.$key.']')); ?>" value="<?php echo esc_attr($image_id); ?>"><input type="hidden" data-rfec-fallback value="<?php echo esc_url($fallback); ?>"><button type="button" class="button" data-rfec-select-image>Select or upload image</button> <button type="button" class="button-link-delete" data-rfec-remove-image <?php echo $image_id ? '' : 'hidden'; ?>>Remove custom image</button></div></div></td>
            </tr>
        <?php endforeach; ?>
        </tbody></table>
        <div class="rfec-multiplier-groups">
        <?php
        $this->multiplier_group('Roof pitch', 'Accounts for safety, staging and installation difficulty as pitch increases.', 'pitch_factors', ['low'=>'Low slope','standard'=>'Standard pitch','steep'=>'Steep pitch','very_steep'=>'Very steep pitch'], $s['pitch_factors']);
        $this->multiplier_group('Building height', 'Accounts for staging, lifting and fall-protection requirements.', 'stories_factors', ['one'=>'1 story','two'=>'2 stories','three_plus'=>'3+ stories'], $s['stories_factors']);
        $this->multiplier_group('Accessibility', 'Accounts for limited bin placement, lifting distance and hand-carry conditions.', 'access_factors', ['open'=>'Open access','limited'=>'Limited access','hand_carry'=>'Hand-carry access'], $s['access_factors']);
        ?>
        </div>
        <?php $this->settings_save_actions(); ?>
        </section>

        <section class="rfec-settings-panel" data-rfec-panel="emails" hidden>
        <h2>Email Notifications</h2>
        <p class="rfec-tab-intro">The lead and contractor emails use tested, responsive templates. Customize only the three homeowner-facing text blocks below. Project details, pricing, the estimate range, tax information and the ballpark disclaimer remain automatic.</p>
        <div class="rfec-email-settings">
            <div class="rfec-email-controls">
                <div class="rfec-email-editor"><h3>Email logo</h3><p><label><input type="checkbox" name="<?php echo esc_attr($this->field_name('lead_email_show_logo')); ?>" value="1" <?php checked(!empty($s['lead_email_show_logo'])); ?>> <strong>Show Logo</strong></label></p><?php $this->email_logo_control($s); ?><p class="description">The selected logo appears at the top of the lead email and is constrained to fit the responsive template.</p></div>
                <?php $this->email_editor('Intro message', 'lead_email_intro', $s['lead_email_intro'], 'Shown near the beginning of the lead email.'); ?>
                <?php $this->email_editor('Scheduling section message', 'lead_email_schedule', $s['lead_email_schedule'], 'The message remains visible even when the scheduling button is hidden. Links may be added here.'); ?>
                <p><label><input type="checkbox" name="<?php echo esc_attr($this->field_name('lead_email_show_schedule_button')); ?>" value="1" <?php checked(!empty($s['lead_email_show_schedule_button'])); ?>> <strong>Show Scheduling Button</strong></label></p>
                <?php $this->email_editor('Email signature', 'lead_email_signature', $s['lead_email_signature'], 'Shown at the end of the lead email. Use Add Media to place an image within the signature.', true); ?>
                <div class="rfec-placeholder-reference"><h3>Available placeholders</h3><p><code>{contractor_name}</code> <code>{site_name}</code> <code>{lead_name}</code> <code>{estimate_range}</code> <code>{booking_url}</code> <code>{currency_code}</code></p><p>These values come from the Business Info and Market Form tabs or from the submitted estimate.</p></div>
            </div>
            <div class="rfec-email-previews">
                <h3>Lead email preview</h3><?php echo $this->email_preview(false); ?>
            </div>
        </div>
        <?php $this->settings_save_actions(); ?>
        </section>
        </form>
        <section class="rfec-settings-panel" data-rfec-panel="test" hidden>
        <h2>Test Estimate Form</h2>
        <p class="rfec-tab-intro">Use this internal form after saving your settings. It uses the same pricing, roof types, allowances and multipliers as the public calculator, but it does not save an estimate, send emails, trigger milestones or send anything to Quiet City Design.</p>
        <div class="rfec-test-warning"><strong>Save first:</strong> this tester reads the last saved calculator settings. Unsaved changes on other tabs will not be reflected until you click <strong>Save Settings</strong>.</div>
        <?php echo $this->test_estimate_form(); ?>
        </section>
        <form id="rfec-qcd-optin-form" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"></form>
        </div>
        <?php
    }

    private function settings_save_actions() {
        $this->admin_ui->save_actions();
    }

    private function email_editor($label, $key, $value, $help = '', $media_buttons = false) {
        $this->admin_ui->email_editor($label,$key,$value,$help,$media_buttons);
    }

    private function email_logo_control($settings) {
        $this->admin_ui->email_logo_control($settings);
    }

    private function email_preview($contractor) {
        $s=$this->settings();
        $lead=['name'=>'Jamie Homeowner','email'=>'jamie@example.com','phone'=>'555-555-0148','address'=>'123 Sample Street','city'=>'Sampletown','postal'=>'A1A 1A1'];
        $calc=['lines'=>[['label'=>'2,000 sq ft Architectural asphalt roofing system','amount'=>18500],['label'=>'Removal and disposal','amount'=>3300]],'low'=>19620,'high'=>23980,'total'=>21800,'tax_included'=>false,'tax_label'=>$s['tax_label']];
        return '<div class="rfec-email-preview">'.$this->email_html($lead['name'],$lead['email'],$lead['phone'],$lead['address'],$lead['city'],$lead['postal'],[],$calc,$contractor).'</div>';
    }

    private function qcd_optin_panel($compact = false) {
        if (!current_user_can('manage_options')) return;
        $optin = get_option('rfec_qcd_optin', []);
        $user = wp_get_current_user();
        $prefill = sanitize_email($_GET['rfec_owner_email'] ?? ($optin['owner_email'] ?? $user->user_email));
        $failed = isset($_GET['rfec_optin']) && $_GET['rfec_optin'] === 'failed';
        if (!empty($optin['success'])): ?>
            <div class="<?php echo $compact ? 'rfec-optin-confirmation' : 'rfec-qcd-panel'; ?>">
                <?php if (!$compact): ?><p class="rfec-qcd-kicker">Quiet City Design</p><?php endif; ?>
                <h2>Access Granted</h2>
                <p>Check your Inbox for setup instructions, calculator updates and advanced tactics from Quiet City Design. If you don't see a confirmation email in 2 minutes, check your junk filter and move the message to the Inbox. You may unsubscribe using the link in any email.</p>
                <p><strong>Subscribed email:</strong> <?php echo esc_html($optin['owner_email'] ?? ''); ?><br><strong>Consent date:</strong> <?php echo esc_html($optin['consent_timestamp'] ?? ''); ?></p>
            </div>
        <?php return; endif; ?>
        <div class="<?php echo $compact ? 'rfec-optin-inline' : 'rfec-qcd-panel'; ?>">
            <?php if (!$compact): ?><p class="rfec-qcd-kicker">Quiet City Design</p><h2>Access Calculator Training</h2><p>Get practical setup training, calculator improvements and contractor onboarding tactics from Quiet City Design. This optional form sends your email address and basic website details to Quiet City Design.</p><?php endif; ?>
            <?php if ($failed): ?><div class="notice notice-error inline"><p><?php echo esc_html(get_transient('rfec_qcd_optin_error') ?: 'The opt-in could not be submitted. Please try again.'); ?></p></div><?php endif; ?>
                <input form="rfec-qcd-optin-form" type="hidden" name="_wpnonce" value="<?php echo esc_attr(wp_create_nonce('rfec_qcd_optin')); ?>">
                <input form="rfec-qcd-optin-form" type="hidden" name="action" value="rfec_qcd_optin">
                <input form="rfec-qcd-optin-form" type="hidden" name="wordpress_user_email" value="<?php echo esc_attr($user->user_email); ?>">
                <div class="rfec-optin-fields">
                    <label for="rfec-owner-email">Email address</label>
                    <input form="rfec-qcd-optin-form" class="regular-text" id="rfec-owner-email" type="email" name="owner_email" value="<?php echo esc_attr($prefill); ?>" required>
                    <span></span>
                    <label class="rfec-optin-consent"><input form="rfec-qcd-optin-form" type="checkbox" name="consent" value="1" required> Send me setup training, calculator updates and contractor onboarding tactics from Quiet City Design. I can unsubscribe at any time.</label>
                </div>
                <p class="rfec-optin-submit"><?php submit_button('Access Training', 'secondary', 'submit', false, ['form'=>'rfec-qcd-optin-form']); ?></p>
        </div>
        <?php
    }

    private function field_name($key) {
        // Convert material_rates[cedar] to rfec_settings[material_rates][cedar].
        // Wrapping the full string in one pair of brackets prevents WordPress
        // from building the nested arrays expected by sanitize_settings().
        return $this->admin_ui->field_name($key);
    }
    private function text_row($label,$key,$value,$type='text',$help='') { $this->admin_ui->text_row($label,$key,$value,$type,$help); }
    private function textarea_row($label,$key,$value,$help='') { $this->admin_ui->textarea_row($label,$key,$value,$help); }
    private function color_row($label,$key,$value,$help='') { $this->admin_ui->color_row($label,$key,$value,$help); }
    private function multiplier_group($title,$description,$group,$items,$values) { $this->admin_ui->multiplier_group($title,$description,$group,$items,$values); }
    private function number_row($label,$key,$value,$step='0.01') { $this->admin_ui->number_row($label,$key,$value,$step); }
    private function select_row($label,$key,$value,$options,$help='') { $this->admin_ui->select_row($label,$key,$value,$options,$help); }

    public function materials() { return ['architectural_asphalt'=>'Architectural asphalt shingles','premium_asphalt'=>'Premium / designer asphalt shingles','metal_shingles'=>'Metal shingles','standing_seam'=>'Standing seam metal','cedar'=>'Cedar shakes','slate'=>'Natural or synthetic slate','flat_membrane'=>'Flat / low-slope membrane']; }
    private function custom_materials() { return ['custom_1'=>'Custom Roof System 1','custom_2'=>'Custom Roof System 2','custom_3'=>'Custom Roof System 3']; }
    private function default_complexity_options() { return ['simple'=>'Simple gable','average'=>'Average','complex'=>'Complex','very_complex'=>'Very complex']; }
    public function complexity_options($settings = null) { $settings=$settings?:$this->settings(); return wp_parse_args($settings['complexity_names']??[], $this->default_complexity_options()); }
    public function complexity_image_url($key, $settings = null) { $settings=$settings?:$this->settings(); $image_id=absint($settings['complexity_images'][$key]??0); return ($image_id?wp_get_attachment_image_url($image_id,'medium_large'):'') ?: RFEC_URL.'assets/complexity/'.$key.'.jpeg'; }
    private function style_options() { return ['standard'=>'Standard system','premium'=>'Premium system','designer'=>'Designer / specialty system']; }
    private function style_compatibility() {
        $all=array_keys($this->style_options());
        return [
            'architectural_asphalt'=>['standard','premium'],
            'premium_asphalt'=>$all,
            'metal_shingles'=>['standard','premium'],
            'standing_seam'=>['standard','premium'],
            'cedar'=>$all,
            'slate'=>$all,
            'flat_membrane'=>['standard','premium'],
            'custom_1'=>$all,
            'custom_2'=>$all,
            'custom_3'=>$all,
        ];
    }

    public function admin_assets($hook) {
        $screen = get_current_screen();
        $is_settings = $hook === 'rfec_submission_page_rfec-settings';
        $is_estimates = $hook === 'edit.php' && $screen && $screen->post_type === 'rfec_submission';
        $is_dashboard = $hook === 'index.php';
        if (!$is_settings && !$is_estimates && !$is_dashboard) return;
        if ($is_settings) {
            wp_enqueue_media();
            wp_enqueue_script('rfec-admin', RFEC_URL.'assets/qcd-admin-core.js', ['jquery'], RFEC_VERSION, true);
            wp_enqueue_style('rfec-admin-core', RFEC_URL.'assets/qcd-admin-core.css', [], RFEC_VERSION);
            wp_enqueue_style('rfec', RFEC_URL.'assets/calculator-1.2.0.css', [], RFEC_VERSION);
            wp_enqueue_script('rfec', RFEC_URL.'assets/calculator-1.5.0.js', [], RFEC_VERSION, true);
        }
        wp_add_inline_style('common', '.rfec-settings-header{align-items:flex-start;display:flex;gap:24px;justify-content:space-between;max-width:1100px}.rfec-settings-header h1{margin-bottom:4px}.rfec-settings-header p{margin-top:4px}.rfec-settings-tabs{max-width:1100px}.rfec-settings-tabs .nav-tab{cursor:pointer}.rfec-settings-panel{max-width:1100px;padding:18px 2px 4px}.rfec-settings-panel[hidden]{display:none}.rfec-settings-panel>h2:first-child{margin-top:0}.rfec-tab-intro{background:#fff;border-left:4px solid #72aee6;box-sizing:border-box;color:#3c434a;line-height:1.55;margin:0 0 22px;max-width:1060px;padding:13px 16px}.rfec-tab-save{align-items:center;border-top:1px solid #dcdcde;display:flex;flex-wrap:wrap;gap:8px;margin-top:24px;max-width:1100px;padding-top:18px}.rfec-tab-save .description{flex-basis:100%;margin:0}.rfec-test-warning{background:#fff8e5;border-left:4px solid #dba617;box-sizing:border-box;margin:0 0 18px;max-width:1060px;padding:12px 16px}.rfec__test-note{background:#f6f7f7;border-left:4px solid var(--rfec-accent,#1d5dbf);box-sizing:border-box;margin:18px 0;padding:12px 14px}.rfec-material-settings{max-width:1100px}.rfec-material-settings input.regular-text{max-width:190px}.rfec-material-settings input[type=number]{width:100px}.rfec-material-settings th,.rfec-material-settings td{vertical-align:middle}.rfec-admin-image{align-items:center;display:flex;gap:14px}.rfec-admin-image img{aspect-ratio:16/10;border:1px solid #c3c4c7;border-radius:6px;height:80px;object-fit:cover;width:128px}.rfec-admin-image .button-link-delete{margin-left:6px}.rfec-calculation-note{background:#fff;border-left:4px solid #1d5dbf;margin:12px 0 18px;max-width:1060px;padding:14px 18px}.rfec-calculation-note p{line-height:1.55;margin:5px 0}.rfec-multiplier-groups{display:grid;gap:16px;grid-template-columns:repeat(2,minmax(0,520px));max-width:1060px}.rfec-multiplier-group{background:#fff;border:1px solid #dcdcde;padding:18px}.rfec-multiplier-group h3{font-size:16px;margin:0 0 5px}.rfec-multiplier-group>p{color:#50575e;line-height:1.45;margin:0 0 14px;min-height:40px}.rfec-multiplier-group table{border-collapse:collapse;width:100%}.rfec-multiplier-group th,.rfec-multiplier-group td{border-top:1px solid #eee;padding:8px 5px;text-align:left}.rfec-multiplier-group thead th{border-top:0;color:#646970;font-size:11px;text-transform:uppercase}.rfec-multiplier-group input{width:82px}@media(max-width:782px){.rfec-settings-header{align-items:stretch;flex-direction:column;gap:4px}.rfec-settings-tabs{display:flex;overflow-x:auto}.rfec-settings-tabs .nav-tab{flex:0 0 auto;margin-bottom:0}.rfec-admin-image{align-items:flex-start;flex-direction:column}}@media(max-width:900px){.rfec-multiplier-groups{grid-template-columns:1fr}.rfec-multiplier-group>p{min-height:0}}.rfec-qcd-panel{background:#fff;border:1px solid #dcdcde;border-left:4px solid #1d5dbf;box-shadow:0 1px 1px rgba(0,0,0,.04);box-sizing:border-box;margin:16px 0;padding:16px 20px;max-width:1100px}.rfec-qcd-panel--settings{margin-top:28px}.rfec-qcd-panel h2{font-size:18px;margin:2px 0 8px}.rfec-qcd-panel p{font-size:13px;line-height:1.55;margin:6px 0}.rfec-qcd-kicker{color:#646970;font-size:11px!important;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.rfec-dashboard-summary{display:grid;gap:10px;grid-template-columns:1.25fr 1fr 1fr;margin:4px 0 10px}.rfec-dashboard-summary p{background:#fff;border:1px solid #72aee6;box-shadow:inset 4px 0 0 #2271b1;margin:0;padding:12px}.rfec-dashboard-summary strong{display:block;margin-bottom:8px}.rfec-dashboard-summary span{color:#1d2327;display:block;font-size:15px;font-weight:600;line-height:1.45}.rfec-dashboard-summary__primary span{font-size:42px;font-weight:700;line-height:1}@media(max-width:900px){.rfec-dashboard-summary{grid-template-columns:1fr}}#rfec_dashboard_activity .widefat{margin-top:10px}#rfec_dashboard_activity .notice h3{margin-bottom:6px}#rfec_dashboard_activity .button{margin-top:4px}.edit-php.post-type-rfec_submission .notice-success h3{margin:8px 0}.edit-php.post-type-rfec_submission .notice-success{padding-bottom:10px}.rfec-welcome-grid{display:grid;gap:18px;grid-template-columns:minmax(0,1fr) minmax(0,1.2fr);max-width:1100px}.rfec-welcome-card{background:#fff;border:1px solid #dcdcde;box-shadow:0 1px 1px rgba(0,0,0,.04);box-sizing:border-box;min-width:0;padding:18px 22px}.rfec-welcome-card h3{font-size:16px;margin:0 0 10px}.rfec-welcome-card p,.rfec-welcome-card li{line-height:1.55}.rfec-welcome-card ol{margin-left:18px}.rfec-toolkit-callout{border-top:1px solid #dcdcde;margin-top:18px;padding-top:16px}.rfec-toolkit-callout h3{font-size:15px;margin-bottom:6px}.rfec-toolkit-callout p{margin:6px 0}.rfec-optin-fields{align-items:start;display:grid;gap:12px 14px;grid-template-columns:90px minmax(0,1fr);margin-top:22px}.rfec-optin-fields>label:first-child{font-weight:600;padding-top:5px}.rfec-optin-fields input[type=email]{box-sizing:border-box;max-width:100%;width:100%}.rfec-optin-consent{line-height:1.45}.rfec-optin-consent input{margin-top:2px}.rfec-optin-inline .rfec-optin-submit{margin:14px 0 0 104px}.rfec-optin-confirmation{background:#f6f7f7;border-left:4px solid #00a32a;margin-top:14px;padding:12px 14px}@media(max-width:900px){.rfec-welcome-grid{grid-template-columns:1fr}}@media(max-width:782px){.rfec-optin-fields{grid-template-columns:1fr}.rfec-optin-fields>span{display:none}.rfec-optin-inline .rfec-optin-submit{margin-left:0}}');
    }

    private function qcd_url($placement) {
        return RFEC_QCD_Client::tracked_url($placement);
    }

    private function qcd_toolkit_url($placement, $path = 'the-toolkit/universal-inbox/') {
        return RFEC_QCD_Client::tracked_url($placement,$path);
    }

    private function qcd_endpoint_url() {
        return RFEC_QCD_Client::endpoint_url();
    }

    public function handle_qcd_optin() {
        if (!current_user_can('manage_options')) wp_die('Permission denied.');
        check_admin_referer('rfec_qcd_optin');
        $owner_email = sanitize_email(wp_unslash($_POST['owner_email'] ?? ''));
        $wp_email = sanitize_email(wp_unslash($_POST['wordpress_user_email'] ?? ''));
        $user = wp_get_current_user();
        if (!is_email($owner_email) || empty($_POST['consent'])) {
            set_transient('rfec_qcd_optin_error', 'Please enter a valid email address and confirm consent.', 60);
            wp_safe_redirect(add_query_arg(['page'=>'rfec-settings','rfec_optin'=>'failed','rfec_owner_email'=>$owner_email], admin_url('edit.php?post_type=rfec_submission')));
            exit;
        }
        $timestamp = current_time('c');
        $payload = [
            'event' => 'plugin_optin',
            'plugin' => 'contractor-roof-estimate-calculator',
            'plugin_version' => RFEC_VERSION,
            'owner_email' => $owner_email,
            'wordpress_user_email' => $wp_email,
            'first_name' => sanitize_text_field($user->first_name),
            'last_name' => sanitize_text_field($user->last_name),
            'business_name' => get_bloginfo('name'),
            'website_url' => home_url('/'),
            'consent' => true,
            'consent_timestamp' => $timestamp,
        ];
        $result = $this->post_qcd_event($payload);
        $record = [
            'owner_email' => $owner_email,
            'wordpress_user_email' => $wp_email,
            'first_name' => $payload['first_name'],
            'last_name' => $payload['last_name'],
            'business_name' => $payload['business_name'],
            'website_url' => $payload['website_url'],
            'consent_timestamp' => $timestamp,
            'success' => !is_wp_error($result),
            'last_error' => is_wp_error($result) ? $result->get_error_message() : '',
        ];
        update_option('rfec_qcd_optin', $record, false);
        if (is_wp_error($result)) {
            set_transient('rfec_qcd_optin_error', $result->get_error_message(), 60);
            wp_safe_redirect(add_query_arg(['page'=>'rfec-settings','rfec_optin'=>'failed','rfec_owner_email'=>$owner_email], admin_url('edit.php?post_type=rfec_submission')));
            exit;
        }
        wp_safe_redirect(add_query_arg(['page'=>'rfec-settings','rfec_optin'=>'success'], admin_url('edit.php?post_type=rfec_submission')));
        exit;
    }

    private function post_qcd_event(array $payload) {
        return RFEC_QCD_Client::post_event($payload);
    }

    public function dashboard_widget() {
        if (!$this->can_view_estimates()) return;
        wp_add_dashboard_widget('rfec_dashboard_activity', 'Roof Estimate Activity', [$this, 'render_dashboard_widget']);
    }

    private function can_view_estimates() {
        return current_user_can('edit_posts') || current_user_can('manage_options');
    }

    public function render_dashboard_widget() {
        $s = $this->settings();
        $stats = $this->estimate_stats();
        $notice = $this->latest_undismissed_milestone();
        if ($notice) echo $this->milestone_notice_html($notice, $s, 'dashboard-milestone', true);
        echo '<div class="rfec-dashboard-summary"><p class="rfec-dashboard-summary__primary"><strong>Total estimates</strong><span>'.esc_html(number_format_i18n($stats['total'])).'</span></p><p><strong>Estimated project value</strong><span>'.esc_html($this->money($stats['low'], $s)).' to '.esc_html($this->money($stats['high'], $s)).'</span></p><p><strong>Estimates this month</strong><span>'.esc_html(number_format_i18n($stats['month'])).'</span></p></div>';
        echo '<p class="description">Based on estimate ranges generated by the calculator. This does not represent booked or completed work.</p>';
        if (!$stats['total']) {
            echo '<p>No estimate submissions have been saved yet.</p><p><a class="button button-secondary" href="'.esc_url(admin_url('edit.php?post_type=rfec_submission&page=rfec-settings')).'">Review Calculator Settings</a></p>';
            return;
        }
        $recent = get_posts(['post_type'=>'rfec_submission','post_status'=>'publish','numberposts'=>5,'orderby'=>'date','order'=>'DESC']);
        echo '<table class="widefat striped"><thead><tr><th>Lead name</th><th>Roof type</th><th>Estimate range</th><th>Submitted</th></tr></thead><tbody>';
        foreach ($recent as $post) {
            $calc = get_post_meta($post->ID, '_rfec_calculation', true);
            echo '<tr><td><a href="'.esc_url(get_edit_post_link($post->ID)).'">'.esc_html(get_the_title($post)).'</a></td><td>'.esc_html($this->calculation_roof_label($calc)).'</td><td>'.esc_html($calc ? $this->money($calc['low'], $s).' to '.$this->money($calc['high'], $s) : '').'</td><td>'.esc_html(get_date_from_gmt(get_gmt_from_date($post->post_date), 'M j, Y')).'</td></tr>';
        }
        echo '</tbody></table><p><a class="button button-secondary" href="'.esc_url(admin_url('edit.php?post_type=rfec_submission')).'">View All Estimates</a></p>';
    }

    private function milestone_notice_html($notice, $settings, $placement, $inline = false) {
        $milestone = absint($notice['milestone']);
        $dismiss = wp_nonce_url(admin_url('admin-post.php?action=rfec_dismiss_milestone&milestone='.$milestone), 'rfec_dismiss_milestone_'.$milestone);
        return '<div class="notice notice-success '.($inline ? 'inline' : '').'"><h3>'.esc_html($milestone).' estimates generated</h3><p>Your calculator has now captured '.esc_html($milestone).' estimate requests, representing an estimated project value of '.esc_html($this->money($notice['estimated_value_low'], $settings)).' to '.esc_html($this->money($notice['estimated_value_high'], $settings)).'.</p><p>At this stage, response speed may have more impact than generating more traffic.</p><p><a class="button button-primary" href="'.esc_url($this->qcd_toolkit_url($placement)).'" target="_blank" rel="noopener noreferrer">Review Your Lead Response Process</a> <a class="button" href="'.esc_url($dismiss).'">Dismiss</a></p></div>';
    }

    public function dismiss_milestone() {
        $milestone = absint($_GET['milestone'] ?? 0);
        if (!$this->can_view_estimates() || !$milestone) wp_die('Permission denied.');
        check_admin_referer('rfec_dismiss_milestone_'.$milestone);
        update_user_meta(get_current_user_id(), 'rfec_dismissed_milestone_'.$milestone, 1);
        wp_safe_redirect(admin_url('index.php'));
        exit;
    }

    private function calculation_roof_label($calc) {
        if (empty($calc['lines'][0]['label'])) return '';
        return preg_replace('/ roof \(.*/', '', (string) $calc['lines'][0]['label']);
    }

    private function estimate_count() {
        return (int) wp_count_posts('rfec_submission')->publish;
    }

    private function estimate_stats() {
        $stats = ['total'=>0,'month'=>0,'low'=>0,'high'=>0];
        $ids = get_posts(['post_type'=>'rfec_submission','post_status'=>'publish','numberposts'=>-1,'fields'=>'ids']);
        $stats['total'] = count($ids);
        $start = new DateTimeImmutable(wp_date('Y-m-01 00:00:00'), wp_timezone());
        foreach ($ids as $id) {
            $calc = get_post_meta($id, '_rfec_calculation', true);
            if (is_array($calc)) {
                $stats['low'] += (float)($calc['low'] ?? 0);
                $stats['high'] += (float)($calc['high'] ?? 0);
            }
            $post = get_post($id);
            if ($post) {
                $submitted = new DateTimeImmutable($post->post_date, wp_timezone());
                if ($submitted >= $start) $stats['month']++;
            }
        }
        return $stats;
    }

    private function milestones_up_to($count) {
        $milestones = [];
        foreach ([10,25,50] as $milestone) if ($count >= $milestone) $milestones[] = $milestone;
        for ($milestone = 100; $milestone <= $count; $milestone += 50) $milestones[] = $milestone;
        return $milestones;
    }

    private function reached_milestone($count) {
        if (in_array($count, [10,25,50], true)) return $count;
        return $count >= 100 && $count % 50 === 0 ? $count : 0;
    }

    private function check_milestone_after_submission() {
        $count = $this->estimate_count();
        $milestone = $this->reached_milestone($count);
        if (!$milestone) return;
        $triggered = get_option('rfec_triggered_milestones', []);
        if (!empty($triggered[$milestone])) return;
        $stats = $this->estimate_stats();
        $record = [
            'milestone' => $milestone,
            'total_estimates' => $count,
            'estimated_value_low' => round($stats['low'], 2),
            'estimated_value_high' => round($stats['high'], 2),
            'timestamp' => current_time('mysql'),
            'status' => 'local',
            'attempts' => 0,
            'last_error' => '',
        ];
        $triggered[$milestone] = $record;
        update_option('rfec_triggered_milestones', $triggered, false);
        $this->send_milestone_if_opted_in($milestone);
    }

    private function latest_undismissed_milestone() {
        $triggered = get_option('rfec_triggered_milestones', []);
        if (!is_array($triggered)) return null;
        krsort($triggered, SORT_NUMERIC);
        foreach ($triggered as $milestone => $record) {
            if (($record['status'] ?? '') === 'historical') continue;
            if (get_user_meta(get_current_user_id(), 'rfec_dismissed_milestone_'.absint($milestone), true)) continue;
            return $record + ['milestone'=>absint($milestone)];
        }
        return null;
    }

    private function milestone_payload($milestone) {
        $triggered = get_option('rfec_triggered_milestones', []);
        $record = $triggered[$milestone] ?? [];
        $optin = get_option('rfec_qcd_optin', []);
        $s = $this->settings();
        return [
            'event' => 'estimate_milestone',
            'plugin' => 'contractor-roof-estimate-calculator',
            'plugin_version' => RFEC_VERSION,
            'owner_email' => sanitize_email($optin['owner_email'] ?? ''),
            'wordpress_user_email' => sanitize_email($optin['wordpress_user_email'] ?? ''),
            'first_name' => sanitize_text_field($optin['first_name'] ?? ''),
            'last_name' => sanitize_text_field($optin['last_name'] ?? ''),
            'business_name' => sanitize_text_field($optin['business_name'] ?? get_bloginfo('name')),
            'website_url' => home_url('/'),
            'milestone' => absint($milestone),
            'total_estimates' => absint($record['total_estimates'] ?? $this->estimate_count()),
            'estimated_value_low' => round((float)($record['estimated_value_low'] ?? 0), 2),
            'estimated_value_high' => round((float)($record['estimated_value_high'] ?? 0), 2),
            'currency_code' => $s['currency'],
            'currency_symbol' => $s['currency_symbol'],
            'timestamp' => current_time('c'),
        ];
    }

    private function send_milestone_if_opted_in($milestone) {
        $optin = get_option('rfec_qcd_optin', []);
        if (empty($optin['success']) || empty($optin['owner_email']) || !is_email($optin['owner_email'])) return;
        $this->send_milestone_event($milestone);
    }

    private function send_milestone_event($milestone) {
        $payload = $this->milestone_payload($milestone);
        $result = $this->post_qcd_event($payload);
        $triggered = get_option('rfec_triggered_milestones', []);
        if (empty($triggered[$milestone])) return;
        $triggered[$milestone]['attempts'] = (int)($triggered[$milestone]['attempts'] ?? 0) + 1;
        if (is_wp_error($result)) {
            $triggered[$milestone]['status'] = 'send_failed';
            $triggered[$milestone]['last_error'] = $result->get_error_message();
            if ($triggered[$milestone]['attempts'] < 3 && !wp_next_scheduled('rfec_retry_milestone_event', [$milestone])) {
                wp_schedule_single_event(time() + HOUR_IN_SECONDS, 'rfec_retry_milestone_event', [$milestone]);
            }
        } else {
            $triggered[$milestone]['status'] = 'sent';
            $triggered[$milestone]['sent_timestamp'] = current_time('mysql');
            $triggered[$milestone]['last_error'] = '';
        }
        update_option('rfec_triggered_milestones', $triggered, false);
    }

    public function retry_milestone_event($milestone) {
        $triggered = get_option('rfec_triggered_milestones', []);
        if (empty($triggered[$milestone]) || ($triggered[$milestone]['status'] ?? '') === 'sent') return;
        $this->send_milestone_if_opted_in(absint($milestone));
    }

    public function estimates_page_promotion() {
        $screen = get_current_screen();
        if (!$screen || $screen->base !== 'edit' || $screen->post_type !== 'rfec_submission') return;
        $notice = $this->latest_undismissed_milestone();
        if ($notice) echo $this->milestone_notice_html($notice, $this->settings(), 'estimates-milestone');
    }

    public function available_materials($settings = null) {
        $settings = $settings ?: $this->settings();
        $materials = $this->materials();
        foreach ($this->custom_materials() as $key=>$fallback) {
            $name = trim((string)($settings['material_names'][$key] ?? ''));
            if ($name && (float)($settings['material_rates'][$key] ?? 0) > 0) $materials[$key] = $name;
        }
        return array_filter($materials, function($key) use ($settings) { return !empty($settings['material_enabled'][$key]); }, ARRAY_FILTER_USE_KEY);
    }

    public function material_image_url($key, $settings = null) {
        $settings = $settings ?: $this->settings();
        $image_id = absint($settings['material_images'][$key] ?? 0);
        $fallback = isset($this->custom_materials()[$key]) ? 'custom-placeholder.svg' : $key.'.jpeg';
        $custom = $image_id ? wp_get_attachment_image_url($image_id, 'medium_large') : '';
        return $custom ?: add_query_arg('ver', RFEC_VERSION, RFEC_URL.'assets/roofs/'.$fallback);
    }

    public function register_assets() {
        wp_register_style('rfec', RFEC_URL.'assets/calculator-1.2.0.css', [], RFEC_VERSION);
        // The filename changes when the calculator flow changes. Some hosts and
        // CDNs ignore query-string versions and otherwise serve stale JavaScript.
        wp_register_script('rfec', RFEC_URL.'assets/calculator-1.5.0.js', [], RFEC_VERSION, true);
    }

    public function shortcode() {
        $s = $this->settings();
        wp_enqueue_style('rfec'); wp_enqueue_script('rfec');
        // Keep each calculator's data beside its markup. This remains reliable
        // when performance plugins move, defer, or combine enqueued scripts.
        $config = [
            'ajaxUrl'=>admin_url('admin-ajax.php'), 'nonce'=>wp_create_nonce('rfec_submit'), 'settings'=>$s,
            'materials'=>$this->available_materials($s), 'complexities'=>$this->complexity_options($s),
            'styleOptions'=>$this->style_options(), 'styleCompatibility'=>$this->style_compatibility(),
        ];
        ob_start(); include RFEC_DIR.'templates/calculator.php'; return RFEC_Estimator_Core::regionalize(ob_get_clean(), $s);
    }

    private function test_estimate_form() {
        $s = $this->settings();
        $config = [
            'ajaxUrl'=>admin_url('admin-ajax.php'), 'nonce'=>wp_create_nonce('rfec_submit'), 'settings'=>$s,
            'materials'=>$this->available_materials($s), 'complexities'=>$this->complexity_options($s),
            'styleOptions'=>$this->style_options(), 'styleCompatibility'=>$this->style_compatibility(),
            'mode'=>'test',
        ];
        $admin_test = true;
        ob_start(); include RFEC_DIR.'templates/calculator.php'; return RFEC_Estimator_Core::regionalize(ob_get_clean(), $s);
    }

    private function clean_payload($raw) {
        $allowed = ['material','pitch','stories','complexity','access','solar_array'];
        $p = [];
        foreach ($allowed as $k) $p[$k] = sanitize_key($raw[$k] ?? '');
        foreach (['area','vents','skylights','chimneys'] as $k) $p[$k] = max(0, (float)($raw[$k] ?? 0));
        foreach (['permit','disposal','ventilation_upgrade'] as $k) $p[$k] = !empty($raw[$k]);
        return $p;
    }

    public function calculate($p) {
        $s=$this->settings(); $materials=$this->available_materials($s);
        if (!isset($materials[$p['material']])) $p['material']=array_key_first($materials);
        $complexity_waste=['simple'=>-4,'average'=>0,'complex'=>4,'very_complex'=>8][$p['complexity']]??0;
        $size_waste=$p['area']<1500?2:($p['area']>3500?-1:0);
        $waste_percent=max(5,min(25,(float)$s['waste_percent']+$complexity_waste+$size_waste));
        $area=$p['area']*(1+$waste_percent/100); $lines=[];
        $add=function($label,$amount,$range_key,$fixed=false) use (&$lines,$s) { if($amount<=0)return; $spread=$fixed?0:(float)($s['category_ranges'][$range_key]??0)/100; $lines[]=['label'=>$label,'amount'=>$amount,'low'=>$amount*(1-$spread),'high'=>$amount*(1+$spread),'fixed'=>$fixed]; };
        $base=$area*$s['material_rates'][$p['material']];
        $factor=($s['pitch_factors'][$p['pitch']]??1)*($s['stories_factors'][$p['stories']]??1)*($s['complexity_factors'][$p['complexity']]??1)*($s['access_factors'][$p['access']]??1);
        $add('Roofing system',$base*$factor,'system');
        if($p['disposal']) $add('Removal and disposal',$area*$s['tearoff_per_sqft'],'removal');
        $protection=$area*$s['underlayment_per_sqft']; if(in_array($p['complexity'],['complex','very_complex'],true)) $protection+=$s['flashing_allowance']; $add('Roof protection',$protection,'protection');
        $penetrations=$p['vents']*$s['addon_rates']['vent']+$p['skylights']*$s['addon_rates']['skylight']+$p['chimneys']*$s['addon_rates']['chimney']; if($p['ventilation_upgrade'])$penetrations+=$s['addon_rates']['vent']*4; $add('Roof penetrations',$penetrations,'penetrations');
        if($p['permit']) $add('Permit allowance',$s['permit_allowance'],'permit',true);
        $subtotal=array_sum(array_column($lines,'amount')); if($subtotal<$s['minimum_job'])$add('Minimum project allowance',$s['minimum_job']-$subtotal,'minimum',true);
        $total=array_sum(array_column($lines,'amount')); $low=array_sum(array_column($lines,'low')); $high=array_sum(array_column($lines,'high'));
        if(!empty($s['include_tax'])&&$s['tax_rate']>0){$rate=$s['tax_rate']/100;$tax=['label'=>$s['tax_label'].' ('.number_format_i18n($s['tax_rate'],2).'%)','amount'=>$total*$rate,'low'=>$low*$rate,'high'=>$high*$rate,'fixed'=>false];$lines[]=$tax;$total+=$tax['amount'];$low+=$tax['low'];$high+=$tax['high'];}
        return ['lines'=>$lines,'total'=>round($total,2),'low'=>round($low,2),'high'=>round($high,2),'currency'=>$s['currency'],'symbol'=>$s['currency_symbol'],'tax_included'=>!empty($s['include_tax'])&&$s['tax_rate']>0,'tax_label'=>$s['tax_label']];
    }    public function submit() {
        check_ajax_referer('rfec_submit','nonce');
        if (!empty($_POST['website'])) wp_send_json_error(['message'=>'Unable to submit.'],400);
        $email=sanitize_email(wp_unslash($_POST['email']??'')); $name=sanitize_text_field(wp_unslash($_POST['name']??''));
        $phone=sanitize_text_field(wp_unslash($_POST['phone']??'')); $address=sanitize_text_field(wp_unslash($_POST['address']??'')); $city=sanitize_text_field(wp_unslash($_POST['city']??'')); $postal=sanitize_text_field(wp_unslash($_POST['postal']??''));
        if (!$name || !is_email($email) || !$phone || !$address || !$city || !$postal) wp_send_json_error(['message'=>'Please complete all contact and address fields.'],422);
        $payload=json_decode(wp_unslash($_POST['estimate']??'{}'),true); $p=$this->clean_payload(is_array($payload)?$payload:[]);
        if ($p['area']<100) wp_send_json_error(['message'=>'Please enter a valid roof area.'],422);
        if (!isset($this->available_materials()[$p['material']])) wp_send_json_error(['message'=>'That roof type is not currently available. Please choose another option.'],422);
        $calc=$this->calculate($p); $s=$this->settings();
        $id=wp_insert_post(['post_type'=>'rfec_submission','post_status'=>'publish','post_title'=>$name.' – '.current_time('M j, Y g:i a')]);
        if (is_wp_error($id)) wp_send_json_error(['message'=>'The estimate could not be saved. Please try again.'],500);
        foreach (['name'=>$name,'email'=>$email,'phone'=>$phone,'address'=>$address,'city'=>$city,'postal'=>$postal,'inputs'=>$p,'calculation'=>$calc] as $k=>$v) update_post_meta($id,'_rfec_'.$k,$v);
        $visitor_html=$this->email_html($name,$email,$phone,$address,$city,$postal,$p,$calc,false);
        $contractor_html=$this->email_html($name,$email,$phone,$address,$city,$postal,$p,$calc,true);
        $headers=['Content-Type: text/html; charset=UTF-8'];
        wp_mail($email,'Your roof project estimate from '.$s['contractor_name'],$visitor_html,$headers);
        wp_mail($s['contractor_email'],'New roof estimate lead: '.$name,$contractor_html,$headers);
        $this->check_milestone_after_submission();
        wp_send_json_success(['message'=>'Your estimate has been saved and emailed.','submission'=>$id,'redirect'=>$s['booking_url']]);
    }

    private function money($n,$s) { return RFEC_Estimator_Core::money($n,$s); }
    private function line_money($line,$s) { return !empty($line['fixed']) || abs((float)($line['high']??0)-(float)($line['low']??0))<1 ? $this->money($line['amount'],$s) : $this->money($line['low'],$s).' – '.$this->money($line['high'],$s); }
    private function email_html($name,$email,$phone,$address,$city,$postal,$p,$calc,$for_contractor=false) {
        $s=$this->settings();
        $solar_note='';
        $solar_labels=['small'=>'Small array (1 to 10 panels)','medium'=>'Medium array (11–20 panels)','large'=>'Large array (21 or more panels)'];
        if($for_contractor && isset($solar_labels[$p['solar_array']??''])) {
            $process=trim((string)($s['solar_handling_note']??''));
            $solar_note='<div style="background:#fff8e5;border-left:4px solid #dba617;margin:18px 0;padding:14px 16px"><strong>Solar panels reported: '.esc_html($solar_labels[$p['solar_array']]).'</strong>'.($process?'<br>'.esc_html($process):'').'</div>';
        }
        $qcd=$for_contractor?'<div style="border:1px solid #d9e2ef;border-left:4px solid #1d5dbf;border-radius:6px;margin-top:30px;padding:20px 22px;color:#4f5660;font-size:13px;line-height:1.65;background:#fbfdff"><strong style="color:#252a31;font-size:15px">Speed matters with a new lead.</strong><br>The Lead System Toolkit can reply in under 60 seconds and keep prospects warm with automatic follow-up messages while you focus on the job.<p><a href="'.esc_url($this->qcd_url('contractor-email')).'">Automate Lead Follow-Up</a></p><span style="font-size:11px">Roof Estimate Calculator by Quiet City Design</span></div>':'';
        return RFEC_Email_Renderer::render(['settings'=>$s,'lead'=>compact('name','email','phone','address','city','postal'),'calculation'=>$calc,'for_contractor'=>$for_contractor,'heading'=>$for_contractor?'Roof project estimate requested':'Your roof project estimate','money'=>function($value)use($s){return $this->money($value,$s);},'line_money'=>function($line)use($s){return $this->line_money($line,$s);},'extra_html'=>$solar_note,'contractor_footer'=>$qcd]);
    }

    public function columns($cols) { return ['cb'=>$cols['cb'],'title'=>'Customer','email'=>'Email','phone'=>'Phone','range'=>'Estimate range','date'=>$cols['date']]; }
    public function column_value($column,$id) { $s=$this->settings(); if($column==='email') echo esc_html(get_post_meta($id,'_rfec_email',true)); if($column==='phone') echo esc_html(get_post_meta($id,'_rfec_phone',true)); if($column==='range'){ $c=get_post_meta($id,'_rfec_calculation',true); if($c) echo esc_html($this->money($c['low'],$s).' – '.$this->money($c['high'],$s)); } }
    public function meta_boxes() { add_meta_box('rfec_details','Estimate Details',[$this,'details_box'],'rfec_submission','normal','high'); }
    public function details_box($post) { $s=$this->settings(); $c=get_post_meta($post->ID,'_rfec_calculation',true); $p=get_post_meta($post->ID,'_rfec_inputs',true); echo '<p><strong>Email:</strong> '.esc_html(get_post_meta($post->ID,'_rfec_email',true)).'<br><strong>Phone:</strong> '.esc_html(get_post_meta($post->ID,'_rfec_phone',true)).'<br><strong>Street address:</strong> '.esc_html(get_post_meta($post->ID,'_rfec_address',true)).'<br><strong>City:</strong> '.esc_html(get_post_meta($post->ID,'_rfec_city',true)).'<br><strong>Postal/ZIP:</strong> '.esc_html(get_post_meta($post->ID,'_rfec_postal',true)).'</p>'; if($c){ echo '<table class="widefat striped"><tbody>'; foreach($c['lines'] as $l) echo '<tr><td>'.esc_html($l['label']).'</td><td>'.esc_html($this->line_money($l,$s)).'</td></tr>'; echo '</tbody></table><h3>'.esc_html($this->money($c['low'],$s).' – '.$this->money($c['high'],$s)).'</h3>'; } if($p) echo '<details><summary>Submitted factors</summary><pre>'.esc_html(wp_json_encode($p,JSON_PRETTY_PRINT)).'</pre></details>'; }
}

RFEC_Plugin::instance();
