// v3/panels.jsx — All panel sections: normal ops + CSAR master mode

// ── Shared primitives ─────────────────────────────────────────
// Context for batched collapse/expand of every Sec inside a rail. When the
// parent provides a numeric `epoch`, every Sec's internal state resets to
// `forced` on each epoch change — used by CollapsibleRail to drive a
// "collapse all / expand all" toggle that doesn't have to know about each
// section individually.
const SecBatchContext = React.createContext(null);

function CollapsibleRail({ children, label = 'Rail', expanded: expandedProp, onExpandedChange, minimized = false, count = 0, urgent = false }) {
  // Default: expanded. Each toggle bumps `epoch` so child Sec components
  // remount their internal open state to whatever `forced` currently is.
  // Optionally controlled — pass `expanded` + `onExpandedChange` to lift the
  // state to the parent (so the rail's outer width can shrink when fully
  // collapsed).
  // `count` + `urgent` give the header a leading count chip that mirrors
  // the Case Action Queue: when urgent the chip pulses + the whole header
  // gets the critical-magenta wash, surfacing "you have pending work in
  // here" even when the rail is minimized to a sliver.
  const [internalForced, setInternalForced] = React.useState(true);
  const [epoch, setEpoch] = React.useState(0);
  const isControlled = expandedProp !== undefined && typeof onExpandedChange === 'function';
  const forced = isControlled ? expandedProp : internalForced;
  const toggleAll = () => {
    if (isControlled) onExpandedChange(!forced);
    else setInternalForced(v => !v);
    setEpoch(e => e + 1);
  };
  const showChip      = count > 0;
  const chipBg        = urgent ? C.criticalM : C.s3;
  const chipColor     = urgent ? C.critical  : C.fg3;
  // Minimized layout: vertical sliver with "◂ LABEL" rotated. When the rail
  // has pending work we surface the count chip just below the chevron so
  // the operator can see "X pending" even with the rail collapsed — the
  // whole point of bringing the count up to the rail header.
  if (minimized) {
    return (
      <button
        onClick={toggleAll}
        title={`Expand ${label}${showChip ? ` · ${count} pending` : ''}`}
        style={{
          display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'flex-start',
          gap:8, width:'100%', height:'100%',
          padding:'10px 0',
          background: urgent ? C.criticalM : C.s2,
          border:'none',
          cursor:'pointer', flexShrink:0,
        }}>
        <span style={{ fontFamily:'Roboto Mono',fontSize:10,color:urgent?C.critical:C.fg2,letterSpacing:'0.06em' }}>◂</span>
        {showChip && (
          <span style={{
            background: chipBg, color: chipColor,
            fontFamily:'Roboto Mono', fontSize:8, fontWeight:500,
            padding:'1px 6px', borderRadius:10, lineHeight:1,
            animation: urgent ? 'pulseFade 0.7s infinite' : undefined,
          }}>{count}</span>
        )}
        <span data-typo="label" style={{
          fontFamily:'Roboto Mono',fontSize:8,fontWeight:500,
          color: urgent ? C.critical : C.fg2,
          letterSpacing:'0.16em',textTransform:'uppercase',
          writingMode:'vertical-rl', transform:'rotate(180deg)',
        }}>{label}</span>
      </button>
    );
  }
  return (
    <SecBatchContext.Provider value={{ forced, epoch }}>
      {/* Master toggle row — sits at the top of the rail. Layout mirrors the
          Case Action Queue header:
          | (count) LABEL                        Collapse ▸ |
          The count chip on the left pulses when urgent. */}
      <button
        onClick={toggleAll}
        style={{
          display:'flex', alignItems:'center', gap:8, width:'100%',
          padding:'8px 12px',
          background: urgent ? C.criticalM : C.s2,
          border:'none',
          borderBottom:`1px solid ${urgent ? C.critical+'44' : C.border}`,
          transition:'background 0.4s',
          cursor:'pointer',
          textAlign:'left', flexShrink:0, boxSizing:'border-box',
          // Lock content row to the same intrinsic height as the Case Action
          // Queue header (which contains a 10px-tall count badge with 1px
          // vertical padding, totalling 12px). Both headers end up at exactly
          // 8 + 12 + 8 = 28px tall, eliminating the 1px drift caused by
          // line-height differences across font-sizes.
          height:28, lineHeight:1,
        }}>
        {showChip && (
          <span style={{
            background: chipBg, color: chipColor,
            fontFamily:'Roboto Mono', fontSize:8, fontWeight:500,
            padding:'1px 6px', borderRadius:10, lineHeight:1, flexShrink:0,
            animation: urgent ? 'pulseFade 0.7s infinite' : undefined,
          }}>{count}</span>
        )}
        <span data-typo="label" style={{ fontFamily:'Roboto Mono',fontSize:8.5,fontWeight:500,color:urgent?C.critical:C.fg2,letterSpacing:'0.11em',textTransform:'uppercase',flex:1,lineHeight:1 }}>
          {label}
        </span>
        <span data-typo="label" style={{ fontFamily:'Roboto Mono',fontSize:7.5,color:C.fg3,letterSpacing:'0.11em',textTransform:'uppercase',lineHeight:1 }}>
          {forced ? 'Collapse ▸' : '◂ Expand'}
        </span>
      </button>
      {children}
    </SecBatchContext.Provider>
  );
}

function Sec({ title, badge, children, flush, compact, collapsible, defaultOpen = true, grow, open: openProp, onOpenChange }) {
  // When `collapsible` is true the title bar becomes a button that toggles the
  // body. A small chevron sits to the left of the title so it visually reads
  // as expandable. Non-collapsible Sec stays a passive header.
  // When `grow` is true the section absorbs remaining vertical space in its
  // parent (used by CommsLog so the chat panel grows from the bottom up as
  // the action queue above it shrinks/grows).
  // Optionally controlled: if `open` + `onOpenChange` are passed, the parent
  // owns the open state — used by Rail 1 to drive a collapse-all/expand-all
  // toggle that batches every section's state at once.
  // Also picks up SecBatchContext when present: each rail-level toggle bumps
  // an `epoch`, which we depend on to reset internal state to `forced`. Local
  // toggles still work afterward — the next batch toggle re-resets everyone.
  const batch = React.useContext(SecBatchContext);
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
  React.useEffect(()=>{
    if (batch) setInternalOpen(batch.forced);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [batch && batch.epoch]);
  const isControlled = openProp !== undefined && typeof onOpenChange === 'function';
  const open  = isControlled ? openProp : internalOpen;
  const setOpen = isControlled
    ? (v => onOpenChange(typeof v === 'function' ? v(openProp) : v))
    : setInternalOpen;
  const headerStyle = {
    display:'flex',alignItems:'center',gap:8,padding:'6px 12px',
    background:C.s2,
    borderBottom: open ? `1px solid ${C.border}` : 'none',
    cursor: collapsible ? 'pointer' : 'default',
  };
  const titleSpan = (
    <>
      {collapsible && (
        <span style={{ fontFamily:'Roboto Mono',fontSize:8,color:C.fg3,width:8,textAlign:'center',flexShrink:0 }}>
          {open ? '▾' : '▸'}
        </span>
      )}
      <span data-typo="label" style={{ fontFamily:'Roboto Mono',fontSize:8.5,fontWeight:500,color:C.fg2,letterSpacing:'0.11em',textTransform:'uppercase',flex:1 }}>{title}</span>
      {badge}
    </>
  );
  // Outer container behaviour: default sections shrink to content (legacy
  // behaviour). `grow` flips it to flex:1 + column-flex so the body can
  // host another flex child that fills the remaining height.
  const outer = grow
    ? { borderBottom:`1px solid ${C.border}`, flex:1, display:'flex', flexDirection:'column', minHeight:0 }
    : { borderBottom:`1px solid ${C.border}`, flexShrink:0 };
  const bodyStyle = grow
    ? { padding: flush ? 0 : compact ? '7px 10px' : '9px 11px', flex:1, display:'flex', flexDirection:'column', minHeight:0 }
    : { padding: flush ? 0 : compact ? '7px 10px' : '9px 11px' };
  return (
    <div style={outer}>
      {collapsible ? (
        <button onClick={()=>setOpen(v=>!v)} style={{ ...headerStyle, width:'100%', border:'none', textAlign:'left' }}>
          {titleSpan}
        </button>
      ) : (
        <div style={headerStyle}>{titleSpan}</div>
      )}
      {open && (
        <div style={bodyStyle}>{children}</div>
      )}
    </div>
  );
}

function Pill({ level, label }) {
  const m = { critical:{bg:C.criticalM,c:C.critical}, advisory:{bg:C.advisoryM,c:C.advisory}, info:{bg:C.accentM,c:C.accent}, normal:{bg:C.normalM,c:C.normal} };
  const s = m[level]||m.info;
  const tipMap = {
    critical: 'CRITICAL — Immediate action required. Missing the deadline triggers a cascade.',
    advisory: 'ADVISORY — Time-sensitive but not yet critical.',
    info: 'INFO — Informational priority.',
    normal: 'NORMAL — Routine priority.',
  };
  return <span title={tipMap[level]} data-typo="label" style={{ background:s.bg,color:s.c,fontFamily:'Roboto Mono',fontSize:7.5,fontWeight:500,letterSpacing:'0.09em',padding:'2px 5px',borderRadius:2 }}>{label||level.toUpperCase()}</span>;
}

function Tag({ label, color }) {
  return <span data-typo="label" style={{ fontFamily:'Roboto Mono',fontSize:7,color:color||C.fg3,background:color?`${color}18`:C.s3,padding:'1px 4px',borderRadius:2,flexShrink:0 }}>{label}</span>;
}

// ── DT Button shim ────────────────────────────────────────────
// Mirrors @accelint/design-toolkit Button (filled variant) using DT dark-theme
// color tokens + typography scale. Prototype can't import the real component
// (no bundler / Tailwind / design-foundation), so we replicate the look.
const DT_BUTTON_COLORS = {
  // [bg, bgHover, bgPressed, fg]  — from design-foundation dark theme
  accent:   ['#004f7e', '#0068a1', '#002c52', '#ffffff'],
  serious:  ['#bb7a00', '#e69600', '#634101', '#ffffff'],
  critical: ['#ff2e27', '#d40b04', '#a10701', '#ffffff'], // critical-400/500/600 · vivid red
  'mono-bold':  ['#ffffff', '#eff1f2', '#c3c5c7', '#212223'],
  'mono-muted': ['#38393a', '#414245', '#000000', '#ffffff'],
};
// DT disabled: solid grey fill, muted text, no hover/press state change
const DT_BUTTON_DISABLED = ['#414245', '#414245', '#414245', '#565759']; // neutral-600 bg · neutral-500 fg
const DT_BUTTON_SIZES = {
  large:  { fontSize:16, lineHeight:'24px', letterSpacing:'0.5px',  padding:'8px 16px', minWidth:96, gap:4 },
  medium: { fontSize:14, lineHeight:'20px', letterSpacing:'0.25px', padding:'4px 12px', minWidth:68, gap:4 },
  small:  { fontSize:12, lineHeight:'16px', letterSpacing:'0.4px',  padding:'4px 8px',  minWidth:54, gap:2 },
  xsmall: { fontSize:10, lineHeight:'14px', letterSpacing:'0.4px',  padding:'4px',      minWidth:41, gap:2 },
};
function DTButton({ color='accent', variant='filled', size='medium', fullWidth=false, disabled=false, style, onMouseEnter, onMouseLeave, onMouseDown, onMouseUp, children, ...rest }) {
  const [bg, bgHover, bgPressed, fg] = disabled ? DT_BUTTON_DISABLED : (DT_BUTTON_COLORS[color] || DT_BUTTON_COLORS.accent);
  const sz = DT_BUTTON_SIZES[size] || DT_BUTTON_SIZES.medium;
  const [state, setState] = React.useState('idle'); // idle | hover | pressed
  const stateColor = state === 'pressed' ? bgPressed : state === 'hover' ? bgHover : bg;
  // When disabled, always render as a solid grey filled button regardless of variant prop
  const isOutline = variant === 'outline' && !disabled;
  const currentBg     = isOutline && state === 'idle' ? 'transparent' : stateColor;
  const currentFg     = isOutline && state === 'idle' ? '#eff1f2' : (isOutline ? '#ffffff' : fg);
  const currentBorder = `1px solid ${isOutline ? stateColor : 'transparent'}`;
  return (
    <button
      {...rest}
      disabled={disabled}
      onMouseEnter={e => { if (!disabled) setState('hover'); onMouseEnter?.(e); }}
      onMouseLeave={e => { if (!disabled) setState('idle'); onMouseLeave?.(e); }}
      onMouseDown={e => { if (!disabled) setState('pressed'); onMouseDown?.(e); }}
      onMouseUp={e => { if (!disabled) setState('hover'); onMouseUp?.(e); }}
      style={{
        display:'inline-flex', alignItems:'center', justifyContent:'center',
        gap: sz.gap, padding: sz.padding, minWidth: fullWidth ? 0 : sz.minWidth,
        width: fullWidth ? '100%' : 'auto',
        background: currentBg, color: currentFg,
        border: currentBorder, borderRadius: 4,
        fontFamily: 'Roboto', fontWeight: 600,
        fontSize: sz.fontSize, lineHeight: sz.lineHeight, letterSpacing: sz.letterSpacing,
        cursor: disabled ? 'not-allowed' : 'pointer',
        transition: 'background 0.15s, color 0.15s, border-color 0.15s',
        ...style,
      }}
    >
      {children}
    </button>
  );
}

// ═══════════════════════════════════════════════════════════════
// NORMAL OPS PANELS
// ═══════════════════════════════════════════════════════════════

// ── Active Missions ───────────────────────────────────────────
// Basemap picker — sits above the map-layers toggles in Home/COP mode's
// outer rail. Three options: Carto Dark (default), Carto Light, Satellite.
// CSAR mode ignores this control (always uses satellite). Implementation
// is a styled <select> following the same chrome the upload modal's
// type/AOR pickers use.
function BasemapPicker({ value, onChange, dimming, onDimmingChange }) {
  const options = [
    { id:'carto-dark',  label:'Carto Dark',  desc:'dark cartography, no labels' },
    { id:'carto-light', label:'Carto Light', desc:'light cartography, no labels' },
    { id:'satellite',  label:'Satellite',   desc:'Esri World Imagery' },
  ];
  // Brightness slider visible only when the satellite basemap is
  // active. Slider value is the *brightness* (0..1, higher = brighter)
  // for operator-friendly framing; we invert it to a dim value for
  // the overlay opacity (1 - brightness). Default 50% brightness =
  // 0.5 dim opacity, which softens the raw Esri imagery enough to
  // read tracks against without losing the underlying features.
  const dimValue = (typeof dimming === 'number') ? dimming : 0.5;
  const brightness = Math.round((1 - dimValue) * 100);
  return (
    <Sec title="Basemap" flush>
      <div style={{ padding:'10px 12px' }}>
        <select
          value={value}
          onChange={(e) => onChange(e.target.value)}
          title="Basemap — Underlying tile source. Carto Dark/Light are vector cartography; Satellite is Esri raster imagery."
          style={{
            width:'100%', padding:'7px 10px', borderRadius:3,
            background:C.s3, border:`1px solid ${C.border2}`,
            color:C.fg1, fontFamily:'Roboto', fontSize:11,
            cursor:'pointer', outline:'none',
          }}>
          {options.map(o => (
            <option key={o.id} value={o.id}>{o.label} — {o.desc}</option>
          ))}
        </select>
        {value === 'satellite' && onDimmingChange && (
          <div style={{ marginTop:10 }}>
            <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:4 }}>
              <span data-typo="label" style={{ fontFamily:'Roboto Mono', fontSize:8, color:C.fg3, letterSpacing:'0.10em' }}>
                BRIGHTNESS
              </span>
              <span style={{ fontFamily:'Roboto Mono', fontSize:9, color:C.fg2, letterSpacing:'0.04em' }}>
                {brightness}%
              </span>
            </div>
            <input
              type="range"
              min={0} max={100} step={1}
              value={brightness}
              onChange={(e) => onDimmingChange(1 - (Number(e.target.value) / 100))}
              title="BRIGHTNESS — Satellite imagery brightness. Lower values darken the basemap so colored tracks and overlays read more clearly."
              style={{
                width:'100%',
                accentColor: C.accent,
                cursor:'pointer',
              }}
            />
          </div>
        )}
      </div>
    </Sec>
  );
}

