/**
 * NuAsk HIS overlay — Hold Intelligence Space chrome.
 * WHY: persistent holographic field over dimmed NOW (not a press-drag picker).
 * Hit math: pages/logic/ex1-radial-hit.js (SF_EX1_RADIAL_HIT).
 * Product: docs/future/EX1_HOLD_INTELLIGENCE_SPACE.md (§3 field · §6 session · §9 reform).
 * Unlock: Phases 1–5 + experience pass 1–2 UNLOCK_UI_GOLD 2026-08-20.
 * Pass 3 §9 reform REVERTED 2026-08-20 (Aaron: messy / “don’t know what”) — restore quiet field.
 * Optional `companionLine` — server prose only (never invent on miss).
 * Optional `spacePrompt` — floating phase question above the wheel.
 * Optional `loading` — HIS busy = confined hub breathe (NOW cadence); no circling word. Calm skeleton under hub.
 * A18: welcome Top leads into first ask; loading / pendingSelect hide seats until the *new* stage is ready.
 * A20–A20.2: glass/glow experiments REVERTED layout (founder: everything out of place). Busy pulse only; geometry = tip `105c7392`.
 * A20.3: busy halo/hub breathe must nest scale (never replace translate(-50%)); Top = space_prompt only (no companion twin under ask).
 * Optional `memoryChips` — faint memory trail (not form breadcrumbs).
 * Optional `focusCard` ({ title, meta, why }) — POI focus in hologram.
 * Optional `fieldStatus` — Governor-in-space status.
 * Optional `hisMode` — HIS visual language (fuller stage scrim, soft hub, one-tap seats).
 * Optional `hisTv` — HIS TV payload ({ mode, steps, focus, status, result_line, empty_label }).
 * Optional `onConclude(optId)` — post-decision / standby conclude exits (parent owns nav).
 * HIS draft (2026-08-21): fixed large TV · mid-band radial · classy enter/exit
 * (pre-Ex.1 seam-drawer motion language) · guided conclude when standby/empty.
 * A8: HIS TV always keeps full path chips under status/focus/result (never wipe).
 * A10: conclude buttons call onConclude (not all Back); primary opt styled.
 * A12: enter fail-closed → unavailable conclude plate; mid-field status gated off conclude.
 * HIS is not gold — iterate freely; do not edit pre-Ex.1 NuAsk chrome here.
 * Scrim tap does NOT close; explicit Back / conclude / onCancel only.
 * Legacy Alive may still use scrim dismiss + open grace.
 */
