// ============================================================================
// Mudanzas Internacionales — Quote Calculator
//
// 4-stage flow (form → price → lead → done) wired to the live Walio public
// rates API as a **relocation** request — i.e. every quote sends
// `cargo.householdGoods` + `commodity: 'Personal Effects'` (and for LCL also
// `origin.isResidential: true`), which makes Walio's backend apply the
// Relocation FCL / Relocation LCL markup tier instead of the commercial tier.
//
// Audience-gating note (load-bearing — same rule as the homepage hero):
// For FCL + LCL, the move trigger fires on cargoType === 'move' ALONE, with
// no audience check. Business + Move = Individual + Move = relocation rates,
// because the cargo IS personal effects regardless of who's shipping.
//
// Only FCL + LCL — no Air. Per-mode mapping:
//   • Studio / 1 bedroom / ~10–20 boxes / small or medium LCL → LCL
//   • 2+ bedrooms / partial move / whole-house / 20' or 40' container → FCL
//
// Source of truth for the relocation request shape:
//   • components/HeroAnimated.jsx → buildRequest() (FCL ~L4491, LCL ~L4622)
//   • CLAUDE.md → "Cargo type → Walio markup tier" table
// ============================================================================

(function () {
  const { useState, useMemo, useRef, useEffect } = React;
  const STR = window.STR;

  // Helper for rendering markup-containing strings
  const Html = ({ as: Tag = 'span', html, ...rest }) =>
    React.createElement(Tag, { ...rest, dangerouslySetInnerHTML: { __html: html } });

  // ───────────────────────────────────────────────────────────────────────
  // Catalog
  // ───────────────────────────────────────────────────────────────────────

  // Calling-codes for the lead form. Kept short — landing audience is
  // overwhelmingly LATAM + USA + Spain.
  const COUNTRY_CODES = [
    { code: '+57',   flag: '🇨🇴' }, { code: '+58',   flag: '🇻🇪' },
    { code: '+54',   flag: '🇦🇷' }, { code: '+56',   flag: '🇨🇱' },
    { code: '+51',   flag: '🇵🇪' }, { code: '+593',  flag: '🇪🇨' },
    { code: '+55',   flag: '🇧🇷' }, { code: '+52',   flag: '🇲🇽' },
    { code: '+507',  flag: '🇵🇦' }, { code: '+1809', flag: '🇩🇴' },
    { code: '+34',   flag: '🇪🇸' }, { code: '+1',    flag: '🇺🇸' },
  ];

  // ───────────────────────────────────────────────────────────────────────
  // Preset catalog + request builders — DELEGATED to landing/quote-builders.js
  // ───────────────────────────────────────────────────────────────────────
  //
  // Defaults (cbm/pieces/cuft/labels for 12 presets) and the FCL+LCL request
  // builders live in `landing/quote-builders.js` so the admin's Quote Config
  // editor uses the exact same code path as the customer-facing calculator.
  // That keeps the editor's live request-preview honest — what the admin
  // sees mid-edit is what the next customer will trigger.
  //
  // The Firestore config doc at `config/landing-international-moves` can
  // override any default; merging happens via __landingBuilders.applyConfig.
  const LB = window.__landingBuilders;
  // Hard fallback: if quote-builders.js never loaded (shouldn't happen on
  // a normal page render), surface a clear error so we don't silently send
  // empty requests to Walio.
  if (!LB) {
    console.error('[abgs landing] quote-builders.js failed to load — calculator cannot run.');
    return;
  }

  // Pull the total from whichever shape the Walio endpoint returned.
  // Order matches RichRateCard.rateTotalOf in HeroAnimated.jsx — the FCL
  // endpoint returns `completeQuote.totalCost`, LCL returns `pricing.total`,
  // and a few legacy responses use `totalPrice` / `totalCost` directly.
  function rateTotalOf(r) {
    if (!r) return null;
    if (r.completeQuote && typeof r.completeQuote.totalCost === 'number') return r.completeQuote.totalCost;
    if (r.pricing && typeof r.pricing.total === 'number') return r.pricing.total;
    if (typeof r.totalPrice === 'number') return r.totalPrice;
    if (typeof r.totalCost === 'number') return r.totalCost;
    return null;
  }

  // Pull the per-line breakdown from a rate. Mirrors the homepage's
  // RichRateCard.collectBreakdown (components/HeroAnimated.jsx L11700-11834):
  //
  //   • Prefers `completeQuote.*` as the sole source when present. The
  //     other shapes (`pricing.breakdown`, root `additionalServices`) are
  //     used only when completeQuote is absent — they're legacy formats
  //     for older endpoints. Importantly we DO NOT fall through to
  //     `pricing.breakdown` for Ocean rates: that's the carrier's raw rate
  //     subset, not the all-in total, and mixing it in produced rows that
  //     didn't sum to the displayed price.
  //
  //   • Iterates `additionalServices[]` and `extraCosts[]` as ARRAYS of
  //     `{ name, amount }` — each becomes its own line. `name` comes from
  //     the carrier (usually English) and stays verbatim; only the canonical
  //     labels (ocean / pickup / delivery / etc.) are translated via STR.
  //
  //   • Conditional rules taken straight from RichRateCard:
  //       - `drayageCost` only renders if "drayage" isn't already in
  //         additionalServices (case-insensitive).
  //       - `servicesCost` only renders as a bundled "Services" line when
  //         additionalServices is missing or empty.
  //
  //   • Dedupe (label.toLowerCase().replace(/\s+/g, '') + ':' + amount)
  //     defends against a single source repeating itself.
  function collectBreakdown(r) {
    if (!r) return [];
    const out = []; const seen = new Set();
    const push = (lab, val) => {
      if (!lab || typeof val !== 'number' || !isFinite(val) || val === 0) return;
      const key = String(lab).toLowerCase().replace(/\s+/g, '') + ':' + (Math.round(val * 100) / 100);
      if (seen.has(key)) return;
      seen.add(key);
      out.push({ lab: String(lab).trim(), val: Math.round(val * 100) / 100 });
    };
    const L = STR.calc.breakdownLabels;

    // Source 1 (preferred): completeQuote.* — order matches RichRateCard.
    if (r.completeQuote) {
      const cq = r.completeQuote;
      const br = cq.breakdown || {};

      // Main freight leg
      if (typeof cq.oceanFreight === 'number') push(L.ocean, cq.oceanFreight);
      if (typeof cq.airFreight   === 'number') push(L.ocean, cq.airFreight); // air uses the ocean label slot

      // Optional linehaul (air with cross-country LTL connection)
      if (typeof cq.linehaulCost === 'number' && cq.linehaulCost > 0) {
        push(L.linehaul, cq.linehaulCost);
      } else if (typeof br.linehaul === 'number' && br.linehaul > 0) {
        push(L.linehaul, br.linehaul);
      }

      // Fuel + security surcharges live inside completeQuote.breakdown
      if (typeof br.fuel     === 'number' && br.fuel     > 0) push(L.fuelSurcharge,     br.fuel);
      if (typeof br.security === 'number' && br.security > 0) push(L.securitySurcharge, br.security);

      // extraCosts: array of { name, amount } OR a bundled number in breakdown
      if (Array.isArray(cq.extraCosts)) {
        cq.extraCosts.forEach(s => push(s.name || L.extra, s.amount));
      } else if (typeof br.extraCosts === 'number') {
        push(L.extra, br.extraCosts);
      }

      // additionalServices: itemize each — carrier-provided names stay verbatim
      let servicesItemized = false;
      if (Array.isArray(cq.additionalServices) && cq.additionalServices.length > 0) {
        cq.additionalServices.forEach(s => push(s.name || L.services, s.amount));
        servicesItemized = true;
      }

      // Drayage only renders if it isn't already double-counted inside additionalServices
      const drayageInServices = servicesItemized
        && cq.additionalServices.some(s => /drayage/i.test(s.name || ''));
      if (typeof cq.drayageCost === 'number' && cq.drayageCost > 0 && !drayageInServices) {
        push(L.drayage, cq.drayageCost);
      }

      // servicesCost is a bundled fallback — only render if we didn't itemize
      if (!servicesItemized && typeof cq.servicesCost === 'number' && cq.servicesCost > 0) {
        push(L.services, cq.servicesCost);
      }

      // Pickup / delivery legs — surfaced as their own rows
      if (typeof cq.pickupCost   === 'number' && cq.pickupCost   > 0) push(L.pickup,   cq.pickupCost);
      if (typeof cq.deliveryCost === 'number' && cq.deliveryCost > 0) push(L.delivery, cq.deliveryCost);

      if (out.length > 0) return out;
    }

    // Source 2: pricingBreakdown[] — { label, amount } pairs (legacy FCL shape).
    if (Array.isArray(r.pricingBreakdown) && r.pricingBreakdown.length > 0) {
      r.pricingBreakdown.forEach(it => push(it.label || it.name, it.amount || it.value));
      if (out.length > 0) return out;
    }

    // Source 3: additionalServices[] at root (legacy air shape).
    if (Array.isArray(r.additionalServices)) {
      r.additionalServices.forEach(s => push(s.name || L.services, s.cost || s.amount));
    }
    return out;
  }

  // Transit days come on different fields per mode. Returns [min, max] or null.
  function rateTransitDays(r) {
    if (!r) return null;
    if (Array.isArray(r.transitDays) && r.transitDays.length === 2) return r.transitDays;
    if (r.transitTimeDays && r.transitTimeDays.min != null) {
      return [r.transitTimeDays.min, r.transitTimeDays.max || r.transitTimeDays.min];
    }
    if (typeof r.transitDays === 'number') return [r.transitDays, r.transitDays];
    if (typeof r.transit === 'number') return [r.transit, r.transit];
    return null;
  }

  // Format USD with comma separators, no decimals (for the big number)
  const fmtUSD = n => Number(n || 0).toLocaleString('en-US', { maximumFractionDigits: 0 });

  // Generate a deterministic quote id from inputs
  const makeQuoteId = (origin, dest, sizeKey) => {
    const seed = (origin + dest + sizeKey).split('').reduce((a, c) => a + c.charCodeAt(0), 0);
    return 'ABG-' + String(24000 + (seed % 89999)).padStart(5, '0');
  };

  // Date helpers (locale comes from i18n)
  const todayPlus = (days) => {
    const d = new Date(); d.setDate(d.getDate() + days);
    return d.toLocaleDateString(STR.calc.dateLocale, { day: '2-digit', month: 'short' });
  };

  // ───────────────────────────────────────────────────────────────────────
  // Google Places autocomplete input
  // ───────────────────────────────────────────────────────────────────────
  //
  // Lightweight version of HeroAnimated.jsx's QuotePlacesInput. Uses the
  // programmatic AutocompleteService + PlacesService.getDetails to resolve
  // a picked suggestion to {country, state, city, zip, lat, lng}. Editing
  // after a pick clears the resolved place via onSelect(null) so we never
  // submit stale coords.

  function QuotePlacesInput({ value, onChange, onSelect, place, placeholder, label, hint }) {
    const wrapRef = useRef(null);
    const inputRef = useRef(null);
    const svcRef = useRef(null);
    const detailsSvcRef = useRef(null);
    const tokenRef = useRef(null);
    const debounceRef = useRef(null);
    const [ready, setReady] = useState(
      !!(window.google && window.google.maps && window.google.maps.places)
    );
    const [suggestions, setSuggestions] = useState([]);
    const [open, setOpen] = useState(false);
    const [active, setActive] = useState(-1);
    // Position is tracked here because the dropdown renders via a portal to
    // <body> (so it escapes the .calc card's overflow: hidden) and needs
    // viewport coordinates instead of relying on its parent's positioning.
    const [pos, setPos] = useState({ top: 0, left: 0, width: 0 });

    // Watch the global Maps loader so we hook in even if the SDK was still
    // loading when this component first mounted.
    useEffect(() => {
      if (ready) return;
      if (window.__abgsGooglePlacesReady) { setReady(true); return; }
      const onR = () => setReady(true);
      window.__abgsGooglePlacesListeners = window.__abgsGooglePlacesListeners || [];
      window.__abgsGooglePlacesListeners.push(onR);
    }, [ready]);

    // Lazy-create the Places services + session token (Places billing best
    // practice: a session = autocomplete-then-details for one selection).
    useEffect(() => {
      if (!ready || svcRef.current) return;
      try {
        svcRef.current = new window.google.maps.places.AutocompleteService();
        tokenRef.current = new window.google.maps.places.AutocompleteSessionToken();
        const host = document.createElement('div');
        detailsSvcRef.current = new window.google.maps.places.PlacesService(host);
      } catch (e) { /* degrades to plain text — submit will be blocked */ }
    }, [ready]);

    // Close the dropdown when clicking outside the field (or its portal-rendered
    // dropdown). The portal node is tagged with [data-abgs-places-portal] so
    // mousedown inside it doesn't count as "outside the field".
    useEffect(() => {
      const onDocDown = (e) => {
        if (wrapRef.current && wrapRef.current.contains(e.target)) return;
        if (e.target && typeof e.target.closest === 'function'
            && e.target.closest('[data-abgs-places-portal]')) return;
        setOpen(false);
      };
      document.addEventListener('mousedown', onDocDown);
      return () => document.removeEventListener('mousedown', onDocDown);
    }, []);

    // Recompute the portal-dropdown's screen position whenever the input
    // moves (window scroll, resize, or the field re-renders). The input ref
    // is the source of truth — `top` sits 4px below the input bottom edge.
    const recomputePos = () => {
      const el = inputRef.current;
      if (!el) return;
      const r = el.getBoundingClientRect();
      setPos({ top: r.bottom + 4, left: r.left, width: r.width });
    };
    useEffect(() => {
      if (!open) return;
      recomputePos();
      const onMove = () => recomputePos();
      window.addEventListener('scroll', onMove, true);
      window.addEventListener('resize', onMove);
      return () => {
        window.removeEventListener('scroll', onMove, true);
        window.removeEventListener('resize', onMove);
      };
    }, [open, suggestions.length]);

    const resolvePlace = (placeId, fallback) => {
      const svc = detailsSvcRef.current;
      if (!svc || !placeId) { onSelect && onSelect(null); return; }
      svc.getDetails(
        { placeId,
          fields: ['geometry', 'address_components', 'name', 'formatted_address'],
          sessionToken: tokenRef.current },
        (p, status) => {
          const ok = status === window.google.maps.places.PlacesServiceStatus.OK;
          if (!ok || !p) { onSelect && onSelect(null); return; }
          const ac = p.address_components || [];
          const find = (type) => ac.find(c => c.types.includes(type));
          const country = (find('country') || {}).short_name || '';
          const state   = (find('administrative_area_level_1') || {}).short_name || '';
          const city    = (find('locality') || find('postal_town')
                           || find('administrative_area_level_2') || {}).long_name || '';
          const zip     = (find('postal_code') || {}).short_name || '';
          const loc = p.geometry && p.geometry.location;
          onSelect && onSelect({
            description: fallback, place_id: placeId,
            country, state, city, zip,
            lat: loc ? loc.lat() : null,
            lng: loc ? loc.lng() : null,
          });
          // Refresh session token (billing best practice).
          try { tokenRef.current = new window.google.maps.places.AutocompleteSessionToken(); } catch (e) {}
        }
      );
    };

    const fetchSuggestions = (input) => {
      const svc = svcRef.current;
      if (!svc || !input || input.length < 2) { setSuggestions([]); setOpen(false); return; }
      svc.getPlacePredictions(
        { input, types: ['(regions)'], sessionToken: tokenRef.current },
        (preds, status) => {
          const ok = status === window.google.maps.places.PlacesServiceStatus.OK;
          if (ok && preds && preds.length > 0) {
            setSuggestions(preds); setOpen(true); setActive(-1);
          } else { setSuggestions([]); setOpen(false); }
        }
      );
    };

    const handleChange = (e) => {
      const v = e.target.value;
      onChange(v);
      // Editing invalidates the previously-resolved place.
      if (place) onSelect(null);
      if (debounceRef.current) clearTimeout(debounceRef.current);
      debounceRef.current = setTimeout(() => fetchSuggestions(v), 220);
    };

    const selectSuggestion = (pred) => {
      const text = pred.description || '';
      onChange(text);
      setSuggestions([]); setOpen(false); setActive(-1);
      resolvePlace(pred.place_id, text);
    };

    const handleKeyDown = (e) => {
      if (!open || suggestions.length === 0) return;
      if (e.key === 'ArrowDown') { e.preventDefault(); setActive(p => Math.min(p + 1, suggestions.length - 1)); }
      else if (e.key === 'ArrowUp')   { e.preventDefault(); setActive(p => Math.max(p - 1, 0)); }
      else if (e.key === 'Enter' && active >= 0) { e.preventDefault(); selectSuggestion(suggestions[active]); }
      else if (e.key === 'Escape') { setOpen(false); }
    };

    const showHint = !!value && !place && !open;

    return (
      <div className="field" ref={wrapRef} style={{ position: 'relative' }}>
        <label className="field__label">
          {label} <span className="field__label-hint">{hint}</span>
        </label>
        <input
          ref={inputRef}
          type="text"
          className="field__input"
          placeholder={placeholder}
          value={value || ''}
          onChange={handleChange}
          onKeyDown={handleKeyDown}
          onFocus={() => { if (suggestions.length > 0) setOpen(true); }}
          autoComplete="off"
          spellCheck="false"
        />
        {showHint && (
          <div style={{
            marginTop: 4, fontSize: 11, color: '#B54708',
            display: 'flex', alignItems: 'center', gap: 4,
          }}>
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor"
              strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="12" cy="12" r="10"/><path d="M12 8 V8.01 M12 12 V16"/>
            </svg>
            {STR.calc.placesPickHint}
          </div>
        )}
        {open && suggestions.length > 0 && ReactDOM.createPortal(
          // Rendered to <body> so it escapes .calc's `overflow: hidden`.
          // Position is fixed-to-viewport, recomputed on scroll/resize.
          <div data-abgs-places-portal="1" style={{
            position: 'fixed', top: pos.top, left: pos.left, width: pos.width,
            zIndex: 1000,
            background: '#fff', border: '1px solid rgba(16,24,40,0.10)',
            borderRadius: 10, boxShadow: '0 12px 28px -8px rgba(16,24,40,0.18)',
            maxHeight: 224, overflowY: 'auto',
          }}>
            {suggestions.map((p, idx) => {
              const main = (p.structured_formatting && p.structured_formatting.main_text) || p.description;
              const secondary = p.structured_formatting && p.structured_formatting.secondary_text;
              const isActive = idx === active;
              return (
                <div key={p.place_id}
                  onMouseDown={(e) => { e.preventDefault(); selectSuggestion(p); }}
                  onMouseEnter={() => setActive(idx)}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 10,
                    padding: '8px 12px', cursor: 'pointer',
                    background: isActive ? '#F0F6FF' : 'transparent',
                    borderBottom: idx === suggestions.length - 1 ? 'none' : '1px solid #F3F4F6',
                  }}>
                  <span style={{
                    width: 24, height: 24, borderRadius: 9999,
                    background: '#EEF4FF', display: 'inline-grid', placeItems: 'center',
                    flexShrink: 0, color: '#1E57D6', fontSize: 11,
                  }}>📍</span>
                  <div style={{ minWidth: 0, flex: 1 }}>
                    <div style={{
                      fontSize: 13, fontWeight: 600, color: '#0B1220',
                      whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                    }}>{main}</div>
                    {secondary && (
                      <div style={{
                        fontSize: 11.5, color: '#667085', marginTop: 1,
                        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                      }}>{secondary}</div>
                    )}
                  </div>
                </div>
              );
            })}
          </div>,
          document.body
        )}
      </div>
    );
  }

  // ───────────────────────────────────────────────────────────────────────
  // Calculator component
  // ───────────────────────────────────────────────────────────────────────

  // ─── Port-select dropdown ──────────────────────────────────────────────
  // Native styled <select> with a port icon. Auto-populated when the parent
  // resolves a Google Places pick to country+coords. Renders "Select origin
  // city first" / "Searching ports…" / error states based on `state`.
  function CalcPortSelect({ side, state, onPick, placeholder }) {
    const isReady = !state.loading && state.list && state.list.length > 0;
    const hint = state.loading
      ? STR.calc.portsLoading
      : state.error || (!state.selected ? placeholder : null);
    return (
      <div style={{ position: 'relative' }}>
        <span style={{ position: 'absolute', left: 12, top: '50%',
          transform: 'translateY(-50%)', color: '#1846B0', pointerEvents: 'none',
          display: 'inline-flex', alignItems: 'center' }}>
          <Icon name="container" size={14}/>
        </span>
        <select
          value={(state.selected && state.selected.unCode) || ''}
          onChange={(e) => {
            const next = state.list.find(p => p.unCode === e.target.value);
            if (next) onPick(next);
          }}
          disabled={!isReady}
          aria-label={side === 'origin' ? STR.calc.originPortLabel : STR.calc.destPortLabel}
          style={{
            width: '100%', appearance: 'none', WebkitAppearance: 'none',
            padding: '10px 30px 10px 34px', borderRadius: 8,
            border: '1px solid ' + (state.selected ? '#12B76A' : '#d0d5dd'),
            background: '#fff',
            fontSize: 13, fontWeight: 500, color: state.selected ? '#0B1220' : '#98a2b3',
            letterSpacing: '-0.005em', cursor: isReady ? 'pointer' : 'not-allowed',
            fontFamily: 'inherit',
          }}>
          {!state.selected && <option value="">{hint || placeholder}</option>}
          {(state.list || []).map(p => (
            <option key={p.unCode} value={p.unCode}>
              {p.city || p.portName} · {p.unCode}
              {p.distance != null ? ` (${Math.round(p.distance)} km)` : ''}
            </option>
          ))}
        </select>
        <span style={{ position: 'absolute', right: 10, top: '50%',
          transform: 'translateY(-50%)', color: '#667085', pointerEvents: 'none' }}>
          <Icon name="chevronDown" size={12}/>
        </span>
      </div>
    );
  }

  // ─── Inland-transportation radio column ───────────────────────────────
  // Two stacked radio cards (CY = customer arranges, SD = AB Group arranges).
  // Identical contract to the homepage hero's InlandTransportColumn — the
  // selected token (customer_deliver / abgroup_pickup / customer_pickup /
  // abgroup_deliver) lands in options.originTransport / destinationTransport
  // of the Walio rate request.
  function CalcInlandRadios({ label, value, onChange, options }) {
    return (
      <div>
        <div style={{ fontSize: 10, fontWeight: 800, color: '#475467',
          letterSpacing: '0.12em', textTransform: 'uppercase',
          paddingLeft: 2, marginBottom: 6 }}>
          {label}
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          {options.map(opt => {
            const active = value === opt.value;
            return (
              <button type="button" key={opt.value} onClick={() => onChange(opt.value)}
                style={{ textAlign: 'left',
                  padding: '8px 10px', borderRadius: 9,
                  background: active ? '#F0F5FF' : '#FFFFFF',
                  border: '1px solid ' + (active ? '#1E57D6' : 'rgba(16,24,40,0.1)'),
                  boxShadow: active
                    ? '0 0 0 2px rgba(30,87,214,0.12), 0 1px 2px rgba(16,24,40,0.04)'
                    : '0 1px 2px rgba(16,24,40,0.04), inset 0 1px 0 rgba(255,255,255,0.9)',
                  cursor: 'pointer', fontFamily: 'inherit',
                  display: 'flex', alignItems: 'flex-start', gap: 8 }}>
                <span style={{ marginTop: 2, width: 12, height: 12, borderRadius: 9999,
                  border: '2px solid ' + (active ? '#1E57D6' : 'rgba(16,24,40,0.25)'),
                  flexShrink: 0, display: 'inline-grid', placeItems: 'center', background: '#fff' }}>
                  {active && <span style={{ width: 5, height: 5, borderRadius: 9999,
                    background: '#1E57D6' }}/>}
                </span>
                <span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                  padding: '2px 6px', borderRadius: 5, flexShrink: 0, marginTop: 1,
                  background: active
                    ? 'linear-gradient(180deg, #2E6AE8 0%, #1E57D6 60%, #1846B0 100%)'
                    : 'rgba(30,87,214,0.1)',
                  color: active ? '#fff' : '#1846B0',
                  fontSize: 9.5, fontWeight: 800, letterSpacing: '0.05em' }}>
                  {opt.code}
                </span>
                <span style={{ minWidth: 0, flex: 1 }}>
                  <span style={{ display: 'block', fontSize: 11.5, fontWeight: 700,
                    color: '#0B1220', lineHeight: 1.3 }}>{opt.title}</span>
                  <span style={{ display: 'block', fontSize: 10, color: '#667085',
                    marginTop: 2, lineHeight: 1.35 }}>{opt.desc}</span>
                </span>
              </button>
            );
          })}
        </div>
      </div>
    );
  }

  function QuoteCalculator() {
    // 'form' → 'lead' → 'price'  (lead gates the rates API, matches the
    // homepage hero pattern. 'price' is the terminal success state.)
    const [stage, setStage] = useState('form');

    const [mode, setMode] = useState('rooms'); // rooms | boxes | type
    const [originText, setOriginText] = useState('');
    const [destText, setDestText] = useState('');
    const [originPlace, setOriginPlace] = useState(null);
    const [destPlace, setDestPlace] = useState(null);
    const [sizeRooms, setSizeRooms] = useState('1bed');
    const [sizeBoxes, setSizeBoxes] = useState('md');
    const [sizeType, setSizeType] = useState('lcl-md');
    // Custom-rooms extrapolator (4+ bedrooms / large household moves). When
    // `customRoomsActive` is true we ignore the standard sizeRooms preset
    // and build a synthetic one from `customRoomCount` × 1bed baseline.
    // Mode auto-flips to FCL once cargo exceeds 25 m³, with the right
    // number of 40' HC containers picked (capacity ~67 m³ each).
    const [customRoomsActive, setCustomRoomsActive] = useState(false);
    const [customRoomCount, setCustomRoomCount] = useState(4);
    // Container MIX for the Type tab's FCL presets. Lets the customer build
    // any combination — e.g. 1×20' + 2×40' HC — instead of being limited to
    // a single container type. Stored as { '20': N, '40HC': N } to match
    // Walio's request shape. The stepper UI renders one row per type and
    // multiplies the preset's containers/cbm/cuft before the request goes
    // out. Resets when the customer flips between FCL presets (see effect
    // below the size-meta block).
    const [containerMix, setContainerMix] = useState({ '20': 0, '40HC': 1 });
    // Multi-vehicle picker — mirrors the homepage hero's RELOCATION_VEHICLES
    // pattern. Each vehicle reserves 1100 cuft (one 20' bay) and pushes the
    // sizeMeta up to FCL with the right container mix from recommendFclForCuft.
    // Format: [{ id, type }] where `type` is a key from
    // window.__abgsSharedCalculator.RELOCATION_VEHICLES.
    const [relocationVehicles, setRelocationVehicles] = useState([]);
    const [vehicle, setVehicle] = useState(false);
    // Inland-transportation tokens — same defaults as the homepage hero
    // (port-to-port). User flips either side to SD to add an AB Group
    // pickup/delivery leg. The legacy single `pickup` boolean used to drive
    // origin transport on its own; now both sides are independent.
    const [originTransport,      setOriginTransport]      = useState('customer_deliver');
    const [destinationTransport, setDestinationTransport] = useState('customer_pickup');
    // ECU door selections when the user is on SD. The shared DoorPicker
    // (landing/door-picker-shared.jsx) writes back here on pick. The door's
    // .code is what flows into origin.doorCode / destination.doorCode in the
    // outbound rate request — that's the trigger for door-aware pickup
    // pricing on Walio's side.
    const [originDoor, setOriginDoor] = useState(null);
    const [destDoor,   setDestDoor]   = useState(null);
    // Port state — populated on origin/destination place pick via Walio
    // /v1/locations/ports. Shape matches the homepage hero's originPorts /
    // destPorts (HeroAnimated.jsx L3801-3804).
    const [originPorts, setOriginPorts] = useState({
      loading: false, list: [], selected: null, error: null });
    const [destPorts,   setDestPorts]   = useState({
      loading: false, list: [], selected: null, error: null });

    // Firestore-hydrated per-landing config. null until the fetch resolves;
    // missing keys fall back silently to __landingBuilders.DEFAULTS so the
    // calculator keeps working if Firestore is unreachable or the doc is
    // empty. We don't block the UI on config — the form renders immediately
    // with defaults and re-renders once overrides arrive (typically <300ms).
    const [overrides, setOverrides] = useState(null);

    // The calc is the primary CTA on the landing — it must NEVER be invisible
    // just because IntersectionObserver hasn't fired. The page-level reveal-
    // on-scroll script (in international-moving.html) starts every `.reveal`
    // at opacity: 0 and waits for the element to enter the viewport. On
    // narrow viewports (when the hero grid stacks vertically) the calc lands
    // below the fold and stays hidden until the user scrolls — which can
    // look like a hard freeze, especially because the rest of the hero is
    // already painted. Force-add `.in` on mount so the fade-in still plays
    // but the calc always becomes visible regardless of position.
    const calcRef = useRef(null);
    useEffect(() => {
      if (!calcRef.current) return;
      const el = calcRef.current;
      // Wait one frame so the browser registers the initial `.reveal`
      // opacity:0 before transitioning to opacity:1. Without rAF, the
      // class change happens in the same paint and the transition is
      // skipped — element pops in instead of fading.
      requestAnimationFrame(() => el.classList.add('in'));
    }, []);

    useEffect(() => {
      const fetcher = window.__abgsConfig && window.__abgsConfig.getQuoteConfig;
      if (!fetcher) return; // firebase-public.js not loaded → defaults only
      let cancelled = false;
      // Slug must match the admin Landing Pages catalog entry + the URL
      // basename: landing/international-moving.html (English version).
      fetcher('international-moving').then((cfg) => {
        if (!cancelled && cfg) setOverrides(cfg);
      });
      return () => { cancelled = true; };
    }, []);

    // Effective tables — defaults merged with the Firestore overrides. Memo'd
    // so re-renders don't rebuild the merge on every keystroke.
    const effective = useMemo(() => LB.applyConfig(overrides), [overrides]);
    const lang = STR.lang || 'es';

    // ─── Port auto-discovery ──────────────────────────────────────────
    // When either origin or destination resolves via Google Places, hit
    // Walio /v1/locations/ports for that country + coords. Auto-pick the
    // nearest (list[0]); user can swap via the dropdown. Mirrors the
    // homepage hero's port effects (HeroAnimated.jsx L3964-4014).
    useEffect(() => {
      if (!LB.walioSearchPorts) return;
      if (!originPlace || !originPlace.country) {
        setOriginPorts({ loading: false, list: [], selected: null, error: null });
        return;
      }
      let cancelled = false;
      setOriginPorts(p => ({ ...p, loading: true, error: null }));
      LB.walioSearchPorts({
        country: originPlace.country,
        lat: originPlace.lat, lng: originPlace.lng,
        mode: 'sea', limit: 10,
      }).then(ports => {
        if (cancelled) return;
        setOriginPorts(prev => ({
          loading: false,
          list: ports.slice(0, 10),
          selected: prev.selected && ports.some(p => p.unCode === prev.selected.unCode)
            ? prev.selected
            : (ports[0] || null),
          error: ports.length === 0 ? STR.calc.errNoPorts : null,
        }));
      });
      return () => { cancelled = true; };
    }, [originPlace && originPlace.country, originPlace && originPlace.lat, originPlace && originPlace.lng]);

    useEffect(() => {
      if (!LB.walioSearchPorts) return;
      if (!destPlace || !destPlace.country) {
        setDestPorts({ loading: false, list: [], selected: null, error: null });
        return;
      }
      let cancelled = false;
      setDestPorts(p => ({ ...p, loading: true, error: null }));
      LB.walioSearchPorts({
        country: destPlace.country,
        lat: destPlace.lat, lng: destPlace.lng,
        mode: 'sea', limit: 10,
      }).then(ports => {
        if (cancelled) return;
        setDestPorts(prev => ({
          loading: false,
          list: ports.slice(0, 10),
          selected: prev.selected && ports.some(p => p.unCode === prev.selected.unCode)
            ? prev.selected
            : (ports[0] || null),
          error: ports.length === 0 ? STR.calc.errNoPorts : null,
        }));
      });
      return () => { cancelled = true; };
    }, [destPlace && destPlace.country, destPlace && destPlace.lat, destPlace && destPlace.lng]);
    const labelOf = (p) => (lang === 'en') ? (p.labelEn || p.label || '') : (p.label || p.labelEn || '');
    const subOf   = (p) => (lang === 'en') ? (p.subEn   || p.sub   || '') : (p.sub   || p.subEn   || '');

    const [leadName, setLeadName] = useState('');
    const [leadEmail, setLeadEmail] = useState('');
    const [leadPhone, setLeadPhone] = useState('');
    const [leadCountry, setLeadCountry] = useState('+57');
    // Honeypot — bots fill every visible field; this one is hidden via CSS.
    const [leadHoneypot, setLeadHoneypot] = useState('');

    // Single in-flight flag for the lead-submit step (covers both saveLead
    // and the Walio rate fetch since they're sequenced into one user action).
    const [isLeadSaving, setLeadSaving] = useState(false);
    const [apiError, setApiError] = useState(null);
    const [leadError, setLeadError] = useState(null);

    // Walio response state
    const [rates, setRates] = useState(null);            // array of rate cards (null = not fetched)
    const [pendingRequest, setPendingRequest] = useState(null); // { url, body }

    // Form-opened timestamps used by the anti-spam time gate. Captured at
    // first calc-page render and re-captured when the lead form first shows.
    const calcOpenedAtRef = useRef(Date.now());
    const leadOpenedAtRef = useRef(null);
    useEffect(() => {
      if (stage === 'lead' && leadOpenedAtRef.current == null) {
        leadOpenedAtRef.current = Date.now();
      }
    }, [stage]);

    const currentSizeKey = mode === 'rooms' ? sizeRooms
                          : mode === 'boxes' ? sizeBoxes : sizeType;
    // `mode` is 'rooms' | 'boxes' | 'type' (the tab id) — builders + Firestore
    // config use the plural 'types' for the third tab, so we translate here.
    const presetGroupKey = mode === 'type' ? 'types' : mode;
    const sizeTable = effective[presetGroupKey];
    // When the customer flipped the Rooms tab into "Custom" mode (the 5th
    // chip), we ignore the standard sizeTable lookup and synthesize a preset
    // for N bedrooms via the shared module. Falls back to the default 1bed
    // lookup if the shared module isn't loaded yet (e.g. defer race).
    const usingCustomRooms = mode === 'rooms' && customRoomsActive
      && window.__abgsSharedCalculator
      && window.__abgsSharedCalculator.buildCustomRoomsPreset;
    const rawBaseSizeMeta = usingCustomRooms
      ? window.__abgsSharedCalculator.buildCustomRoomsPreset(
          customRoomCount, sizeTable['1bed'])
      : (sizeTable[currentSizeKey] || Object.values(sizeTable)[1]);
    // Container-mix override — only when the customer is on the Type tab and
    // the picked preset is an FCL container (20' / 40'). The mix overrides
    // the preset's single-container default with whatever combination the
    // customer assembled in the stepper (e.g. 1×20' + 1×40'), then we
    // rescale cbm/cuft/pieces by the total container volume relative to the
    // preset's single-container baseline. Walio's body just needs the
    // container map — cbm/cuft are kept consistent for the on-card summary.
    // Per-container approximate volumes (CBM) used for the rescale math:
    //   20'   → 28 m³ baseline
    //   40HC  → 55 m³ baseline
    const CONTAINER_CBM = { '20': 28, '40HC': 55 };
    const mixTotalContainers = (mode === 'type'
      && rawBaseSizeMeta && rawBaseSizeMeta.mode === 'FCL'
      && rawBaseSizeMeta.fcl && rawBaseSizeMeta.fcl.containers)
      ? ((containerMix['20'] || 0) + (containerMix['40HC'] || 0)) : 0;
    const baseSizeMeta = mixTotalContainers > 0 ? (() => {
      const containers = {};
      if ((containerMix['20']    || 0) > 0) containers['20']   = containerMix['20'];
      if ((containerMix['40HC']  || 0) > 0) containers['40HC'] = containerMix['40HC'];
      const totalCbm = (containerMix['20'] || 0) * CONTAINER_CBM['20']
                     + (containerMix['40HC'] || 0) * CONTAINER_CBM['40HC'];
      const baseCbm = Math.max(rawBaseSizeMeta.cbm || 1, 1);
      const scale = totalCbm / baseCbm;
      return {
        ...rawBaseSizeMeta,
        cbm:    Math.round(totalCbm),
        cuft:   Math.round((rawBaseSizeMeta.cuft   || 0) * scale),
        pieces: Math.round((rawBaseSizeMeta.pieces || 0) * scale),
        fcl: { ...rawBaseSizeMeta.fcl, containers },
      };
    })() : rawBaseSizeMeta;
    // Vehicle add-on: vehicles can't ship in LCL groupage — they need a
    // dedicated container. When any vehicle is added we:
    //   1. Total the cuft of vehicles (each = VEHICLE_SLOT_CUFT) + the
    //      preset's cuft to get household_cuft + vehicles_cuft.
    //   2. Call recommendFclForCuft() to pick the container mix (may return
    //      3× 40' + 1× 20' for big moves).
    //   3. Promote the preset to mode: 'FCL' if it was LCL.
    // The household items[] list is preserved so packages still ride in the
    // same containers alongside the vehicles.
    const SC = window.__abgsSharedCalculator || {};
    const vehiclesCuft = relocationVehicles.reduce((s, v) => {
      const def = SC.RELOCATION_VEHICLE_BY_KEY && SC.RELOCATION_VEHICLE_BY_KEY[v.type];
      return s + (def ? def.cuft : 0);
    }, 0);
    const vehiclesKg = relocationVehicles.reduce((s, v) => {
      const def = SC.RELOCATION_VEHICLE_BY_KEY && SC.RELOCATION_VEHICLE_BY_KEY[v.type];
      return s + (def ? def.weightKg : 0);
    }, 0);
    const hasVehicles = relocationVehicles.length > 0;
    const promotedToFcl = hasVehicles && baseSizeMeta.mode === 'LCL';
    const sizeMeta = (hasVehicles || promotedToFcl) ? (() => {
      const baseCuft = Number(baseSizeMeta.cuft) || Math.round((baseSizeMeta.cbm || 8) * 35.31);
      const totalCuft = baseCuft + vehiclesCuft;
      const totalCbm = SC.cuftToCbm ? SC.cuftToCbm(totalCuft) : Math.round(totalCuft / 35.31 * 100) / 100;
      const containers = SC.recommendFclForCuft
        ? SC.recommendFclForCuft(totalCuft)
        : { '20': 0, '40': 1, '40HC': 0, '45': 0 };
      const mixStr = SC.formatContainersMix ? SC.formatContainersMix(containers) : '';
      return {
        ...baseSizeMeta,
        mode: 'FCL',
        cbm: totalCbm,
        cuft: totalCuft,
        sub: (baseSizeMeta.sub || '') + (mixStr ? ' + ' + mixStr : ''),
        subEn: (baseSizeMeta.subEn || baseSizeMeta.sub || '') + (mixStr ? ' + ' + mixStr : ''),
        fcl: { containers },
      };
    })() : baseSizeMeta;
    const walioMode = sizeMeta.mode; // 'FCL' | 'LCL'

    // The form CTA enables once both places have a resolved country + coords
    // AND both port selects have a chosen port. The rate API rejects
    // city-only strings without lat/lng + benefits hugely from explicit
    // port codes for pickup/drayage pricing.
    const canCalculate = !!(originPlace && originPlace.country && originPlace.lat
                            && destPlace && destPlace.country && destPlace.lat
                            && originPorts.selected && destPorts.selected);

    // Seed the container mix when the customer picks a different FCL preset
    // on the Type tab. fcl-20 → { '20': 1, '40HC': 0 }; fcl-40 → reverse.
    // Switching to LCL clears the mix so an old FCL count doesn't bleed in
    // if they bounce back.
    useEffect(() => {
      if (mode !== 'type') { setContainerMix({ '20': 0, '40HC': 0 }); return; }
      if (sizeType === 'fcl-20') setContainerMix({ '20': 1, '40HC': 0 });
      else if (sizeType === 'fcl-40') setContainerMix({ '20': 0, '40HC': 1 });
      else setContainerMix({ '20': 0, '40HC': 0 });
    }, [sizeType, mode]);

    // Cheapest rate after the API responds — used everywhere the result UI
    // would have read the placeholder's `quote` object.
    const cheapest = (rates && rates.length > 0) ? rates[0] : null;
    const cheapestTotal = cheapest ? rateTotalOf(cheapest) : null;
    const cheapestTransit = cheapest ? rateTransitDays(cheapest) : null;
    const breakdown = cheapest ? collectBreakdown(cheapest) : [];

    const quoteId = makeQuoteId(originText, destText, currentSizeKey);

    // Publish state for the sticky quote bar to read. Mirrors the original
    // contract — the bar listens for 'abgs:quote-update' events and reads
    // window.__ABGS_QUOTE_STATE on mount.
    useEffect(() => {
      const state = {
        originCity: originPlace ? originPlace.city : originText.split(',')[0],
        destCity: destPlace ? destPlace.city : destText.split(',')[0],
        destCountry: destPlace ? destPlace.country : '',
        sizeLabel: labelOf(sizeMeta),
        vehicle,
        total: stage === 'form' ? null : cheapestTotal,
        stage,
      };
      window.__ABGS_QUOTE_STATE = state;
      window.dispatchEvent(new CustomEvent('abgs:quote-update', { detail: state }));
    }, [originPlace, destPlace, originText, destText, currentSizeKey, customRoomsActive, customRoomCount, containerMix, relocationVehicles, vehicle, walioMode, stage, mode, cheapestTotal]);

    // ─── Handlers ────────────────────────────────────────────────────────
    //
    // Flow (matches the homepage's quote widget pattern from
    // components/HeroAnimated.jsx L5050+):
    //
    //   stage='form'  → onSubmitForm: build the request body, advance to 'lead'
    //                                 (no Walio call yet — same as the homepage,
    //                                 which gates the rate API behind the lead
    //                                 form so every fetch is attributable to a
    //                                 captured contact).
    //
    //   stage='lead'  → onLeadSubmit:
    //                     1. saveLead to Firestore (with the full requestBody)
    //                     2. stamp the contact into the rate payload so Walio's
    //                        persistQuoteSession attaches it to walio_quote_sessions
    //                     3. POST to /v1/rates/{fcl|lcl}, sort by cheapest
    //                     4. patch the lead doc with the offers we'll show
    //                     5. advance to 'price'
    //                   On lead-save failure: stay on 'lead' with inline error.
    //                   On rate-fetch failure: still go to 'price', show the
    //                                          "premium custom quote" pivot
    //                                          (lead is already captured).
    //
    //   stage='price' → terminal success: shows the locked rate + breakdown +
    //                                     WhatsApp CTA + "new quote" reset.

    // Build the request body for the picked size + places + port + transport.
    // Pure — no fetch. Same args shape for FCL and LCL: the builders pull the
    // right defaults internally (FCL sends transport tokens raw; LCL maps
    // abgroup_* → maersk_*).
    const buildRequestArgs = {
      origin: originPlace, dest: destPlace,
      preset: sizeMeta, globals: effective.globals,
      lang,
      // Port codes from the auto-populated selectors.
      originPortCode: originPorts.selected ? originPorts.selected.unCode : null,
      destPortCode:   destPorts.selected   ? destPorts.selected.unCode   : null,
      // Inland transportation tokens from the two CY/SD radio groups.
      originTransport, destinationTransport,
      // ECU doorCodes when the user picked a real door from the shared
      // DoorPicker (only used when transport is SD — null otherwise so
      // CY flows behave exactly as before).
      originDoorCode: (originTransport === 'abgroup_pickup' && originDoor
        && originDoor.code && !originDoor.isGoogleFallback) ? originDoor.code : null,
      destinationDoorCode: (destinationTransport === 'abgroup_deliver' && destDoor
        && destDoor.code && !destDoor.isGoogleFallback) ? destDoor.code : null,
    };
    const buildRequestForCurrentForm = () => walioMode === 'FCL'
      ? LB.buildFCL(buildRequestArgs)
      : LB.buildLCL(buildRequestArgs);

    // Step 1: form → lead. Validate the form, build the request, store it
    // for the lead-submit step to use, advance.
    const onSubmitForm = () => {
      if (!canCalculate) return;
      setApiError(null);
      setLeadError(null);
      setRates(null);
      try {
        const req = buildRequestForCurrentForm();
        setPendingRequest(req);
        setStage('lead');
      } catch (err) {
        setApiError('rates_unavailable');
      }
    };

    // Step 2: lead → price. Save the lead first (so Walio can be attributed
    // to a contact even if the rate fetch fails), then fetch rates, then
    // patch the lead with the offers, then show 'price'.
    const onLeadSubmit = async (e) => {
      e.preventDefault();
      if (isLeadSaving) return;
      setLeadSaving(true);
      setLeadError(null);
      setApiError(null);

      // Re-build in case the form was edited via "Modify" between the
      // first form submit and the lead submit. pendingRequest stays as a
      // cache but we always trust the freshest place/size state.
      let req;
      try {
        req = buildRequestForCurrentForm();
        setPendingRequest(req);
      } catch (err) {
        setLeadSaving(false);
        setLeadError(STR.calc.errRatesUnavailable);
        return;
      }

      const phoneCombined = (leadCountry + ' ' + leadPhone).trim();

      // Which landing variant the visitor came in on — feeds the admin's
      // attribution column so we can see EN vs ES conversions.
      const isES = typeof location !== 'undefined' && /mudanzas/.test(location.pathname || '');
      const attribution = isES ? 'landing-mudanzas-internacionales-es'
                                : 'landing-international-moving-en';

      const leadDoc = {
        name: leadName.trim(),
        email: leadEmail.trim().toLowerCase(),
        company: '',
        phone: phoneCombined,
        // English-stable values for admin attribution consistency.
        audience: 'Individual',
        cargoType: 'International Move',
        service: walioMode === 'FCL' ? 'Ocean FCL' : 'Ocean LCL',
        mode: walioMode,
        lane: {
          from: (originPlace ? originPlace.description : originText) || '',
          to:   (destPlace   ? destPlace.description   : destText)   || '',
        },
        origin:      req.body.origin,
        destination: req.body.destination,
        cargo:       req.body.cargo,
        commodity: 'Personal Effects',
        // Pickup choice — applies to both LCL and FCL relocations now.
        // The Leads dashboard renders this row in the detail panel.
        pickup: {
          // Capture both transport sides in the lead doc so sales reps see at
          // a glance whether the customer wants port-to-port or door-to-door.
          type:        originTransport      === 'abgroup_pickup'  ? 'door' : 'port',
          destination: destinationTransport === 'abgroup_deliver' ? 'door' : 'port',
        },
        requestUrl:  req.url,
        requestBody: req.body,
        source: 'landing-international-moves',
        attribution,
        pageUrl: typeof location !== 'undefined' ? location.href : null,
        userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : null,
      };

      // ─── A. Save lead first — gate the API call ─────────────────────
      let leadId = null;
      try {
        if (!(window.__abgsLeads && window.__abgsLeads.saveLead)) {
          throw new Error(STR.calc.errLeadUnavailable);
        }
        leadId = await window.__abgsLeads.saveLead(leadDoc, {
          honeypot: leadHoneypot,
          formOpenedAt: leadOpenedAtRef.current || calcOpenedAtRef.current,
        });
      } catch (err) {
        setLeadSaving(false);
        setLeadError((err && err.message) ? err.message : STR.calc.errLeadSave);
        return;
      }

      // ─── B. Fetch rates — stamp the lead onto the payload so the Walio
      // quote session in walio_quote_sessions gets the contact + attribution
      // (mirrors HeroAnimated.jsx L5121-5133). ──────────────────────────
      const ratePayload = {
        ...req.body,
        lead: {
          name:    leadDoc.name    || null,
          email:   leadDoc.email   || null,
          phone:   leadDoc.phone   || null,
          company: leadDoc.company || null,
          source: leadDoc.source,
          attribution: leadDoc.attribution,
          pageUrl: leadDoc.pageUrl,
          ...(leadId ? { abgsLeadId: leadId } : {}),
        },
      };

      let list = [];
      try {
        const res = await fetch(req.url, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            ...((window.__WALIO_API && window.__WALIO_API.authHeaders) || {}),
          },
          body: JSON.stringify(ratePayload),
        });
        if (!res.ok) throw new Error('rates_http_' + res.status);
        const json = await res.json();
        list = Array.isArray(json.rates) ? json.rates
          : (json.data && Array.isArray(json.data.rates)) ? json.data.rates
          : [];
        list.sort((a, b) => (rateTotalOf(a) || Infinity) - (rateTotalOf(b) || Infinity));
        if (list.length === 0) setApiError('no_rates');
      } catch (err) {
        // Lead is already saved — landing on 'price' shows the no-rates pivot
        // (premium custom quote) so the user still has a path forward.
        setApiError(err && err.message ? err.message : 'rates_unavailable');
      }

      setRates(list);

      // ─── C. Patch the lead with the offers we'll show. Best-effort —
      // failure is swallowed inside updateLeadRates. ────────────────────
      if (leadId && window.__abgsLeads.updateLeadRates) {
        if (list.length > 0) {
          const offers = list.slice(0, 5).map(r => ({
            carrier: r.carrierName || r.carrier || r.provider || null,
            service: r.service || r.serviceType || null,
            total: rateTotalOf(r),
            currency: r.currency || 'USD',
            transitDays: rateTransitDays(r),
          })).filter(o => typeof o.total === 'number');
          const cheapestNow = offers.length > 0 && typeof offers[0].total === 'number'
            ? offers[0].total : null;
          window.__abgsLeads.updateLeadRates(leadId, {
            rateOffers: offers,
            rateOffersCount: offers.length,
            cheapestTotal: cheapestNow,
            currency: 'USD',
          });
        } else {
          // Record the fact that we showed zero offers so sales sees the gap.
          window.__abgsLeads.updateLeadRates(leadId, {
            rateOffersCount: 0,
            ratesError: (apiError || 'no_rates').toString().slice(0, 480),
          });
        }
      }

      setLeadSaving(false);
      setStage('price');
    };

    const reset = () => {
      setStage('form');
      setApiError(null);
      setLeadError(null);
      setRates(null);
      setPendingRequest(null);
      // Clear the lead-opened timestamp so re-entering 'lead' resets the
      // time-gate baseline (legitimate users may take >2.5s to retype).
      leadOpenedAtRef.current = null;
    };

    // ─── Render ──────────────────────────────────────────────────────────
    return (
      <div ref={calcRef} className="calc reveal d2">
        {(stage === 'form') && (
          <>
            <div className="calc__body">
              <div className="calc__head">
                <div className="calc__head-title">{STR.calc.headTitle}</div>
                <div className="calc__head-meta">{STR.calc.headMeta}</div>
              </div>

              {/* ─── ORIGIN + DESTINATION ROUTE ──────────────────────
                  Each side is city/door → port → inland radios. On mobile
                  + tablet the two stack vertically. On desktop (≥1025px)
                  they pair side-by-side via `.calc__route`'s 2-col grid —
                  same horizontal feel as the homepage hero's Step-5 card. */}
              <div className="calc__route">
              <div className="calc__route-side">
                {/* When SD (door pickup) is selected, swap the Google Places
                    input for the shared ECU DoorPicker. The picker searches
                    Walio's /doors API and writes the selected door back as a
                    Place-shaped object (country/state/city/zip/lat/lng) so
                    the existing CalcPortSelect / request builder keep
                    working. Country auto-derives from originPlace.country
                    (preserved across CY/SD flips) with US fallback. */}
                {originTransport === 'abgroup_pickup' && window.__abgsSharedDoorUi ? (
                  <div className="field">
                    <label className="field__label">{STR.calc.labelOrigin}</label>
                    {React.createElement(window.__abgsSharedDoorUi.DoorPicker, {
                      side: 'origin',
                      country: (originPlace && originPlace.country) || 'US',
                      onCountryChange: (code) => {
                        setOriginPlace({ country: code, city: '', state: '',
                          zip: '', lat: null, lng: null });
                        setOriginDoor(null);
                        setOriginPorts(prev => ({ ...prev,
                          list: [], selected: null, loading: false, error: null }));
                      },
                      seedSearch: (originPlace
                        && ((originPlace.zip && /^\d{4,6}$/.test(originPlace.zip))
                          ? originPlace.zip : originPlace.city)) || '',
                      selected: originDoor,
                      onSelect: (picked) => {
                        if (!picked) return;
                        setOriginDoor(picked.door);
                        setOriginPlace({
                          country: picked.country,
                          state:   picked.state,
                          city:    picked.city,
                          zip:     picked.zip,
                          lat:     picked.lat,
                          lng:     picked.lng,
                          description: picked.door && picked.door.displayName,
                        });
                        setOriginText(picked.door && picked.door.displayName || '');
                      },
                    })}
                  </div>
                ) : (
                  <QuotePlacesInput
                    label={STR.calc.labelOrigin} hint={STR.calc.labelOriginHint}
                    placeholder={STR.calc.phOrigin}
                    value={originText} onChange={setOriginText}
                    place={originPlace} onSelect={setOriginPlace}/>
                )}
                <div className="field">
                  <label className="field__label">{STR.calc.originPortLabel}</label>
                  <CalcPortSelect side="origin" state={originPorts}
                    placeholder={STR.calc.originPortPh}
                    onPick={(p) => setOriginPorts(prev => ({ ...prev, selected: p }))}/>
                </div>
                <CalcInlandRadios
                  label={STR.calc.originInlandLabel}
                  value={originTransport}
                  onChange={(next) => {
                    // Flipping back to CY clears the selected door — keeps
                    // the request body honest (no doorCode without intent).
                    if (next === 'customer_deliver') setOriginDoor(null);
                    setOriginTransport(next);
                  }}
                  options={[
                    { value: 'customer_deliver', code: 'CY',
                      title: STR.calc.cyOriginTitle, desc: STR.calc.cyOriginDesc },
                    { value: 'abgroup_pickup',   code: 'SD',
                      title: STR.calc.sdOriginTitle, desc: STR.calc.sdOriginDesc },
                  ]}/>
              </div>

              {/* ─── DESTINATION SIDE ───────────────────────────────
                  Mirror of origin: input → port → inland choice. */}
              <div className="calc__route-side">
                {destinationTransport === 'abgroup_deliver' && window.__abgsSharedDoorUi ? (
                  <div className="field">
                    <label className="field__label">{STR.calc.labelDest}</label>
                    {React.createElement(window.__abgsSharedDoorUi.DoorPicker, {
                      side: 'destination',
                      country: (destPlace && destPlace.country) || 'US',
                      onCountryChange: (code) => {
                        setDestPlace({ country: code, city: '', state: '',
                          zip: '', lat: null, lng: null });
                        setDestDoor(null);
                        setDestPorts(prev => ({ ...prev,
                          list: [], selected: null, loading: false, error: null }));
                      },
                      seedSearch: (destPlace
                        && ((destPlace.zip && /^\d{4,6}$/.test(destPlace.zip))
                          ? destPlace.zip : destPlace.city)) || '',
                      selected: destDoor,
                      onSelect: (picked) => {
                        if (!picked) return;
                        setDestDoor(picked.door);
                        setDestPlace({
                          country: picked.country,
                          state:   picked.state,
                          city:    picked.city,
                          zip:     picked.zip,
                          lat:     picked.lat,
                          lng:     picked.lng,
                          description: picked.door && picked.door.displayName,
                        });
                        setDestText(picked.door && picked.door.displayName || '');
                      },
                    })}
                  </div>
                ) : (
                  <QuotePlacesInput
                    label={STR.calc.labelDest} hint={STR.calc.labelDestHint}
                    placeholder={STR.calc.phDest}
                    value={destText} onChange={setDestText}
                    place={destPlace} onSelect={setDestPlace}/>
                )}
                <div className="field">
                  <label className="field__label">{STR.calc.destPortLabel}</label>
                  <CalcPortSelect side="destination" state={destPorts}
                    placeholder={STR.calc.destPortPh}
                    onPick={(p) => setDestPorts(prev => ({ ...prev, selected: p }))}/>
                </div>
                <CalcInlandRadios
                  label={STR.calc.destInlandLabel}
                  value={destinationTransport}
                  onChange={(next) => {
                    if (next === 'customer_pickup') setDestDoor(null);
                    setDestinationTransport(next);
                  }}
                  options={[
                    { value: 'customer_pickup',  code: 'CY',
                      title: STR.calc.cyDestTitle, desc: STR.calc.cyDestDesc },
                    { value: 'abgroup_deliver',  code: 'SD',
                      title: STR.calc.sdDestTitle, desc: STR.calc.sdDestDesc },
                  ]}/>
              </div>
              </div>

              {/* Size selector — the tab strip above picks which catalog
                  (rooms / boxes / shipment-type) the grid below renders.
                  Tabs sit inline here (not at the card top) because they're
                  a sub-control of the picker, not a card-level nav. */}
              <div className="calc__tabs calc__tabs--inline" role="tablist">
                <button className={`calc__tab ${mode === 'rooms' ? 'is-active' : ''}`}
                  onClick={() => setMode('rooms')} role="tab">
                  <Icon name="home"/> {STR.calc.tabRooms}
                </button>
                <button className={`calc__tab ${mode === 'boxes' ? 'is-active' : ''}`}
                  onClick={() => setMode('boxes')} role="tab">
                  <Icon name="box"/> {STR.calc.tabBoxes}
                </button>
                <button className={`calc__tab ${mode === 'type' ? 'is-active' : ''}`}
                  onClick={() => setMode('type')} role="tab">
                  <Icon name="container"/> {STR.calc.tabType}
                </button>
              </div>
              <div className="field" style={{ marginBottom: 14 }}>
                <label className="field__label">
                  {mode === 'rooms' ? STR.calc.sizeLabelRooms :
                   mode === 'boxes' ? STR.calc.sizeLabelBoxes : STR.calc.sizeLabelType}
                  <span className="field__label-hint">{STR.calc.sizeHint}</span>
                </label>
                <div className={`calc__size-grid ${mode === 'rooms' ? 'calc__size-grid--5' : ''}`}>
                  {Object.entries(sizeTable).map(([key, size]) => {
                    // Custom mode active → de-emphasise standard chips so the
                    // customer sees the stepper as the source of truth.
                    const isActive = !customRoomsActive && key === currentSizeKey;
                    const setter = mode === 'rooms' ? setSizeRooms
                                 : mode === 'boxes' ? setSizeBoxes : setSizeType;
                    return (
                      <button key={key} type="button"
                        className={`calc__size ${isActive ? 'is-active' : ''}`}
                        onClick={() => { setter(key); if (mode === 'rooms') setCustomRoomsActive(false); }}>
                        <div className="calc__size-icon"><SizeIcon mode={mode} keyName={key}/></div>
                        <div className="calc__size-label">{labelOf(size)}</div>
                        <div className="calc__size-sub">{subOf(size)}</div>
                      </button>
                    );
                  })}
                  {/* Custom-rooms chip — only on the Rooms tab. Opens the
                      stepper below so customers with 4+ bedroom houses can
                      get a real quote without admin maintaining a chip
                      for every possible room count. */}
                  {mode === 'rooms' && (
                    <button type="button"
                      className={`calc__size ${customRoomsActive ? 'is-active' : ''}`}
                      style={{ borderStyle: customRoomsActive ? 'solid' : 'dashed',
                               borderColor: customRoomsActive ? '#1E57D6' : 'rgba(30,87,214,0.5)' }}
                      onClick={() => setCustomRoomsActive(true)}>
                      <div className="calc__size-icon">
                        <Icon name="homePlus" size={32}/>
                      </div>
                      <div className="calc__size-label">
                        {(STR.lang === 'es') ? 'Personalizado' : 'Custom'}
                      </div>
                      <div className="calc__size-sub">
                        {(STR.lang === 'es') ? 'Más habitaciones…' : 'More rooms…'}
                      </div>
                    </button>
                  )}
                </div>
                {/* Stepper for the synthetic preset. Showing the derived CBM
                    and container count keeps the customer oriented — they
                    see exactly what shipment shape they're being quoted. */}
                {mode === 'rooms' && customRoomsActive && (
                  <div style={{ marginTop: 12, padding: 14, borderRadius: 10,
                                background: 'rgba(30,87,214,0.06)',
                                border: '1px solid rgba(30,87,214,0.2)' }}>
                    <div style={{ display: 'flex', alignItems: 'center',
                                  justifyContent: 'space-between',
                                  gap: 12, flexWrap: 'wrap' }}>
                      <div>
                        <div style={{ fontSize: 13, fontWeight: 700, color: '#0B1220',
                                      letterSpacing: '-0.005em', marginBottom: 3 }}>
                          {(STR.lang === 'es') ? 'Número de habitaciones' : 'Number of bedrooms'}
                        </div>
                        <div style={{ fontSize: 11, color: '#1846B0',
                                      fontFamily: 'JetBrains Mono, monospace', fontWeight: 600 }}>
                          {(() => {
                            // sizeMeta is already the synthetic preset when usingCustomRooms.
                            const cnt = sizeMeta.fcl && sizeMeta.fcl.containers
                                        && sizeMeta.fcl.containers['40HC'];
                            const tag = cnt
                              ? ' · ' + cnt + "× 40' HC"
                              : ' · LCL groupage';
                            return '~' + sizeMeta.cbm + ' m³' + tag;
                          })()}
                        </div>
                      </div>
                      <div style={{ display: 'inline-flex', alignItems: 'center', gap: 4,
                                    border: '1px solid rgba(16,24,40,0.12)', borderRadius: 9,
                                    background: '#fff', padding: 4 }}>
                        <button type="button"
                          onClick={() => setCustomRoomCount(c => Math.max(1, c - 1))}
                          style={{ width: 32, height: 32, borderRadius: 7,
                                   background: 'transparent', border: 0, cursor: 'pointer',
                                   color: '#475467', fontSize: 18, fontWeight: 600,
                                   fontFamily: 'inherit', lineHeight: 1 }}>−</button>
                        <div style={{ minWidth: 32, textAlign: 'center',
                                      fontSize: 15, fontWeight: 700, color: '#0B1220',
                                      fontFamily: 'JetBrains Mono, monospace',
                                      fontVariantNumeric: 'tabular-nums' }}>
                          {customRoomCount}
                        </div>
                        <button type="button"
                          onClick={() => setCustomRoomCount(c => Math.min(50, c + 1))}
                          style={{ width: 32, height: 32, borderRadius: 7,
                                   background: 'transparent', border: 0, cursor: 'pointer',
                                   color: '#1846B0', fontSize: 18, fontWeight: 600,
                                   fontFamily: 'inherit', lineHeight: 1 }}>+</button>
                      </div>
                    </div>
                  </div>
                )}
                {/* Container-mix stepper — renders on the Type tab when the
                    customer picks any FCL preset. Two rows (20' and 40' HC)
                    so they can build any combination (e.g. 1×20' + 1×40').
                    Floor on the sum is 1 so the form can't submit a zero-
                    container request. */}
                {mode === 'type' && rawBaseSizeMeta && rawBaseSizeMeta.mode === 'FCL'
                  && rawBaseSizeMeta.fcl && rawBaseSizeMeta.fcl.containers && (
                  <div style={{ marginTop: 12, padding: 14, borderRadius: 10,
                                background: 'rgba(30,87,214,0.06)',
                                border: '1px solid rgba(30,87,214,0.2)' }}>
                    <div style={{ fontSize: 13, fontWeight: 700, color: '#0B1220',
                                  letterSpacing: '-0.005em', marginBottom: 10 }}>
                      {(STR.lang === 'es') ? 'Contenedores' : 'Containers'}
                      <span style={{ fontSize: 11, color: '#1846B0', fontWeight: 600,
                                     fontFamily: 'JetBrains Mono, monospace',
                                     marginLeft: 8 }}>
                        {'~' + sizeMeta.cbm + ' m³'}
                      </span>
                    </div>
                    {[
                      { key: '20',   label: (STR.lang === 'es') ? "Contenedor 20'"     : "20' container",     sub: '~28 m³' },
                      { key: '40HC', label: (STR.lang === 'es') ? "Contenedor 40' HC"  : "40' HC container",  sub: '~55 m³' },
                    ].map(row => (
                      <div key={row.key} style={{ display: 'flex', alignItems: 'center',
                                                  justifyContent: 'space-between',
                                                  gap: 12, padding: '6px 0' }}>
                        <div>
                          <div style={{ fontSize: 13, fontWeight: 600, color: '#0B1220' }}>
                            {row.label}
                          </div>
                          <div style={{ fontSize: 10.5, color: '#667085',
                                        fontFamily: 'JetBrains Mono, monospace',
                                        fontWeight: 600, letterSpacing: 0.5 }}>
                            {row.sub}
                          </div>
                        </div>
                        <div style={{ display: 'inline-flex', alignItems: 'center', gap: 4,
                                      border: '1px solid rgba(16,24,40,0.12)', borderRadius: 9,
                                      background: '#fff', padding: 4 }}>
                          <button type="button"
                            onClick={() => setContainerMix(m => {
                              const next = { ...m, [row.key]: Math.max(0, (m[row.key] || 0) - 1) };
                              // Floor: don't let total drop below 1.
                              if (((next['20'] || 0) + (next['40HC'] || 0)) < 1) return m;
                              return next;
                            })}
                            style={{ width: 32, height: 32, borderRadius: 7,
                                     background: 'transparent', border: 0, cursor: 'pointer',
                                     color: '#475467', fontSize: 18, fontWeight: 600,
                                     fontFamily: 'inherit', lineHeight: 1 }}>−</button>
                          <div style={{ minWidth: 32, textAlign: 'center',
                                        fontSize: 15, fontWeight: 700, color: '#0B1220',
                                        fontFamily: 'JetBrains Mono, monospace',
                                        fontVariantNumeric: 'tabular-nums' }}>
                            {containerMix[row.key] || 0}
                          </div>
                          <button type="button"
                            onClick={() => setContainerMix(m => ({ ...m, [row.key]: Math.min(10, (m[row.key] || 0) + 1) }))}
                            style={{ width: 32, height: 32, borderRadius: 7,
                                     background: 'transparent', border: 0, cursor: 'pointer',
                                     color: '#1846B0', fontSize: 18, fontWeight: 600,
                                     fontFamily: 'inherit', lineHeight: 1 }}>+</button>
                        </div>
                      </div>
                    ))}
                  </div>
                )}
              </div>

              {/* Vehicles — same shared picker the homepage hero uses. Adding
                  vehicles auto-promotes the move to FCL with the right
                  container mix (3× 40' + 1× 20' etc.) via recommendFclForCuft.
                  Customers can add multiple cars / motos / boats per move. */}
              <div className="field" style={{ marginBottom: 14 }}>
                <div style={{ display: 'flex', alignItems: 'center',
                              justifyContent: 'space-between',
                              gap: 8, marginBottom: 6, flexWrap: 'wrap' }}>
                  <label className="field__label" style={{ marginBottom: 0 }}>
                    {STR.calc.extrasLabel}
                    <span className="field__label-hint">
                      {(STR.lang === 'es') ? 'Auto, moto, bote — opcional' : 'Cars, motos, boats — optional'}
                    </span>
                  </label>
                  {window.__abgsSharedCalculator
                    && React.createElement(window.__abgsSharedCalculator.RelocationVehiclePicker, {
                      vehicles: relocationVehicles,
                      onChange: setRelocationVehicles,
                      theme: 'landing',
                    })}
                </div>
                {/* Summary line — shows derived totals + container mix the
                    customer is being quoted on. Updates live as they add/remove. */}
                {relocationVehicles.length > 0 && (
                  <div style={{ marginTop: 4, padding: '6px 10px', borderRadius: 8,
                                background: 'rgba(30,87,214,0.05)',
                                border: '1px solid rgba(30,87,214,0.12)',
                                fontSize: 11.5, color: '#1846B0',
                                fontFamily: 'JetBrains Mono, monospace', fontWeight: 600 }}>
                    + {relocationVehicles.length}
                    {(STR.lang === 'es') ? ' vehículo(s)' : ' vehicle(s)'}
                    {' · ~'}{sizeMeta.cbm}{' m³'}
                    {sizeMeta.fcl && sizeMeta.fcl.containers && (
                      <span style={{ marginLeft: 8, color: '#0B1220' }}>
                        → {window.__abgsSharedCalculator
                            && window.__abgsSharedCalculator.formatContainersMix(sizeMeta.fcl.containers)}
                      </span>
                    )}
                  </div>
                )}
              </div>

              {/* CTA — opens the lead form. Walio is NOT called yet. */}
              <div className="calc__cta-row">
                <button type="button" className="calc__cta" onClick={onSubmitForm}
                  disabled={!canCalculate}>
                  {STR.calc.ctaCalc} <Icon name="arrowRight"/>
                </button>
              </div>

              <div className="calc__foot">
                <div className="calc__foot-trust">
                  <Icon name="check"/> {STR.calc.footTrust}
                </div>
                <div className="calc__foot-secure">
                  <Icon name="lock" size={11}/> {STR.calc.footSecure}
                </div>
              </div>
            </div>
          </>
        )}

        {(stage === 'lead' || stage === 'price') && (
          <>
            <div className="calc__tabs" style={{ gridTemplateColumns: '1fr auto' }}>
              <div style={{ padding: '14px 18px', fontSize: 13, fontWeight: 600,
                color: '#1846B0', display: 'flex', alignItems: 'center', gap: 8 }}>
                <Icon name="check"/> {STR.calc.readyLabel} · {quoteId}
              </div>
              <button className="calc__tab" onClick={reset}
                style={{ borderBottom: 0, padding: '12px 18px', color: '#667085' }}>
                <Icon name="edit"/> {STR.calc.modifyLabel}
              </button>
            </div>

            <div className="calc__result">
              {/* Lane + sizing header — visible on both 'lead' and 'price'
                  so the customer keeps context across the two screens. */}
              <div className="calc__result-head">
                <div>
                  <div className="calc__result-lane">
                    {(originPlace ? originPlace.city : originText.split(',')[0])
                      || originText} <span style={{ color: '#98A2B3' }}>→</span>{' '}
                    {(destPlace
                       ? (destPlace.city + (destPlace.country ? ', ' + destPlace.country : ''))
                       : destText)}
                  </div>
                  <div className="calc__result-title">
                    {labelOf(sizeMeta)} · {subOf(sizeMeta)} · {walioMode}
                    {relocationVehicles.length > 0 && <span> · {relocationVehicles.length}× vehicle</span>}
                    {originTransport      === 'abgroup_pickup'  && <span> · {STR.calc.originSdShort}</span>}
                    {destinationTransport === 'abgroup_deliver' && <span> · {STR.calc.destSdShort}</span>}
                  </div>
                </div>
                {stage === 'price' && cheapestTotal != null
                  && <span className="calc__result-pill">{STR.calc.ratePill}</span>}
              </div>

              {/* ─── Stage: lead ────────────────────────────────────────
                  Lead capture form — gates the rate API call. No price is
                  visible yet; the customer earns it by submitting contact
                  info. Matches the homepage hero's pattern (HeroAnimated.jsx
                  L5050+: saveLead → stamp onto payload → fetch). */}
              {stage === 'lead' && (
                <form className="calc__lead" onSubmit={onLeadSubmit}
                  style={{ marginTop: 0, borderTop: 0, paddingTop: 0 }}>
                  <div className="calc__lead-title">
                    <Icon name="shield"/> {STR.calc.leadStageTitle}
                  </div>
                  <div className="calc__lead-sub">{STR.calc.leadStageSub}</div>

                  <div className="calc__lead-row">
                    <div className="field">
                      <input className="field__input" placeholder={STR.calc.phName} required
                        value={leadName} onChange={e => setLeadName(e.target.value)}/>
                    </div>
                    <div className="field">
                      <input className="field__input" type="email" placeholder={STR.calc.phEmail} required
                        value={leadEmail} onChange={e => setLeadEmail(e.target.value)}/>
                    </div>
                  </div>
                  <div className="calc__lead-phone-row">
                    <select className="field__select" value={leadCountry}
                      onChange={e => setLeadCountry(e.target.value)}>
                      {COUNTRY_CODES.map(c => (
                        <option key={c.code} value={c.code}>{c.flag} {c.code}</option>
                      ))}
                    </select>
                    <input className="field__input" type="tel" placeholder={STR.calc.phPhone} required
                      value={leadPhone} onChange={e => setLeadPhone(e.target.value)}/>
                  </div>

                  {/* Honeypot — bots fill every visible-looking input; this
                      one is invisible to humans via off-screen positioning.
                      If non-empty at submit, saveLead throws "Submission rejected." */}
                  <input type="text" name="website" tabIndex="-1" autoComplete="off"
                    value={leadHoneypot} onChange={e => setLeadHoneypot(e.target.value)}
                    aria-hidden="true"
                    style={{ position: 'absolute', left: '-10000px',
                             width: 1, height: 1, opacity: 0, pointerEvents: 'none' }}/>

                  {leadError && (
                    <div style={{
                      marginBottom: 10, padding: '8px 12px',
                      background: '#FEF3F2', border: '1px solid #FDA29B',
                      borderRadius: 8, color: '#B42318',
                      fontSize: 12.5, fontWeight: 500,
                    }}>{leadError}</div>
                  )}

                  <button type="submit" className="calc__cta"
                    style={{ width: '100%' }} disabled={isLeadSaving}>
                    {isLeadSaving
                      ? <><Icon name="spinner"/> {STR.calc.ctaCalculating}</>
                      : <>{STR.calc.ctaCalc} <Icon name="arrowRight"/></>}
                  </button>
                  <div className="calc__foot" style={{ marginTop: 12 }}>
                    <div className="calc__foot-trust">
                      <Icon name="check"/> {STR.calc.leadSecureNote}
                    </div>
                    <div className="calc__foot-secure">
                      <Icon name="lock" size={11}/> {STR.calc.footSecure}
                    </div>
                  </div>
                </form>
              )}

              {/* ─── Stage: price (terminal) ────────────────────────────
                  Lead is already saved + the API has already responded.
                  Show the locked rate + breakdown + WhatsApp CTA + new-quote
                  reset. If the API failed or returned zero rates, pivot to
                  the "premium custom quote" path (lead is still captured so
                  the booking team can follow up). */}
              {stage === 'price' && (
                <>
                  {cheapestTotal != null ? (
                    <>
                      {/* DOOR-LEG WARNINGS — fire when the customer asked
                          for an SD origin or destination but the cheapest
                          rate's breakdown lines don't include that leg's
                          cost. The booking team will quote the missing leg
                          over WhatsApp once they confirm pickup specifics. */}
                      {(() => {
                        const wantsOriginDoor = originTransport      === 'abgroup_pickup';
                        const wantsDestDoor   = destinationTransport === 'abgroup_deliver';
                        const labelHasOriginPickup = breakdown.some(it =>
                          /origin\s+(pickup|drayage)/i.test(it.lab || ''));
                        const labelHasDestDelivery = breakdown.some(it =>
                          /destination\s+(delivery|drayage)/i.test(it.lab || ''));
                        const originGap = wantsOriginDoor && !labelHasOriginPickup
                          && !cheapest?.hasPickup && typeof cheapest?.pickupCost !== 'number';
                        const destGap   = wantsDestDoor   && !labelHasDestDelivery
                          && !cheapest?.hasDelivery && typeof cheapest?.deliveryCost !== 'number'
                          && !cheapest?.hasDrayage  && typeof cheapest?.drayageCost  !== 'number';
                        if (!originGap && !destGap) return null;
                        const msg = originGap && destGap
                          ? STR.calc.warnBothMissing
                          : originGap
                          ? STR.calc.warnOriginMissing
                          : STR.calc.warnDestMissing;
                        return (
                          <div style={{ marginTop: 14, padding: '9px 12px',
                            borderRadius: 10,
                            background: 'rgba(247,144,9,0.08)',
                            border: '1px solid rgba(247,144,9,0.28)',
                            display: 'flex', alignItems: 'flex-start', gap: 8 }}>
                            <svg width="14" height="14" viewBox="0 0 24 24" fill="none"
                              stroke="#B54708" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
                              style={{ flexShrink: 0, marginTop: 1 }}>
                              <path d="M12 9 V13 M12 17 V17.01"/>
                              <path d="M10.3 3.8 L2 18 a2 2 0 0 0 1.7 3 H20.3 a2 2 0 0 0 1.7 -3 L13.7 3.8 a2 2 0 0 0 -3.4 0 Z"/>
                            </svg>
                            <div style={{ fontSize: 12, color: '#7A2E0E', lineHeight: 1.4,
                              fontWeight: 500 }}>
                              {msg}
                            </div>
                          </div>
                        );
                      })()}

                      {/* RATE LIST — same RichRateCard the homepage renders
                          (components/rich-rate-card.jsx). Single source of
                          truth for the card UI; this list adds: scroll
                          container so long result sets don't blow the page
                          layout, setStats so the "Fastest" / "Free-time"
                          badges describe the visible set, and forwards
                          originTransport/destinationTransport so the card
                          filters its "Port-to-Port" feature chip when the
                          customer requested door pickup or delivery. */}
                      {(() => {
                        const RichRateCard = window.RichRateCard;
                        const H = window.__abgsRateHelpers || {};
                        if (!RichRateCard || !rates || rates.length === 0) return null;
                        const transits = rates
                          .map(r => H.rateTransitDays ? H.rateTransitDays(r) : null)
                          .filter(t => t && typeof t[0] === 'number')
                          .map(t => t[0]);
                        const frees = rates
                          .map(r => typeof r.freeDays === 'number' ? r.freeDays : null)
                          .filter(v => v != null);
                        const setStats = {
                          count: rates.length,
                          minTransit: transits.length ? Math.min(...transits) : null,
                          maxFree:    frees.length    ? Math.max(...frees)    : null,
                        };
                        const containers = sizeMeta && sizeMeta.fcl && sizeMeta.fcl.containers;
                        return (
                          <div style={{ display: 'flex', flexDirection: 'column', gap: 10,
                            maxHeight: 520, overflowY: 'auto', overflowX: 'hidden',
                            marginTop: 14, paddingRight: 4, marginRight: -4 }}>
                            {rates.map((r, i) => (
                              <RichRateCard key={r.rateId || r.id || i} r={r}
                                highlight={i === 0} bestPrice={i === 0}
                                setStats={setStats}
                                requestedContainers={containers}
                                originTransport={originTransport}
                                destinationTransport={destinationTransport}/>
                            ))}
                          </div>
                        );
                      })()}

                      {/* Urgency — kept as a marketing element since the live
                          API doesn't return next-sailing dates on every response.
                          The date below is "today + 6 days" as soft urgency. */}
                      <div className="calc__urgency">
                        <div className="calc__urgency-icon">
                          <Icon name="ship" size={20}/>
                        </div>
                        <div>
                          <Html as="div" className="calc__urgency-main"
                            html={window.tpl(STR.calc.urgencyMainHtml,
                              { city: (destPlace && destPlace.city) || destText.split(',')[0],
                                date: todayPlus(6) })}/>
                          <div className="calc__urgency-sub">{STR.calc.urgencySub}</div>
                        </div>
                      </div>

                      {/* Success confirmation — lead was already saved when
                          the user submitted the contact form one screen back.
                          Slimmer than the original full-width success block:
                          smaller checkmark, tighter padding, single-line CTA
                          so the WhatsApp button stays readable instead of
                          wrapping word-by-word at narrow widths. */}
                      <div className="calc__lead"
                        style={{ textAlign: 'center', padding: '14px 0 4px' }}>
                        <div style={{ width: 36, height: 36, borderRadius: 9999,
                          background: '#ECFDF3', color: '#12b76a', margin: '0 auto 8px',
                          display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                          <Icon name="check" size={18}/>
                        </div>
                        <Html as="div" className="calc__lead-sub"
                          style={{ textAlign: 'center', maxWidth: 380, margin: '0 auto',
                            fontSize: 12.5, lineHeight: 1.45 }}
                          html={window.tpl(STR.calc.doneSubHtml, {
                            email: leadEmail || STR.calc.doneFallbackEmail,
                            date: todayPlus(6),
                          })}/>
                        <div style={{ marginTop: 12, display: 'flex', gap: 10,
                          justifyContent: 'center', alignItems: 'center', flexWrap: 'wrap' }}>
                          <a className="calc__cta"
                            style={{ flex: '0 0 auto', padding: '9px 14px',
                              fontSize: 13, whiteSpace: 'nowrap' }}
                            href={`https://wa.me/13056352525?text=${encodeURIComponent(
                              window.tpl(STR.calc.whatsappPresetTpl, {
                                id: quoteId,
                                origin: originText,
                                city: (destPlace && destPlace.city) || destText.split(',')[0],
                              })
                            )}`} target="_blank" rel="noopener">
                            <Icon name="whatsapp" size={14}/> {STR.calc.doneTalkCta}
                          </a>
                          <button className="calc__lead-secondary"
                            style={{ whiteSpace: 'nowrap' }} onClick={reset}>
                            {STR.calc.doneNewQuote}
                          </button>
                        </div>
                      </div>
                    </>
                  ) : (
                    // Zero rates or fetch error — lead is already captured
                    // upstream. Render the shared NoRatesGlass surface (same
                    // "Cotización Inteligente AB Group" experience the
                    // homepage hero uses) so the customer sees a premium
                    // custom-quote confirmation instead of a plain warning
                    // box. The `compact` prop drops the inner header (back/
                    // edit/close) because the landing already wraps the
                    // result with its own "Quote ready · ABG-XXX / Modify"
                    // strip just above. NoRatesGlass falls back to a
                    // standalone div if RichRateCard's shared module hasn't
                    // loaded yet (defensive — the script tag in the page
                    // ensures it does).
                    (() => {
                      const NoRatesGlass = window.NoRatesGlass;
                      const sizeUnit = sizeMeta && sizeMeta.fcl && sizeMeta.fcl.containers
                        ? Object.entries(sizeMeta.fcl.containers)
                            .filter(([, n]) => n > 0)
                            .map(([code, n]) => `${n}× ${code}`)
                            .join(' + ')
                        : (sizeMeta && (sizeMeta.cbm ? `~${Math.round(sizeMeta.cbm)} m³` : null));
                      const summary = {
                        modeIcon: walioMode === 'FCL' ? 'box' : 'package',
                        modeName: walioMode === 'FCL' ? 'FCL Marítimo' : 'LCL Marítimo',
                        unit: sizeUnit,
                        ready: null,
                        fromLabel: (originPlace && originPlace.city) || originText.split(',')[0] || originText,
                        toLabel:   (destPlace   && destPlace.city)   || destText.split(',')[0]   || destText,
                        fromPlace: originPlace ? { country: originPlace.country } : null,
                        toPlace:   destPlace   ? { country: destPlace.country   } : null,
                      };
                      const leadInfo = {
                        name:    leadName  || null,
                        email:   leadEmail || null,
                        phone:   (leadPhone ? `${leadCountry} ${leadPhone}` : null),
                        company: null,
                      };
                      if (NoRatesGlass) {
                        return (
                          <div style={{ marginTop: 14 }}>
                            <NoRatesGlass compact
                              summary={summary} lead={leadInfo}
                              onChangeService={reset}/>
                          </div>
                        );
                      }
                      // Defensive fallback — shouldn't fire in production because
                      // ../components/rich-rate-card.jsx is loaded before this
                      // script in international-moving.html / mudanzas-internacionales.html.
                      return (
                        <div style={{
                          marginTop: 12, padding: 14,
                          background: '#FFFAEB', border: '1px solid #FEC84B',
                          borderRadius: 12, color: '#B54708',
                          fontSize: 13.5, lineHeight: 1.55, fontWeight: 500,
                        }}>
                          {apiError === 'no_rates' ? STR.calc.errNoRates : STR.calc.errRatesUnavailable}
                        </div>
                      );
                    })()
                  )}
                </>
              )}
            </div>
          </>
        )}
      </div>
    );
  }

  // ───────────────────────────────────────────────────────────────────────
  // Mode-specific size icons (inline SVG) — unchanged from the design
  // ───────────────────────────────────────────────────────────────────────
  function SizeIcon({ mode, keyName }) {
    if (mode === 'rooms') {
      const beds = keyName === 'studio' ? 0 : keyName === '1bed' ? 1 : keyName === '2bed' ? 2 : 3;
      return (
        <svg width="32" height="32" viewBox="0 0 32 32" fill="none">
          <path d="M5 26 L5 14 L16 6 L27 14 L27 26 Z"
            stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/>
          <path d="M5 26 L27 26" stroke="currentColor" strokeWidth="1.6"/>
          {beds >= 1 && <rect x="8" y="18" width="6" height="5" rx="1" stroke="currentColor" strokeWidth="1.4" fill="none"/>}
          {beds >= 2 && <rect x="18" y="18" width="6" height="5" rx="1" stroke="currentColor" strokeWidth="1.4" fill="none"/>}
          {beds >= 3 && <rect x="13" y="11" width="6" height="4" rx="1" stroke="currentColor" strokeWidth="1.4" fill="none"/>}
        </svg>
      );
    }
    if (mode === 'boxes') {
      const stacks = keyName === 'sm' ? 1 : keyName === 'md' ? 2 : keyName === 'lg' ? 3 : 4;
      return (
        <svg width="32" height="32" viewBox="0 0 32 32" fill="none">
          {[...Array(stacks)].map((_, i) => (
            <g key={i}>
              <rect x={4 + i*5.5} y={26 - i*4} width="9" height="4" rx="0.5"
                stroke="currentColor" strokeWidth="1.4" fill="none"/>
            </g>
          ))}
          <rect x="15" y="14" width="13" height="10" rx="1" stroke="currentColor" strokeWidth="1.6" fill="none"/>
          <path d="M15 18 L28 18" stroke="currentColor" strokeWidth="1.2"/>
          <path d="M21.5 14 L21.5 18" stroke="currentColor" strokeWidth="1.2"/>
        </svg>
      );
    }
    // type
    return (
      <svg width="32" height="32" viewBox="0 0 32 32" fill="none">
        <rect x="3" y="11" width={keyName === 'lcl-sm' ? 14 : keyName === 'lcl-md' ? 18 : keyName === 'fcl-20' ? 22 : 26}
          height={keyName.startsWith('fcl') ? 14 : 10} rx="1"
          stroke="currentColor" strokeWidth="1.6" fill="none"/>
        {[0,1,2,3,4].slice(0, keyName === 'lcl-sm' ? 3 : keyName === 'lcl-md' ? 4 : 5).map(i => (
          <line key={i} x1={6 + i*3.5} y1={keyName.startsWith('fcl') ? 14 : 14}
            x2={6 + i*3.5} y2={keyName.startsWith('fcl') ? 22 : 19}
            stroke="currentColor" strokeWidth="1.1"/>
        ))}
        <circle cx="9" cy={keyName.startsWith('fcl') ? 27 : 24} r="2" stroke="currentColor" strokeWidth="1.4" fill="none"/>
        <circle cx={keyName === 'lcl-sm' ? 14 : keyName === 'lcl-md' ? 16 : keyName === 'fcl-20' ? 20 : 24}
          cy={keyName.startsWith('fcl') ? 27 : 24} r="2" stroke="currentColor" strokeWidth="1.4" fill="none"/>
      </svg>
    );
  }

  // ───────────────────────────────────────────────────────────────────────
  // Inline icon set (no external lib — drop-in clean strokes)
  // ───────────────────────────────────────────────────────────────────────
  function Icon({ name, size = 16 }) {
    const props = {
      width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
      stroke: 'currentColor', strokeWidth: 1.8,
      strokeLinecap: 'round', strokeLinejoin: 'round',
    };
    switch (name) {
      case 'home':      return <svg {...props}><path d="M3 10 L12 3 L21 10 V20 a1 1 0 0 1 -1 1 H4 a1 1 0 0 1 -1 -1 Z"/><path d="M9 21 V13 h6 v8"/></svg>;
      case 'box':       return <svg {...props}><path d="M21 8 L12 13 L3 8"/><path d="M3 8 L12 3 L21 8 V16 L12 21 L3 16 Z"/><path d="M12 13 V21"/></svg>;
      case 'container': return <svg {...props}><rect x="2" y="7" width="20" height="10" rx="1"/><path d="M6 7 V17 M10 7 V17 M14 7 V17 M18 7 V17"/></svg>;
      case 'arrowRight':return <svg {...props}><path d="M5 12 H19 M13 6 L19 12 L13 18"/></svg>;
      case 'check':     return <svg {...props}><path d="M4 12 L10 18 L20 6"/></svg>;
      case 'lock':      return <svg {...props}><rect x="5" y="11" width="14" height="10" rx="1.5"/><path d="M8 11 V7 a4 4 0 0 1 8 0 V11"/></svg>;
      case 'shield':    return <svg {...props}><path d="M12 3 L20 6 V12 c0 5 -3.5 8.5 -8 10 c-4.5 -1.5 -8 -5 -8 -10 V6 Z"/><path d="M9 12 L11 14 L15 10"/></svg>;
      case 'ship':      return <svg {...props}><path d="M3 17 L21 17 L19 21 H5 Z"/><path d="M5 17 V11 H19 V17"/><path d="M12 7 V11 M9 7 H15"/><path d="M3 15 q3 1.5 6 0 t6 0 t6 0"/></svg>;
      case 'edit':      return <svg {...props}><path d="M14 4 L20 10 L8 22 H2 V16 Z"/></svg>;
      case 'info':      return <svg {...props}><circle cx="12" cy="12" r="10"/><path d="M12 8 V8.01 M12 12 V16"/></svg>;
      case 'car':       return <svg {...props}><path d="M5 17 H3 V13 L5 8 H19 L21 13 V17 H19"/><circle cx="7" cy="17" r="2"/><circle cx="17" cy="17" r="2"/></svg>;
      case 'plus':      return <svg {...props}><path d="M12 5 V19 M5 12 H19"/></svg>;
      case 'homePlus':  return <svg width={size} height={size} viewBox="0 0 32 32" fill="none"><path d="M5 26 L5 14 L16 6 L27 14 L27 26 Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/><path d="M5 26 L27 26" stroke="currentColor" strokeWidth="1.6"/><path d="M16 14 V22 M12 18 H20" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>;
      case 'whatsapp':  return <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor"><path d="M17.5 14.4c-.3-.1-1.7-.8-2-.9-.3-.1-.5-.1-.7.1-.2.3-.7.9-.9 1.1-.2.2-.3.2-.6.1-.3-.2-1.2-.4-2.4-1.4-.9-.8-1.5-1.7-1.6-2-.2-.3 0-.5.1-.6.1-.1.3-.3.4-.5.1-.2.2-.3.3-.5.1-.2 0-.4 0-.5 0-.1-.7-1.7-.9-2.3-.2-.6-.5-.5-.7-.5-.2 0-.4 0-.6 0s-.5.1-.7.4c-.3.3-1 .9-1 2.3 0 1.4 1 2.7 1.1 2.9.1.2 2 3 4.8 4.2 1.7.7 2.4.8 3.2.7.5-.1 1.6-.7 1.8-1.3.2-.6.2-1.1.2-1.3 0-.1-.2-.2-.5-.3zM12 2C6.5 2 2 6.5 2 12c0 1.7.4 3.4 1.3 4.9L2 22l5.3-1.4c1.4.8 3 1.2 4.7 1.2 5.5 0 10-4.5 10-10S17.5 2 12 2z"/></svg>;
      case 'spinner':   return <svg {...props} style={{ animation: 'ld-spin 0.9s linear infinite' }}><circle cx="12" cy="12" r="9" strokeOpacity="0.25"/><path d="M21 12 a9 9 0 0 0 -9 -9"/></svg>;
      default: return null;
    }
  }

  // expose to global so the page bundle can mount it
  window.QuoteCalculator = QuoteCalculator;
  window.LandingIcon = Icon;
})();