// Map layer visibility panel — lives in the outer right rail in
// Home/COP mode. Six toggles, all default ON. Each toggle flips a
// flag on App's `mapLayers` state which the map's render-time logic
// reads to show/hide the corresponding layer group.
// Small segmented control used as a sub-option inside a Map Layers row
// (e.g. hostile-track styling: Generic vs By type). Two-or-more pill
// switch styled to match the existing FLAT/GLOBE projection toggle.
function MapLayerSegmented({ value, options, onChange, disabled }) {
  return (
    <div style={{
      display:'inline-flex', background:C.s3,
      border:`1px solid ${C.border2}`, borderRadius:3,
      overflow:'hidden', opacity: disabled ? 0.4 : 1,
      pointerEvents: disabled ? 'none' : 'auto',
    }}>
      {options.map(o => {
        const active = o.value === value;
        return (
          <button
            key={o.value}
            onClick={(e) => { e.stopPropagation(); onChange(o.value); }}
            title={o.desc || o.label}
            style={{
              padding:'3px 10px', borderRadius:2,
              background: active ? C.accent : 'transparent',
              color: active ? '#000' : C.fg2,
              border:'none', cursor: active ? 'default' : 'pointer',
              fontFamily:'Roboto Mono', fontSize:8.5, fontWeight:600,
              letterSpacing:'0.10em',
            }}>
            {o.label}
          </button>
        );
      })}
    </div>
  );
}

// Shared toggle row — button row with the pill-toggle + label + desc.
// Used by both the Global section and the per-AOR sections so the
// affordance reads the same way everywhere in the panel.
function MapLayerToggleRow({ on, onClick, label, desc, sub, indent=0 }) {
  return (
    <div>
      <button
        onClick={onClick}
        title={`${label}${desc ? ' — ' + desc : ''}\n${on ? 'Visible on the map. Click to hide.' : 'Hidden. Click to show on the map.'}`}
        style={{
          display:'flex', alignItems:'center', gap:10,
          width:'100%', padding:`9px 12px 9px ${12 + indent}px`,
          background:'transparent', border:'none',
          cursor:'pointer', textAlign:'left',
          transition:'background 0.12s',
        }}
        onMouseEnter={(e)=>{ e.currentTarget.style.background = C.s3; }}
        onMouseLeave={(e)=>{ e.currentTarget.style.background = 'transparent'; }}>
        <div style={{
          width:24, height:14, borderRadius:7, flexShrink:0,
          background: on ? C.accent : 'transparent',
          border:`1px solid ${on ? C.accent : C.border2}`,
          position:'relative',
          transition:'all 0.12s',
        }}>
          <div style={{
            position:'absolute', top:1, left: on ? 11 : 1, width:10, height:10,
            borderRadius:'50%', background: on ? '#fff' : C.fg3,
            transition:'left 0.12s, background 0.12s',
          }}/>
        </div>
        <div style={{ flex:1, minWidth:0 }}>
          <div style={{ fontFamily:'Roboto', fontSize:11, fontWeight:500,
            color: on ? C.fg1 : C.fg3, letterSpacing:'0.02em' }}>
            {label}
          </div>
          {desc && (
            <div style={{ fontFamily:'Roboto Mono', fontSize:8, color:C.fg3,
              letterSpacing:'0.06em', marginTop:2 }}>
              {desc}
            </div>
          )}
        </div>
      </button>
      {sub && on && (
        <div style={{ padding:`2px 12px 8px ${46 + indent}px` }}>
          {sub}
        </div>
      )}
    </div>
  );
}