(function () {
  'use strict';

  const { useEffect, useState, useRef, useCallback, useMemo } = React;
  /** Ignore scrim dismiss briefly after open — arming thumb-up must not close HIS. */
  const OPEN_DISMISS_GRACE_MS = 650;
  /** Match pre-Ex.1 hub drawer sheet timing (sylvan-tech-theme sf-hub-tools-*). */
  const HIS_LEAVE_MS = 760;
  const HIT = window.SF_EX1_RADIAL_HIT;
  if (!HIT) {
    console.warn('[Ex.1] SF_EX1_RADIAL_HIT missing — load ex1-radial-hit.js before overlay');
  }

  const GLASS = {
    backgroundColor: 'rgba(10, 36, 42, 0.72)',
    backdropFilter: 'blur(20px) saturate(1.4)',
    WebkitBackdropFilter: 'blur(20px) saturate(1.4)'
  };

  /** Soft hologram scrim — NOW must remain faintly readable underneath (§3). */
  const SCRIM = {
    background:
      'radial-gradient(ellipse 80% 70% at 50% 62%, rgba(4, 48, 56, 0.52) 0%, rgba(2, 18, 24, 0.78) 100%)',
    touchAction: 'none'
  };

  /** HIS stage owns the viewport — opaque enough that underlay is not part of the read. */
  const SCRIM_HIS = {
    background:
      'radial-gradient(ellipse 95% 90% at 50% 48%, rgba(3, 28, 34, 0.96) 0%, rgba(0, 6, 10, 0.985) 100%)',
    touchAction: 'none'
  };

  const DISK_R = HIT?.DISK_R || 132;
  const HUB_R = HIT?.HUB_R || 34;
  const L2_R = HIT?.L2_R || 188;

  /**
   * Draft HIS stage geometry (not final chrome).
   * Top ask band + mid radial + fixed large HIS TV (NuAsk-TV-scale plate, not a growing bar).
   */
  function hisStageMetrics(vh) {
    const h = Math.max(480, vh | 0);
    const topBand = Math.round(Math.min(120, Math.max(96, h * 0.12)));
    const tvH = Math.round(Math.min(h * 0.4, Math.max(220, h * 0.34)));
    const tvBottom = 12;
    const midTop = topBand;
    const midBottom = tvBottom + tvH;
    const midH = Math.max(160, h - midTop - midBottom);
    const cy = midTop + midH * 0.48;
    return { topBand, tvH, tvBottom, midTop, midBottom, cy };
  }

  /** HIS: pack seats evenly around a full circle (no empty hemisphere). */
  function packHisCircle(wedges) {
    const raw = Array.isArray(wedges) ? wedges : [];
    const n = raw.length;
    if (n <= 0) return raw;
    const step = 360 / n;
    return raw.map(function (w, i) {
      return Object.assign({}, w, {
        _angle: step * i,
        _pack: 'his_circle'
      });
    });
  }

  const L1_SHORT = Object.freeze({
    'Want somewhere indoors?': 'Indoors?',
    'Want somewhere indoor?': 'Indoors?',
    'How do you feel?': 'Feel?',
    'How are you feeling?': 'Feel?',
    'Day tank': 'Day tank',
    'Happy w/ reco?': 'Happy?',
    "Who's turn": "Who's turn",
    'Interrupt?': 'Interrupt?',
    "That's enough": 'Enough',
    'Thats enough': 'Enough'
  });

  function shortL2Label(label) {
    const s = String(label || '').trim();
    if (!s) return '';
    const map = {
      'Need rest': 'Rest',
      'Happy here': 'Happy',
      Overwhelmed: 'Overwhelmed',
      'Promised stop': 'Promised',
      'Evening highlight': 'Evening',
      'Nothing special': 'Nothing',
      'Stay quiet': 'Quiet',
      'OK to nudge': 'Nudge',
      Energised: 'Energised'
    };
    if (map[s]) return map[s];
    if (s.length <= 10) return s;
    const parts = s.split(/\s+/);
    if (parts.length >= 2) return parts[0];
    return s.slice(0, 9);
  }

  /**
   * Dynamic L1 display — angular budget + seat count; never fight neighbors.
   * Returns { text, fontSize, maxWidthPx, title }.
   */
  function packL1Label(label, opts) {
    const raw = String(label || '').trim();
    const isInfo = !!(opts && opts.isInfo);
    const seatCount = Math.max(1, (opts && opts.seatCount) || 1);
    const spanDeg = Math.max(28, (opts && opts.spanDeg) || 60);
    const labelR = (HUB_R + DISK_R) / 2;
    const chord = 2 * labelR * Math.sin(((spanDeg * 0.92) / 2) * (Math.PI / 180));
    const maxWidthPx = Math.max(40, Math.min(88, Math.floor(chord - 10)));

    let text = L1_SHORT[raw] || raw;
    const soft = text.replace(/\?+$/, '');
    let fontSize = isInfo ? 12 : 13;
    if (seatCount >= 5) fontSize = Math.min(fontSize, 12);
    if (seatCount >= 6) fontSize = 12;

    const charBudget = Math.max(5, Math.floor(maxWidthPx / (fontSize * 0.52)));
    if (soft.length > charBudget) {
      const parts = soft.split(/\s+/).filter(Boolean);
      if (parts.length >= 2) {
        const last = parts[parts.length - 1];
        const first = parts[0];
        if (last.length <= charBudget && last.length >= 4) text = last;
        else if (first.length <= charBudget) text = first;
        else text = soft.slice(0, Math.max(4, charBudget - 1));
      } else {
        text = soft.slice(0, Math.max(4, charBudget - 1));
      }
      if (/\?$/.test(raw) && !/\?$/.test(text)) text += '?';
    } else {
      text = soft.length < raw.length && /\?$/.test(raw) ? soft + '?' : soft || text;
    }

    if (text.length > charBudget + 1) {
      fontSize = 12;
      text = text.slice(0, charBudget);
      if (/\?$/.test(raw)) text = text.replace(/\?*$/, '') + '?';
    }

    return { text, fontSize: Math.max(12, fontSize), maxWidthPx, title: raw };
  }

  function Ex1NowRadialOverlay({
    open,
    anchor,
    wedges,
    onCancel,
    onSelect,
    onConclude,
    companionLine,
    loading,
    memoryChips,
    focusCard,
    fieldStatus,
    spacePrompt,
    hisMode,
    hisTv
  }) {
    const [armedL1, setArmedL1] = useState(null);
    const [armedL2, setArmedL2] = useState(null);
    const [emergeKey, setEmergeKey] = useState(0);
    const [leaving, setLeaving] = useState(false);
    const [pendingSelect, setPendingSelect] = useState(false);
    const rootRef = useRef(null);
    /** True only after pointerdown on scrim — arming thumb-up never sets this. */
    const scrimDismissArmedRef = useRef(false);
    const ignoreDismissUntilRef = useRef(0);
    const leaveTimerRef = useRef(null);
    const isLoading = loading === true;
    const isHis = hisMode === true;

    // Lock page + nested NuAsk scroll/pan while radial is open.
    useEffect(() => {
      if (!open) return undefined;
      const html = document.documentElement;
      const body = document.body;
      const prevHtml = html.style.overflow;
      const prevBody = body.style.overflow;
      const prevTouch = body.style.touchAction;
      html.style.overflow = 'hidden';
      body.style.overflow = 'hidden';
      body.style.touchAction = 'none';
      const blockSelect = (e) => {
        e.preventDefault();
      };
      const blockPan = (e) => {
        e.preventDefault();
      };
      document.addEventListener('selectstart', blockSelect, true);
      document.addEventListener('contextmenu', blockSelect, true);
      document.addEventListener('touchmove', blockPan, { passive: false, capture: true });
      document.addEventListener('wheel', blockPan, { passive: false, capture: true });
      try {
        const sel = window.getSelection && window.getSelection();
        if (sel && sel.removeAllRanges) sel.removeAllRanges();
      } catch (_) { /* ignore */ }
      return () => {
        html.style.overflow = prevHtml;
        body.style.overflow = prevBody;
        body.style.touchAction = prevTouch;
        document.removeEventListener('selectstart', blockSelect, true);
        document.removeEventListener('contextmenu', blockSelect, true);
        document.removeEventListener('touchmove', blockPan, true);
        document.removeEventListener('wheel', blockPan, true);
      };
    }, [open]);

    // Phase reform: clear arm + pending + bump emerge animation when wedge set changes.
    useEffect(() => {
      setArmedL1(null);
      setArmedL2(null);
      setPendingSelect(false);
      setEmergeKey((k) => k + 1);
    }, [wedges]);

    useEffect(() => {
      if (!open) {
        setArmedL1(null);
        setArmedL2(null);
        setLeaving(false);
        setPendingSelect(false);
        scrimDismissArmedRef.current = false;
        ignoreDismissUntilRef.current = 0;
        if (leaveTimerRef.current) {
          clearTimeout(leaveTimerRef.current);
          leaveTimerRef.current = null;
        }
        return;
      }
      // Disk layer is pointer-events-none → arming lift lands on scrim. Require
      // down+up on scrim AND a short open grace so Hold stays persistent.
      scrimDismissArmedRef.current = false;
      ignoreDismissUntilRef.current = Date.now() + OPEN_DISMISS_GRACE_MS;
      setLeaving(false);
      setPendingSelect(false);
    }, [open]);

    useEffect(() => {
      return () => {
        if (leaveTimerRef.current) clearTimeout(leaveTimerRef.current);
      };
    }, []);

    // Soft-fail / lag: never leave the stage frozen in "Shaping…" forever.
    useEffect(() => {
      if (!pendingSelect) return undefined;
      const t = setTimeout(() => setPendingSelect(false), 4200);
      return () => clearTimeout(t);
    }, [pendingSelect]);

    useEffect(() => {
      if (fieldStatus) setPendingSelect(false);
    }, [fieldStatus]);

    const requestLeave = useCallback(() => {
      if (isLoading) return;
      if (!isHis) {
        onCancel && onCancel();
        return;
      }
      if (leaving) return;
      setLeaving(true);
      if (leaveTimerRef.current) clearTimeout(leaveTimerRef.current);
      leaveTimerRef.current = setTimeout(() => {
        leaveTimerRef.current = null;
        onCancel && onCancel();
      }, HIS_LEAVE_MS);
    }, [isHis, isLoading, leaving, onCancel]);

    const vh = typeof window !== 'undefined' ? window.innerHeight : 800;
    const vw = typeof window !== 'undefined' ? window.innerWidth : 390;
    const hisGeom = isHis ? hisStageMetrics(vh) : null;
    // HIS: stage-owned mid-band centre (not NOW-orb Y). Alive keeps orb anchor.
    const cx = isHis
      ? vw / 2
      : anchor?.x ?? vw / 2;
    const cy = isHis
      ? hisGeom.cy
      : anchor?.y ?? vh * 0.72;

    const list = useMemo(() => {
      const normalized = HIT ? HIT.normalizeWedges(wedges) : wedges || [];
      if (hisMode === true) return packHisCircle(normalized);
      return normalized;
    }, [wedges, hisMode]);
    const span = isHis && list.length
      ? Math.min(78, Math.max(42, (360 / list.length) * 0.9))
      : HIT
        ? HIT.wedgeSpan(list)
        : 60;

    const commitL2 = useCallback(
      (l1Id, l2Id) => {
        if (!l1Id || !l2Id || isLoading || pendingSelect || leaving) return;
        if (isHis) setPendingSelect(true);
        onSelect && onSelect({ wedge_id: l1Id, value: l2Id, layer: 'L2' });
        setArmedL1(null);
        setArmedL2(null);
      },
      [onSelect, isLoading, pendingSelect, leaving, isHis]
    );

    const onScrimPointerDown = useCallback((evt) => {
      if (isHis) return; // HIS: absolute persistence — no scrim dismiss
      if (evt.target?.getAttribute?.('data-ex1-radial-scrim') !== '1') return;
      scrimDismissArmedRef.current = true;
    }, [isHis]);

    const onScrimPointerCancel = useCallback(() => {
      scrimDismissArmedRef.current = false;
    }, []);

    const onScrimPointerUp = useCallback(
      (evt) => {
        if (isHis) return; // HIS: background tap does nothing
        const armed = scrimDismissArmedRef.current;
        scrimDismissArmedRef.current = false;
        if (isLoading) return;
        if (Date.now() < ignoreDismissUntilRef.current) return;
        if (!armed) return;
        if (evt.target?.getAttribute?.('data-ex1-radial-scrim') !== '1') return;
        onCancel && onCancel();
      },
      [onCancel, isLoading, isHis]
    );

    if (!open) return null;

    const polar = HIT?.polar || ((x, y, r, d) => ({ x, y }));
    const describeArc = HIT?.describeArc;
    const armedW = list.find((w) => w.id === armedL1);
    const l2opts = armedW && !armedW._display ? armedW.L2 || [] : [];
    const vbPad = L2_R + 56;
    const vbSize = vbPad * 2;
    const svgCx = vbPad;
    const svgCy = vbPad;
    const isBusy = isLoading || pendingSelect;
    // A18: never show the previous stage's seats/prompt while the next stage is still loading.
    const showSeats = !isBusy && list.length > 0;
    const promptText =
      typeof spacePrompt === 'string' && spacePrompt.trim()
        ? spacePrompt.trim()
        : null;
    const showPrompt = !isBusy && !!promptText;
    // Top band owns the ask (space_prompt). Companion is context — never twin under Top
    // (A20.3: "Before we shape…" + "No plan for today yet…" double messaging).
    const showCompanion =
      showSeats &&
      !promptText &&
      companionLine &&
      String(companionLine).trim();
    const ackLine = null;
    const chips = Array.isArray(memoryChips)
      ? memoryChips.map((c) => String(c || '').trim()).filter(Boolean).slice(0, 6)
      : [];
    const showChips = showSeats && chips.length > 0;
    const focus =
      focusCard && (focusCard.title || focusCard.why) ? focusCard : null;
    const showFocus = showSeats && !!focus;
    const statusLine =
      typeof fieldStatus === 'string' && fieldStatus.trim() ? fieldStatus.trim() : '';
    const tv = hisTv && typeof hisTv === 'object' ? hisTv : null;
    const tvSteps = tv && Array.isArray(tv.steps) ? tv.steps : [];
    const tvFocus = tv && tv.focus && (tv.focus.title || tv.focus.why) ? tv.focus : null;
    const tvStatus =
      (tv && typeof tv.status === 'string' && tv.status.trim()) || statusLine || '';
    const tvResult =
      tv && typeof tv.result_line === 'string' && tv.result_line.trim()
        ? tv.result_line.trim()
        : null;
    const showHisTv = isHis && !!tv && !isBusy;
    const isConclude =
      isHis &&
      !isBusy &&
      ((tv && tv.mode === 'conclude') || list.length === 0);
    // HIS: path/memory lives inside the fixed TV — never mid-field chips over the radial.
    const showChipsHis = showChips && !isHis;
    const showFocusMid = showFocus && !showHisTv;
    const showRadial = showSeats && !isConclude;
    const concludePrompt =
      (tv && typeof tv.conclude_prompt === 'string' && tv.conclude_prompt.trim()) ||
      'What would you like to do next?';
    const concludeOptions =
      tv && Array.isArray(tv.conclude_options) && tv.conclude_options.length
        ? tv.conclude_options
        : [
            { id: 'done', label: "I'm good" },
            { id: 'back_to_now', label: 'Back to NOW' }
          ];
    const topBand =
      (promptText ? 1 : 0) +
      (showCompanion || ackLine ? 1 : 0) +
      (showChipsHis ? 1 : 0);
    const hisMotionIn = leaving ? 'ex1-his-stage-out' : 'ex1-his-stage-in';
    const hisSheetIn = leaving ? 'ex1-his-sheet-out' : 'ex1-his-sheet-in';
    const hisBackdrop = leaving ? 'ex1-his-backdrop-out' : 'ex1-his-backdrop-in';

    return (
      <div
        ref={rootRef}
        className="fixed inset-0 z-[280]"
        role="dialog"
        aria-modal="true"
        aria-label="Hold intelligence space"
        aria-busy={isLoading || statusLine ? 'true' : undefined}
        data-ex1-radial-overlay="1"
        data-ex1-living-chrome="1"
        data-ex1-nuask-his={isHis ? '1' : '0'}
        data-ex1-radial-loading={isLoading ? '1' : '0'}
        data-ex1-hold-persistent="1"
        data-ex1-his-absolute-persist={isHis ? '1' : '0'}
        data-ex1-hold-governor={statusLine ? '1' : '0'}
        style={{
          touchAction: 'none',
          WebkitUserSelect: 'none',
          userSelect: 'none',
          WebkitTouchCallout: 'none',
          overscrollBehavior: 'none'
        }}
      >
        <style>{`
          /* Busy hub only — scale on an INNER node; outer keeps translate(-50%).
             Never put scale() on the same transform that centres the hub (A20.3). */
          @keyframes ex1-his-orb-breathe {
            0%, 100% { transform: scale(1); opacity: 0.55; }
            50% { transform: scale(1.22); opacity: 0.88; }
          }
          @keyframes ex1-his-emerge {
            from { opacity: 0; transform: translate(-50%, -50%) scale(0.94); }
            to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
          }
          @keyframes ex1-his-hub-pulse {
            0%, 100% { box-shadow: 0 0 0 0 rgba(80, 220, 210, 0.28), 0 4px 18px rgba(0,0,0,0.45); }
            50% { box-shadow: 0 0 0 10px rgba(80, 220, 210, 0.0), 0 4px 22px rgba(0,0,0,0.4); }
          }
          /* Pre-Ex.1 hub drawer motion language (sf-hub-tools-*) — HIS stage fit.
             Sheet keyframes MUST keep translateX(-50%) — never replace this animation. */
          @keyframes ex1-his-backdrop-in {
            from { opacity: 0; }
            to { opacity: 1; }
          }
          @keyframes ex1-his-backdrop-out {
            from { opacity: 1; }
            to { opacity: 0; }
          }
          @keyframes ex1-his-sheet-in {
            from { opacity: 0; transform: translate(-50%, 18%); }
            to { opacity: 1; transform: translate(-50%, 0); }
          }
          @keyframes ex1-his-sheet-out {
            from { opacity: 1; transform: translate(-50%, 0); }
            to { opacity: 0; transform: translate(-50%, 14%); }
          }
          @keyframes ex1-his-stage-in {
            from { opacity: 0; }
            to { opacity: 1; }
          }
          @keyframes ex1-his-stage-out {
            from { opacity: 1; }
            to { opacity: 0; }
          }
          @keyframes ex1-his-calm-fade {
            from { opacity: 0; }
            to { opacity: 1; }
          }
          @keyframes ex1-his-field-in {
            from { opacity: 0; }
            to { opacity: 1; }
          }
          @keyframes ex1-his-alive-pulse {
            0%, 100% { opacity: 0.55; }
            50% { opacity: 1; }
          }
          .ex1-his-backdrop-in { animation: ex1-his-backdrop-in 0.56s ease-out both; }
          .ex1-his-backdrop-out { animation: ex1-his-backdrop-out 0.56s ease-in both; }
          .ex1-his-sheet-in { animation: ex1-his-sheet-in 0.76s cubic-bezier(0.22, 1, 0.36, 1) both; }
          .ex1-his-sheet-out { animation: ex1-his-sheet-out 0.76s cubic-bezier(0.4, 0, 0.2, 1) both; }
          .ex1-his-stage-in { animation: ex1-his-stage-in 0.72s cubic-bezier(0.22, 1, 0.36, 1) both; }
          .ex1-his-stage-out { animation: ex1-his-stage-out 0.56s cubic-bezier(0.4, 0, 0.2, 1) both; }
          @media (prefers-reduced-motion: reduce) {
            .ex1-his-backdrop-in, .ex1-his-backdrop-out,
            .ex1-his-stage-in, .ex1-his-stage-out { animation: none !important; }
            .ex1-his-sheet-in, .ex1-his-sheet-out {
              animation: none !important;
              transform: translateX(-50%) !important;
            }
            [data-ex1-his-busy-halo] { animation: none !important; }
          }
        `}</style>

        <div
          className={`absolute inset-0${isHis ? ` ${hisBackdrop}` : ''}`}
          data-ex1-radial-scrim="1"
          data-ex1-scrim-dismiss-guard={isHis ? '0' : '1'}
          data-ex1-his-no-scrim-dismiss={isHis ? '1' : '0'}
          style={isHis ? SCRIM_HIS : SCRIM}
          aria-hidden
          onPointerDown={onScrimPointerDown}
          onPointerUp={onScrimPointerUp}
          onPointerCancel={onScrimPointerCancel}
        />

        {isHis ? (
          <button
            type="button"
            data-ex1-his-back="1"
            className={`pointer-events-auto absolute left-3 z-[4] rounded-full border border-white/40 bg-transparent px-3 py-1.5 text-[13px] font-semibold text-white/90 shadow-[0_4px_18px_rgba(0,0,0,0.25),inset_0_1px_0_rgba(255,255,255,0.28)] transition duration-200 motion-safe:active:scale-95 ${hisMotionIn}`}
            style={{
              top: 'max(0.75rem, env(safe-area-inset-top, 0px) + 0.5rem)',
              backdropFilter: 'blur(14px) saturate(1.35)',
              WebkitBackdropFilter: 'blur(14px) saturate(1.35)',
              backgroundColor: 'rgba(8, 28, 34, 0.55)'
            }}
            onClick={(e) => {
              e.stopPropagation();
              requestLeave();
            }}
          >
            Back
          </button>
        ) : null}

        {showHisTv || isConclude ? (
          <div
            data-ex1-his-tv="1"
            data-ex1-his-tv-fixed="1"
            data-ex1-his-conclude={isConclude ? '1' : '0'}
            className={`absolute left-1/2 z-[3] w-[min(24rem,94vw)] overflow-hidden rounded-[1.75rem] border border-cyan-100/25 ${hisSheetIn}`}
            style={{
              bottom: `max(${hisGeom ? hisGeom.tvBottom : 12}px, env(safe-area-inset-bottom, 0px) + 0.5rem)`,
              height: hisGeom ? hisGeom.tvH : 'min(40svh, 22rem)',
              minHeight: '13.5rem',
              backgroundColor: 'rgba(4, 22, 28, 0.96)',
              backdropFilter: 'blur(22px) saturate(1.35)',
              WebkitBackdropFilter: 'blur(22px) saturate(1.35)',
              boxShadow:
                '0 16px 48px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.12), 0 0 0 1px rgba(45, 226, 197, 0.08)',
              pointerEvents: isConclude ? 'auto' : 'none'
            }}
            aria-live="polite"
          >
            <div
              className="flex h-full w-full flex-col items-center justify-center overflow-y-auto px-5 py-4 text-center"
              style={{ WebkitOverflowScrolling: 'touch' }}
            >
              {isConclude ? (
                <>
                  <p className="text-[15px] font-medium leading-snug text-white/70">
                    {(tv && tv.empty_label) || 'Nothing more to change right now.'}
                  </p>
                  <p className="mt-3 text-[17px] font-semibold leading-snug text-white/96">
                    {concludePrompt}
                  </p>
                  {tvSteps.length ? (
                    <p
                      className="mt-2 text-[11px] font-medium tracking-wide"
                      style={{ color: 'rgba(180, 220, 225, 0.45)' }}
                    >
                      {tvSteps.map((s) => s.label).join(' · ')}
                    </p>
                  ) : null}
                  <div className="mt-5 flex w-full max-w-[18rem] flex-col gap-2.5">
                    {concludeOptions.map((opt) => {
                      const isPrimary = opt.primary === true;
                      return (
                        <button
                          key={opt.id}
                          type="button"
                          data-ex1-his-conclude-opt={opt.id}
                          data-ex1-his-conclude-primary={isPrimary ? '1' : '0'}
                          className="pointer-events-auto w-full rounded-2xl border px-4 py-3 text-[14px] font-semibold text-white/95 shadow-[0_4px_18px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.28)] transition duration-200 motion-safe:active:scale-[0.98]"
                          style={{
                            borderColor: isPrimary
                              ? 'rgba(94, 234, 212, 0.55)'
                              : 'rgba(255, 255, 255, 0.35)',
                            backgroundColor: isPrimary
                              ? 'rgba(14, 70, 72, 0.92)'
                              : 'rgba(10, 40, 46, 0.82)',
                            backdropFilter: 'blur(14px)',
                            WebkitBackdropFilter: 'blur(14px)'
                          }}
                          onClick={(e) => {
                            e.stopPropagation();
                            if (typeof onConclude === 'function') {
                              onConclude(opt.id);
                              return;
                            }
                            requestLeave();
                          }}
                        >
                          {opt.label}
                        </button>
                      );
                    })}
                  </div>
                </>
              ) : (
                <div className="w-full max-w-[18rem]">
                  {/* Primary line: status / focus / result / guide — never wipe the path chain. */}
                  {tvStatus ? (
                    <p className="text-[15px] font-semibold leading-snug text-cyan-50">
                      {tvStatus}
                    </p>
                  ) : pendingSelect ? (
                    <p
                      className="text-[15px] font-semibold text-cyan-100/90"
                      style={{ animation: 'ex1-his-alive-pulse 1.1s ease-in-out infinite' }}
                    >
                      Got it — shaping…
                    </p>
                  ) : tvFocus ? (
                    <>
                      <p className="text-[17px] font-semibold leading-snug text-white/96">
                        {tvFocus.title}
                      </p>
                      {tvFocus.meta ? (
                        <p className="mt-1.5 text-[12px] font-medium text-cyan-100/68">
                          {tvFocus.meta}
                        </p>
                      ) : null}
                      {tvFocus.why ? (
                        <p className="mt-2 text-[13px] leading-snug text-white/78">
                          {tvFocus.why}
                        </p>
                      ) : null}
                    </>
                  ) : tvResult ? (
                    <p className="text-[15px] font-semibold text-white/90">{tvResult}</p>
                  ) : (
                    <p className="text-[15px] font-semibold leading-snug text-white/94">
                      {(tv && (tv.guide_line || tv.empty_label)) || 'Choose above to continue.'}
                    </p>
                  )}
                  {/* Full built path always visible when present (A1 bottom zone). */}
                  {tvSteps.length ? (
                    <div className="mt-4 flex flex-wrap items-center justify-center gap-1.5">
                      {tvSteps.map((s, i) => (
                        <span
                          key={`${s.role}-${s.label}-${i}`}
                          className="rounded-full border border-white/15 px-2.5 py-1 text-[12px] font-medium text-white/80"
                          style={{ backgroundColor: 'rgba(255,255,255,0.06)' }}
                        >
                          {s.label}
                        </span>
                      ))}
                    </div>
                  ) : chips.length ? (
                    <p className="mt-3 text-[13px] font-medium leading-snug text-white/70">
                      {chips.join(' · ')}
                    </p>
                  ) : null}
                </div>
              )}
            </div>
          </div>
        ) : null}

        {showPrompt || (isConclude && concludePrompt) ? (
          <div
            data-ex1-space-prompt="1"
            className={`pointer-events-none absolute left-1/2 z-[2] w-[min(22rem,90vw)] -translate-x-1/2 px-4 text-center ${isHis ? hisMotionIn : ''}`}
            style={{
              top: isHis
                ? 'max(2.75rem, env(safe-area-inset-top, 0px) + 2.25rem)'
                : 'max(1.5rem, env(safe-area-inset-top, 0px) + 1rem)',
              fontSize: '19px',
              lineHeight: 1.3,
              fontWeight: 580,
              letterSpacing: '-0.01em',
              color: 'rgba(255,255,255,0.97)',
              textShadow: '0 2px 18px rgba(0,12,16,0.85)',
              WebkitUserSelect: 'none',
              userSelect: 'none'
            }}
            aria-live="polite"
          >
            {showPrompt ? promptText : concludePrompt}
          </div>
        ) : null}

        {showCompanion ? (
          <div
            data-ex1-companion-line="1"
            className="pointer-events-none absolute left-1/2 z-[2] w-[min(22rem,88vw)] -translate-x-1/2 px-4 text-center"
            style={{
              top: 'max(1.25rem, env(safe-area-inset-top, 0px) + 0.75rem)',
              fontSize: '17px',
              lineHeight: 1.35,
              fontWeight: 560,
              color: 'rgba(255,255,255,0.96)',
              textShadow: '0 1px 14px rgba(0,20,24,0.95)',
              WebkitUserSelect: 'none',
              userSelect: 'none'
            }}
            aria-live="polite"
          >
            {String(companionLine).trim()}
          </div>
        ) : null}

        {ackLine && !pendingSelect ? (
          <div
            data-ex1-companion-line="1"
            className={`pointer-events-none absolute left-1/2 z-[2] w-[min(20rem,86vw)] -translate-x-1/2 px-4 text-center ${isHis ? hisMotionIn : ''}`}
            style={{
              top: isHis
                ? 'max(5.4rem, env(safe-area-inset-top, 0px) + 4.6rem)'
                : 'max(4.1rem, env(safe-area-inset-top, 0px) + 3.5rem)',
              fontSize: '13px',
              lineHeight: 1.35,
              fontWeight: 500,
              color: 'rgba(200, 240, 245, 0.72)',
              textShadow: '0 1px 10px rgba(0,20,24,0.8)'
            }}
            aria-live="polite"
          >
            {ackLine}
          </div>
        ) : null}

        {isHis && pendingSelect ? (
          <div
            data-ex1-his-alive="1"
            className="pointer-events-none absolute left-1/2 z-[5] w-[min(18rem,80vw)] -translate-x-1/2 rounded-full border border-cyan-200/30 px-3 py-1.5 text-center text-[12px] font-semibold text-cyan-50"
            style={{
              top: 'max(5.2rem, env(safe-area-inset-top, 0px) + 4.4rem)',
              backgroundColor: 'rgba(6, 40, 46, 0.88)',
              backdropFilter: 'blur(12px)',
              animation: 'ex1-his-alive-pulse 1.1s ease-in-out infinite'
            }}
            role="status"
          >
            Got it — one moment…
          </div>
        ) : null}

        {showChipsHis ? (
          <div
            data-ex1-memory-chips="1"
            className="pointer-events-none absolute left-1/2 z-[2] max-w-[min(22rem,90vw)] -translate-x-1/2 px-4 text-center"
            style={{
              top:
                topBand >= 2
                  ? 'max(6.4rem, env(safe-area-inset-top, 0px) + 5.6rem)'
                  : promptText || showCompanion
                    ? 'max(4.4rem, env(safe-area-inset-top, 0px) + 3.7rem)'
                    : 'max(1.25rem, env(safe-area-inset-top, 0px) + 0.75rem)'
            }}
            aria-hidden
          >
            <p
              className="text-[12px] font-medium tracking-wide"
              style={{
                color: 'rgba(180, 220, 225, 0.55)',
                textShadow: '0 1px 8px rgba(0,0,0,0.45)'
              }}
            >
              {chips.join(' · ')}
            </p>
          </div>
        ) : null}

        {showFocusMid ? (
          <div
            data-ex1-poi-focus="1"
            className="pointer-events-none absolute left-1/2 z-[2] w-[min(21rem,88vw)] -translate-x-1/2 rounded-3xl border border-cyan-100/25 px-4 py-3 text-center"
            style={{
              top: Math.max(88, cy - DISK_R - (focus.why ? 124 : 92)),
              backgroundColor: 'rgba(8, 32, 38, 0.78)',
              backdropFilter: 'blur(18px) saturate(1.35)',
              WebkitBackdropFilter: 'blur(18px) saturate(1.35)',
              boxShadow: '0 12px 40px rgba(0,0,0,0.32), inset 0 1px 0 rgba(255,255,255,0.12)',
              animation: 'ex1-his-field-in 380ms ease-out'
            }}
            aria-live="polite"
          >
            <p className="text-[16px] font-semibold leading-snug text-white/96">{focus.title}</p>
            {focus.meta ? (
              <p className="mt-1 text-[12px] font-medium text-cyan-100/68">{focus.meta}</p>
            ) : null}
            {focus.why ? (
              <p className="mt-1.5 text-[13px] leading-snug text-white/78">{focus.why}</p>
            ) : null}
          </div>
        ) : null}

        {/* Mid-field status is for in-flow governor only — never band-aid over conclude options. */}
        {statusLine && !showHisTv && !isConclude ? (
          <div
            data-ex1-field-status="1"
            className="pointer-events-none absolute left-1/2 z-[3] w-[min(20rem,88vw)] -translate-x-1/2 rounded-2xl border border-cyan-300/35 px-3.5 py-2.5 text-center"
            style={{
              bottom: 'max(5.5rem, env(safe-area-inset-bottom, 0px) + 4.5rem)',
              backgroundColor: 'rgba(4, 40, 46, 0.92)',
              backdropFilter: 'blur(16px)',
              WebkitBackdropFilter: 'blur(16px)',
              boxShadow: '0 0 28px rgba(45, 226, 197, 0.18)'
            }}
            role="status"
            aria-live="assertive"
          >
            <p className="text-[14px] font-semibold leading-snug text-cyan-50">{statusLine}</p>
          </div>
        ) : null}

        <div
          className={`pointer-events-none absolute inset-0${isHis ? ` ${hisMotionIn}` : ''}`}
          style={{ touchAction: 'none' }}
          data-ex1-his-radial-stage={isHis ? '1' : '0'}
        >
          <svg
            width={vbSize}
            height={vbSize}
            className="absolute -translate-x-1/2 -translate-y-1/2 overflow-visible"
            style={{ left: cx, top: cy }}
            aria-hidden
          >
            <circle
              cx={svgCx}
              cy={svgCy}
              r={DISK_R + 6}
              fill="rgba(2, 18, 22, 0.72)"
              stroke="rgba(140, 220, 230, 0.22)"
              strokeWidth="1.25"
            />
            <circle
              cx={svgCx}
              cy={svgCy}
              r={DISK_R}
              fill="rgba(10, 40, 46, 0.78)"
              stroke="rgba(180, 230, 235, 0.32)"
              strokeWidth="1.25"
            />
            <circle
              cx={svgCx}
              cy={svgCy}
              r={HUB_R + 2}
              fill="rgba(4, 24, 30, 0.88)"
              stroke="rgba(180, 230, 235, 0.4)"
              strokeWidth="1"
            />

            {showRadial && describeArc
              ? list.map((w) => {
                  const a0 = (w._angle || 0) - span / 2;
                  const a1 = (w._angle || 0) + span / 2;
                  const on = armedL1 === w.id;
                  const isInfo = w.arc === 'info' || w._display;
                  const fill = on
                    ? isInfo
                      ? 'rgba(56, 189, 210, 0.72)'
                      : 'rgba(45, 212, 180, 0.74)'
                    : isInfo
                      ? 'rgba(14, 60, 68, 0.88)'
                      : 'rgba(18, 72, 78, 0.90)';
                  const stroke = on
                    ? isInfo
                      ? 'rgba(165, 243, 252, 0.95)'
                      : 'rgba(110, 231, 183, 0.95)'
                    : 'rgba(140, 210, 220, 0.42)';
                  return (
                    <path
                      key={`seg-${w.id}`}
                      data-ex1-l1-seg={w.id}
                      d={describeArc(svgCx, svgCy, HUB_R + 4, DISK_R - 2, a0, a1)}
                      fill={fill}
                      stroke={stroke}
                      strokeWidth={on ? 2 : 1}
                    />
                  );
                })
              : null}
          </svg>

          <div
            className="absolute"
            style={{
              left: cx,
              top: cy,
              width: HUB_R * 2.6,
              height: HUB_R * 2.6,
              transform: 'translate(-50%, -50%)'
            }}
            data-ex1-his-hub-anchor="1"
          >
            {isBusy ? (
              <span
                data-ex1-his-busy-halo="1"
                data-ex1-radial-loading-pulse="1"
                className="pointer-events-none absolute inset-0 rounded-full"
                style={{
                  transformOrigin: 'center center',
                  background:
                    'radial-gradient(circle, rgba(45,226,197,0.28) 0%, rgba(45,226,197,0.08) 50%, rgba(45,226,197,0) 78%)',
                  animation: `ex1-his-orb-breathe ${pendingSelect ? '0.95s' : '1.05s'} ease-in-out infinite`
                }}
                aria-hidden
              />
            ) : null}
            <div
              className="absolute flex items-center justify-center rounded-full border border-solid border-cyan-100/35 text-[12px] font-semibold uppercase tracking-wide text-white"
              style={{
                left: '50%',
                top: '50%',
                width: HUB_R * 2,
                height: HUB_R * 2,
                marginLeft: -HUB_R,
                marginTop: -HUB_R,
                transformOrigin: 'center center',
                ...GLASS,
                animation: isBusy
                  ? `ex1-his-orb-breathe ${pendingSelect ? '0.95s' : '1.05s'} ease-in-out infinite`
                  : isHis
                    ? 'ex1-his-hub-pulse 2.8s ease-in-out infinite'
                    : undefined,
                boxShadow: isBusy
                  ? '0 0 16px rgba(45,226,197,0.4), 0 4px 18px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.22)'
                  : '0 4px 18px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.2)'
              }}
              data-ex1-his-hub={isHis ? '1' : undefined}
            >
              {isBusy || isHis ? null : 'NOW'}
            </div>
          </div>

          {isBusy && isHis ? (
            <p
              data-ex1-his-open-skeleton="1"
              className="pointer-events-none absolute left-1/2 z-[2] w-[min(16rem,70vw)] -translate-x-1/2 text-center text-[13px] font-medium text-cyan-50/75"
              style={{
                top: cy + DISK_R + 18,
                animation: 'ex1-his-calm-fade 0.5s ease-out both'
              }}
            >
              {pendingSelect ? 'Getting your next step ready…' : 'Getting your seats ready…'}
            </p>
          ) : null}

          {showRadial
            ? list.map((w, j) => {
                const labelR = (HUB_R + DISK_R) / 2;
                const p = polar(cx, cy, labelR, w._angle || 0);
                const on = armedL1 === w.id;
                const isInfo = w.arc === 'info' || w._display;
                const packed = packL1Label(w.label, {
                  isInfo,
                  seatCount: list.length,
                  spanDeg: span
                });
                return (
                  <button
                    type="button"
                    key={`lab-${w.id}-${emergeKey}`}
                    data-ex1-l1={w.id}
                    data-ex1-arc={w.arc || (isInfo ? 'info' : 'action')}
                    data-ex1-l1-packed="1"
                    title={packed.title}
                    className={`pointer-events-auto absolute -translate-x-1/2 -translate-y-1/2 border-0 bg-transparent px-0.5 text-center font-semibold leading-snug text-white ${
                      on ? 'scale-105' : ''
                    }`}
                    style={{
                      left: p.x,
                      top: p.y,
                      width: packed.maxWidthPx,
                      maxWidth: packed.maxWidthPx,
                      fontSize: packed.fontSize,
                      textShadow: '0 1px 10px rgba(0,0,0,0.9)',
                      cursor: isInfo ? 'default' : 'pointer',
                      animation: isHis ? 'ex1-his-emerge 380ms ease-out' : undefined,
                      animationDelay: isHis ? `${Math.min(j * 40, 200)}ms` : undefined,
                      animationFillMode: isHis ? 'both' : undefined
                    }}
                    onClick={(e) => {
                      e.stopPropagation();
                      if (isInfo || isLoading || pendingSelect || leaving) return;
                      const opts = Array.isArray(w.L2) ? w.L2 : [];
                      // HIS: single L2 → one tap (tags / places / commit seats).
                      if (isHis && opts.length === 1 && opts[0]?.id) {
                        commitL2(w.id, opts[0].id);
                        return;
                      }
                      setArmedL1(w.id);
                      setArmedL2(null);
                    }}
                  >
                    {packed.text}
                    {isInfo ? (
                      <span className="mt-0.5 block text-[12px] font-medium uppercase tracking-wide text-cyan-100/55">
                        knows
                      </span>
                    ) : null}
                  </button>
                );
              })
            : null}

          {showRadial &&
            armedL1 &&
            l2opts.map((o, j) => {
              const spread = HIT?.l2Spread
                ? HIT.l2Spread(l2opts.length)
                : Math.min(42, Math.max(28, 130 / Math.max(l2opts.length, 1)));
              const ang = HIT?.l2Angle
                ? HIT.l2Angle(armedW?._angle || 0, j, l2opts.length)
                : (armedW?._angle || 0) + (j - (l2opts.length - 1) / 2) * spread;
              const p = polar(cx, cy, L2_R, ang);
              const on = armedL2 === o.id;
              const chipLabel = shortL2Label(o.label);
              return (
                <button
                  type="button"
                  key={`${armedL1}-${o.id}`}
                  data-ex1-l2={o.id}
                  title={o.label || o.id}
                  className={`pointer-events-auto absolute flex min-h-[2.5rem] w-[4.35rem] max-w-[4.35rem] -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-2xl border border-solid px-1.5 py-1 text-center font-semibold leading-tight text-white shadow-[0_4px_16px_rgba(0,0,0,0.4),inset_0_1px_0_rgba(255,255,255,0.22)] ${
                    on ? 'border-cyan-200/90 scale-105' : 'border-white/40'
                  }`}
                  style={{
                    left: p.x,
                    top: p.y,
                    fontSize: '12px',
                    ...GLASS,
                    backgroundColor: on ? 'rgba(16, 90, 98, 0.94)' : 'rgba(8, 32, 38, 0.92)',
                    cursor: 'pointer'
                  }}
                    onClick={(e) => {
                      e.stopPropagation();
                      if (isBusy || leaving) return;
                      setArmedL2(o.id);
                      commitL2(armedL1, o.id);
                    }}
                >
                  {chipLabel}
                </button>
              );
            })}

          <p
            className="pointer-events-none absolute left-1/2 max-w-[18rem] -translate-x-1/2 text-center text-[12px] font-medium text-white/90"
            style={{
              top: Math.min(
                cy + L2_R + 40,
                (typeof window !== 'undefined' ? window.innerHeight : 800) - 48
              ),
              textShadow: '0 1px 8px rgba(0,0,0,0.7)',
              display: showHisTv ? 'none' : undefined
            }}
          >
            {isBusy
              ? // HIS open / step skeleton owns prose — don’t twin in the footer.
                isHis
                  ? ''
                  : 'Getting your space ready…'
              : tvStatus
                ? ''
                : !showSeats
                  ? isHis
                    ? 'Use Back to leave'
                    : 'Tap outside to leave'
                  : armedL1 && armedW && armedW._display
                    ? 'Info only'
                    : armedL1
                      ? 'Tap an option'
                      : isHis
                        ? 'Choose'
                        : 'Tap a question · tap outside to leave'}
          </p>
        </div>
      </div>
    );
  }

  Ex1NowRadialOverlay.packL1Label = packL1Label;
  Ex1NowRadialOverlay.SCRIM = SCRIM;
  window.Ex1NowRadialOverlay = Ex1NowRadialOverlay;
})();
