/* AUTO-BUNDLED — edit the source .jsx files then rebuild */

/* ===== icons.jsx ===== */
/* ============================================================
   icons.jsx — Lucide icon wrapper + shared UI atoms
   ============================================================ */
const { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } = React;

/* Lucide UMD exposes PascalCase IconNodes. Map a few renamed/aliased ones. */
const ICON_ALIAS = {
  'train': 'TrainFront',
  'alert-triangle': 'TriangleAlert',
  'sliders': 'SlidersHorizontal',
  'shield': 'ShieldCheck',
  'circle-check': 'CircleCheck',
  'circle-alert': 'CircleAlert',
};
function toPascal(name) {
  return name.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
}
function lucideNode(name) {
  const L = window.lucide;
  if (!L) return null;
  const set = L.icons || L;
  const key = ICON_ALIAS[name] || toPascal(name);
  return set[key] || set[toPascal(name)] || null;
}

function Icon({ name, size = 18, stroke = 2, className = '', style }) {
  const ref = useRef(null);
  useEffect(() => {
    const node = lucideNode(name);
    const host = ref.current;
    if (!host) return;
    host.innerHTML = '';
    if (!node) { return; }
    const ns = 'http://www.w3.org/2000/svg';
    const svg = document.createElementNS(ns, 'svg');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('width', size);
    svg.setAttribute('height', size);
    svg.setAttribute('fill', 'none');
    svg.setAttribute('stroke', 'currentColor');
    svg.setAttribute('stroke-width', stroke);
    svg.setAttribute('stroke-linecap', 'round');
    svg.setAttribute('stroke-linejoin', 'round');
    // Lucide IconNode shape: ['svg', svgAttrs, [ [tag, attrs], ... ]]
    const childNodes = Array.isArray(node[2]) ? node[2]
      : (Array.isArray(node) && Array.isArray(node[0]) ? node : []);
    childNodes.forEach(child => {
      if (!Array.isArray(child)) return;
      const [tag, attrs] = child;
      const el = document.createElementNS(ns, tag);
      Object.entries(attrs || {}).forEach(([k, v]) => el.setAttribute(k, v));
      svg.appendChild(el);
    });
    host.appendChild(svg);
  }, [name, size, stroke]);
  return <span ref={ref} className={className} style={{ display: 'inline-flex', lineHeight: 0, ...style }} aria-hidden="true" />;
}

/* ---- mode color tokens ---- */
const MODE = {
  rail:     { color: '#0F172A', soft: '#e2e8f0', text: '#0F172A', icon: 'train',   label: 'High-speed rail' },
  air:      { color: '#0284C7', soft: '#e0f2fe', text: '#075985', icon: 'plane',   label: 'Aviation' },
  local:    { color: '#059669', soft: '#d1fae5', text: '#065f46', icon: 'tram-front', label: 'Regional transit' },
  coach:    { color: '#4f46e5', soft: '#e0e7ff', text: '#3730a3', icon: 'bus',     label: 'Coach' },
  transfer: { color: '#D97706', soft: '#fef3c7', text: '#92400e', icon: 'footprints', label: 'Transfer' },
  walk:     { color: '#64748b', soft: '#f1f5f9', text: '#475569', icon: 'footprints', label: 'On foot' },
};