// Section header used inside Map Layers. Group title + optional summary
// chip on the right, optional collapse chevron, optional "all on/off"
// quick-action. The Global section renders without collapse; per-AOR
// sections render with collapse + bulk-toggle.
function MapLayerSectionHeader({ title, sublabel, summary, expandable, expanded, onToggle, onBulkOn, onBulkOff, accent }) {
  const accentColor = accent || C.fg2;
  return (
    <div style={{
      display:'flex', alignItems:'center', gap:8, padding:'8px 12px',
      background:C.s2, borderTop:`1px solid ${C.border}`, borderBottom:`1px solid ${C.border}`,
      cursor: expandable ? 'pointer' : 'default',
    }}
    onClick={expandable ? onToggle : undefined}>
      {/* Accent rule on the left so each AOR section reads as its own
          colored band. Global section gets a neutral one. */}
      <div style={{ width:2, alignSelf:'stretch', background:accentColor, borderRadius:1, flexShrink:0 }}/>
      <div style={{ flex:1, minWidth:0 }}>
        <div data-typo="label" style={{ fontFamily:'Roboto Mono', fontSize:9, fontWeight:600,
          letterSpacing:'0.14em', color:accentColor,
          overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
          {title}
        </div>
        {sublabel && (
          <div style={{ fontFamily:'Roboto Mono', fontSize:8, color:C.fg4,
            letterSpacing:'0.06em', marginTop:1,
            overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
            {sublabel}
          </div>
        )}
      </div>
      {summary && (
        <div title="Total layers in this AOR" style={{ fontFamily:'Roboto Mono', fontSize:8, color:C.fg3,
          letterSpacing:'0.06em', flexShrink:0 }}>
          {summary}
        </div>
      )}
      {(onBulkOn || onBulkOff) && (
        <div style={{ display:'flex', gap:4, flexShrink:0 }} onClick={(e)=>e.stopPropagation()}>
          {onBulkOn && (
            <button data-typo="label" onClick={onBulkOn} title="ON — Toggle every layer in this group visible." style={{
              padding:'2px 6px', borderRadius:2, border:`1px solid ${C.border2}`,
              background:'transparent', color:C.fg3, cursor:'pointer',
              fontFamily:'Roboto Mono', fontSize:7.5, fontWeight:500, letterSpacing:'0.10em',
            }}>ON</button>
          )}
          {onBulkOff && (
            <button data-typo="label" onClick={onBulkOff} title="OFF — Hide every layer in this group." style={{
              padding:'2px 6px', borderRadius:2, border:`1px solid ${C.border2}`,
              background:'transparent', color:C.fg3, cursor:'pointer',
              fontFamily:'Roboto Mono', fontSize:7.5, fontWeight:500, letterSpacing:'0.10em',
            }}>OFF</button>
          )}
        </div>
      )}
      {expandable && (
        <span style={{ color:C.fg3, fontSize:9, transition:'transform 0.18s', flexShrink:0,
          transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}>▾</span>
      )}
    </div>
  );
}

function MapLayersPanel({ layers, onToggle, onSetValue, onToggleAor, onSetAorBulk }) {
  // Per-AOR sections are independently collapsible so the operator can
  // focus on one AOR's toggles without scrolling past five other walls
  // of toggles. INDOPACOM is expanded on cold load (it's the default
  // primary AOR); others collapse so the panel reads tight by default.
  // Persistent across mode switches via React state lifted to the panel
  // (panel itself is unmounted when the rail collapses, so state resets
  // only with the rail — fine for the prototype).
  const aors = window.AORS || [];
  const _initialExpanded = {};
  aors.forEach(a => { _initialExpanded[a.id] = a.isPrimary === true; });
  const [aorExpanded, setAorExpanded] = React.useState(_initialExpanded);
  const toggleAorSection = (id) => {
    setAorExpanded(s => ({ ...s, [id]: !s[id] }));
  };

  // Color the section accent rule per AOR. INDOPACOM cyan (accent2),
  // CENTCOM amber (advisory) since that's the active engagement zone,
  // EUCOM accent (blue) for the wide-NATO frame, AFRICOM critical (red)
  // matching the Niger/Sahel hostile cluster, NORTHCOM normal (green),
  // SOUTHCOM fg2 (neutral) since we don't have content there. Cosmetic
  // — operator can fast-scan which section they're looking at.
  const aorAccentByAor = {
    indopacom: C.accent2,
    centcom:   C.advisory,
    eucom:     C.accent,
    africom:   C.critical,
    northcom:  C.normal,
    southcom:  C.fg2,
  };

  // ── Global rows ────────────────────────────────────────────
  // Things that don't scope to a single AOR. Grids, AOR boundaries,
  // trans-AOR mission lines, geopolitical washes, commercial air
  // (planet-wide ADS-B), and the CONUS-only US-restricted-airspace
  // toggle which is technically NORTHCOM-scoped but reads more
  // naturally beside the other geo overlays.
  const globalRows = [
    { key:'aorBoundaries',        label:'AOR boundaries',        desc:'COCOM boundary lines' },
    { key:'transAorRoutes',       label:'Trans-AOR routes',      desc:'planned mission polylines' },
    { key:'commercialAir',        label:'Commercial air',        desc:'live ADS-B (OpenSky) · global' },
    { key:'callsignLabels',       label:'Callsign labels',       desc:'small label near each visible track' },
    { key:'mgrsGrid',             label:'MGRS grid',             desc:'GZD bands · 100km/10km/1km cells' },
    { key:'garsGrid',             label:'GARS grid',             desc:'30min cells · finer at higher zoom' },
    { key:'russianAirspace',      label:'Russian airspace',      desc:'red wash · RU + BY + Crimea' },
    { key:'chineseAirspace',      label:'Chinese airspace',      desc:'amber wash · PRC mainland + Hainan' },
    { key:'natoAirspace',         label:'NATO airspace',         desc:'cyan wash · NATO + UA-aligned' },
    { key:'restrictedAirspaceUS', label:'US restricted airspace',desc:'FAA SUA · R/MOA/Warning/Alert (CONUS)' },
  ];

  return (
    <Sec title="Map Layers" flush>
      <div style={{ padding:'4px 0 0' }}>
        {/* ── GLOBAL section ────────────────────────────────── */}
        <MapLayerSectionHeader title="GLOBAL" sublabel="layers that span every AOR" />
        <div style={{ padding:'2px 0' }}>
          {globalRows.map(r => (
            <MapLayerToggleRow key={r.key}
              on={!!layers[r.key]}
              onClick={() => onToggle(r.key)}
              label={r.label}
              desc={r.desc}
            />
          ))}
        </div>

        {/* ── PER-AOR sections ──────────────────────────────── */}
        {aors.map(a => {
          const expanded = !!aorExpanded[a.id];
          const layerState = (layers.perAor && layers.perAor[a.id]) || {};
          const counts = (window.getAorLayerCounts && window.getAorLayerCounts(a.id)) || { friendly:0, hostile:0, threats:0, acms:0 };
          const summary = `${counts.friendly}f · ${counts.hostile}h · ${counts.threats}t · ${counts.acms}a`;
          const accent = aorAccentByAor[a.id] || C.fg2;
          // Per-AOR rows. `friendly`, `hostile`, `threats`, `acms` —
          // four standard tactical layer toggles that scope to the
          // tracks/threats/ACMs tagged with this AOR. The hostile-track
          // style picker (generic vs by-type glyph) ride under the
          // hostile row when on; it's a global rendering choice but
          // only meaningful when at least one AOR's hostile layer is
          // visible, so we put the sub-control here for proximity.
          const aorRows = [
            { key:'friendly', label:'Friendly tracks', desc:`${counts.friendly} blue + ATO platforms` },
            { key:'hostile',  label:'Hostile land tracks', desc:`${counts.hostile} ground / naval picture`,
              sub: (
                <MapLayerSegmented
                  value={layers.hostileTrackStyle || 'generic'}
                  options={[
                    { value:'generic', label:'TARGETS' },
                    { value:'byType',  label:'BY TYPE' },
                  ]}
                  onChange={(v) => onSetValue('hostileTrackStyle', v)}
                  disabled={!layerState.hostile}
                />
              ),
            },
            { key:'threats', label:'Threat rings', desc:`${counts.threats} SAM / IADS engagement zones` },
            { key:'acms',    label:'ACMs',         desc:`${counts.acms} ROZ / AAR / corridor / killbox` },
            { key:'bases',   label:'Blue bases',   desc:`${counts.bases || 0} US / coalition installations` },
          ];
          return (
            <div key={a.id}>
              <MapLayerSectionHeader
                title={a.short}
                sublabel={a.label + (a.isPrimary ? ' · PRIMARY' : '')}
                summary={summary}
                expandable
                expanded={expanded}
                onToggle={() => toggleAorSection(a.id)}
                onBulkOn={() => onSetAorBulk(a.id, true)}
                onBulkOff={() => onSetAorBulk(a.id, false)}
                accent={accent}
              />
              {expanded && (
                <div style={{ padding:'2px 0' }}>
                  {aorRows.map(r => (
                    <MapLayerToggleRow key={r.key}
                      on={!!layerState[r.key]}
                      onClick={() => onToggleAor(a.id, r.key)}
                      label={r.label}
                      desc={r.desc}
                      sub={r.sub}
                      indent={6}
                    />
                  ))}
                </div>
              )}
            </div>
          );
        })}
      </div>
    </Sec>
  );
}

// ── COP Watchlist (Mission Overview rail) ────────────────────
// Replaces the old static ActiveMissions + BlueForceOverview pair.
// Surfaces operator-relevant events for the active AOR(s) — pop-up
// SAMs, envelope breaches, ID changes, ACM conflicts — so the rail
// reads as "what changed I should action" instead of "list of every
// platform on screen". The list of every platform lives on the map
// already (every dot is a track) and in Mission Admin (full table).
//
// Click any event row → flies the camera to its `flyTo` coords.
// Click "Resolve" → moves the row into the resolved bucket (App-state
// move, persists for the session, resets on reset).
//
// Severity drives the left-rail accent + dot color:
//   critical → red    (SAM activation, envelope breach, pop-up)
//   advisory → amber  (ID change, ACM conflict)
//   info     → fg3    (low-priority logging)
function WatchEventRow({ event, onFly, onResolve }) {
  const severityColor = event.severity === 'critical' ? C.critical
                      : event.severity === 'advisory' ? C.advisory
                      : C.fg3;
  return (
    <div style={{
      display:'flex', alignItems:'stretch', gap:0,
      borderBottom:`1px solid ${C.border}`,
      transition:'background 0.12s',
    }}
    onMouseEnter={(e)=>{ e.currentTarget.style.background = C.s2; }}
    onMouseLeave={(e)=>{ e.currentTarget.style.background = 'transparent'; }}>
      {/* Left rail accent — color-coded by severity. Stretches the
          full row height so it reads as a status band. */}
      <div style={{ width:3, background:severityColor, flexShrink:0 }}/>
      {/* Click area = the whole row body except the resolve button */}
      <button onClick={() => onFly && event.flyTo && onFly(event.flyTo)}
        style={{
          flex:1, minWidth:0, padding:'8px 11px',
          background:'transparent', border:'none', cursor: event.flyTo ? 'pointer' : 'default',
          textAlign:'left',
        }}>
        {/* Title row: severity dot + caps title + time chip */}
        <div style={{ display:'flex', alignItems:'center', gap:7, marginBottom:3 }}>
          <span style={{
            width:6, height:6, borderRadius:'50%', background:severityColor,
            flexShrink:0,
            animation: event.severity === 'critical' ? 'pulseFade 1.4s infinite' : 'none',
          }}/>
          <span style={{ flex:1, minWidth:0,
            fontFamily:'Roboto Mono', fontSize:9, fontWeight:600,
            color:severityColor, letterSpacing:'0.10em',
            overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
            {event.title}
          </span>
          <span style={{ fontFamily:'Roboto Mono', fontSize:8.5, color:C.fg3,
            letterSpacing:'0.04em', flexShrink:0 }}>
            {window.formatWatchTimeSince ? window.formatWatchTimeSince(event.secondsAgo) : ''}
          </span>
        </div>
        {/* Detail lines — track / threat names + the operator-relevant
            one-liner. Truncates to single lines so the row stays scan-
            able; full string available on hover via title attribute. */}
        {event.detail1 && (
          <div title={event.detail1} style={{
            fontFamily:'Roboto', fontSize:11, color:C.fg1, marginLeft:13,
            overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
            {event.detail1}
          </div>
        )}
        {event.detail2 && (
          <div title={event.detail2} style={{
            fontFamily:'Roboto Mono', fontSize:9, color:C.fg3, marginTop:1, marginLeft:13,
            letterSpacing:'0.04em',
            overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
            {event.detail2}
          </div>
        )}
      </button>
      {/* Resolve button — small mono caps "OK" affordance on the right.
          Stops propagation so it doesn't fire the row's flyTo. */}
      {onResolve && (
        <button onClick={(e) => { e.stopPropagation(); onResolve(event.id); }}
          title="Resolve · move to handled list"
          style={{
            width:30, padding:0, flexShrink:0,
            background:'transparent', border:'none', borderLeft:`1px solid ${C.border}`,
            color:C.fg3, cursor:'pointer',
            fontFamily:'Roboto Mono', fontSize:9, fontWeight:500, letterSpacing:'0.10em',
          }}
          onMouseEnter={(e)=>{ e.currentTarget.style.background = C.s3; e.currentTarget.style.color = C.fg1; }}
          onMouseLeave={(e)=>{ e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = C.fg3; }}>
          OK
        </button>
      )}
    </div>
  );
}

function WatchlistPanel({ activeAors, events, resolved, onFly, onResolve }) {
  // Filter active events to AORs the operator currently has selected.
  // If activeAors is empty/missing, fall back to "all" so the panel
  // doesn't go blank in non-Mission-Admin entry paths.
  const aorSet = (activeAors && activeAors.size) ? activeAors : null;
  const visibleEvents = (events || []).filter(e => !aorSet || !e.aor || aorSet.has(e.aor));
  const visibleResolved = resolved || [];

  // Subhead text: which AORs this rail is reading
  const aors = (window.AORS || []);
  const aorShorts = aorSet
    ? aors.filter(a => aorSet.has(a.id)).map(a => a.short).join(', ')
    : 'all AORs';

  const [resolvedExpanded, setResolvedExpanded] = React.useState(false);

  return (
    <Sec title="Active Watch" flush
      badge={
        <span style={{
          fontFamily:'Roboto Mono', fontSize:9, fontWeight:600,
          padding:'1px 6px', borderRadius:10,
          background:C.s3, color:C.fg2,
          letterSpacing:'0.04em',
        }}>{visibleEvents.length}</span>
      }>
      {/* Subhead — which AORs the watch covers. Reads in the same row
          as the count badge above (which lives on the Sec header). */}
      <div style={{ padding:'2px 12px 8px',
        fontFamily:'Roboto Mono', fontSize:8, color:C.fg3, letterSpacing:'0.10em',
        borderBottom:`1px solid ${C.border}` }}>
        SCOPE · {aorShorts.toUpperCase()}
      </div>
      {/* Active events */}
      {visibleEvents.length === 0 ? (
        <div style={{ padding:'18px 12px', textAlign:'center',
          fontFamily:'Roboto Mono', fontSize:9, color:C.fg3, letterSpacing:'0.10em' }}>
          NO ACTIVE EVENTS
        </div>
      ) : (
        visibleEvents.map(e => (
          <WatchEventRow key={e.id} event={e} onFly={onFly} onResolve={onResolve}/>
        ))
      )}
      {/* Resolved fold — collapsed by default. Click header to expand. */}
      {visibleResolved.length > 0 && (
        <div>
          <button onClick={() => setResolvedExpanded(v => !v)} style={{
            display:'flex', alignItems:'center', gap:8, width:'100%',
            padding:'8px 12px',
            background:C.s2, border:'none', borderTop:`1px solid ${C.border}`,
            cursor:'pointer', textAlign:'left',
          }}>
            <span style={{ fontFamily:'Roboto Mono', fontSize:8, color:C.fg3,
              letterSpacing:'0.10em', flex:1 }}>
              RESOLVED
            </span>
            <span style={{ fontFamily:'Roboto Mono', fontSize:9, color:C.fg3,
              letterSpacing:'0.04em' }}>{visibleResolved.length}</span>
            <span style={{ color:C.fg3, fontSize:9, transition:'transform 0.18s',
              transform: resolvedExpanded ? 'rotate(180deg)' : 'rotate(0deg)' }}>▾</span>
          </button>
          {resolvedExpanded && (
            <div>
              {visibleResolved.map(r => (
                <div key={r.id} style={{
                  display:'flex', alignItems:'center', gap:7,
                  padding:'6px 12px 6px 15px',
                  borderBottom:`1px solid ${C.border}`,
                  fontFamily:'Roboto Mono', fontSize:9, color:C.fg3,
                  letterSpacing:'0.04em',
                }}>
                  <span style={{ color:C.fg4 }}>▸</span>
                  <span style={{ color:C.fg2, fontWeight:500 }}>{r.title}</span>
                  <span style={{ color:C.fg4 }}>·</span>
                  <span style={{ flex:1, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
                    {r.detail}
                  </span>
                </div>
              ))}
            </div>
          )}
        </div>
      )}
    </Sec>
  );
}

// ── Blue Force Overview ───────────────────────────────────────
function BlueForceOverview() {
  return (
    <Sec title="Blue Force Overview" flush>
      {window.ALL_TRACKS.map(t => (
        <div key={t.id} style={{ display:'flex',alignItems:'center',gap:8,padding:'5px 11px',borderBottom:`1px solid ${C.border}` }}>
          <div style={{ width:5,height:5,borderRadius:'50%',background:C.normal,flexShrink:0 }} />
          <span style={{ fontFamily:'Roboto Mono',fontWeight:500,fontSize:9.5,color:C.fg1,width:68,flexShrink:0 }}>{t.cs}</span>
          <span style={{ fontFamily:'Roboto Mono',fontSize:8,color:C.fg3,flex:1 }}>{t.type} · {t.role}</span>
          {t.fuel != null && (
            <div style={{ display:'flex',alignItems:'center',gap:4 }}>
              <div style={{ width:36,height:3,background:C.border,borderRadius:2,overflow:'hidden' }}>
                <div style={{ width:`${t.fuel}%`,height:'100%',background:t.fuel<40?C.critical:t.fuel<60?C.advisory:C.normal,borderRadius:2 }} />
              </div>
              <span style={{ fontFamily:'Roboto Mono',fontSize:8,color:C.fg2 }}>{t.fuel}%</span>
            </div>
          )}
        </div>
      ))}
    </Sec>
  );
}

// ── AOR Posture Rollup ────────────────────────────────────────
// Per-AOR snapshot of the operational picture, surfaced under the
// watchlist on the COP. Reads directly from existing fixtures so the
// numbers always match the map: friendlies = ATO_TASKUNITS by mission
// type, threats = MULTI_AOR_THREATS by range tier, ACMs = ACO_ACMS by
// type. Each AOR section is a collapsible block — primary expanded by
// default, adjacents collapsed so the rail reads tight.
//
// Mission-type bucket policy: the ATO carries hundreds of mission
// codes (CAP, STRIKE, SEAD, ISR, TANKER, CAS, INTERDICT, etc.) but
// the operator only needs ~5 buckets at this level. We collapse the
// long tail into a single OTHER bucket. SEAD covers SEAD/DEAD; ISR
// covers AEW/AWACS/ASW/ELINT/SIGINT/RECCO too.
const _missionBucketFor = (mission) => {
  const m = String(mission || '').toUpperCase();
  if (m === 'CAP' || m === 'OCA' || m === 'DCA') return 'CAP';
  if (m.includes('STRIKE') || m === 'INTERDICT' || m === 'CAS') return 'STRIKE';
  if (m.includes('SEAD') || m.includes('DEAD')) return 'SEAD';
  if (m === 'TANKER' || m === 'AAR') return 'TANKER';
  if (m === 'ISR' || m === 'AEW' || m === 'AWACS' || m.includes('SIGINT')
      || m.includes('ELINT') || m.includes('ASW') || m === 'RECCO') return 'ISR';
  return 'OTHER';
};

// ACM buckets follow the type field on ACO_ACMS — just the four
// canonical buckets ATO/ACO authors actually publish.
const _acmBucketFor = (type) => {
  const t = String(type || '').toUpperCase();
  if (t === 'ROZ')        return 'ROZ';
  if (t === 'AAR')        return 'AAR';
  if (t === 'CAP')        return 'CAP';
  if (t === 'KILLBOX' || t === 'KB' || t === 'JEZ') return 'KB';
  return 'OTHER';
};

function AorPostureBlock({ aor, expanded, onToggle }) {
  // Live aggregations — recomputed on each render. Cheap (a few hundred
  // rows total across all fixtures); panel only renders when the rail
  // is expanded.
  const tracks = (window.ALL_TRACKS || []).filter(t => t.aor === aor.id);
  const threats = (window.MULTI_AOR_THREATS || []).filter(r => r.aor === aor.id);
  const acms   = (window.ACO_ACMS || []).filter(a => a.aor === aor.id);
  const hostiles = (window.HOSTILE_TRACKS || []).filter(h => h.aor === aor.id);

  const friendlyByBucket = { CAP:0, STRIKE:0, SEAD:0, ISR:0, TANKER:0, OTHER:0 };
  tracks.forEach(t => { friendlyByBucket[_missionBucketFor(t.mission || t.role)]++; });

  const longThreats  = threats.filter(r => r.range === 'long').length;
  const pointThreats = threats.filter(r => r.range !== 'long').length;

  const acmByBucket = { ROZ:0, AAR:0, CAP:0, KB:0, OTHER:0 };
  acms.forEach(a => { acmByBucket[_acmBucketFor(a.type)]++; });

  // Tone — primary AOR gets the accent rule, adjacents fade. Doesn't
  // duplicate the Map Layers panel's per-AOR accent palette, this is
  // a more muted state-board treatment.
  const isPrimary = aor.isPrimary === true;
  const accent = isPrimary ? C.accent : C.fg3;

  const StatLine = ({ label, count, breakdown }) => (
    <div style={{ display:'flex', alignItems:'baseline', gap:8, padding:'7px 12px 4px' }}>
      <span style={{ flex:1,
        fontFamily:'Roboto Mono', fontSize:8.5, color:C.fg3, letterSpacing:'0.10em' }}>
        {label}
      </span>
      <span style={{ fontFamily:'Roboto', fontSize:13, fontWeight:600, color:C.fg1 }}>
        {count}
      </span>
      {breakdown && (
        <span style={{ fontFamily:'Roboto Mono', fontSize:7.5, color:C.fg4,
          letterSpacing:'0.08em' }}>
          {breakdown}
        </span>
      )}
    </div>
  );
  // Detail row (mono caps, dim) for sub-bucket breakdowns. Ride below
  // the StatLine inside the same section so the headline numbers stay
  // scannable.
  const DetailRow = ({ children }) => (
    <div style={{ padding:'0 12px 8px',
      fontFamily:'Roboto Mono', fontSize:8.5, color:C.fg3, letterSpacing:'0.06em',
      display:'flex', flexWrap:'wrap', gap:'2px 12px' }}>
      {children}
    </div>
  );
  const Pip = ({ color, label, count }) => (
    <span style={{ display:'inline-flex', alignItems:'center', gap:4, whiteSpace:'nowrap' }}>
      <span style={{ width:5, height:5, borderRadius:'50%', background:color, flexShrink:0 }}/>
      <span>{count} {label}</span>
    </span>
  );

  return (
    <div style={{ borderBottom:`1px solid ${C.border}` }}>
      {/* Section header — collapsible. AOR short label + primary mark
          + total-tracks summary chip on the right. */}
      <button onClick={onToggle} style={{
        display:'flex', alignItems:'center', gap:8, width:'100%', padding:'8px 12px',
        background:expanded ? C.s2 : 'transparent', border:'none',
        borderTop:`1px solid ${C.border}`, cursor:'pointer', textAlign:'left',
        transition:'background 0.12s',
      }}>
        <div style={{ width:2, alignSelf:'stretch', background:accent, borderRadius:1, flexShrink:0 }}/>
        <span style={{ fontFamily:'Roboto Mono', fontSize:9, fontWeight:600,
          color:accent, letterSpacing:'0.10em', flex:1 }}>
          {aor.short}
          {isPrimary && (
            <span style={{ marginLeft:8, fontFamily:'Roboto Mono', fontSize:7.5,
              color:C.accent, letterSpacing:'0.10em' }}>PRIMARY</span>
          )}
        </span>
        <span style={{ fontFamily:'Roboto Mono', fontSize:8.5, color:C.fg3, letterSpacing:'0.06em' }}>
          {tracks.length}f · {hostiles.length}h · {threats.length}t · {acms.length}a
        </span>
        <span style={{ color:C.fg3, fontSize:9, transition:'transform 0.18s', flexShrink:0,
          transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}>▾</span>
      </button>
      {expanded && (
        <div style={{ paddingBottom:6 }}>
          {/* FRIENDLIES */}
          <StatLine label="FRIENDLIES" count={tracks.length} breakdown="ATO PLATFORMS"/>
          <DetailRow>
            <Pip color={C.accent}    label="CAP"    count={friendlyByBucket.CAP}/>
            <Pip color={C.advisory}  label="STRIKE" count={friendlyByBucket.STRIKE}/>
            <Pip color={C.critical}  label="SEAD"   count={friendlyByBucket.SEAD}/>
            <Pip color={C.normal}    label="ISR"    count={friendlyByBucket.ISR}/>
            <Pip color={C.fg2}       label="TANKER" count={friendlyByBucket.TANKER}/>
            {friendlyByBucket.OTHER > 0 && <Pip color={C.fg4} label="OTHER" count={friendlyByBucket.OTHER}/>}
          </DetailRow>

          {/* THREAT PICTURE */}
          <StatLine label="THREAT PICTURE" count={threats.length} breakdown="SAM / IADS RINGS"/>
          <DetailRow>
            <span>{longThreats} long</span>
            <span style={{ color:C.fg4 }}>·</span>
            <span>{pointThreats} point</span>
            {hostiles.length > 0 && (
              <>
                <span style={{ color:C.fg4 }}>·</span>
                <span>{hostiles.length} hostile tracks</span>
              </>
            )}
          </DetailRow>

          {/* AIRSPACE / ACMs */}
          <StatLine label="AIRSPACE" count={acms.length} breakdown="ACMS"/>
          <DetailRow>
            <span>{acmByBucket.ROZ} ROZ</span>
            <span style={{ color:C.fg4 }}>·</span>
            <span>{acmByBucket.AAR} AAR</span>
            <span style={{ color:C.fg4 }}>·</span>
            <span>{acmByBucket.CAP} CAP</span>
            <span style={{ color:C.fg4 }}>·</span>
            <span>{acmByBucket.KB} KB</span>
            {acmByBucket.OTHER > 0 && (
              <>
                <span style={{ color:C.fg4 }}>·</span>
                <span>{acmByBucket.OTHER} other</span>
              </>
            )}
          </DetailRow>
        </div>
      )}
    </div>
  );
}

function AorPostureRollup({ activeAors }) {
  const aors = (window.AORS || []);
  const aorSet = (activeAors && activeAors.size) ? activeAors : null;
  // Visible AORs follow the same scope rule as the watchlist —
  // whatever the operator has toggled active in Mission Admin.
  // No-active-set fallback shows every AOR (defensive).
  const visibleAors = aorSet ? aors.filter(a => aorSet.has(a.id)) : aors;
  // Per-AOR expand state. Primary defaults open; adjacents collapsed.
  const [expandedById, setExpandedById] = React.useState(() => {
    const init = {};
    aors.forEach(a => { init[a.id] = a.isPrimary === true; });
    return init;
  });
  const toggle = (id) => setExpandedById(s => ({ ...s, [id]: !s[id] }));
  return (
    <Sec title="AOR Posture" flush>
      {visibleAors.length === 0 ? (
        <div style={{ padding:'18px 12px', textAlign:'center',
          fontFamily:'Roboto Mono', fontSize:9, color:C.fg3, letterSpacing:'0.10em' }}>
          NO AORS ACTIVE
        </div>
      ) : (
        visibleAors.map(a => (
          <AorPostureBlock key={a.id} aor={a}
            expanded={!!expandedById[a.id]}
            onToggle={() => toggle(a.id)}
          />
        ))
      )}
    </Sec>
  );
}

// ── Normal Ops Alert Feed (light) ─────────────────────────────
function NormalAlertFeed() {
  const items = [
    { id:'n1', level:'info',     time:'1420Z', text:'CHECK 41 on station. ISR orbit established.' },
    { id:'n2', level:'info',     time:'1425Z', text:'KING 11 tanker track active. Ready for refueling.' },
    { id:'n3', level:'advisory', time:'1432Z', text:'SA-6 site ACTIVE — MANPADS threat updated NE sector.' },
    { id:'n4', level:'info',     time:'1435Z', text:'DUKE 31/32 ingressing strike corridor ALPHA.' },
  ];
  return (
    <Sec title="System Alerts" collapsible defaultOpen flush>
      {items.map(a => (
        <div key={a.id} style={{ display:'flex',gap:8,alignItems:'flex-start',padding:'6px 11px',borderBottom:`1px solid ${C.border}` }}>
          <div style={{ width:3,alignSelf:'stretch',background:a.level==='advisory'?C.advisory:C.accent,borderRadius:2,flexShrink:0 }} />
          <div style={{ flex:1 }}>
            <span style={{ fontFamily:'Roboto',fontSize:10.5,color:C.fg2,lineHeight:1.4 }}>{a.text}</span>
            <div style={{ fontFamily:'Roboto Mono',fontSize:7.5,color:C.fg3,marginTop:2 }}>{a.time}</div>
          </div>
        </div>
      ))}
    </Sec>
  );
}

// ── Normal Ops Weather (compact) ──────────────────────────────
function NormalWeather({ darkScore }) {
  const labels = ['DAYLIGHT','LOW LIGHT','OPERATIONAL DARK','DEGRADED DARK','FULL DARK'];
  const colors = ['#ffc14c','#fbab16','#39b7fa','#0068a1','#004f7e'];
  const cur = { label: labels[darkScore-1], color: colors[darkScore-1] };
  return (
    <Sec title="Environmental" compact>
      <div style={{ display:'flex',gap:10,alignItems:'center',marginBottom:8 }}>
        <div style={{ display:'flex',gap:3 }}>
          {[1,2,3,4,5].map(i=>(
            <div key={i} style={{ width:11,height:11,borderRadius:2,background:i<=darkScore?colors[i-1]+'33':C.s3,border:`1px solid ${i===darkScore?cur.color:C.border}` }} />
          ))}
        </div>
        <div>
          <div style={{ fontFamily:'Roboto Mono',fontSize:9,fontWeight:500,color:cur.color }}>{cur.label}</div>
          <div style={{ fontFamily:'Roboto Mono',fontSize:7.5,color:C.fg3,marginTop:1 }}>AMA-29 · {darkScore}/5</div>
        </div>
      </div>
      {[
        {l:'METAR',v:'RCYU 281400Z 09008KT 9999 FEW025 SCT080 26/19 Q1014'},
        {l:'VIS',  v:'10 KM'},
        {l:'WIND', v:'090° / 8 KT'},
        {l:'CEILING', v:'2,500 FT AGL'},
      ].map(r=>(
        <div key={r.l} style={{ display:'flex',gap:8,marginBottom:3 }}>
          <span style={{ fontFamily:'Roboto Mono',fontSize:8,color:C.fg3,width:62,flexShrink:0 }}>{r.l}</span>
          <span style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:C.fg2 }}>{r.v}</span>
        </div>
      ))}
    </Sec>
  );
}

// ═══════════════════════════════════════════════════════════════
// CSAR MASTER MODE PANELS
// ═══════════════════════════════════════════════════════════════

// ── Alert Feed ────────────────────────────────────────────────
function AlertFeed({ alerts }) {
  return (
    <Sec title="Alerts" collapsible defaultOpen badge={
      <span style={{ background:C.criticalM,color:C.critical,fontFamily:'Roboto Mono',fontSize:8,fontWeight:500,padding:'1px 6px',borderRadius:10 }}>{alerts.length}</span>
    } flush>
      <div style={{ maxHeight:152,overflowY:'auto' }}>
        {alerts.map(a=>(
          <div key={a.id} title={`${a.level.toUpperCase()} alert${a.time ? ' · ' + a.time : ''}`} style={{ display:'flex',gap:8,alignItems:'flex-start',padding:'7px 11px',borderBottom:`1px solid ${C.border}` }}>
            <div style={{ width:3,alignSelf:'stretch',background:a.level==='critical'?C.critical:a.level==='advisory'?C.advisory:C.accent,borderRadius:2,flexShrink:0 }} />
            <div style={{ flex:1 }}>
              <div style={{ display:'flex',gap:6,alignItems:'center',marginBottom:2 }}>
                <span style={{ fontFamily:'Roboto',fontWeight:500,fontSize:11,color:C.fg1 }}>{a.title}</span>
                <span style={{ fontFamily:'Roboto Mono',fontSize:7.5,color:C.fg3,marginLeft:'auto' }}>{a.time}</span>
              </div>
              <span style={{ fontFamily:'Roboto',fontSize:10,color:C.fg2,lineHeight:1.4 }}>{a.body}</span>
            </div>
          </div>
        ))}
      </div>
    </Sec>
  );
}

// ── Action Queue ──────────────────────────────────────────────
function DeadlineBadge({ deadline, elapsed }) {
  if (deadline==null) return null;
  const rem = Math.max(0,deadline-elapsed);
  const mm = String(Math.floor(rem/60)).padStart(2,'0');
  const ss = String(rem%60).padStart(2,'0');
  const expired = rem===0, urgent = rem<=20&&!expired;
  const color = expired?C.critical:urgent?C.advisory:C.fg2;
  return (
    <span title={expired ? 'Action deadline expired. Cascade consequences may apply.' : `Time remaining until this action\'s deadline (T−mm:ss). Urgent when ≤20s out.`} style={{ fontFamily:'Roboto Mono',fontSize:9.5,color,background:expired||urgent?`${color}18`:'transparent',padding:expired||urgent?'1px 5px':0,borderRadius:2,animation:urgent?'pulseFade 0.8s infinite':'none' }}>
      {expired?'EXPIRED':`T−${mm}:${ss}`}
    </span>
  );
}

// Derive a card's disabled state + blocker reason from app state. Used for both
// the primary button and for queue-ordering (disabled criticals drop below advisories).
function actionBlocker(action, state={}) {
  // j35-alpha is never blocked — ALPHA position is fixed from case open
  if (action.id==='j35-bravo' && !state.bravoLocated) return 'Requires BRAVO position fix (AMA-33)';
  return null;
}

// Map urgency to the matching DT button color so card border and button share a palette.
function buttonColorForUrgency(urgency) {
  if (urgency === 'critical') return 'critical';
  if (urgency === 'normal')   return 'mono-bold';
  return 'serious';
}

// Button-config lookup for each action id. Returns null if no primary button.
function primaryAction(action, handlers, state={}) {
  const color = buttonColorForUrgency(action.urgency);
  const blocker = actionBlocker(action, state);
  const disabled = !!blocker;
  switch (action.id) {
    case 'jprc-notify': return { label:'Review and submit',      color, onClick: handlers.onPreview };
    case 'osc-appoint': return { label:'Review candidates',      color, onClick: handlers.onOsc };
    case 'beacon-ack':  return { label:'Acknowledge',            color, onClick: ()=>handlers.onConfirm(action.id) };
    case 'isop-auth':   return { label:'Review and submit',      color, onClick: handlers.onAuth };
    case 'tx-limit':    return { label:'Review and submit',      color, onClick: handlers.onTx };
    case 'uar-task':    return { label:'Review and submit',      color, onClick: handlers.onUar };
    case 'j35-alpha':   return { label:'Confirm J3.5 · ALPHA',  color, onClick: ()=>handlers.onConfirm(action.id) };
    case 'j35-bravo':   return { label:'Confirm J3.5 · BRAVO',  color, onClick: ()=>handlers.onConfirm(action.id), disabled, blocker };
    case 'coa-review':
    case 'coa-rereview':return { label:'Review COAs',            color, onClick: handlers.onCoa };
    case 'launch-ready':          return { label:'Review launch readiness', color, onClick: handlers.onLaunch };
    case 'disruption-divert':     return { label:'Divert decision',         color, onClick: handlers.onDivert };
    case 'disruption-coa-rereview':return { label:'Review COAs',            color, onClick: handlers.onCoa };
    case 'terminal-handoff':      return { label:'Delegate to OSC',         color:'serious', onClick: handlers.onTerminal };
    case 'pickup-ack':            return { label:'Review 15-Line · pickup', color, onClick: handlers.onPickup };
    case 'medevac-9line':         return { label:'Review 9-Line medevac',   color:'serious', onClick: handlers.onMedevac };
    case 'recovery-complete':     return { label:'Close case',              color, onClick: handlers.onRecovery };
    case 'roz-drop':              return { label:'Review PR ROZ Drop',      color, onClick: handlers.onRoz };
    case '15line-b':              return { label:'Review Section B',        color, onClick: ()=>handlers.onFifteen('B') };
    case '15line-c':              return { label:'Review Section C',        color, onClick: ()=>handlers.onFifteen('C') };
    case '15line-d':              return { label:'Review Section D',        color, onClick: ()=>handlers.onFifteen('D') };
    case '15line-close':          return { label:'Review Closeout',         color, onClick: ()=>handlers.onFifteen('CLOSE') };
    default:            return null;
  }
}

function ActionCard({ action, elapsed, defaultExpanded=false, state={}, onConfirm, onReject, onOsc, onPreview, onAuth, onTx, onUar, onCoa, onLaunch, onDivert, onTerminal, onPickup, onRecovery, onMedevac, onRoz, onFifteen }) {
  const [exp, setExp] = React.useState(defaultExpanded);
  // Sync to defaultExpanded when the card's position in the queue changes
  // (e.g., it becomes the new front of queue after the previous one completes).
  React.useEffect(()=>{ setExp(defaultExpanded); },[defaultExpanded]);
  const btn = primaryAction(action, { onConfirm, onOsc, onPreview, onAuth, onTx, onUar, onCoa, onLaunch, onDivert, onTerminal, onPickup, onRecovery, onMedevac, onRoz, onFifteen }, state);
  const muted = !!btn?.disabled;
  return (
    <div style={(()=>{
      const pal = action.urgency==='critical' ? { rail:C.critical, border:C.critical+'66', glow:`0 0 12px ${C.critical}18` }
                : action.urgency==='normal'   ? { rail:C.normal,   border:C.normal+'66',   glow:`0 0 10px ${C.normal}18`   }
                :                               { rail:C.advisory, border:C.advisory+'55', glow:`0 0 10px ${C.advisory}14` };
      return { background:C.s3, border:`1px solid ${pal.border}`, borderLeft:`4px solid ${pal.rail}`, borderRadius:4, marginBottom:9, overflow:'hidden', boxShadow:muted?'none':pal.glow, opacity:muted?0.4:1, transition:'opacity 0.2s' };
    })()}>
      <div style={{ padding:'7px 9px' }}>
        <div style={{ display:'flex',alignItems:'center',gap:6,marginBottom:4 }}>
          <Pill level={action.urgency} />
          <span style={{ fontFamily:'Roboto Mono',fontSize:7.5,color:C.fg3 }}>{action.amaRef}</span>
          <div style={{ flex:1 }}/>
          <DeadlineBadge deadline={action.deadline} elapsed={elapsed} />
          <button onClick={()=>setExp(e=>!e)} style={{ background:'none',border:'none',cursor:'pointer',color:C.fg3,fontSize:11,padding:'0 3px' }}>{exp?'▲':'▼'}</button>
        </div>
        <div style={{ fontFamily:'Roboto',fontWeight:500,fontSize:11.5,color:C.fg1,marginBottom:3 }}>{action.title}</div>
        <div style={{ fontFamily:'Roboto',fontSize:10.5,color:C.fg2,lineHeight:1.45 }}>{action.body}</div>
      </div>
      {exp && (
        <div style={{ padding:'0 9px 7px',display:'flex',flexDirection:'column',gap:3 }}>
          {action.fields.map(f=>(
            <div key={f.l} style={{ display:'flex',gap:8 }}>
              <span data-typo="label" style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:C.fg3,width:76,flexShrink:0 }}>{f.l}</span>
              <span style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:C.fg1 }}>{f.v}</span>
            </div>
          ))}
        </div>
      )}
      <div style={{ borderTop:`1px solid ${C.border}`, padding:8, display:'flex', flexDirection:'column', gap:6 }}>
        <div style={{ display:'flex', gap:6 }}>
          {btn && (
            <DTButton
              onClick={btn.disabled ? undefined : btn.onClick}
              disabled={btn.disabled}
              title={`${btn.label}${action.title ? ' — ' + action.title : ''}${btn.blocker ? '\nBlocked: ' + btn.blocker : ''}`}
              color={btn.color} size="small" fullWidth
              style={{ borderRadius:3, flex:3 }}
            >
              {btn.label}
            </DTButton>
          )}
          <DTButton onClick={()=>onReject(action.id)} disabled={muted} title="Dismiss — Remove this action from the queue without acting. Logged in the case history." color="mono-muted" variant="outline" size="small" fullWidth style={{ borderRadius:3, flex:1 }}>
            Dismiss
          </DTButton>
        </div>
        {btn && btn.blocker && (
          <div style={{ fontFamily:'Roboto Mono', fontSize:8.5, color:C.advisory, letterSpacing:'0.06em', textAlign:'center' }}>⚠ {btn.blocker}</div>
        )}
      </div>
    </div>
  );
}

function ActionQueue({ actions, elapsed, completed=[], state={}, onConfirm, onReject, onOsc, onPreview, onAuth, onTx, onUar, onCoa, onLaunch, onDivert, onTerminal, onPickup, onRecovery, onMedevac, onRoz, onFifteen, expanded=false, onExpandedChange }) {
  // Queue ordering: enabled critical (0) > advisory (1) > disabled critical (2).
  // Stable sort preserves original order within a tier.
  const sortedActions = [...actions].sort((a,b)=>{
    const rank = x => (x.urgency==='critical' ? 0 : 1) + (actionBlocker(x, state) ? 2 : 0);
    return rank(a) - rank(b);
  });
  // Track cards removed since the last render so we can animate them out
  // before the DOM drops them. Incoming cards get a CSS fade-in via `cardEnter`.
  const prevActionsRef = React.useRef(actions);
  const [outgoing, setOutgoing] = React.useState([]);
  React.useEffect(()=>{
    const prev = prevActionsRef.current;
    const currIds = new Set(actions.map(a=>a.id));
    const removed = prev.filter(a => !currIds.has(a.id));
    if (removed.length) {
      setOutgoing(o => [...o, ...removed]);
      const removedIds = new Set(removed.map(a=>a.id));
      const timer = setTimeout(()=>{
        setOutgoing(o => o.filter(a => !removedIds.has(a.id)));
      }, 320);
      prevActionsRef.current = actions;
      return ()=>clearTimeout(timer);
    }
    prevActionsRef.current = actions;
  },[actions]);
  const hasCritical = sortedActions.some(a => a.urgency === 'critical' && !actionBlocker(a, state));
  // Completed log is collapsed by default. The most recent entry stays
  // visible as a peek so the operator can see "the last thing that finished"
  // without expanding. Click the toggle (or the peek row) to expand the full
  // chronological list.
  const [completedOpen, setCompletedOpen] = React.useState(false);
  // The Action Queue header's right-side affordance is a navigation control,
  // not an inline collapse: clicking "Expand ▸" tells the parent (App) to
  // open a full-screen 5-column expanded view (To Do / Doing / Done /
  // 15-Line Tasks / Chat). The body of THIS component renders normally
  // either way — App swaps the entire Rail 2 layout when expanded.
  const toggleExpanded = () => { if (typeof onExpandedChange === 'function') onExpandedChange(!expanded); };
  // `completed` is supplied chronological (oldest first). Most recent is the
  // last element — that's what the peek row shows when collapsed.
  const recent = completed.length > 0 ? completed[completed.length - 1] : null;
  // Header layout mirrors the CollapsibleRail: count chip on the LEFT (with
  // pulsing treatment when there's a critical action — the chip itself is
  // the urgency cue, replacing the standalone pulse dot), title centered,
  // Expand/Collapse affordance on the RIGHT.
  const countChipBg    = actions.length>0 ? C.criticalM : C.s3;
  const countChipColor = actions.length>0 ? C.critical  : C.fg3;
  return (
    // flexShrink:1 + minHeight:0 + overflowY:auto lets the queue cap at the
    // rail height when there are too many cards — without this it would push
    // the comms log out of view. Comms log has flex:1 so it claims remaining
    // space, leaving the queue with whatever's left up to its natural height.
    <div style={{ borderBottom:`1px solid ${C.border}`, flexShrink:1, minHeight:0, overflowY:'auto' }}>
      <button
        onClick={toggleExpanded}
        style={{
          display:'flex', alignItems:'center', gap:8, width:'100%',
          padding:'8px 12px',
          background: hasCritical ? C.criticalM : C.s2,
          border:'none',
          borderBottom:`1px solid ${hasCritical ? C.critical+'44' : C.border}`,
          transition:'background 0.4s',
          cursor:'pointer', textAlign:'left',
          boxSizing:'border-box', height:28, lineHeight:1,
        }}>
        <span style={{
          background: countChipBg, color: countChipColor,
          fontFamily:'Roboto Mono', fontSize:8, fontWeight:500,
          padding:'1px 6px', borderRadius:10, lineHeight:1, flexShrink:0,
          // Pulse the chip itself when there's a critical action — the chip
          // is now the urgency indicator, no separate dot needed.
          animation: hasCritical ? 'pulseFade 0.7s infinite' : undefined,
        }}>{actions.length}</span>
        <span data-typo="label" style={{ fontFamily:'Roboto Mono',fontSize:8.5,fontWeight:500,color:hasCritical?C.critical:C.fg2,letterSpacing:'0.11em',textTransform:'uppercase',flex:1,lineHeight:1 }}>Case Action Queue</span>
        <span data-typo="label" style={{ fontFamily:'Roboto Mono',fontSize:7.5,color:C.fg3,letterSpacing:'0.11em',textTransform:'uppercase',lineHeight:1 }}>
          {expanded ? '◂ Collapse' : 'Expand ▸'}
        </span>
      </button>
      {/* Body always renders here — App owns the expanded full-screen
          layout, this component just shows the standard rail-width view. */}
      {true && (
        <>
          {/* Active action cards FIRST — top-of-rail priority. When the queue is
              empty (no cards + no outgoing animations), collapse the wrapper so
              the Completed log header sits flush against the Action Queue header
              without an empty buffer between them. */}
          {(sortedActions.length>0 || outgoing.length>0) && (
            <div style={{ padding:'9px 11px' }}>
              {sortedActions.map((a,i)=>(
                <div key={a.id} style={{ animation:'cardEnter 0.28s ease-out' }}>
                  <ActionCard action={a} elapsed={elapsed} defaultExpanded={i===0} state={state} onConfirm={onConfirm} onReject={onReject} onOsc={onOsc} onPreview={onPreview} onAuth={onAuth} onTx={onTx} onUar={onUar} onCoa={onCoa} onLaunch={onLaunch} onDivert={onDivert} onTerminal={onTerminal} onPickup={onPickup} onRecovery={onRecovery} onMedevac={onMedevac} onRoz={onRoz} onFifteen={onFifteen} />
                </div>
              ))}
              {outgoing.map(a=>(
                <div key={a.id} style={{ animation:'cardLeave 0.3s ease-in forwards', overflow:'hidden', pointerEvents:'none' }}>
                  <ActionCard action={a} elapsed={elapsed} defaultExpanded={false} state={state} onConfirm={()=>{}} onReject={()=>{}} onOsc={()=>{}} onPreview={()=>{}} onAuth={()=>{}} onTx={()=>{}} onUar={()=>{}} onCoa={()=>{}} onLaunch={()=>{}} onDivert={()=>{}} onTerminal={()=>{}} onPickup={()=>{}} onRecovery={()=>{}} onMedevac={()=>{}} />
                </div>
              ))}
            </div>
          )}
          {/* "Queue clear" placeholder only when there are also no completed
              items — otherwise the Completed peek row carries the meaning. */}
          {sortedActions.length===0 && outgoing.length===0 && completed.length===0 && (
            <div style={{ color:C.fg3,fontFamily:'Roboto',fontSize:11,textAlign:'center',padding:'10px 0' }}>Queue clear</div>
          )}
          {/* Completed log: collapsed by default, peek row shows the most recent
              entry. Click the toggle row to expand/collapse the full list. No
              dividing borders — the header sits naked against neighbours so the
              empty-queue case reads as one continuous panel. */}
          {completed.length>0 && (
            <div>
              <button
                onClick={()=>setCompletedOpen(v=>!v)}
                style={{
                  width:'100%', display:'flex', alignItems:'baseline', gap:8,
                  padding:'7px 12px', background:'transparent', border:'none',
                  cursor:'pointer', textAlign:'left',
                }}>
                <span data-typo="label" style={{ fontFamily:'Roboto Mono',fontSize:8,color:C.fg3,letterSpacing:'0.11em',textTransform:'uppercase' }}>
                  {completedOpen ? '▾' : '▸'} Completed
                </span>
                <span style={{ fontFamily:'Roboto Mono',fontSize:8,color:C.fg3 }}>({completed.length})</span>
                {!completedOpen && recent && (
                  <>
                    <span style={{ fontFamily:'Roboto Mono',fontSize:9,color:C.fg2,letterSpacing:'0.06em',flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap' }}>{recent.label}</span>
                    <span style={{ fontFamily:'Roboto Mono',fontSize:9,color:C.fg3 }}>{recent.at}</span>
                  </>
                )}
                {completedOpen && <span style={{ flex:1 }} />}
              </button>
              {completedOpen && (
                <div style={{ padding:'7px 12px' }}>
                  {completed.map(c=>(
                    <div key={c.id} style={{ display:'flex',alignItems:'baseline',gap:8,padding:'3px 0' }}>
                      <span style={{ fontFamily:'Roboto Mono',fontSize:9,color:C.fg2,letterSpacing:'0.06em',flex:1 }}>{c.label}</span>
                      <span style={{ fontFamily:'Roboto Mono',fontSize:9,color:C.normal,fontWeight:500,letterSpacing:'0.08em' }}>COMPLETE</span>
                      <span style={{ fontFamily:'Roboto Mono',fontSize:9,color:C.fg3 }}>{c.at}</span>
                    </div>
                  ))}
                </div>
              )}
            </div>
          )}
        </>
      )}
    </div>
  );
}

// ── Expanded Action Queue (full-width 5-column view) ──────────
// Triggered from the Action Queue header's "Expand ▸" affordance. App swaps
// Rail 2 from a 308px column into a fluid panel that absorbs everything to
// the right of Rail 1 — the map disappears for the duration of the expanded
// view. Five columns, left to right:
//   To Do   — blocked queue cards + forecast rows for upcoming phases.
//   Doing   — actions present in the queue with NO blocker (live, can act
//             on right now).
//   Done    — completed array (chronological).
//   15-Line — every section (A, B, C, D, CLOSE) in canonical order, with
//             sent state alongside.
//   Chat    — full comms log so MC/SL can read radio while triaging.
// Each column of the expanded queue shares the same chrome — header strip +
// scrollable body. Defined at module scope (not inside ActionQueueExpanded)
// so React keeps a stable component identity across the parent's 1 Hz
// re-renders. Defining it inside the parent created a new function
// reference every render, which forced React to unmount + remount the
// scrollable div on every tick — wiping scroll position to the top.
function ExpandedQueueColumn({ title, count, accent, children, last }) {
  return (
    <div style={{
      flex:1, minWidth:0, height:'100%',
      display:'flex', flexDirection:'column',
      borderRight: last ? 'none' : `1px solid ${C.border}`,
      background: C.s1,
    }}>
      <div style={{
        display:'flex', alignItems:'center', gap:8,
        padding:'8px 12px', height:28, boxSizing:'border-box', lineHeight:1,
        background: C.s2, borderBottom:`1px solid ${C.border}`, flexShrink:0,
      }}>
        <span style={{
          background:(count>0 && accent) ? accent+'33' : C.s3,
          color:(count>0 && accent) ? accent : C.fg3,
          fontFamily:'Roboto Mono', fontSize:8, fontWeight:500,
          padding:'1px 6px', borderRadius:10, lineHeight:1, flexShrink:0,
        }}>{count}</span>
        <span data-typo="label" style={{ fontFamily:'Roboto Mono',fontSize:8.5,fontWeight:500,color:accent||C.fg2,letterSpacing:'0.11em',textTransform:'uppercase',flex:1,lineHeight:1 }}>{title}</span>
      </div>
      <div style={{ flex:1, overflowY:'auto', padding:'9px 11px', minHeight:0 }}>
        {children}
      </div>
    </div>
  );
}

function ActionQueueExpanded({
  actions, elapsed, completed=[], state={},
  upcoming=[],          // [{ id, phase, phaseLabel, title, ama, trigger, ... }]
  fifteenStatuses=[],   // [{ id:'A'|'B'|'C'|'D'|'CLOSE', label, sent, sentAt }]
  msgs, setMsgs,
  onCollapse,
  onConfirm, onReject, onOsc, onPreview, onAuth, onTx, onUar, onCoa,
  onLaunch, onDivert, onTerminal, onPickup, onRecovery, onMedevac, onRoz, onFifteen,
}) {
  // Split actions into Doing (unblocked) vs To Do (blocked). Critical first
  // within each tier, preserving original order otherwise.
  const sorted = [...actions].sort((a,b)=> (a.urgency==='critical'?0:1) - (b.urgency==='critical'?0:1));
  const doing = sorted.filter(a => !actionBlocker(a, state));
  const toDo  = sorted.filter(a =>  actionBlocker(a, state));
  const hasCritical = doing.some(a => a.urgency === 'critical');
  const countChipBg    = actions.length>0 ? C.criticalM : C.s3;
  const countChipColor = actions.length>0 ? C.critical  : C.fg3;
  const handlers = { onConfirm, onOsc, onPreview, onAuth, onTx, onUar, onCoa, onLaunch, onDivert, onTerminal, onPickup, onRecovery, onMedevac, onRoz, onFifteen };
  const Column = ExpandedQueueColumn;
  return (
    <div style={{ flex:1, display:'flex', flexDirection:'column', minHeight:0, minWidth:0, background:C.s1 }}>
      {/* Top header — same shape as the rail-width Action Queue header so the
          transition reads as "the same panel got bigger". Right side flips
          to "◂ Collapse" to return to dual-rail layout. */}
      <div style={{
        display:'flex', alignItems:'center', gap:8, width:'100%',
        padding:'8px 12px',
        background: hasCritical ? C.criticalM : C.s2,
        borderBottom:`1px solid ${hasCritical ? C.critical+'44' : C.border}`,
        boxSizing:'border-box', height:28, lineHeight:1, flexShrink:0,
      }}>
        <span style={{
          background: countChipBg, color: countChipColor,
          fontFamily:'Roboto Mono', fontSize:8, fontWeight:500,
          padding:'1px 6px', borderRadius:10, lineHeight:1, flexShrink:0,
          animation: hasCritical ? 'pulseFade 0.7s infinite' : undefined,
        }}>{actions.length}</span>
        <span data-typo="label" style={{ fontFamily:'Roboto Mono',fontSize:8.5,fontWeight:500,color:hasCritical?C.critical:C.fg2,letterSpacing:'0.11em',textTransform:'uppercase',flex:1,lineHeight:1 }}>Case Action Queue</span>
        <button
          data-typo="label"
          onClick={onCollapse}
          style={{ background:'none', border:'none', cursor:'pointer', padding:0,
                   fontFamily:'Roboto Mono',fontSize:7.5,color:C.fg3,letterSpacing:'0.11em',textTransform:'uppercase',lineHeight:1 }}>
          ◂ Collapse
        </button>
      </div>
      {/* Five columns. Each gets equal flex; last drops the right border. */}
      <div style={{ flex:1, display:'flex', minHeight:0, overflow:'hidden' }}>
        {/* Order: To Do → Doing → Done. Reads left-to-right as a forward
            timeline of work — what's coming, what's in flight, what's
            shipped. Mirrors how the operator naturally narrates the case. */}
        <Column title="To Do" count={toDo.length + upcoming.length} accent={C.advisory} last={false}>
          {toDo.length === 0 && upcoming.length === 0 ? (
            <div style={{ color:C.fg3,fontFamily:'Roboto',fontSize:11,textAlign:'center',padding:'10px 0' }}>—</div>
          ) : (
            <>
              {/* Blocked actions: cards exist in the queue but are gated by
                  some prereq. Render as full cards so the operator can see
                  the blocker and click through if desired. */}
              {toDo.map(a=>(
                <ActionCard key={a.id} action={a} elapsed={elapsed} defaultExpanded={false} state={state}
                  onConfirm={onConfirm} onReject={onReject} {...handlers}
                  onLaunch={onLaunch} onDivert={onDivert} onTerminal={onTerminal} onPickup={onPickup} onRecovery={onRecovery} onMedevac={onMedevac} onRoz={onRoz} onFifteen={onFifteen} />
              ))}
              {/* Forecast rows: known cards that haven't been triggered yet
                  in the engine. Disabled placeholders, grouped by phase so
                  MC/SL can see the shape of the work ahead. */}
              {upcoming.length > 0 && (() => {
                // Group by phaseLabel preserving the canonical order from
                // UPCOMING_ACTIONS (which is already phase-sequenced).
                const groups = [];
                upcoming.forEach(u => {
                  const last = groups[groups.length - 1];
                  if (last && last.label === u.phaseLabel) last.items.push(u);
                  else groups.push({ label: u.phaseLabel, phase: u.phase, items:[u] });
                });
                return groups.map(g => (
                  <div key={g.label} style={{ marginTop:8, marginBottom:4 }}>
                    <div data-typo="label" style={{
                      fontFamily:'Roboto Mono', fontSize:8, color:C.fg3,
                      letterSpacing:'0.11em', textTransform:'uppercase',
                      padding:'4px 4px 6px', borderBottom:`1px solid ${C.border}`,
                      marginBottom:6,
                    }}>
                      Phase {g.phase} · {g.label}
                    </div>
                    {g.items.map(u => (
                      <div key={u.id} style={{
                        background:C.s3, border:`1px solid ${C.border}`,
                        borderLeft:`3px solid ${C.fg4}`, borderRadius:3,
                        padding:'7px 9px', marginBottom:6, opacity:0.65,
                      }}>
                        <div style={{ display:'flex', alignItems:'center', gap:6, marginBottom:3 }}>
                          <span data-typo="label" style={{
                            fontFamily:'Roboto Mono', fontSize:7.5, fontWeight:500,
                            color:C.fg3, letterSpacing:'0.08em', textTransform:'uppercase',
                            background:C.s2, padding:'1px 5px', borderRadius:9,
                          }}>Pending</span>
                          <span style={{ fontFamily:'Roboto Mono', fontSize:7.5, color:C.fg3 }}>{u.ama}</span>
                        </div>
                        <div style={{ fontFamily:'Roboto', fontWeight:500, fontSize:11, color:C.fg2, marginBottom:2 }}>{u.title}</div>
                        {u.trigger && (
                          <div style={{ fontFamily:'Roboto', fontSize:9.5, color:C.fg3, lineHeight:1.4 }}>{u.trigger}</div>
                        )}
                      </div>
                    ))}
                  </div>
                ));
              })()}
            </>
          )}
        </Column>
        <Column title="Doing" count={doing.length} accent={C.critical} last={false}>
          {doing.length === 0 ? (
            <div style={{ color:C.fg3,fontFamily:'Roboto',fontSize:11,textAlign:'center',padding:'10px 0' }}>Queue clear</div>
          ) : (
            doing.map((a,i)=>(
              <ActionCard key={a.id} action={a} elapsed={elapsed} defaultExpanded={i===0} state={state}
                onConfirm={onConfirm} onReject={onReject} {...handlers}
                onLaunch={onLaunch} onDivert={onDivert} onTerminal={onTerminal} onPickup={onPickup} onRecovery={onRecovery} onMedevac={onMedevac} onRoz={onRoz} onFifteen={onFifteen} />
            ))
          )}
        </Column>
        <Column title="Done" count={completed.length} accent={C.normal} last={false}>
          {completed.length === 0 ? (
            <div style={{ color:C.fg3,fontFamily:'Roboto',fontSize:11,textAlign:'center',padding:'10px 0' }}>—</div>
          ) : (
            completed.map(c=>(
              <div key={c.id} style={{ display:'flex',alignItems:'baseline',gap:8,padding:'5px 0',borderBottom:`1px solid ${C.border}` }}>
                <span style={{ fontFamily:'Roboto Mono',fontSize:9,color:C.fg2,letterSpacing:'0.06em',flex:1 }}>{c.label}</span>
                <span style={{ fontFamily:'Roboto Mono',fontSize:9,color:C.normal,fontWeight:500,letterSpacing:'0.08em' }}>COMPLETE</span>
                <span style={{ fontFamily:'Roboto Mono',fontSize:9,color:C.fg3 }}>{c.at}</span>
              </div>
            ))
          )}
        </Column>
        <Column
          title="15-Line Tasks"
          count={fifteenStatuses.filter(f=>f.sent).length}
          accent={C.accent}
          last={false}>
          {fifteenStatuses.length === 0 ? (
            <div style={{ color:C.fg3,fontFamily:'Roboto',fontSize:11,textAlign:'center',padding:'10px 0' }}>—</div>
          ) : (
            fifteenStatuses.map(f=>(
              <button key={f.id}
                onClick={() => typeof onFifteen === 'function' && onFifteen(f.id)}
                style={{
                  width:'100%', display:'flex', alignItems:'center', gap:8,
                  padding:'8px 10px', marginBottom:6,
                  background: f.sent ? C.s3 : C.s3,
                  border: `1px solid ${f.sent ? C.normal+'44' : C.border}`,
                  borderLeft: `3px solid ${f.sent ? C.normal : C.accent}`,
                  borderRadius:3, cursor:'pointer', textAlign:'left',
                }}>
                <span style={{ fontFamily:'Roboto Mono', fontSize:9, color:C.fg3, letterSpacing:'0.08em', width:48, flexShrink:0 }}>
                  {f.id === 'CLOSE' ? 'CLOSE' : `SEC ${f.id}`}
                </span>
                <span style={{ fontFamily:'Roboto', fontSize:10.5, color:C.fg1, flex:1, lineHeight:1.3 }}>{f.label}</span>
                <span style={{
                  fontFamily:'Roboto Mono', fontSize:8, fontWeight:500, letterSpacing:'0.08em',
                  color: f.sent ? C.normal : C.fg3,
                  padding:'1px 6px', borderRadius:10,
                  background: f.sent ? C.normal+'22' : C.s2,
                }}>
                  {f.sent ? 'SENT' : 'PENDING'}
                </span>
              </button>
            ))
          )}
        </Column>
        {/* Chat column — full comms log, no extra header chrome (its own
            section header lives inside CommsLog/Sec). */}
        <div style={{ flex:1, minWidth:0, height:'100%', display:'flex', flexDirection:'column', background:C.s1, overflow:'hidden' }}>
          <CommsLog msgs={msgs} setMsgs={setMsgs} />
        </div>
      </div>
    </div>
  );
}

// ── Case Summary ──────────────────────────────────────────────
function CaseSummary({ elapsed, phaseLabel='IMMEDIATE ACTIONS', phaseColor=C.advisory, authStatus='PENDING', beaconAck=false }) {
  const mm = String(Math.floor(elapsed/60)).padStart(2,'0');
  const ss = String(elapsed%60).padStart(2,'0');
  const authHi = authStatus==='AUTHENTICATED' ? C.normal : C.advisory;
  const rows = [
    {l:'CASE #',   v:window.CASE.number},{l:'ISOP',v:'DUKE 31 ALPHA / BRAVO'},{l:'AIRCRAFT',v:window.CASE.aircraft},
    {l:'PHASE',    v:phaseLabel,hi:phaseColor},
    {l:'ELAPSED',  v:`T+${mm}:${ss}`,mono:true,hi:C.fg1},
    {l:'MGRS',     v:window.CASE.mgrs,mono:true},{l:'ELEVATION',v:window.CASE.elevation,mono:true},
    {l:'BEACON',   v:beaconAck?`${window.CASE.beacon} · ACK'D`:window.CASE.beacon,mono:true,hi:beaconAck?C.normal:C.fg2},
    {l:'AUTH',     v:authStatus,hi:authHi},
  ];
  return (
    <Sec title="Case Summary" collapsible defaultOpen>
      <div style={{ display:'flex',flexDirection:'column',gap:4 }}>
        {rows.map(r=>(
          <div key={r.l} style={{ display:'flex',gap:8,alignItems:'baseline' }}>
            <span style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:C.fg3,width:72,flexShrink:0,letterSpacing:'0.06em' }}>{r.l}</span>
            <span style={{ fontFamily:r.mono?'Roboto Mono':'Roboto',fontSize:r.mono?9.5:11,color:r.hi||C.fg2 }}>{r.v}</span>
          </div>
        ))}
      </div>
    </Sec>
  );
}

// Modal close X — hover reveals a subtle bg + white text
function CloseButton({ onClose }) {
  const [hover, setHover] = React.useState(false);
  return (
    <button
      onClick={onClose}
      onMouseEnter={()=>setHover(true)}
      onMouseLeave={()=>setHover(false)}
      style={{
        background: hover ? C.s4 : 'transparent',
        border:'none',
        borderRadius:3,
        color: hover ? '#ffffff' : C.fg3,
        fontSize:15,
        width:24, height:24,
        display:'flex', alignItems:'center', justifyContent:'center',
        cursor:'pointer',
        transition:'background 0.15s, color 0.15s',
      }}>✕</button>
  );
}

// Compact pill-style button used inside the Survivor Roster per-crew action strip.
// Tone maps to the action's urgency so visual weight matches what the action does:
//   critical  — J3.5-flipping beacon ack (red)
//   serious   — ISOPREP auth / TX Limit (amber)
//   neutral   — default mono outline
function RosterActionBtn({ disabled, onClick, children, tone='neutral' }) {
  const [hover, setHover] = React.useState(false);
  const palette = disabled ? null
    : tone === 'critical' ? { bg:C.critical, hover:'#d40b04', border:C.critical, fg:'#ffffff' }
    : tone === 'serious'  ? { bg:'#bb7a00',  hover:'#e69600', border:'#bb7a00',  fg:'#ffffff' }
    : null;
  const bg     = disabled ? '#414245' : (palette ? (hover ? palette.hover : palette.bg) : (hover ? C.s4 : C.s2));
  const border = disabled ? 'transparent' : (palette ? palette.border : (hover ? C.fg3 : C.border2));
  const color  = disabled ? '#565759' : (palette ? palette.fg : (hover ? '#ffffff' : C.fg1));
  return (
    <button
      onClick={disabled ? undefined : onClick}
      disabled={disabled}
      onMouseEnter={()=>!disabled && setHover(true)}
      onMouseLeave={()=>setHover(false)}
      style={{
        marginLeft:'auto',
        background: bg,
        border:`1px solid ${border}`,
        borderRadius:3,
        color,
        fontFamily:'Roboto Mono',
        fontSize:8.5, fontWeight:500, letterSpacing:'0.08em',
        padding:'3px 8px',
        cursor: disabled ? 'not-allowed' : 'pointer',
        transition:'background 0.15s, border-color 0.15s, color 0.15s',
      }}>
      {children}
    </button>
  );
}

// One row of the per-crew action strip (BEACON / AUTH / TX LIMIT). Lives at
// module scope — earlier this was nested inside SurvivorRoster, which made
// React tear down the buttons on every parent render (the 1 Hz clock tick
// re-renders the whole tree). Click events that landed between mousedown
// and mouseup were getting dropped, so the user had to click twice.
function RosterActionRow({ label, done, doneAt, disabled, cta, onClick, tone }) {
  // SME-REVIEW: tooltip needs verification
  const labelTip = {
    BEACON: 'BEACON — Beacon acknowledgment from the survivor (CSEL hit confirms position fix).',
    AUTH: 'AUTH — Survivor authentication. Verifies the radio voice on the other end is the actual ISOP, not deception.',
    'TX LIMIT': 'TX LIMIT — Transmission Limit — operational order to the survivor restricting radio use to preserve LOI / battery / OPSEC.',
  }[label];
  return (
    <div title={labelTip} style={{ display:'flex',alignItems:'center',gap:8,padding:'3px 0' }}>
      <span style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:C.fg3,width:78,flexShrink:0,letterSpacing:'0.06em' }}>{label}</span>
      {done ? (
        <>
          <span style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:C.normal,fontWeight:500,letterSpacing:'0.08em' }}>✓ Complete</span>
          <span style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:C.fg3,marginLeft:'auto' }}>{doneAt}</span>
        </>
      ) : (
        <RosterActionBtn disabled={disabled} onClick={onClick} tone={tone}>{disabled?'Locked':cta}</RosterActionBtn>
      )}
    </div>
  );
}

// ── Survivor Roster ───────────────────────────────────────────
function SurvivorRoster({
  phase=0, bravoLocated=false,
  beaconAckAlpha=false, beaconAckAlphaAt=null,
  beaconAckBravo=false, beaconAckBravoAt=null,
  authAlpha=false,      authAlphaAt=null,
  authBravo=false,      authBravoAt=null,
  txSentAlpha=false,    txSentAlphaAt=null,
  txSentBravo=false,    txSentBravoAt=null,
  onAckBeacon=()=>{}, onOpenAuth=()=>{}, onOpenTx=()=>{},
}) {
  const crew = [
    { id:'alpha', cs:'DUKE 31 ALPHA', pos:'PILOT', injuries:'NIL REPORTED',
      posStatus:'CONFIRMED',   mgrs:'51RTH 8451 1234',
      contact:true,             radio:'CSEL-7821 ACTIVE',
      beaconAck:beaconAckAlpha, beaconAt:beaconAckAlphaAt,
      auth:authAlpha,           authAt:authAlphaAt,
      tx:txSentAlpha,           txAt:txSentAlphaAt },
    { id:'bravo', cs:'DUKE 31 BRAVO', pos:'WSO',   injuries:'UNKNOWN',
      posStatus: bravoLocated ? 'CONFIRMED' : 'UNCONFIRMED',
      mgrs: bravoLocated ? '51RTH 8523 1283' : '51RTH 8523 1283 (est)',
      contact: bravoLocated,
      radio: bravoLocated ? 'CSEL-8104 ACTIVE' : 'NO CONTACT YET',
      beaconAck:beaconAckBravo, beaconAt:beaconAckBravoAt,
      auth:authBravo,           authAt:authBravoAt,
      tx:txSentBravo,           txAt:txSentBravoAt },
  ];
  return (
    <Sec title="Survivor Roster — DUKE 31" collapsible defaultOpen>
      {crew.map(c=>{
        const posColor = c.posStatus==='CONFIRMED' ? C.normal : C.advisory;
        const authTag  = c.auth ? 'AUTH\u2019D' : 'PENDING';
        const actionsAvailable = c.contact && phase>=1;
        return (
        <div key={c.id} style={{ marginBottom:8,padding:'7px 8px',background:C.s3,borderRadius:3,border:`1px solid ${C.border2}`,borderLeft:`3px solid ${posColor}` }}>
          <div style={{ display:'flex',gap:6,alignItems:'center',marginBottom:5,flexWrap:'wrap' }}>
            <span style={{ fontFamily:'Roboto Mono',fontWeight:500,fontSize:10.5,color:posColor }}>{c.cs}</span>
            <Tag label={c.pos} color={C.fg3} />
            <Tag label="AMBULATORY" color={C.normal} />
            <Tag label={authTag} color={c.auth?C.normal:C.advisory} />
          </div>
          {[
            {l:'POSITION', v:c.posStatus, hi: c.posStatus==='CONFIRMED'?C.normal:C.advisory},
            {l:'MGRS',     v:c.mgrs},
            {l:'INJURIES', v:c.injuries},
            {l:'COMMS',    v: c.beaconAck ? `${c.radio} · ACK\u2019D` : c.radio,
                           hi: c.beaconAck?C.normal:(c.contact?C.accent:C.fg3) },
          ].map(r=>(
            <div key={r.l} title={(({POSITION:'POSITION — Whether the survivor location has been confirmed (CSEL ack, visual) or remains estimated.',MGRS:'MGRS — Military Grid Reference System coordinates. The survivor plotted location.',INJURIES:'INJURIES — Reported medical status from the survivor or witness account.',COMMS:'COMMS — Active radio / beacon channel and whether it has been acknowledged by the survivor.'})[r.l])} style={{ display:'flex',gap:8,marginBottom:2 }}>
              <span style={{ fontFamily:'Roboto Mono',fontSize:8,color:C.fg3,width:68,flexShrink:0 }}>{r.l}</span>
              <span style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:r.hi||C.fg2 }}>{r.v}</span>
            </div>
          ))}
          {/* Per-IP action affordances */}
          {phase>=1 && (
            <div style={{ marginTop:6,paddingTop:6,borderTop:`1px dashed ${C.border}` }}>
              {actionsAvailable ? (
                <RosterActionStrip
                  beaconAck={c.beaconAck} beaconAt={c.beaconAt}
                  auth={c.auth}           authAt={c.authAt}
                  tx={c.tx}               txAt={c.txAt}
                  onAck={()=>onAckBeacon(c.id)}
                  onAuth={()=>onOpenAuth(c.id)}
                  onTx={()=>onOpenTx(c.id)}
                />
              ) : (
                <div style={{ display:'flex',alignItems:'center',gap:6,padding:'2px 0' }}>
                  <span style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:C.advisory,letterSpacing:'0.08em' }}>⚠ AWAITING CONTACT</span>
                  <span style={{ fontFamily:'Roboto Mono',fontSize:8,color:C.fg3 }}>· actions locked until beacon/visual</span>
                </div>
              )}
            </div>
          )}
        </div>
      );})}
    </Sec>
  );
}

// Per-IP action strip wrapper — collapses the three rows (BEACON / AUTH /
// TX LIMIT) into a single "all complete" summary line once all three are
// done. Click the summary to expand. While any row is still incomplete the
// strip stays expanded so the operator can see what's outstanding.
function RosterActionStrip({ beaconAck, beaconAt, auth, authAt, tx, txAt, onAck, onAuth, onTx }) {
  const allDone = beaconAck && auth && tx;
  // Default to collapsed once everything is done; auto-expand if anything
  // becomes incomplete again (e.g., undo). User can manually toggle either way.
  const [collapsed, setCollapsed] = React.useState(allDone);
  React.useEffect(()=>{ if (!allDone) setCollapsed(false); }, [allDone]);
  if (allDone && collapsed) {
    // Most recent timestamp wins for the summary line — that's the one the
    // operator just acted on, easiest to read at a glance.
    const stamps = [beaconAt, authAt, txAt].filter(Boolean);
    const latestAt = stamps.length ? stamps[stamps.length - 1] : '';
    return (
      <button onClick={()=>setCollapsed(false)} style={{
        display:'flex', alignItems:'center', gap:8, width:'100%',
        background:'transparent', border:'none', cursor:'pointer',
        padding:'2px 0', textAlign:'left',
      }}>
        <span style={{ fontFamily:'Roboto Mono',fontSize:8,color:C.fg3,letterSpacing:'0.06em' }}>▸</span>
        <span style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:C.normal,fontWeight:500,letterSpacing:'0.08em' }}>BEACON · AUTH · TX LIMIT</span>
        <span style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:C.normal,letterSpacing:'0.06em',marginLeft:'auto' }}>✓ All complete{latestAt ? ` · ${latestAt}` : ''}</span>
      </button>
    );
  }
  return (
    <>
      {allDone && (
        <button onClick={()=>setCollapsed(true)} style={{
          display:'flex', alignItems:'center', gap:6, width:'100%',
          background:'transparent', border:'none', cursor:'pointer',
          padding:'1px 0 4px', textAlign:'left',
        }}>
          <span style={{ fontFamily:'Roboto Mono',fontSize:8,color:C.fg3,letterSpacing:'0.06em' }}>▾</span>
          <span style={{ fontFamily:'Roboto Mono',fontSize:8,color:C.fg3,letterSpacing:'0.08em' }}>Hide complete</span>
        </button>
      )}
      <RosterActionRow label="BEACON"    done={beaconAck} doneAt={beaconAt} disabled={false}     cta="Acknowledge" tone="critical" onClick={onAck} />
      <RosterActionRow label="AUTH"      done={auth}      doneAt={authAt}   disabled={!beaconAck} cta="Review"      tone="serious"  onClick={onAuth} />
      <RosterActionRow label="TX LIMIT"  done={tx}        doneAt={txAt}     disabled={!auth}      cta="Compose"     tone="serious"  onClick={onTx} />
    </>
  );
}