/* ---- small pill ---- */
function Chip({ children, tone = 'slate', icon, className = '' }) {
  const tones = {
    slate: 'bg-slate-100 text-slate-600',
    rail:  'bg-slate-900 text-white',
    air:   'bg-sky-50 text-sky-700 ring-1 ring-sky-200',
    local: 'bg-emerald-50 text-emerald-700 ring-1 ring-emerald-200',
    warn:  'bg-amber-50 text-amber-700 ring-1 ring-amber-200',
    red:   'bg-red-50 text-red-700 ring-1 ring-red-200',
    violet:'bg-violet-50 text-violet-700 ring-1 ring-violet-200',
    green: 'bg-emerald-600 text-white',
  };
  return (
    <span className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-semibold tracking-wide ${tones[tone]} ${className}`}>
      {icon && <Icon name={icon} size={12} stroke={2.4} />}
      {children}
    </span>
  );
}

/* ---- circular score dial ---- */
function ScoreRing({ value, size = 64, stroke = 6, tone = '#D97706', label = 'SCORE' }) {
  const r = (size - stroke) / 2;
  const c = 2 * Math.PI * r;
  const off = c * (1 - Math.max(0, Math.min(100, value)) / 100);
  return (
    <div className="relative inline-flex items-center justify-center" style={{ width: size, height: size }}>
      <svg width={size} height={size} className="-rotate-90">
        <circle cx={size/2} cy={size/2} r={r} fill="none" stroke="#eef2f7" strokeWidth={stroke} />
        <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={tone} strokeWidth={stroke}
          strokeLinecap="round" strokeDasharray={c} strokeDashoffset={off}
          style={{ transition: 'stroke-dashoffset .7s cubic-bezier(.22,.61,.36,1)' }} />
      </svg>
      <div className="absolute inset-0 flex flex-col items-center justify-center">
        <span className="num-tab font-bold leading-none" style={{ fontSize: size * 0.30, color: '#0f172a' }}>{Math.round(value)}</span>
        <span className="text-[8px] font-semibold tracking-widest text-slate-400 mt-0.5">{label}</span>
      </div>
    </div>
  );
}

/* horizontal index bar for the cost breakdown */
function IndexBar({ label, value, tone, hint }) {
  return (
    <div>
      <div className="flex items-baseline justify-between mb-1">
        <span className="text-[12px] font-medium text-slate-600">{label}</span>
        <span className="num-tab text-[12px] font-bold text-slate-800">{Math.round(value)}<span className="text-slate-400 font-normal">/100</span></span>
      </div>
      <div className="h-1.5 rounded-full bg-slate-100 overflow-hidden">
        <div className="h-full rounded-full" style={{ width: `${value}%`, background: tone, transition: 'width .6s ease' }} />
      </div>
      {hint && <p className="text-[11px] text-slate-400 mt-1 leading-snug">{hint}</p>}
    </div>
  );
}

Object.assign(window, { Icon, MODE, Chip, ScoreRing, IndexBar });


/* ===== data.jsx ===== */
/* ============================================================
   data.jsx — comfort model, generalized-cost helpers, formatting
   Route data and geocoding are handled by api.js / MOTIS backend.
   ============================================================ */

/* ---------- formatting helpers ---------- */
function fmtDur(min) {
  min = Math.round(min);
  const h = Math.floor(min / 60), m = min % 60;
  if (h === 0) return `${m}m`;
  return m === 0 ? `${h}h` : `${h}h ${String(m).padStart(2, '0')}m`;
}
function eur(n) { return n == null ? '—' : '€' + Number(n).toFixed(2); }

/* ---------- comfort model ---------- */
const COMFORT = {
  tight:    { key:'tight',    label:'Tight',    air:90,  rail:10, tone:'#dc2626', toneName:'red',    badge:'Higher risk',
              blurb:'Minimum legal connection times. A single delay can break the journey.' },
  balanced: { key:'balanced', label:'Balanced', air:180, rail:25, tone:'#059669', toneName:'emerald', badge:'Optimal',
              blurb:'Comfortable safety margins. Recommended for most travellers.' },
  relaxed:  { key:'relaxed',  label:'Relaxed',  air:270, rail:45, tone:'#7c3aed', toneName:'violet',  badge:'Family-friendly',
              blurb:'Generous buffers for families and heavy checked baggage.' },
};
const COMFORT_ORDER = ['tight', 'balanced', 'relaxed'];

function connectionMinutes(leg, comfort, manual) {
  const air = manual?.air ?? comfort.air;
  const rail = manual?.rail ?? comfort.rail;
  switch (leg.kind) {
    case 'self':     return Math.max(leg.walk, leg.connType === 'air' ? air : rail) + (leg.recheck || 0);
    case 'security': return Math.round((leg.connType === 'air' ? air : rail) * 0.72) + 5;
    case 'cross':    return Math.max(leg.walk, rail);
    case 'reclaim':  return leg.walk + Math.round(rail * 0.5);
    default:         return leg.walk;
  }
}

function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }

Object.assign(window, {
  fmtDur, eur, COMFORT, COMFORT_ORDER, connectionMinutes, clamp,
});


/* ===== panel.jsx ===== */
/* ============================================================
   panel.jsx — compact "bar cell" fields for the centered search
   COMP-101 Autocomplete · COMP-102 DateTime / Zone
   ============================================================ */

/* ---------------- COMP-101 : Geographic Autocomplete (bar cell) ---------------- */
function LocationField({ label, value, onChange, placeholder, icon }) {
  const [q, setQ] = useState('');
  const [open, setOpen] = useState(false);
  const [error, setError] = useState(false);
  const [showLocal, setShowLocal] = useState(false);
  const [groups, setGroups] = useState([]);
  const [geocoding, setGeocoding] = useState(false);
  const boxRef = useRef(null);
  const inputRef = useRef(null);
  const debounceRef = useRef(null);

  useEffect(() => {
    function onDoc(e) { if (boxRef.current && !boxRef.current.contains(e.target)) setOpen(false); }
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, []);

  useEffect(() => {
    if (debounceRef.current) clearTimeout(debounceRef.current);
    if (!q.trim()) { setGroups([]); return; }
    debounceRef.current = setTimeout(async () => {
      setGeocoding(true);
      try {
        const results = await searchLocations(q);
        setGroups(results);
      } catch (e) {
        console.warn('[LocationField] searchLocations failed:', e);
        setGroups([]);
      } finally {
        setGeocoding(false);
      }
    }, 300);
    return () => { if (debounceRef.current) clearTimeout(debounceRef.current); };
  }, [q]);

  function pick(loc) { onChange(loc); setError(false); setOpen(false); setQ(''); setGroups([]); }
  function commitTyped() {
    if (value || q.trim().length === 0 || geocoding) return;
    if (groups.length === 0) setError(true);
  }
  function clearValue() { onChange(null); setQ(''); setGroups([]); setOpen(true); setTimeout(() => inputRef.current && inputRef.current.focus(), 30); }

  const modeIcon = value
    ? (value.modes.includes('air') && value.modes.includes('rail') ? 'building-2' : value.modes.includes('air') ? 'plane' : 'train')
    : icon;

  return (
    <div ref={boxRef} className="relative flex-1 min-w-0">
      {value ? (
        <button type="button" onClick={clearValue}
          className="group w-full text-left px-4 py-2 rounded-xl hover:bg-slate-50 transition">
          <span className="block text-[10px] font-bold uppercase tracking-wider text-slate-400">{label}</span>
          <span className="flex items-center gap-2 mt-0.5">
            <Icon name={modeIcon} size={15} className="text-slate-500 shrink-0" />
            <span className="text-[15px] font-semibold text-slate-900 truncate">{value.type==='metro' ? value.city : value.name}</span>
            <span className="num-tab text-[11px] font-bold px-1.5 py-0.5 rounded bg-emerald-50 text-emerald-700 shrink-0">{value.code}</span>
            <Icon name="chevron-down" size={13} className="text-slate-300 ml-auto opacity-0 group-hover:opacity-100 transition" />
          </span>
        </button>
      ) : (
        <div className="px-4 py-2 rounded-xl">
          <span className="block text-[10px] font-bold uppercase tracking-wider text-slate-400">{label}</span>
          <span className="flex items-center gap-2 mt-0.5">
            <Icon name={icon} size={15} className="text-slate-400 shrink-0" />
            <input ref={inputRef} value={q}
              onChange={e => { setQ(e.target.value); setOpen(true); setError(false); }}
              onFocus={() => setOpen(true)} onBlur={commitTyped}
              placeholder={placeholder}
              className="w-full bg-transparent text-[15px] font-medium outline-none placeholder:text-slate-400 placeholder:font-normal" />
            {geocoding && <Icon name="loader-circle" size={13} className="text-slate-300 shrink-0 animate-spin" />}
          </span>
        </div>
      )}

      {error && (
        <p className="absolute top-full left-3 mt-1 z-30 text-[11px] font-medium text-red-600 flex items-center gap-1 whitespace-nowrap bg-white px-1 fade-in">
          <Icon name="circle-alert" size={12} /> Not recognized — pick a valid transit hub
        </p>
      )}

      {open && !value && groups.length > 0 && (
        <div className="absolute z-40 left-1 right-1 top-full mt-2 rounded-xl border border-slate-200 bg-white shadow-xl shadow-slate-300/30 overflow-hidden fade-up min-w-[300px]">
          <div className="max-h-[340px] overflow-y-auto py-1">
            {groups.map(g => {
              const metro = g.items.filter(i => i.type === 'metro');
              const hubs = g.items.filter(i => i.type === 'hub');
              const locals = g.items.filter(i => i.type === 'local');
              return (
                <div key={g.city} className="py-1">
                  <div className="px-3 py-1 flex items-center gap-2">
                    <span className="text-[10px] font-bold uppercase tracking-widest text-slate-400">{g.city}</span>
                    <span className="text-[10px] text-slate-300">{g.country}</span>
                  </div>
                  {metro.map(l => <Suggestion key={l.id} loc={l} onPick={pick} parent />)}
                  {hubs.map(l => <Suggestion key={l.id} loc={l} onPick={pick} />)}
                  {locals.length > 0 && (
                    <div>
                      <button type="button" onMouseDown={e=>{e.preventDefault();setShowLocal(s=>!s);}}
                        className="w-full flex items-center gap-1.5 pl-12 pr-3 py-1.5 text-[11.5px] font-medium text-slate-400 hover:text-slate-600">
                        <Icon name={showLocal ? 'chevron-down' : 'chevron-right'} size={13} />
                        {showLocal ? 'Hide' : 'Show'} {locals.length} local stop{locals.length>1?'s':''}
                      </button>
                      {showLocal && locals.map(l => <Suggestion key={l.id} loc={l} onPick={pick} local />)}
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

function Suggestion({ loc, onPick, parent, local }) {
  const m = loc.modes;
  const ic = m.includes('air') && m.includes('rail') ? 'building-2' : m.includes('air') ? 'plane' : 'train';
  return (
    <button type="button" onMouseDown={e=>{e.preventDefault(); onPick(loc);}}
      className={`w-full flex items-center gap-3 px-3 py-2 hover:bg-slate-50 text-left ${local ? 'pl-12' : ''}`}>
      <span className={`grid place-items-center rounded-lg shrink-0 ${parent ? 'w-8 h-8 bg-slate-900 text-white' : local ? 'w-7 h-7 bg-slate-100 text-slate-500' : 'w-8 h-8 bg-slate-100 text-slate-700'}`}>
        <Icon name={ic} size={parent ? 16 : 14} />
      </span>
      <span className="min-w-0 flex-1">
        <span className={`block truncate ${parent ? 'text-[13.5px] font-bold text-slate-900' : 'text-[13px] font-medium text-slate-700'}`}>{loc.name}</span>
        {loc.note && <span className="block text-[11px] text-slate-400">{loc.note}</span>}
      </span>
      <span className="num-tab text-[11px] font-bold text-slate-400 shrink-0">{loc.code}</span>
    </button>
  );
}

/* ---------------- COMP-102 : DateTime / Zone (bar cell) ---------------- */
const TZ = {
  London: 'GMT · London', Zürich: 'CET · Zürich', Paris: 'CET · Paris',
  Amsterdam: 'CET · Amsterdam', Berlin: 'CET · Berlin',
};
function curfewWarning(mode, value, destCity) {
  if (mode !== 'arrive' || !value) return null;
  const t = new Date(value);
  const h = t.getHours() + t.getMinutes() / 60;
  if (h >= 20 || h < 6) return `${destCity || 'Destination'} airport observes a 23:00–06:00 curfew — arriving this late risks a missed final connection.`;
  return null;
}
function DateTimeField({ mode, setMode, value, onChange }) {
  return (
    <div className="px-4 py-2 rounded-xl min-w-[176px]">
      <div className="flex items-center gap-1 mb-0.5">
        {[['depart','Depart'],['arrive','Arrive by']].map(([k, lbl]) => (
          <button key={k} type="button" onClick={() => setMode(k)}
            className={`text-[10px] font-bold uppercase tracking-wider transition ${mode===k ? 'text-slate-900' : 'text-slate-300 hover:text-slate-500'}`}>
            {lbl}{k==='depart' && <span className="text-slate-200 mx-1">/</span>}
          </button>
        ))}
      </div>
      <div className="flex items-center gap-2">
        <Icon name="calendar" size={15} className="text-slate-400 shrink-0" />
        <input type="datetime-local" value={value} onChange={e => onChange(e.target.value)}
          className="w-full bg-transparent text-[13.5px] font-medium num-tab outline-none" />
      </div>
    </div>
  );
}

Object.assign(window, { LocationField, DateTimeField, TZ, curfewWarning });


/* ===== controls.jsx ===== */
/* ============================================================
   controls.jsx — centered Google-Flights-style SearchBar
   ============================================================ */

/* ---------------- centered SearchBar ---------------- */
function SearchBar({ s, set, onSearch, busy }) {
  function swap() { set({ origin: s.destination, destination: s.origin }); }
  const ready = s.origin && s.destination;
  const tz = TZ[s.destination?.city];
  const curfew = curfewWarning(s.timeMode, s.datetime, s.destination?.city);

  return (
    <div className="rounded-2xl bg-white border border-slate-200 shadow-lg shadow-slate-300/30">
      {/* main bar */}
      <div className="flex flex-col sm:flex-row items-stretch p-1.5">
        <LocationField label="From" icon="map-pin" placeholder="City, hub or code"
          value={s.origin} onChange={v => set({ origin: v })} />

        <div className="flex items-center justify-center px-1 sm:py-0 py-1">
          <button type="button" onClick={swap} disabled={!s.origin && !s.destination}
            className="grid place-items-center w-9 h-9 rounded-full border border-slate-200 bg-white text-slate-500 hover:text-slate-900 hover:border-slate-300 transition-all disabled:opacity-40 hover:rotate-180">
            <span className="sm:hidden inline-flex items-center justify-center"><Icon name="arrow-up-down" size={15} /></span>
            <span className="hidden sm:inline-flex items-center justify-center"><Icon name="arrow-left-right" size={15} /></span>
          </button>
        </div>

        <LocationField label="To" icon="navigation" placeholder="City, hub or code"
          value={s.destination} onChange={v => set({ destination: v })} />

        <div className="hidden sm:block w-px self-stretch bg-slate-100 my-1.5" />

        <DateTimeField mode={s.timeMode} setMode={v => set({ timeMode: v })}
          value={s.datetime} onChange={v => set({ datetime: v })} />

        <div className="p-1 flex">
          <button type="button" onClick={onSearch} disabled={!ready || busy}
            className="w-full sm:w-auto flex items-center justify-center gap-2 rounded-xl bg-slate-900 text-white px-5 py-3 sm:py-0 sm:h-auto text-[14px] font-semibold tracking-wide hover:bg-slate-800 active:scale-[.98] transition disabled:opacity-40 disabled:cursor-not-allowed">
            {busy ? <Icon name="loader-circle" size={18} className="animate-spin" /> : <Icon name="search" size={18} />}
            <span className="sm:hidden lg:inline">{busy ? 'Searching…' : 'Search'}</span>
          </button>
        </div>
      </div>

      {curfew && (
        <div className="border-t border-amber-100 bg-amber-50 px-4 py-2 flex items-center gap-2 text-[11.5px] font-medium text-amber-700 rounded-b-2xl">
          <Icon name="alert-triangle" size={13} className="shrink-0" /> {curfew}
        </div>
      )}
    </div>
  );
}

Object.assign(window, { SearchBar });


/* ===== results.jsx ===== */
/* ============================================================
   results.jsx — COMP-103 telemetry + skeletons,
                 one-per-line route rows, intermodal timeline,
                 generalized-cost (Smart Score) breakdown
   ============================================================ */

/* ---------------- COMP-103 : telemetry console ---------------- */
const TELEMETRY = [
  { t: 0,    icon:'train',  text:'Searching rail corridors…' },
  { t: 1500, icon:'plane',  text:'Fetching flight schedules…' },
  { t: 3000, icon:'shield', text:'Calculating connection safety margins…' },
];
function TelemetryConsole({ stage }) {
  return (
    <div className="rounded-xl bg-slate-900 text-slate-100 p-4 font-mono shadow-lg shadow-slate-900/10 overflow-hidden">
      <div className="flex items-center gap-1.5 mb-3">
        <span className="w-2.5 h-2.5 rounded-full bg-red-400/80" />
        <span className="w-2.5 h-2.5 rounded-full bg-amber-400/80" />
        <span className="w-2.5 h-2.5 rounded-full bg-emerald-400/80" />
        <span className="ml-2 text-[10.5px] uppercase tracking-widest text-slate-400">router · live query</span>
      </div>
      <div className="space-y-1.5 text-[12px]">
        {TELEMETRY.map((line, i) => {
          const active = i === stage, done = i < stage;
          return (
            <div key={i} className="telem-line flex items-center gap-2" style={{ opacity: i > stage ? 0.25 : done ? 0.4 : 1 }}>
              <span className={done ? 'text-emerald-400' : active ? 'text-sky-400' : 'text-slate-600'}>
                <Icon name={done ? 'check' : line.icon} size={13} />
              </span>
              <span className={done ? 'text-slate-400 line-through decoration-slate-600' : 'text-slate-200'}>{line.text}</span>
              {active && <span className="text-sky-400 caret">▌</span>}
            </div>
          );
        })}
      </div>
    </div>
  );
}

function SkeletonRow({ active }) {
  return (
    <div className={`rounded-2xl border border-slate-200 bg-white p-4 ${active ? '' : 'opacity-50'}`}>
      <div className="flex items-center gap-4">
        <div className="skeleton h-16 w-16 rounded-full shrink-0" />
        <div className="w-40 shrink-0 space-y-2"><div className="skeleton h-5 w-24" /><div className="skeleton h-3 w-32" /></div>
        <div className="flex-1 space-y-2"><div className="skeleton h-9 w-full" /><div className="skeleton h-2 w-2/3" /></div>
        <div className="w-24 shrink-0 space-y-2"><div className="skeleton h-6 w-20 ml-auto" /><div className="skeleton h-3 w-16 ml-auto" /></div>
        <div className="w-28 shrink-0"><div className="skeleton h-9 w-full" /></div>
      </div>
    </div>
  );
}

/* ---------------- intermodal journey strip — CLASSIC (fixed transfer width) ---------------- */
function TimelineClassic({ route }) {
  const transitLegs = route.legs.filter(l => !l.transfer);
  const transitTotal = transitLegs.reduce((s, l) => s + l.dur, 0) || 1;
  return (
    <div className="w-full">
      <div className="flex items-stretch" style={{ height: 36, gap: 2 }}>
        {route.legs.map((l, i) => {
          if (l.transfer) {
            const isRisky = l.risk;
            return (
              <div key={i} className="shrink-0 flex flex-col items-center justify-center" style={{ width: 26 }}>
                <div style={{ width: 2, flex: 1, background: isRisky ? '#fca5a5' : '#fcd34d' }} />
                <span className="grid place-items-center rounded-full shrink-0"
                  style={{ width: 20, height: 20, background: isRisky ? '#fee2e2' : '#fef3c7',
                    border: `1.5px solid ${isRisky ? '#f87171' : '#fbbf24'}`,
                    color: isRisky ? '#dc2626' : '#b45309' }}
                  title={`${fmtDur(l.dur)} — ${l.note || 'Transfer'}`}>
                  <Icon name={l.kind==='security'?'shield':l.kind==='reclaim'?'luggage':'footprints'} size={10} stroke={2.2} />
                </span>
                <div style={{ width: 2, flex: 1, background: isRisky ? '#fca5a5' : '#fcd34d' }} />
              </div>
            );
          }
          const m = MODE[l.mode];
          const widthRatio = l.dur / transitTotal;
          return (
            <div key={i} className="relative rounded-xl flex items-center justify-center overflow-hidden"
              style={{ flex: widthRatio, minWidth: 40, background: m.color }}
              title={`${l.op}: ${l.from} → ${l.to} · ${fmtDur(l.dur)}`}>
              <div className="absolute inset-0" style={{ background: 'linear-gradient(160deg,rgba(255,255,255,.13) 0%,transparent 60%)' }} />
              <div className="relative flex items-center gap-1.5 px-2 max-w-full overflow-hidden">
                <Icon name={l.icon || m.icon} size={14} className="text-white shrink-0" stroke={1.8} />
                {widthRatio > 0.18 && (
                  <div className="min-w-0 overflow-hidden">
                    <p style={{ fontSize: 10, fontWeight: 700, color: '#fff', lineHeight: 1, marginBottom: 2 }} className="truncate">{l.op}</p>
                    {widthRatio > 0.10 && <p style={{ fontSize: 9, color: 'rgba(255,255,255,0.65)', lineHeight: 1 }} className="truncate">{fmtDur(l.dur)}</p>}
                  </div>
                )}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* ---------------- intermodal journey strip — PROPORTIONAL (all legs scaled to real time) ---------------- */
function Timeline({ route, height = 'h-9' }) {
  const total = route.legs.reduce((s, l) => s + (l.dur || 0), 0) || 1;
  return (
    <div className="w-full">
      <div className={`flex items-stretch gap-[3px] ${height} overflow-hidden`}>
        {route.legs.map((l, i) => {
          const flex = Math.max(l.dur / total, 0.05);
          if (l.transfer) {
            return (
              <div key={i} title={`${l.from} → ${l.to} · ${fmtDur(l.dur)}`} style={{ flex }}
                className={`relative rounded-md min-w-[24px] grid place-items-center ${l.risk ? 'hatch-red ring-1 ring-red-300' : 'hatch-amber'}`}>
                <Icon name={l.kind==='security'?'shield':l.kind==='reclaim'?'luggage':'footprints'} size={12} className={l.risk ? 'text-red-600' : 'text-amber-700'} stroke={2.4} />
                {l.risk && <span className="absolute -top-1.5 -right-1.5"><Icon name="alert-triangle" size={12} className="text-red-600" /></span>}
              </div>
            );
          }
          const m = MODE[l.mode];
          return (
            <div key={i} title={`${l.op}: ${l.from} → ${l.to} · ${fmtDur(l.dur)}`} style={{ flex, background: m.color }}
              className="relative rounded-md min-w-[30px] grid place-items-center shadow-sm">
              <Icon name={l.icon || m.icon} size={13} className="text-white" stroke={2.2} />
            </div>
          );
        })}
      </div>
    </div>
  );
}


/* ---------------- clear leg-by-leg itinerary ---------------- */
function transferActivity(l) {
  switch (l.kind) {
    case 'self':     return { title: l.note ? l.note.split('—')[0].trim() : 'Self-transfer between stations', sub: 'Separate tickets — collect & re-check your baggage', icon: 'footprints' };
    case 'security': return { title: 'Check-in, security & boarding', sub: 'Bag drop, screening and walk to the gate', icon: 'shield' };
    case 'cross':    return { title: l.note ? l.note.split('—')[0].trim() : 'Cross-station transfer', sub: 'Protected connection — onward travel guaranteed', icon: 'footprints' };
    case 'reclaim':  return { title: 'Baggage reclaim & walk to platform', sub: 'Collect checked bags, then to the rail platform', icon: 'luggage' };
    default:         return { title: l.note || 'Transfer', sub: '', icon: 'footprints' };
  }
}

function ItineraryBreakdown({ route, startISO, showTimes }) {
  const fmtClock = ms => new Date(ms).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
  const titleCase = s => (s || '').toLowerCase().replace(/\b\w/g, c => c.toUpperCase());
  const hasTimes = showTimes && !!startISO;
  const startDay = startISO ? new Date(new Date(startISO).toDateString()).getTime() : 0;
  const dayOf = ms => ms ? Math.round((new Date(new Date(ms).toDateString()).getTime() - startDay) / 86400000) : 0;
  const ClockCell = ({ ms, bold }) => {
    const d = dayOf(ms);
    return (
      <p className={`num-tab text-[15px] leading-none ${bold ? 'font-bold text-slate-900' : 'font-semibold text-slate-600'}`}>
        {fmtClock(ms)}{d > 0 && <sup className="num-tab text-[9px] font-bold text-sky-500 ml-0.5">+{d}</sup>}
      </p>
    );
  };
  let t = startISO ? new Date(startISO).getTime() : 0;

  // shared column widths: [time 56px][rail 28px][content flex-1]
  const Row = ({ time, marker, connector, children, pad = 'pb-3' }) => (
    <div className="flex">
      <div className="w-14 shrink-0 text-right pr-3 pt-px">{time}</div>
      <div className="w-7 shrink-0 flex flex-col items-center">
        {marker}
        {connector !== false && <span className="w-px flex-1 mt-1" style={{ background: connector }} />}
      </div>
      <div className={`flex-1 min-w-0 ${pad}`}>{children}</div>
    </div>
  );

  return (
    <div className="fade-up pt-4 mt-4 border-t border-slate-100">
      <div className="flex items-center justify-between mb-4">
        <p className="text-[10px] font-semibold uppercase tracking-widest text-slate-400">{route.walkOnly ? 'Walking directions' : 'Full itinerary'}</p>
        <span className="num-tab text-[11px] font-semibold text-slate-400">{fmtDur(route.totalDur)} {route.walkOnly ? 'walk' : 'door to door'}</span>
      </div>

      {route.legs.map((l, i) => {
        const dep = t; t += l.dur * 60000; const arr = t;
        const hasNext = i < route.legs.length - 1;

        if (l.transfer) {
          const a = transferActivity(l);
          const tone = l.risk ? '#dc2626' : '#b45309';
          const dash = 'repeating-linear-gradient(#cbd5e1 0 3px, transparent 3px 7px)';
          const prevLeg = route.legs.slice(0, i).reverse().find(x => !x.transfer);
          const nextLeg = route.legs.slice(i + 1).find(x => !x.transfer);
          const gapMs = (prevLeg?.arrMs && nextLeg?.depMs) ? nextLeg.depMs - prevLeg.arrMs : null;
          const walkMin = l.walk || l.dur || 0;
          const waitMin = gapMs !== null ? Math.max(0, Math.round(gapMs / 60000) - walkMin) : null;
          const durLabel = [
            walkMin > 0 ? `${fmtDur(walkMin)} walk` : null,
            waitMin > 0 ? `${fmtDur(waitMin)} wait` : null,
          ].filter(Boolean).join(', ');
          return (
            <Row key={i}
              marker={
                <div className="h-full flex flex-col items-center">
                  <span className="w-px flex-1" style={{ background: dash }} />
                  <span className="grid place-items-center w-6 h-6 rounded-full shrink-0"
                    style={{ background: l.risk ? '#fee2e2' : '#fef3c7', color: tone }}>
                    <Icon name={a.icon} size={12} stroke={2.2} />
                  </span>
                  <span className="w-px flex-1" style={{ background: dash }} />
                </div>}
              connector={false}
              pad="py-4">
              <p className="text-[12px] font-semibold leading-snug" style={{ color: tone }}>
                {a.title}
                {durLabel && <span className="font-normal text-slate-400 ml-1">({durLabel})</span>}
              </p>
              {a.sub && <p className="text-[11px] text-slate-400 mt-0.5 leading-snug">{a.sub}</p>}
              {l.risk && <p className="text-[11px] font-medium text-red-600 mt-1 flex items-center gap-1"><Icon name="alert-triangle" size={11} /> Tight connection</p>}
            </Row>
          );
        }

        const m = MODE[l.mode];
        const showDep = l.depMs || dep;
        const showArr = l.arrMs || arr;
        const nextIsTransfer = hasNext && route.legs[i + 1]?.transfer;
        const prevLeg = i > 0 ? route.legs[i - 1] : null;
        const suppressDep = prevLeg != null && !prevLeg.transfer && prevLeg.to === l.from;

        return (
          <React.Fragment key={i}>
            {/* Departure station — omitted when the previous leg already named this stop */}
            {!suppressDep && (
            <Row
              time={hasTimes && <ClockCell ms={showDep} bold />}
              marker={<span className="w-4 h-4 rounded-full shrink-0" style={{ background: m.color }} />}
              connector={m.color + '55'}
              pad="pb-1">
              <p className="text-[17px] font-bold text-slate-900 leading-tight">{titleCase(l.from)}</p>
            </Row>
            )}

            {/* Leg body — operator info along the connector line */}
            <div className="flex">
              <div className="w-14 shrink-0" />
              <div className="w-7 shrink-0 flex flex-col items-center">
                <span className="w-px flex-1" style={{ background: m.color + '55' }} />
              </div>
              <div className="flex-1 min-w-0 py-2.5">
                <div className="flex items-center gap-2 flex-wrap">
                  <span className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[12px] font-bold"
                    style={{ background: m.soft, color: m.color }}>
                    <Icon name={l.icon || m.icon} size={13} stroke={2.4} />
                    {l.op && <span>{l.op}</span>}
                  </span>
                  {l.route && l.route !== l.op && (
                    <span className="inline-flex items-center rounded-md px-1.5 py-0.5 text-[12px] font-bold"
                      style={{ background: m.soft, color: m.color }}>
                      {l.route}
                    </span>
                  )}
                  <span className="text-[12px] font-medium text-slate-400">{l.night ? 'Night train' : m.label}</span>
                  {l.mode === 'walk' && l.distance > 0 && (
                    <span className="text-[12px] text-slate-400">
                      · {l.distance >= 1000 ? `~${(l.distance / 1000).toFixed(1)} km` : `~${Math.round(l.distance)} m`}
                    </span>
                  )}
                  {l.mode !== 'walk' && <span className="num-tab text-[12px] font-semibold text-slate-400 ml-auto">{fmtDur(l.dur)}</span>}
                </div>
              </div>
            </div>

            {/* Arrival station */}
            <Row
              time={hasTimes && <ClockCell ms={showArr} />}
              marker={<span className="w-4 h-4 rounded-full shrink-0" style={{ background: '#fff', border: `2px solid ${m.color}` }} />}
              connector={hasNext && !nextIsTransfer ? '#e2e8f0' : false}
              pad={hasNext && !nextIsTransfer ? 'pb-1' : 'pb-0'}>
              <p className="text-[17px] font-semibold text-slate-700 leading-tight">{titleCase(l.to)}</p>
            </Row>
          </React.Fragment>
        );
      })}

      {hasTimes && <p className="text-[10.5px] text-slate-400 mt-4 pl-[84px]">Clock times in departure local time · destination may be in a different zone.</p>}
    </div>
  );
}

/* ---------------- score-less rank badge ---------------- */
function RankBadge({ rank, tone, top }) {
  return (
    <div className="shrink-0 grid place-items-center w-16">
      <div className="grid place-items-center w-12 h-12 rounded-full num-tab font-bold text-[20px]"
        style={top ? { background: tone, color: '#fff' } : { background: tone + '14', color: tone, boxShadow: `inset 0 0 0 2px ${tone}33` }}>
        {rank}
      </div>
      <span className="text-[8px] font-bold tracking-widest mt-1" style={{ color: top ? tone : '#94a3b8' }}>{top ? 'TOP PICK' : 'RANKED'}</span>
    </div>
  );
}

/* ---------------- route card (chronological timetable row) ---------------- */
function RouteCard({ route, cardRef, onSelect, startISO, rank, cardStyle, highlightRec = true, showItinTimes = true, timelineVariant = 'proportional', open = false, onToggle }) {
  const top = route.isTop && highlightRec;

  const fmtClock = ms => new Date(ms).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
  const depMs = route.depMs ?? null;
  const arrMs = route.arrMs ?? null;
  const dayOffset = (depMs && arrMs)
    ? Math.round((new Date(new Date(arrMs).toDateString()) - new Date(new Date(depMs).toDateString())) / 86400000)
    : 0;
  const itinStart = depMs ? new Date(depMs).toISOString() : startISO;
  const originCode = route.legs[0].fromCode || route.legs.find(l => !l.transfer)?.fromCode || '';
  const destCode = [...route.legs].reverse().find(l => !l.transfer)?.toCode || '';

  const BADGES = {
    cheapest:  { color:'#059669', label:'Cheapest',    ic:'piggy-bank' },
    fastest:   { color:'#0F172A', label:'Fastest',     ic:'zap'        },
    smartest:  { color:'#D97706', label:'Smartest',    ic:'star'       },
    overnight: { color:'#4338ca', label:'Overnight',   ic:'moon'       },
    coach:     { color:'#0d9488', label:'Coach saver', ic:'bus'        },
  };
  const badge = BADGES[route.id] || { color: route.tone || '#334155', label: route.kind, ic: route.badgeIcon || 'route' };

  return (
    <div ref={cardRef} className="flip">
      <div className="relative rounded-2xl bg-white overflow-hidden lift border border-slate-200 shadow-sm">
        {/* \u2500\u2500 Mobile layout (< sm) \u2500\u2500 */}
        <div className="sm:hidden">
          {/* Row 1: times + badge + duration */}
          <div className="flex items-center gap-2 px-3 py-3">
            <div className="text-center shrink-0">
              <p className="num-tab leading-none" style={{ fontSize: 16, fontWeight: 700, color: '#0f172a' }}>{depMs ? fmtClock(depMs) : '\u2014'}</p>
            </div>
            <span className="inline-flex items-center gap-1 rounded-full text-white px-2 py-0.5 shrink-0"
              style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.04em', background: badge.color }}>
              <Icon name={badge.ic} size={10} className={badge.ic === 'star' ? 'fill-white' : ''} />
              {badge.label}
            </span>
            <div className="flex-1 flex flex-col items-center">
              <div className="flex items-center gap-0.5">
                <Icon name="arrow-right" size={10} stroke={2.5} className="text-slate-400" />
                <span className="num-tab" style={{ fontSize: 10, fontWeight: 600, color: '#64748b' }}>{fmtDur(route.totalDur)}</span>
              </div>
              {route.transfers > 0 && <span style={{ fontSize: 9, color: '#94a3b8' }}>{route.transfers} stop{route.transfers > 1 ? 's' : ''}</span>}
            </div>
            <div className="text-center shrink-0">
              <p className="num-tab leading-none flex items-start justify-center gap-0.5" style={{ fontSize: 16, fontWeight: 700, color: '#0f172a' }}>
                {arrMs ? fmtClock(arrMs) : '\u2014'}
                {dayOffset > 0 && <sup style={{ fontSize: 8, fontWeight: 700, color: '#0ea5e9', marginTop: 1 }}>+{dayOffset}</sup>}
              </p>
            </div>
          </div>
          {/* Row 2: price + risk chips + buttons */}
          <div className="flex items-center gap-2 px-3 py-2 border-t border-slate-100">
            <div className="shrink-0">
              {route.price != null
                ? <p className="num-tab" style={{ fontSize: 18, fontWeight: 700, letterSpacing: '-0.02em', color: '#0f172a' }}>{eur(route.price)}</p>
                : <div className="skeleton h-5 w-16 rounded-md" />}
            </div>
            <div className="flex-1 flex gap-1">
              {route.riskLevel === 'high' && <Chip tone="red" icon="alert-triangle">Tight</Chip>}
              {route.riskLevel === 'watch' && <Chip tone="warn" icon="footprints">Self-transfer</Chip>}
            </div>
            <div className="flex gap-2 shrink-0">
              <button type="button" onClick={() => onSelect(route)}
                className="inline-flex items-center justify-center gap-1 rounded-lg px-3 py-2 transition active:scale-[.98] bg-slate-900 hover:bg-slate-800"
                style={{ fontSize: 12, fontWeight: 600, color: '#fff' }}>
                Select <Icon name="arrow-right" size={12} />
              </button>
              <button type="button" onClick={() => onToggle()}
                className="inline-flex items-center justify-center gap-1 rounded-lg border border-slate-200 px-3 py-2 hover:bg-slate-50 transition"
                style={{ fontSize: 11.5, fontWeight: 600, color: '#475569' }}>
                <Icon name={open ? 'chevron-up' : 'list'} size={12} />
              </button>
            </div>
          </div>
        </div>

        {/* \u2500\u2500 Desktop layout (sm+) \u2500\u2500 */}
        <div className="hidden sm:flex flex-wrap items-stretch">

          {/* Column 1: Departure / arrow+info / Arrival */}
          <div className="shrink-0 border-r border-slate-100 flex flex-col items-center justify-center py-4 px-3 gap-1" style={{ width: 96 }}>
            <div className="text-center">
              <p className="num-tab leading-none" style={{ fontSize: 22, fontWeight: 700, color: '#0f172a' }}>
                {depMs ? fmtClock(depMs) : '\u2014'}
              </p>
            </div>
            <div className="flex flex-col items-center my-0.5" style={{ gap: 3 }}>
              <div style={{ width: 1, height: 5, background: '#e2e8f0' }} />
              <span className="flex items-center gap-0.5">
                <Icon name="arrow-down" size={10} stroke={2.5} className="text-slate-400" />
                <span className="num-tab" style={{ fontSize: 10, fontWeight: 600, color: '#64748b' }}>{fmtDur(route.totalDur)}</span>
              </span>
              {route.transfers > 0 && (
                <span style={{ fontSize: 9, color: '#94a3b8' }}>{route.transfers} stop{route.transfers > 1 ? 's' : ''}</span>
              )}
              <div style={{ width: 1, height: 5, background: '#e2e8f0' }} />
            </div>
            <div className="text-center">
              <p className="num-tab leading-none flex items-start justify-center gap-0.5" style={{ fontSize: 22, fontWeight: 700, color: '#0f172a' }}>
                {arrMs ? fmtClock(arrMs) : '\u2014'}
                {dayOffset > 0 && <sup style={{ fontSize: 9, fontWeight: 700, color: '#0ea5e9', marginTop: 2 }}>+{dayOffset}</sup>}
              </p>
            </div>
          </div>

          {/* Column 2: Badge + Journey strip + chips */}
          <div className="flex flex-1 min-w-0 flex-col justify-center px-4 py-3" style={{ gap: 9 }}>
            <div className="flex items-center gap-2 flex-wrap">
              <span className="inline-flex items-center gap-1 rounded-full text-white px-2.5 py-0.5 shrink-0"
                style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.04em', background: badge.color }}>
                <Icon name={badge.ic} size={11} className={badge.ic === 'star' ? 'fill-white' : ''} />
                {badge.label}
              </span>
              <span className="truncate" style={{ fontSize: 11, fontWeight: 500, color: '#94a3b8' }}>{route.tagline}</span>
            </div>
            <div>{timelineVariant === 'classic' ? <TimelineClassic route={route} /> : <Timeline route={route} />}</div>
            <div className="flex items-center gap-2 flex-wrap">
              {route.riskLevel === 'high' && <Chip tone="red" icon="alert-triangle">Tight connection</Chip>}
              {route.riskLevel === 'watch' && <Chip tone="warn" icon="footprints">Self-transfer</Chip>}
            </div>
          </div>

          {/* Column 3: Price + actions */}
          <div className="shrink-0 flex flex-col items-center justify-center gap-2 px-4 py-3 border-l border-slate-100">
            <div>
              {route.price != null
                ? <p className="num-tab" style={{ fontSize: 20, fontWeight: 700, letterSpacing: '-0.02em', color: '#0f172a' }}>{eur(route.price)}</p>
                : <div className="skeleton h-6 w-16 rounded-md" />}
            </div>
            <div className="flex flex-col gap-2">
              <button type="button" onClick={() => onSelect(route)}
                className="inline-flex items-center justify-center gap-1 rounded-lg px-4 py-2.5 transition active:scale-[.98] bg-slate-900 hover:bg-slate-800"
                style={{ fontSize: 12.5, fontWeight: 600, color: '#fff' }}>
                Select <Icon name="arrow-right" size={13} />
              </button>
              <button type="button" onClick={() => onToggle()}
                className="inline-flex items-center justify-center gap-1 rounded-lg border border-slate-200 px-3 py-2 hover:bg-slate-50 transition"
                style={{ fontSize: 11.5, fontWeight: 600, color: '#475569' }}>
                <Icon name={open ? 'chevron-up' : 'list'} size={12} />
                <span>{open ? 'Hide' : 'Details'}</span>
              </button>
            </div>
          </div>

        </div>

        {open && (
          <div className="border-t border-slate-100 px-4">
            <ItineraryBreakdown route={route} startISO={itinStart} showTimes={showItinTimes} />
          </div>
        )}
      </div>
    </div>
  );
}


/* ---------------- results list with vertical FLIP reorder ---------------- */
function ResultsMatrix({ routes, onSelect, startISO, cardStyle, highlightRec, showItinTimes, onLoadMore, loadingMore, hasMore, dateExhausted }) {
  const [openCards, setOpenCards] = useState({});
  const toggleCard = React.useCallback(id => setOpenCards(p => ({ ...p, [id]: !p[id] })), []);
  const refs = useRef({});
  const prev = useRef({});
  const sentinelRef = useRef(null);
  const onLoadMoreRef = useRef(onLoadMore);
  useEffect(() => { onLoadMoreRef.current = onLoadMore; });

  useLayoutEffect(() => {
    Object.keys(refs.current).forEach(id => {
      const el = refs.current[id];
      if (!el) return;
      const rect = el.getBoundingClientRect();
      const p = prev.current[id];
      if (p != null) {
        const dy = p - rect.top;
        if (Math.abs(dy) > 1) {
          el.style.transition = 'none';
          el.style.transform = `translateY(${dy}px)`;
          requestAnimationFrame(() => {
            el.style.transition = 'transform .5s cubic-bezier(.22,.61,.36,1)';
            el.style.transform = '';
          });
        }
      }
      prev.current[id] = rect.top;
    });
  });

  useEffect(() => {
    const sentinel = sentinelRef.current;
    if (!sentinel || !hasMore) return;
    const observer = new IntersectionObserver(
      entries => { if (entries[0].isIntersecting) onLoadMoreRef.current?.(); },
      { rootMargin: '300px' }
    );
    observer.observe(sentinel);
    return () => observer.disconnect();
  }, [hasMore]);

  return (
    <div className="space-y-3.5 pt-4">
      {routes.map((r, i) => (
        <RouteCard key={r.id} route={r} onSelect={onSelect} startISO={startISO} rank={i + 1}
          cardStyle={cardStyle} highlightRec={highlightRec} showItinTimes={showItinTimes}
          timelineVariant={i % 2 === 0 ? 'classic' : 'proportional'}
          open={!!openCards[r.id]} onToggle={() => toggleCard(r.id)}
          cardRef={el => { refs.current[r.id] = el; }} />
      ))}
      {hasMore && (
        <div ref={sentinelRef} className="py-5 flex items-center justify-center text-[12px] text-slate-400 gap-2">
          {loadingMore
            ? <><Icon name="loader-circle" size={14} className="animate-spin" /> Loading more routes…</>
            : <span className="text-slate-300">Scroll to load more</span>}
        </div>
      )}
      {!hasMore && dateExhausted && (
        <p className="text-center text-[14px] font-medium text-slate-400 py-5">No more departures on this date</p>
      )}
      {!hasMore && !dateExhausted && routes.length > 0 && (
        <p className="text-center text-[13px] text-slate-300 py-3">All departures shown</p>
      )}
    </div>
  );
}

Object.assign(window, { TelemetryConsole, SkeletonRow, Timeline, TimelineClassic, ItineraryBreakdown, RankBadge, RouteCard, ResultsMatrix });


/* ===== booking.jsx ===== */
/* ============================================================
   booking.jsx — COMP-106 multi-ticket disclaimer,
                 disruption guarantee opt-in, booking guard
   ============================================================ */

function BookingModal({ route, s, onClose }) {
  const [accepted, setAccepted] = useState(false);
  const [guarantee, setGuarantee] = useState(false);
  const [shake, setShake] = useState(false);
  const [confirmed, setConfirmed] = useState(false);
  const guardRef = useRef(null);

  const selfTransfer = route.selfTransfer;
  const guaranteeFee = guarantee ? 15 : 0;
  const total = route.price != null ? route.price + guaranteeFee : null;
  const blocked = selfTransfer && !accepted;

  function proceed() {
    if (blocked) {
      setShake(true);
      // nudge the disclaimer
      if (guardRef.current) {
        guardRef.current.classList.remove('pulse-warn');
        void guardRef.current.offsetWidth;
        guardRef.current.classList.add('pulse-warn');
      }
      setTimeout(() => setShake(false), 550);
      return;
    }
    setConfirmed(true);
  }

  return (
    <div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-3 sm:p-6">
      <div className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm fade-in" onClick={onClose} />
      <div className="relative w-full max-w-lg max-h-[92vh] overflow-y-auto rounded-2xl bg-white shadow-2xl fade-up">
        {confirmed ? (
          <ConfirmedView route={route} total={total} guarantee={guarantee} onClose={onClose} />
        ) : (
          <>
            {/* header */}
            <div className="sticky top-0 bg-white/95 backdrop-blur px-5 py-4 border-b border-slate-100 flex items-center justify-between z-10">
              <div className="flex items-center gap-2.5">
                <span className="grid place-items-center w-9 h-9 rounded-lg text-white"
                  style={{ background: {smartest:'#D97706',fastest:'#0F172A',cheapest:'#059669',overnight:'#4338ca',coach:'#0d9488'}[route.id] || route.tone || '#475569' }}>
                  <Icon name={ {smartest:'star',fastest:'zap',cheapest:'piggy-bank',overnight:'moon',coach:'bus'}[route.id] || route.badgeIcon || 'route' } size={17} />
                </span>
                <div>
                  <p className="text-[14px] font-bold text-slate-900 leading-tight">{route.kind} trip · review</p>
                  <p className="text-[11.5px] text-slate-400">{s.origin?.city} → {s.destination?.city} · {route.operators}</p>
                </div>
              </div>
              <button onClick={onClose} className="grid place-items-center w-8 h-8 rounded-full hover:bg-slate-100 text-slate-400 hover:text-slate-700">
                <Icon name="x" size={18} />
              </button>
            </div>

            <div className="p-5 space-y-4">
              {/* journey legs */}
              <div className="rounded-xl border border-slate-200 overflow-hidden">
                {route.legs.map((l, i) => (
                  <div key={i} className={`flex items-center gap-3 px-3.5 py-2.5 ${i>0?'border-t border-slate-100':''} ${l.transfer ? (l.risk?'bg-red-50/60':'bg-amber-50/50') : ''}`}>
                    <span className="grid place-items-center w-7 h-7 rounded-md shrink-0"
                      style={{ background: l.transfer ? (l.risk?'#fee2e2':'#fef3c7') : MODE[l.mode].soft, color: l.transfer ? (l.risk?'#dc2626':'#b45309') : MODE[l.mode].color }}>
                      <Icon name={l.transfer ? (l.kind==='security'?'shield':l.kind==='reclaim'?'luggage':'footprints') : MODE[l.mode].icon} size={13} />
                    </span>
                    <div className="flex-1 min-w-0">
                      <p className="text-[12.5px] font-semibold text-slate-800 truncate">
                        {l.transfer ? (l.note || 'Transfer') : `${l.op} · ${l.from} → ${l.to}`}
                      </p>
                    </div>
                    <span className="num-tab text-[12px] font-bold text-slate-500 shrink-0">{fmtDur(l.dur)}</span>
                  </div>
                ))}
              </div>

              {/* COMP-106 self-transfer disclaimer */}
              {selfTransfer && (
                <div ref={guardRef} className="rounded-xl border-2 border-amber-300 bg-amber-50 p-3.5">
                  <div className="flex items-start gap-2.5">
                    <Icon name="alert-triangle" size={18} className="text-amber-600 mt-0.5 shrink-0" />
                    <div className="flex-1">
                      <p className="text-[13px] font-bold text-amber-900">This journey requires separate tickets</p>
                      <p className="text-[12px] text-amber-800 leading-snug mt-1">
                        Compiled from non-allied operators (virtual interlining). If your train is delayed, the airline is
                        <b> not legally obligated</b> to rebook you onto a later flight.
                      </p>

                      {/* premium opt-in */}
                      <button type="button" onClick={() => setGuarantee(g => !g)}
                        className={`w-full mt-3 flex items-center gap-3 rounded-lg border-2 bg-white px-3 py-2.5 text-left transition ${guarantee ? 'border-emerald-400 ring-2 ring-emerald-100' : 'border-amber-200 hover:border-amber-300'}`}>
                        <span className={`grid place-items-center w-5 h-5 rounded-md border-2 shrink-0 transition ${guarantee ? 'bg-emerald-500 border-emerald-500' : 'border-slate-300'}`}>
                          {guarantee && <Icon name="check" size={13} className="text-white" />}
                        </span>
                        <span className="flex-1">
                          <span className="flex items-center gap-1.5 text-[12.5px] font-bold text-slate-800"><Icon name="shield" size={13} className="text-emerald-600" /> Add Platform Disruption Guarantee</span>
                          <span className="block text-[11px] text-slate-500 mt-0.5">Free rebooking on the next available service if a leg is missed.</span>
                        </span>
                        <span className="num-tab text-[13px] font-bold text-slate-900 shrink-0">+€15.00</span>
                      </button>

                      {/* mandatory acknowledgement (booking guard) */}
                      <label className="flex items-start gap-2.5 mt-3 cursor-pointer select-none">
                        <span className={`grid place-items-center w-5 h-5 rounded-md border-2 shrink-0 mt-px transition ${accepted ? 'bg-amber-600 border-amber-600' : 'border-amber-400 bg-white'} ${shake && !accepted ? 'shake' : ''}`}>
                          {accepted && <Icon name="check" size={13} className="text-white" />}
                          <input type="checkbox" className="sr-only" checked={accepted} onChange={e => setAccepted(e.target.checked)} />
                        </span>
                        <span className="text-[12px] text-amber-900 font-medium">I understand this is a self-transfer itinerary with separate tickets and accept the connection risk.</span>
                      </label>
                    </div>
                  </div>
                </div>
              )}

              {route.protected && (
                <div className="rounded-xl border border-emerald-200 bg-emerald-50 p-3 flex items-start gap-2.5">
                  <Icon name="shield" size={17} className="text-emerald-600 mt-0.5 shrink-0" />
                  <p className="text-[12px] text-emerald-800"><b>Through-ticketed connection.</b> All legs are sold on a single protected fare — if a train is delayed you are automatically rebooked at no cost.</p>
                </div>
              )}

              {/* price breakdown */}
              <div className="rounded-xl bg-slate-50 border border-slate-100 p-3.5 space-y-1.5">
                <Row label={`Base fare · ${route.operators}`} value={route.baseFare != null ? eur(route.baseFare) : <div className="skeleton h-3.5 w-14 rounded" />} />
                {route.bagFee > 0 && <Row label={`Checked baggage × ${route.bags} (flight segments)`} value={`+${eur(route.bagFee)}`} sub />}
                {route.bagFee === 0 && route.bags > 0 && <Row label="Checked baggage (rail — included free)" value="+€0.00" sub green />}
                {guarantee && <Row label="Platform Disruption Guarantee" value="+€15.00" sub />}
                <div className="h-px bg-slate-200 my-1.5" />
                <div className="flex items-center justify-between">
                  <span className="text-[13px] font-bold text-slate-900">Total</span>
                  {total != null
                    ? <span className="num-tab text-[20px] font-bold text-slate-900">{eur(total)}</span>
                    : <div className="skeleton h-6 w-20 rounded-md" />}
                </div>
              </div>
            </div>

            {/* footer / proceed guard */}
            <div className="sticky bottom-0 bg-white/95 backdrop-blur px-5 py-3.5 border-t border-slate-100">
              <button type="button" onClick={proceed}
                className={`w-full flex items-center justify-center gap-2 rounded-xl py-3.5 text-[14px] font-bold text-white transition ${shake ? 'shake' : ''} ${blocked ? 'bg-slate-300 cursor-not-allowed' : 'bg-slate-900 hover:bg-slate-800 active:scale-[.99]'}`}>
                {blocked ? <><Icon name="lock" size={16} /> Accept the disclaimer to continue</> : <><Icon name="check" size={16} /> Proceed to booking{total != null ? ` · ${eur(total)}` : ''}</>}
              </button>
              {blocked && <p className="text-center text-[11px] text-amber-600 font-medium mt-1.5">Tick the acknowledgement above to unlock</p>}
            </div>
          </>
        )}
      </div>
    </div>
  );
}

function Row({ label, value, sub, green }) {
  return (
    <div className="flex items-center justify-between">
      <span className={`text-[12px] ${sub ? 'text-slate-500' : 'text-slate-700 font-medium'}`}>{label}</span>
      <span className={`num-tab text-[12.5px] font-semibold ${green ? 'text-emerald-600' : 'text-slate-700'}`}>{value}</span>
    </div>
  );
}

function ConfirmedView({ route, total, guarantee, onClose }) {
  return (
    <div className="p-8 text-center">
      <div className="mx-auto grid place-items-center w-16 h-16 rounded-full bg-emerald-100 text-emerald-600 mb-4 fade-up">
        <Icon name="check" size={32} stroke={2.6} />
      </div>
      <h3 className="text-[19px] font-bold text-slate-900">Journey held</h3>
      <p className="text-[13px] text-slate-500 mt-1.5 max-w-xs mx-auto">
        Your {route.kind.toLowerCase()} {route.operators} itinerary is reserved for 20 minutes.
        {total != null && <> Total <b className="text-slate-800">{eur(total)}</b></>}
        {guarantee && <> with Platform Disruption Guarantee</>}.
      </p>
      <div className="num-tab inline-flex items-center gap-2 mt-4 rounded-lg bg-slate-50 border border-slate-200 px-3 py-2 text-[12px] font-mono text-slate-600">
        <Icon name="ticket" size={14} /> PNR · {route.id.slice(0,3).toUpperCase()}{Math.floor(Math.random()*900+100)}
      </div>
      <button onClick={onClose} className="block w-full mt-6 rounded-xl bg-slate-900 text-white py-3 text-[13.5px] font-semibold hover:bg-slate-800">Back to results</button>
    </div>
  );
}

Object.assign(window, { BookingModal });


/* ===== controls.jsx FilterBar ===== */
const _ALL_MODES = ['rail', 'flight', 'coach', 'regional'];
const _MODE_LABELS = { rail: '🚄 Rail', flight: '✈ Flight', coach: '🚌 Coach', regional: '🚃 Regional' };

function FilterBar({ filters, setFilter }) {
  const xferOpts = [
    { label: 'Direct', value: 0 },
    { label: '≤1',     value: 1 },
    { label: '≤2',     value: 2 },
    { label: 'Any',    value: null },
  ];
  const durOpts = [
    { label: '2h',  value: 120 },
    { label: '4h',  value: 240 },
    { label: '8h',  value: 480 },
    { label: 'Any', value: null },
  ];

  const activeModes = filters.modes || _ALL_MODES;

  function toggleMode(key) {
    const cur = filters.modes || _ALL_MODES;
    const next = cur.includes(key) ? cur.filter(k => k !== key) : [...cur, key].sort();
    setFilter({ modes: next.length === _ALL_MODES.length ? null : next });
  }

  function chipCls(active) {
    return 'px-3 py-1 rounded-full text-[12px] font-semibold transition-all cursor-pointer select-none ' +
      (active ? 'bg-slate-900 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-slate-400');
  }

  return (
    <div className="flex flex-wrap items-center gap-x-5 gap-y-2 px-1 pt-2.5 pb-1">
      <div className="flex items-center gap-1.5">
        <span className="text-[11px] font-medium text-slate-400 mr-0.5">Changes</span>
        {xferOpts.map(o => (
          <button key={String(o.value)} onClick={() => setFilter({ maxTransfers: o.value })}
            className={chipCls(filters.maxTransfers === o.value)}>{o.label}</button>
        ))}
      </div>
      <div className="flex items-center gap-1.5">
        <span className="text-[11px] font-medium text-slate-400 mr-0.5">Duration</span>
        {durOpts.map(o => (
          <button key={String(o.value)} onClick={() => setFilter({ maxTravelTime: o.value })}
            className={chipCls(filters.maxTravelTime === o.value)}>{o.label}</button>
        ))}
      </div>
      <div className="flex items-center gap-1.5">
        <span className="text-[11px] font-medium text-slate-400 mr-0.5">Modes</span>
        {_ALL_MODES.map(k => {
          const on = activeModes.includes(k);
          return (
            <button key={k} onClick={() => toggleMode(k)}
              className={chipCls(on) + (on ? '' : ' opacity-40 line-through')}>
              {_MODE_LABELS[k]}
            </button>
          );
        })}
      </div>
    </div>
  );
}


/* ===== app.jsx ===== */
/* ============================================================
   app.jsx — orchestrator / state machine
   ============================================================ */

function defaultDateTime() {
  const d = new Date();
  d.setDate(d.getDate() + 3);
  d.setHours(8, 30, 0, 0);
  const pad = n => String(n).padStart(2, '0');
  return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "highlightRec": true,
  "showItinTimes": true
}/*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [s, setS] = useState({
    origin: null,
    destination: null,
    timeMode: 'depart',
    datetime: defaultDateTime(),
    comfort: 'balanced',
    manual: { air: null, rail: null },
  });
  const set = patch => setS(prev => ({ ...prev, ...patch }));
  const [filters, setFilters] = useState({ maxTransfers: null, maxTravelTime: null, modes: null });
  const setFilter = patch => setFilters(prev => ({ ...prev, ...patch }));

  const [screen, setScreen] = useState('idle'); // idle | loading | results | noresults | error
  const [stage, setStage] = useState(0);
  const [routes, setRoutes] = useState([]);
  const [apiError, setApiError] = useState(null);
  const [selected, setSelected] = useState(null);
  const [nextCursor, setNextCursor] = useState(null);
  const [loadingMore, setLoadingMore] = useState(false);
  const [dateExhausted, setDateExhausted] = useState(false);
  const timers = useRef([]);
  function clearTimers() { timers.current.forEach(clearTimeout); timers.current = []; }
  useEffect(() => clearTimers, []);

  function runSearch() {
    if (!s.origin?.lat || !s.destination?.lat) return;
    clearTimers();
    setApiError(null);
    setRoutes([]);
    setNextCursor(null);
    setDateExhausted(false);
    setScreen('loading');
    setStage(0);

    let animDone = false, apiFetched = false, apiResult = [], cursorResult = null;
    function tryFinish() {
      if (!animDone || !apiFetched) return;
      setStage(3);
      setRoutes(apiResult);
      setNextCursor(cursorResult);
      setScreen(apiResult.length > 0 ? 'results' : 'noresults');
    }

    timers.current.push(setTimeout(() => setStage(1), 1500));
    timers.current.push(setTimeout(() => setStage(2), 3000));
    timers.current.push(setTimeout(() => { animDone = true; tryFinish(); }, 5000));

    planTrip(s.origin, s.destination, s.datetime, s.timeMode === 'arrive', filters)
      .then(({ routes: r, nextPageCursor }) => {
        apiResult = r; cursorResult = nextPageCursor; apiFetched = true; tryFinish();
      })
      .catch(err => {
        setApiError(err.message);
        clearTimers();
        setScreen('error');
      });
  }

  async function loadMore() {
    if (!nextCursor || loadingMore) return;
    setLoadingMore(true);
    try {
      const { routes: more, nextPageCursor } = await planTrip(
        s.origin, s.destination, s.datetime, s.timeMode === 'arrive', filters, nextCursor
      );
      const selectedDateStr = new Date(s.datetime).toDateString();
      const sameDay = more.filter(r => new Date(r.depMs).toDateString() === selectedDateStr);
      if (sameDay.length < more.length) {
        setDateExhausted(true);
        setNextCursor(null);
      } else {
        setNextCursor(nextPageCursor);
      }
      if (sameDay.length > 0) {
        setRoutes(prev => {
          const seen = new Set(prev.map(r => r.depMs + '_' + r.totalDur));
          const fresh = sameDay.filter(r => !seen.has(r.depMs + '_' + r.totalDur));
          return [...prev, ...fresh].sort((a, b) => a.depMs - b.depMs);
        });
      }
    } catch (e) {
      console.warn('[loadMore] failed:', e);
    } finally {
      setLoadingMore(false);
    }
  }

  return (
    <div className="min-h-screen">
      <header className="sticky top-0 z-30 bg-white/80 backdrop-blur-md border-b border-slate-200/70">
        <div className="max-w-[1180px] mx-auto px-4 sm:px-5 lg:px-8 h-14 sm:h-16 flex items-center justify-between">
          <div className="flex items-center gap-2.5">
            <span className="grid place-items-center w-9 h-9 rounded-lg bg-slate-900 text-white"><Icon name="route" size={19} /></span>
            <div className="leading-none">
              <p className="text-[15px] font-bold tracking-tight text-slate-900">Interrail<span className="text-air">·</span>Air</p>
              <p className="hidden sm:block text-[10.5px] text-slate-400 font-medium tracking-wide mt-0.5">Intermodal route engine</p>
            </div>
          </div>
          <div className="hidden sm:flex items-center gap-4 text-[11.5px] font-medium text-slate-500">
            <span className="inline-flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-rail" /> Rail</span>
            <span className="inline-flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-air" /> Air</span>
            <span className="inline-flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-local" /> Regional</span>
            <span className="inline-flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-coach" /> Coach</span>
            <span className="inline-flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm hatch-amber border border-amber-300" /> Transfer</span>
          </div>
        </div>
      </header>

      <div className="max-w-[1000px] mx-auto px-3 sm:px-5 lg:px-8 pt-5 sm:pt-7 pb-1">
        {screen === 'idle' && (
          <div className="text-center mb-5 fade-in">
            <h1 className="text-[26px] sm:text-[30px] font-bold tracking-tight text-slate-900">Where to next?</h1>
            <p className="text-[14px] text-slate-500 mt-2 max-w-xl mx-auto">
              One engine for European air, high-speed rail and regional transit — ranked on a generalized-cost model of time, fare and effort.
            </p>
          </div>
        )}
        <SearchBar s={s} set={set} onSearch={runSearch} busy={screen==='loading'} />
        <FilterBar filters={filters} setFilter={setFilter} />
      </div>

      <main className="max-w-[1000px] mx-auto px-3 sm:px-5 lg:px-8 py-4 sm:py-5 min-h-[40vh]">
        {screen === 'idle'      && <IdleState s={s} />}
        {screen === 'loading'   && <LoadingState stage={stage} />}
        {screen === 'results'   && <ResultsState s={s} routes={routes} onSelect={setSelected} tweaks={t} onLoadMore={loadMore} loadingMore={loadingMore} hasMore={!!nextCursor} dateExhausted={dateExhausted} />}
        {screen === 'noresults' && <NoResultsState s={s} />}
        {screen === 'error'     && <ErrorState message={apiError} />}
      </main>

      {selected && <BookingModal route={selected} s={s} onClose={() => setSelected(null)} />}

      <TweaksPanel>
        <TweakSection label="Result cards" />
        <TweakToggle label="Highlight recommended" value={t.highlightRec} onChange={v => setTweak('highlightRec', v)} />
        <TweakSection label="Itinerary details" />
        <TweakToggle label="Show clock times" value={t.showItinTimes} onChange={v => setTweak('showItinTimes', v)} />
      </TweaksPanel>
    </div>
  );
}

/* ---------------- screen states ---------------- */
function IdleState({ s }) {
  const items = [
    { ic:'train', tone:'rail', t:'High-speed rail', d:'Eurostar, TGV, ICE corridors prioritised for productive, low-stress time.' },
    { ic:'plane', tone:'air', t:'Aviation', d:'Legacy & budget carriers, with honest door-to-door security buffers.' },
    { ic:'footprints', tone:'warn', t:'Smart transfers', d:'Self-transfer risk, baggage re-check and curfews modelled explicitly.' },
  ];
  return (
    <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 fade-in">
      {items.map(it => (
        <div key={it.t} className="rounded-2xl border border-slate-200 bg-white p-5">
          <span className="grid place-items-center w-10 h-10 rounded-xl mb-3" style={{ background: MODE[it.tone==='warn'?'transfer':it.tone].soft, color: MODE[it.tone==='warn'?'transfer':it.tone].color }}>
            <Icon name={it.ic} size={19} />
          </span>
          <p className="text-[14px] font-bold text-slate-900">{it.t}</p>
          <p className="text-[12.5px] text-slate-500 mt-1 leading-snug">{it.d}</p>
        </div>
      ))}
    </div>
  );
}

function LoadingState({ stage }) {
  return (
    <div className="space-y-4 fade-in">
      <TelemetryConsole stage={stage} />
      <div className="space-y-3.5">
        <SkeletonRow active={stage >= 0} />
        <SkeletonRow active={stage >= 0} />
        <SkeletonRow active={stage >= 1} />
        <SkeletonRow active={stage >= 1} />
        <SkeletonRow active={stage >= 2} />
      </div>
      <p className="text-center text-[12px] text-slate-400 flex items-center justify-center gap-1.5">
        <Icon name="loader-circle" size={13} className="animate-spin" /> Querying the routing engine…
      </p>
    </div>
  );
}

function ResultsState({ s, routes, onSelect, tweaks, onLoadMore, loadingMore, hasMore, dateExhausted }) {
  return (
    <div className="fade-in">
      <div className="flex flex-wrap items-center justify-between gap-3">
        <div>
          <h2 className="text-[17px] font-bold text-slate-900 flex items-center gap-2">
            {s.origin?.city} <Icon name="arrow-right" size={15} className="text-slate-300" /> {s.destination?.city}
            <span className="text-[13px] font-medium text-slate-400">· {routes.length} departure{routes.length !== 1 ? 's' : ''}</span>
          </h2>
          <p className="text-[12px] text-slate-400 mt-0.5 flex items-center gap-1.5">
            <Icon name="clock" size={12} /> From {new Date(s.datetime).toLocaleTimeString('en-GB', {hour:'2-digit',minute:'2-digit'})} · chronological order
          </p>
        </div>
      </div>
      <ResultsMatrix routes={routes} onSelect={onSelect} startISO={s.datetime}
        highlightRec={tweaks.highlightRec} showItinTimes={tweaks.showItinTimes}
        onLoadMore={onLoadMore} loadingMore={loadingMore} hasMore={hasMore} dateExhausted={dateExhausted} />
    </div>
  );
}

function NoResultsState({ s }) {
  return (
    <div className="text-center py-16 fade-in">
      <span className="grid place-items-center w-16 h-16 rounded-2xl bg-slate-100 mx-auto mb-4">
        <Icon name="search-x" size={28} className="text-slate-400" />
      </span>
      <p className="text-[17px] font-bold text-slate-900">No routes found</p>
      <p className="text-[13px] text-slate-500 mt-2 max-w-sm mx-auto">
        No connections were found between {s.origin?.city} and {s.destination?.city}.
        Try adjusting the date or selecting different stops.
      </p>
    </div>
  );
}

function ErrorState({ message }) {
  return (
    <div className="text-center py-16 fade-in">
      <span className="grid place-items-center w-16 h-16 rounded-2xl bg-red-50 mx-auto mb-4">
        <Icon name="circle-alert" size={28} className="text-red-400" />
      </span>
      <p className="text-[17px] font-bold text-slate-900">Connection error</p>
      <p className="text-[13px] text-slate-500 mt-2 max-w-sm mx-auto">
        {message || 'Could not reach the routing service. Please try again.'}
      </p>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);