// ── Group Comms ───────────────────────────────────────────────
// Multi-party group chat — every CSAR participant pushes into the same
// log so MC/SL has a single timeline of voice traffic. Each message is
// coloured by the sender's role (command, survivor, CSARTF, OSC/SANDY,
// AMC/ABM, JPRC, UAR/CIA, ISR). Tx (operator-sent) messages get a
// right-aligned bubble; everything else is a left-aligned rx bubble.
function CommsLog({ msgs: msgsProp, setMsgs: setMsgsProp }) {
  const [fallbackMsgs, setFallbackMsgs] = React.useState(window.INIT_MSGS);
  const msgs    = msgsProp    !== undefined ? msgsProp    : fallbackMsgs;
  const setMsgs = setMsgsProp !== undefined ? setMsgsProp : setFallbackMsgs;
  const [draft, setDraft] = React.useState('');
  const [filter, setFilter] = React.useState('all'); // 'all' | role key
  const scrollRef = React.useRef(null);
  // Track whether the user has scrolled up to read history. We default to
  // "pinned to bottom" (so first render auto-scrolls and new messages keep
  // the latest in view), and only suspend auto-scroll while the user is
  // actively scrolled away from the bottom. Re-pin if they scroll back down.
  const pinnedRef = React.useRef(true);
  const onScroll = (e) => {
    const el = e.currentTarget;
    const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
    pinnedRef.current = distFromBottom < 30;
  };
  // Snap to bottom on every render (covers initial mount + every new msg)
  // unless the user has scrolled up. layoutEffect runs after DOM is updated
  // but before the browser paints, so we don't see a flash of "scrolled to
  // top" before the snap.
  React.useLayoutEffect(()=>{
    const el = scrollRef.current;
    if (!el) return;
    if (pinnedRef.current) el.scrollTop = el.scrollHeight;
  },[msgs, filter]);
  const roleOf = (m) => m.role || (window.COMM_ROSTER && window.COMM_ROSTER[m.from]) || 'amc';
  const colorFor = (m) => {
    const r = roleOf(m);
    return (window.COMM_ROLES && window.COMM_ROLES[r] && window.COMM_ROLES[r].color) || C.fg2;
  };
  const send = () => {
    if (!draft.trim()) return;
    setMsgs(m=>[...m,{id:Date.now(),dir:'tx',from:'SUNDOWN',role:'command',text:draft.trim(),time:new Date().toISOString().slice(11,16).replace(':','')+'Z'}]);
    setDraft('');
  };
  const filtered = filter === 'all' ? msgs : msgs.filter(m => roleOf(m) === filter);
  const roles = window.COMM_ROLES || {};
  const filterChip = (key, label) => (
    <button key={key} onClick={()=>setFilter(key)} style={{
      background: filter===key ? (key==='all' ? C.accentM : (roles[key] && roles[key].color+'33')) : 'transparent',
      border: `1px solid ${filter===key ? (key==='all' ? C.accent : (roles[key] && roles[key].color)) : C.border2}`,
      color: filter===key ? (key==='all' ? C.accent : (roles[key] && roles[key].color)) : C.fg3,
      borderRadius:10, padding:'2px 7px', fontFamily:'Roboto Mono', fontSize:7.5, letterSpacing:'0.06em',
      cursor:'pointer', flexShrink:0,
    }}>{label}</button>
  );
  return (
    <Sec title="Group Comms — All Parties" grow badge={
      <span style={{ display:'flex',alignItems:'center',gap:4 }}>
        <span style={{ width:5,height:5,borderRadius:'50%',background:C.normal,display:'inline-block',animation:'pulseFade 2s infinite' }} />
        <span style={{ fontFamily:'Roboto Mono',fontSize:7.5,color:C.normal }}>LIVE · {msgs.length}</span>
      </span>
    }>
      {/* role filter strip — fixed height, doesn't grow */}
      <div style={{ display:'flex',gap:4,marginBottom:6,flexWrap:'wrap',flexShrink:0 }}>
        {filterChip('all', 'ALL')}
        {Object.keys(roles).map(k => filterChip(k, roles[k].label))}
      </div>
      {/* Message scrollback — flex:1 so it absorbs whatever vertical space
          the parent gives us. minHeight ≈ "input + 2 messages" so a heavy
          action queue can't squash chat down to just the input bar. */}
      <div ref={scrollRef} onScroll={onScroll} style={{ flex:1,minHeight:120,overflowY:'auto',display:'flex',flexDirection:'column',gap:5,marginBottom:7 }}>
        {filtered.map((m,i)=>{
          const col = colorFor(m);
          const isTx = m.dir === 'tx';
          // Latest message at full contrast (fg1). Older messages step down
          // two design-token levels to fg3 (#888a8f · neutral-400 · tertiary)
          // so the focal point is clearly the newest traffic. Hover restores
          // fg1 so older entries are easy to re-read on intent.
          const isLatest = i === filtered.length - 1;
          return (
            <div key={m.id} style={{ display:'flex',flexDirection:'column',alignItems:isTx?'flex-end':'flex-start',gap:2 }}>
              <div style={{
                maxWidth:'92%', padding:'5px 8px', borderRadius:3,
                background: isTx ? col+'1a' : C.s3,
                borderLeft: isTx ? undefined : `3px solid ${col}`,
                border: `1px solid ${col}33`,
              }}>
                <p
                  style={{
                    margin:0,fontFamily:'Roboto Mono',fontSize:9.5,
                    color: isLatest ? C.fg1 : C.fg3,
                    lineHeight:1.45,
                    transition:'color 0.15s ease-out',
                  }}
                  onMouseEnter={isLatest ? undefined : (e)=>{ e.currentTarget.style.color = C.fg1; }}
                  onMouseLeave={isLatest ? undefined : (e)=>{ e.currentTarget.style.color = C.fg3; }}
                >{m.text}</p>
              </div>
              <span style={{ fontFamily:'Roboto Mono',fontSize:7.5,color:col,letterSpacing:'0.04em' }}>{m.from} · {m.time}</span>
            </div>
          );
        })}
      </div>
      {/* Input row — pinned at the bottom; never shrinks. */}
      <div style={{ display:'flex',gap:5,flexShrink:0 }}>
        <input value={draft} onChange={e=>setDraft(e.target.value)} onKeyDown={e=>e.key==='Enter'&&send()} placeholder="SUNDOWN — broadcast directive…"
          title="Compose a broadcast as SUNDOWN (Mission Commander callsign). Press Enter or TX to send."
          style={{ flex:1,background:C.s3,border:`1px solid ${C.border2}`,borderRadius:3,color:C.fg1,fontFamily:'Roboto Mono',fontSize:9.5,padding:'5px 7px',outline:'none' }} />
        <button onClick={send} title="TX — Transmit your composed message to the group comms log." style={{ background:C.accent,border:'none',borderRadius:3,color:'#fff',fontFamily:'Roboto',fontWeight:600,fontSize:11,padding:'5px 10px',cursor:'pointer' }}>TX</button>
      </div>
    </Sec>
  );
}

// ── Asset Status ──────────────────────────────────────────────
function AssetStatus({ oscId }) {
  // Fold DUKE 32 (initial OSC) in when it's been appointed — preserves the
  // callout we lost by removing the bottom CSARTF strip.
  const duke32 = { id:'bf6', cs:'DUKE 32', type:'F-15E', role:'INITIAL OSC', play:'0+18', fuel:25, bingo:45, nvg:true, sandy:true, status:'normal' };
  const displayAssets = oscId==='bf6' ? [duke32, ...window.ASSETS] : window.ASSETS;
  return (
    <Sec title="Asset Status — CSARTF" collapsible defaultOpen flush>
      {displayAssets.map(a=>{
        const col = a.status==='advisory'?C.advisory:C.normal;
        const isOsc = a.id===oscId || (oscId==='bf6'&&a.id==='bf6');
        return (
          <div key={a.id} title={`${a.cs} — ${a.type}, ${a.role}. ${isOsc ? 'Currently appointed OSC (On-Scene Commander). ' : ''}${a.sandy ? 'SANDY-qualified (CSAR escort role). ' : ''}Playtime ${a.play}.`} style={{ display:'flex',alignItems:'center',gap:8,padding:'6px 11px',borderBottom:`1px solid ${C.border}`,background:isOsc?C.accentM:'transparent' }}>
            <div style={{ width:4,height:4,borderRadius:'50%',background:col,flexShrink:0 }} />
            <div style={{ flex:1 }}>
              <div style={{ display:'flex',alignItems:'center',gap:5 }}>
                <span style={{ fontFamily:'Roboto Mono',fontSize:10,fontWeight:500,color:isOsc?C.accent:C.fg1 }}>{a.cs}</span>
                {isOsc && <Tag label="OSC" color={C.accent} />}
                {a.sandy && <Tag label="SANDY" color={C.accent2} />}
              </div>
              <span style={{ fontFamily:'Roboto Mono',fontSize:8,color:C.fg3 }}>{a.type} · {a.role} · {a.play}</span>
            </div>
            <div style={{ display:'flex',flexDirection:'column',alignItems:'flex-end',gap:2 }}>
              <div title={`Fuel: ${a.fuel}% remaining. Bingo at ${a.bingo} minutes.`} style={{ display:'flex',alignItems:'center',gap:4 }}>
                <div style={{ width:44,height:3,background:C.border,borderRadius:2,overflow:'hidden' }}>
                  <div style={{ width:`${a.fuel}%`,height:'100%',background:a.fuel<30?C.critical:a.fuel<55?C.advisory:C.normal,borderRadius:2 }} />
                </div>
                <span style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:C.fg2 }}>{a.fuel}%</span>
              </div>
              <span title="B — Bingo fuel time remaining in minutes. The point where the asset must turn for home or refuel." style={{ fontFamily:'Roboto Mono',fontSize:7.5,color:C.fg3 }}>B:{a.bingo}′</span>
            </div>
          </div>
        );
      })}
    </Sec>
  );
}

// ── Mission Timeline ──────────────────────────────────────────
function MissionTimeline({ elapsed, jprcDone, oscDone, j35Done=false, phase=0, rozPublished=false, fifteenLineSent=false }) {
  const phases = [
    {p:0,l:'Ph.0',sub:'Imm.',active:phase===0},{p:1,l:'Ph.1',sub:'Loc.',active:phase===1},{p:2,l:'Ph.2',sub:'Aspc.',active:phase===2},{p:3,l:'Ph.3',sub:'Exec.'},{p:4,l:'Ph.4',sub:'Egrs.'},
  ];
  const milestones = [
    {l:'JPRC Notify [T+1:00]',  done:jprcDone, deadline:60,  jtbd:'JTBD-02'},
    {l:'OSC Appoint [T+2:00]',  done:oscDone,  deadline:120, jtbd:'JTBD-03'},
    {l:'PR ROZ Drop [T+5:00]',  done:rozPublished, deadline:300, jtbd:'JTBD-15'},
    {l:'J3.1→J3.5 [T+10:00]',  done:j35Done,  deadline:600, jtbd:'JTBD-09'},
    {l:'15-Line Section A',     done:fifteenLineSent, deadline:null, jtbd:'JTBD-17'},
  ];
  return (
    <Sec title="Mission Timeline" collapsible defaultOpen>
      <div style={{ display:'flex',gap:3,marginBottom:9 }}>
        {phases.map(p=>(
          <div key={p.p} style={{ flex:1,padding:'3px 0',borderRadius:3,textAlign:'center',background:p.active?C.accentM:C.s3,border:`1px solid ${p.active?C.accent:C.border2}` }}>
            <div style={{ fontFamily:'Roboto Mono',fontSize:8,fontWeight:500,color:p.active?C.accent:C.fg3 }}>{p.l}</div>
          </div>
        ))}
      </div>
      <div style={{ display:'flex',flexDirection:'column',gap:4 }}>
        {milestones.map(m=>{
          const rem = m.deadline?Math.max(0,m.deadline-elapsed):null;
          const exp = rem===0;
          const c = m.done?C.normal:exp?C.critical:rem&&rem<30?C.advisory:C.fg3;
          return (
            <div key={m.l} style={{ display:'flex',alignItems:'center',gap:7 }}>
              <div style={{ width:8,height:8,borderRadius:2,background:m.done?C.normal:exp?C.critical:C.s3,border:`1px solid ${c}`,flexShrink:0 }} />
              <span style={{ fontFamily:'Roboto',fontSize:10,color:m.done?C.normal:exp?C.critical:C.fg2,flex:1 }}>{m.l}</span>
              <span style={{ fontFamily:'Roboto Mono',fontSize:8,color:c }}>{m.jtbd}</span>
            </div>
          );
        })}
      </div>
    </Sec>
  );
}

// ── Environmental widget ──────────────────────────────────────
function EnvWidget({ darkScore }) {
  const labels = ['DAYLIGHT','LOW LIGHT','OPERATIONAL DARK','DEGRADED DARK','FULL DARK'];
  const colors = ['#ffc14c','#fbab16','#39b7fa','#0068a1','#004f7e'];
  const cur = { l:labels[darkScore-1], c:colors[darkScore-1] };
  // Section uses the standard collapsible chevron — no separate "show more"
  // toggle. When open, we show the full readout (current state + key
  // timeline rows + moon callout). Collapse the section entirely to hide.
  return (
    <Sec title="Environmental" collapsible defaultOpen>
      <div style={{ display:'flex',alignItems:'center',gap:10,marginBottom:10 }}>
        <div style={{ display:'flex',gap:3 }}>
          {[1,2,3,4,5].map(i=>(
            <div key={i} title={labels[i-1]} style={{ width:13,height:13,borderRadius:2,background:i<=darkScore?colors[i-1]+'33':C.s3,border:`1px solid ${i===darkScore?cur.c:C.border}` }} />
          ))}
        </div>
        <div>
          <div style={{ fontFamily:'Roboto Mono',fontSize:9,fontWeight:500,color:cur.c }}>{cur.l}</div>
          <div style={{ fontFamily:'Roboto Mono',fontSize:7.5,color:C.fg3,marginTop:1 }}>AMA-29 · {darkScore}/5</div>
        </div>
      </div>
      <div style={{ display:'flex',flexDirection:'column',gap:4 }}>
        {[
          {l:'CIVIL TWILIGHT',v:'19:24L',done:true},{l:'NAUTICAL TWILIGHT',v:'19:51L',done:true},
          {l:'ASTRONOMICAL DARK',v:'20:19L',active:darkScore>=3},{l:'FULL DARK',v:'20:52L',active:darkScore>=5},{l:'MOONRISE',v:'23:38L'},
        ].map(r=>(
          <div key={r.l} style={{ display:'flex',alignItems:'center',gap:7 }}>
            <div style={{ width:5,height:5,borderRadius:'50%',background:r.active?cur.c:r.done?C.fg3:C.border2,flexShrink:0 }} />
            <span style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:r.active?cur.c:r.done?C.fg3:C.fg2,flex:1 }}>{r.l}</span>
            <span style={{ fontFamily:'Roboto Mono',fontSize:8.5,color:C.fg2 }}>{r.v}</span>
          </div>
        ))}
        <div style={{ marginTop:3,padding:'5px 7px',background:C.s3,borderRadius:3,border:`1px solid ${C.border2}` }}>
          <div style={{ fontFamily:'Roboto Mono',fontSize:7.5,color:C.fg3,marginBottom:2 }}>MOON · AMA-27</div>
          <div style={{ fontFamily:'Roboto Mono',fontSize:9.5,color:C.fg1 }}>12% · WAXING CRESCENT · NOT OPERATIONALLY SIGNIFICANT</div>
        </div>
      </div>
    </Sec>
  );
}

Object.assign(window, {
  WatchlistPanel, AorPostureRollup, BlueForceOverview, NormalAlertFeed, NormalWeather, MapLayersPanel, BasemapPicker,
  AlertFeed, ActionQueue, ActionQueueExpanded, CaseSummary, SurvivorRoster, CommsLog, AssetStatus, MissionTimeline, EnvWidget,
  CollapsibleRail, Sec,
  DTButton, CloseButton,
});
