/* login-app.jsx — точка входа страницы логина */
const { useState, useEffect, useRef, useCallback } = React;

/* ---- RP config (in production comes from query params) ---- */
const RP = AUTH.RP;
/* i18n: window.tr задаётся контрактом i18n/i18n.js (ru — fallback-строки в коде) */
const tr = window.tr || ((k, f) => f);
/* минимальная длина пароля — из hydrate-данных (AUTH.POLICY.passwordMin), fallback 8 */
const pwMin = () => ((AUTH.POLICY || {}).passwordMin) || 8;
/* организации пользователя для мультиорг-чузера (login-hydrate.orgs или мок AUTH.ORGS) */
const loginOrgs = () => (window.LOGIN_HYDRATE || {}).orgs || (window.AUTH && window.AUTH.ORGS) || [];
/* экран после успешной аутентификации: организация уже выбрана → согласие;
   несколько организаций и не выбрана → экран выбора; иначе → согласие */
function postAuthState(org) { return org ? "consent" : (loginOrgs().length > 1 ? "chooseorg" : "consent"); }
/* canonical result действия: resolve {next?} — next говорит, каким шагом продолжить
   (standalone-journey); "postauth" → выбор организации/согласие. В prod адаптер
   делает реальный redirect и не возвращает next — UI никуда не переходит сам. */
function nextState(r, org) {
  const n = r && r.next;
  if (!n) return null;
  return n === "postauth" ? postAuthState(org) : n;
}
/* поток доступен только при точном action (fail-closed) */
const LA = () => window.LOGIN_ACTIONS || {};
function FlowUnavailable({ go, back = "main", label }) {
  return (
    <div className="ln-wrap ln-enter">
      <RPStrip />
      <div role="status" style={{ padding: "10px 0 4px" }}>
        <div className="eyebrow" style={{ marginBottom: 6 }}>{tr("flow.unavailable.eyebrow","Поток недоступен")}</div>
        <p className="dim t-sm" style={{ margin: 0, lineHeight: 1.55 }}>
          {label || tr("flow.unavailable","Этот способ входа не настроен для данного запроса авторизации.")}
        </p>
      </div>
      <button className="btn block" style={{ marginTop: 16 }} onClick={() => go(back)}><Icon name="back" size={14} />{tr("common.back","Назад")}</button>
    </div>
  );
}

/* ---- micro icon ---- */
const PATHS = {
  mail:"M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2zm0 2l8 5 8-5M4 6l8 5 8-5",
  key:"M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4",
  shield:"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z",
  check:"M20 6L9 17l-5-5",
  x:"M18 6L6 18M6 6l12 12",
  arrowR:"M5 12h14M12 5l7 7-7 7",
  back:"M19 12H5M12 19l-7-7 7-7",
  user:"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 7a4 4 0 1 0 0-8 4 4 0 0 0 0 8z",
  bolt:"M13 2L3 14h9l-1 8 10-12h-9l1-8z",
  ok:"M22 11.08V12a10 10 0 1 1-5.93-9.14M22 4L12 14.01l-3-3",
  alert:"M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0zM12 9v4M12 17h.01",
  lock:"M19 11H5a2 2 0 00-2 2v7a2 2 0 002 2h14a2 2 0 002-2v-7a2 2 0 00-2-2zM7 11V7a5 5 0 0110 0v4",
  google:"", // handled separately
  command:"M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z",
};
function Icon({ name, size = 18, style }) {
  const d = PATHS[name];
  if (!d) return null;
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
      style={{ flexShrink: 0, ...style }}>
      {d.split("M").filter(Boolean).map((seg, i) => <path key={i} d={"M" + seg} />)}
    </svg>
  );
}

function GoogleIcon({ size = 18 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 48 48">
      <path fill="#FFC107" d="M43.6 20.5H42V20H24v8h11.3c-1.6 4.6-6 8-11.3 8-6.6 0-12-5.4-12-12s5.4-12 12-12c3 0 5.8 1.1 7.9 3l5.7-5.7C34 6.1 29.3 4 24 4 12.9 4 4 12.9 4 24s8.9 20 20 20 20-8.9 20-20c0-1.3-.1-2.4-.4-3.5z"/>
      <path fill="#FF3D00" d="M6.3 14.7l6.6 4.8C14.7 16 19 13 24 13c3 0 5.8 1.1 7.9 3l5.7-5.7C34 6.1 29.3 4 24 4 16.3 4 9.7 8.3 6.3 14.7z"/>
      <path fill="#4CAF50" d="M24 44c5.2 0 9.9-2 13.4-5.2l-6.2-5.2c-2 1.4-4.5 2.3-7.2 2.3-5.2 0-9.7-3.3-11.3-7.9l-6.5 5C9.5 39.6 16.2 44 24 44z"/>
      <path fill="#1976D2" d="M43.6 20.5H42V20H24v8h11.3c-.8 2.2-2.2 4.1-4.1 5.5l6.2 5.2C40.7 35.5 44 30.2 44 24c0-1.3-.1-2.4-.4-3.5z"/>
    </svg>
  );
}

function SkinToggle() {
  const [skin, setSkin] = useState(() => localStorage.getItem("ri_skin") || "paper");
  function toggle() {
    const next = skin === "paper" ? "ink" : "paper";
    setSkin(next); localStorage.setItem("ri_skin", next);
    document.documentElement.setAttribute("data-skin", next);
  }
  return <button className="skin-btn" onClick={toggle}>{skin === "paper" ? "☽ Тёмная" : "☀ Светлая"}</button>;
}

/* ---- left brand pane ---- */
function LoginAside() {
  return (
    <aside className="login-aside" aria-hidden="true">
      <div className="aurora" />
      <div style={{ position:"relative", zIndex:1, display:"flex", flexDirection:"column", height:"100%", padding:"38px 44px" }}>
        <div style={{ display:"flex", alignItems:"center", gap:11 }}>
          <div style={{ width:32, height:32, borderRadius:9, background:"var(--ink)", color:"var(--paper)", display:"grid", placeItems:"center", fontFamily:"var(--f-display)", fontWeight:700 }}>A</div>
          <b style={{ fontFamily:"var(--f-display)", fontWeight:700, letterSpacing:"-.02em", fontSize:"calc(var(--u) + 2px)" }}>{AUTH.BRAND.title}</b>
        </div>
        <div style={{ flex:1, display:"grid", placeItems:"center" }}>
          <div className="login-orbit">
            <span className="lo-ring" /><span className="lo-ring r2" /><span className="lo-ring r3" />
            <span className="lo-core"><Icon name="shield" size={28} /></span>
            {[0,1,2,3,4].map(i => <span key={i} className={"lo-node n"+i} />)}
          </div>
        </div>
        <div>
          <h2 className="display" style={{ fontSize:"calc(var(--u) + 12px)", lineHeight:1.06, margin:"0 0 10px", maxWidth:340 }}>
            Один вход —<br />весь доступ организации.</h2>
          <p className="dim t-sm" style={{ margin:0, maxWidth:320, lineHeight:1.55 }}>
            Безопасный вход по OIDC / OAuth 2.1. Passkey, SSO и 2FA из коробки.</p>
          <div style={{ display:"flex", gap:18, marginTop:22, flexWrap:"wrap" }}>
            {[["OIDC / OAuth 2.1","shield"],["Passkey · SSO","key"],["RFC-совместимо","check"]].map(([t,ic]) => (
              <span key={t} className="t-xs" style={{ display:"inline-flex", alignItems:"center", gap:6, color:"var(--ink-2)", fontWeight:600 }}>
                <Icon name={ic} size={13} style={{ color:"var(--accent)" }} />{t}</span>
            ))}
          </div>
        </div>
      </div>
    </aside>
  );
}

/* ---- RP strip (shared) ---- */
function RPStrip() {
  return (
    <div className="rp-strip">
      <div className="rp-logo">{RP.initial}</div>
      <div style={{ flex:1, minWidth:0 }}>
        <div className="eyebrow" style={{ marginBottom:1 }}>{tr("rp.eyebrow","Запрос авторизации")}</div>
        <div style={{ fontWeight:600, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>{RP.name}</div>
      </div>
      <span className="pill t-xs mono">OIDC</span>
    </div>
  );
}

/* ---- state: MAIN (choose method) ---- */
/* Кнопки методов входа рендерятся ТОЛЬКО из window.LOGIN_HYDRATE.providers[] (контракт login-hydrate v1):
   kind=password → локальный переход go("email") (без selectProvider — реальный вызов один, на шаге пароля);
   kind=oidc → await LOGIN_ACTIONS.selectProvider(id) — реальный редирект делает адаптер, отказ → баннер под кнопками;
   kind=passkey / kind=magic — рендерятся только если сервер прислал такой элемент (сегодня — нет);
   providers:[] — валидно: ни одной кнопки метода, остаётся статический текст согласия. */
function MainPane({ go, provErr, setProvErr }) {
  const providers = (window.LOGIN_HYDRATE || {}).providers || [];
  async function pick(p) {
    setProvErr(null);
    const fn = LA().selectProvider;
    if (!fn) { setProvErr({ message: tr("main.providerUnavailable", "Провайдер входа недоступен: адаптер не подключён") }); return; }
    go("redirect", p.id); // busy-экран «Переход…», пока промис не разрешился
    try {
      const r = await fn(p.id);
      /* prod: адаптер уже увёл со страницы реальным redirect'ом (resolve без next);
         standalone: mock возвращает canonical next — продолжаем journey ПОСЛЕ resolve */
      const n = nextState(r, null);
      if (n) go(n);
    } catch (e) {
      setProvErr(e && e.message ? e : { message: tr("main.providerFail", "Не удалось перейти к провайдеру входа") });
      go("main");
    }
  }
  const big = { height:"calc(var(--ctrl-h) + 8px)", justifyContent:"flex-start", gap:12, paddingLeft:14 };
  return (
    <div className="ln-wrap ln-enter">
      <RPStrip />
      <div style={{ marginBottom:22 }}>
        <div className="eyebrow" style={{ marginBottom:8 }}>{tr("main.eyebrow","Войти в аккаунт")}</div>
        <h1 className="display" style={{ fontSize:"clamp(24px,4vw,32px)", lineHeight:1.06, margin:0 }}>{tr("main.title","Добро пожаловать")}</h1>
        <p className="dim t-sm" style={{ margin:"7px 0 0", lineHeight:1.5 }}>{tr("main.sub","Выберите способ входа.")}</p>
      </div>
      <div style={{ display:"flex", flexDirection:"column", gap:9 }}>
        {providers.map(p => {
          if (p.kind === "password") return (
            <button key={p.id} className="btn block accent" style={{ height:"calc(var(--ctrl-h) + 8px)" }} onClick={() => go("email")}>
              <Icon name="mail" size={16} />{p.label || tr("main.email","Войти по e-mail")}
            </button>
          );
          if (p.kind === "passkey") return (
            <button key={p.id} className="btn block" style={big} onClick={() => pick(p)}>
              <Icon name="key" size={18} /><span style={{ flex:1, textAlign:"left" }}>{p.label || "Passkey / WebAuthn"}</span>
              <span className="pill t-xs ok">{tr("main.recommended","Рекомендовано")}</span>
            </button>
          );
          if (p.kind === "magic") return (
            <button key={p.id} className="btn block" style={big} onClick={() => go("magic")}>
              <Icon name="bolt" size={16} /><span style={{ flex:1, textAlign:"left" }}>{p.label || tr("main.magic","Одноразовый код на почту")}</span>
            </button>
          );
          /* kind=oidc (и неизвестные kind будущих версий контракта) — внешний провайдер */
          return (
            <button key={p.id} className="btn block" style={big} onClick={() => pick(p)}>
              {p.iconHint === "google" ? <GoogleIcon size={18} /> : <Icon name="shield" size={18} />}
              <span style={{ flex:1, textAlign:"left" }}>{p.label}</span>
            </button>
          );
        })}
      </div>
      {provErr && <p className="t-xs" style={{ color:"var(--danger)", marginTop:10, textAlign:"center" }}>{provErr.message || tr("main.providerFail","Не удалось перейти к провайдеру входа")}</p>}
      <p className="t-xs faint" style={{ textAlign:"center", marginTop:18, lineHeight:1.5 }}>
        Входя, вы принимаете <a href="#" onClick={e=>e.preventDefault()} style={{ color:"var(--accent)" }}>условия использования</a> и <a href="#" onClick={e=>e.preventDefault()} style={{ color:"var(--accent)" }}>политику конфиденциальности</a>.
      </p>
      <div className="or-line">{tr("main.or","или")}</div>
      <p className="t-xs faint" style={{ textAlign:"center" }}>
        {tr("main.noAccount","Нет аккаунта?")} <button className="btn ghost sm" style={{ padding:0,color:"var(--accent)",fontWeight:600 }} onClick={()=>go("register")}>{tr("main.create","Создать")}</button>
      </p>
    </div>
  );
}

/* ---- state: EMAIL ---- */
function EmailPane({ go, email, setEmail }) {
  const [touched, setTouched] = useState(false);
  const [shake, setShake] = useState(false);
  const ref = useRef(null);
  useEffect(() => { ref.current?.focus(); }, []);
  const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  // подсказка SSO-домена — из гидрации (LOGIN_HYDRATE.sso); fallback — мок AUTH.SSO_DOMAINS
  const SSO_DOMAINS = (window.LOGIN_HYDRATE || {}).sso || AUTH.SSO_DOMAINS || {};
  const sso = valid ? SSO_DOMAINS[email.split("@")[1]] : null;
  function submit(e) {
    e.preventDefault(); setTouched(true);
    if (!valid) { setShake(true); ref.current?.focus(); setTimeout(() => setShake(false), 340); return; }
    go("password");
  }
  return (
    <div className="ln-wrap ln-enter">
      <RPStrip />
      <div style={{ marginBottom:20 }}>
        <button className="btn ghost sm" style={{ marginBottom:12, padding:0 }} onClick={() => go("main")}><Icon name="back" size={14} />Назад</button>
        <div className="eyebrow" style={{ marginBottom:6 }}>{tr("email.step","Шаг 1 из 2 · e-mail")}</div>
        <h1 className="display" style={{ fontSize:"clamp(22px,3.5vw,30px)", lineHeight:1.06, margin:0 }}>Ваш <b>e-mail</b></h1>
        <p className="dim t-sm" style={{ margin:"7px 0 0", lineHeight:1.5 }}>
          Определим SSO для вашей организации или запросим пароль.</p>
      </div>
      <form onSubmit={submit}>
        <div className="field" style={{ marginBottom:14 }}>
          <label>{tr("email.label","Рабочий или личный e-mail")}</label>
          <div className={shake ? "shake" : ""}>
            <input ref={ref} className="input" type="email" autoComplete="email" placeholder="you@company.com"
              value={email} onChange={e => { setEmail(e.target.value); setTouched(false); }}
              onBlur={() => setTouched(true)}
              style={touched && email && !valid ? { borderColor:"var(--danger)" } : {}} />
          </div>
          {touched && email && !valid && <span className="t-xs" style={{ color:"var(--danger)", marginTop:4, display:"block" }}>{tr("email.invalid","Введите корректный адрес")}</span>}
          {sso && <div className="t-xs" style={{ color:"var(--ok)", marginTop:6, display:"flex", alignItems:"center", gap:6 }}><Icon name="ok" size={13} />Обнаружен {sso} для вашей организации</div>}
        </div>
        <button type="submit" className="btn accent block" style={{ height:"calc(var(--ctrl-h) + 6px)" }}>
          {sso ? <>Войти через {sso}<Icon name="arrowR" size={15} /></> : <>{tr("common.continue","Продолжить")}<Icon name="arrowR" size={15} /></>}
        </button>
      </form>
    </div>
  );
}

/* ---- state: PASSWORD ---- */
function PasswordPane({ go, email, org }) {
  const [pw, setPw] = useState("");
  const [shake, setShake] = useState(false);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null); // { code, message } из reject LOGIN_ACTIONS.submitPassword
  const ref = useRef(null);
  useEffect(() => { ref.current?.focus(); }, []);
  async function submit(e) {
    e.preventDefault();
    if (!pw) { setShake(true); ref.current?.focus(); setTimeout(() => setShake(false), 340); return; }
    if (!LA().submitPassword) { setErr({ message: tr("password.unavailable", "Вход по паролю недоступен: адаптер не подключён") }); return; }
    setBusy(true); setErr(null);
    try {
      const r = await LA().submitPassword({ email, password: pw, sharedComputer: false });
      /* в проде адаптер уже увёл со страницы реальным редиректом (resolve без next);
         в standalone mock возвращает canonical next — строго ПОСЛЕ await, не оптимистично */
      const n = nextState(r, org);
      if (n) go(n);
      else setBusy(false);
    } catch (e2) {
      setBusy(false);
      setErr(e2 && e2.message ? e2 : { message: tr("password.fail", "Не удалось войти. Попробуйте ещё раз.") });
    }
  }
  return (
    <div className="ln-wrap ln-enter">
      <RPStrip />
      <div style={{ marginBottom:20 }}>
        <button className="btn ghost sm" style={{ marginBottom:12, padding:0 }} onClick={() => go("email")}><Icon name="back" size={14} />Назад</button>
        <div className="eyebrow" style={{ marginBottom:6 }}>{tr("password.step","Шаг 2 из 2 · пароль")}</div>
        <h1 className="display" style={{ fontSize:"clamp(22px,3.5vw,30px)", lineHeight:1.06, margin:0 }}>Ваш <b>пароль</b></h1>
      </div>
      <div className="who-strip" style={{ marginBottom:16 }}>
        <div className="who-av">{email[0]?.toUpperCase()}</div>
        <div style={{ flex:1, minWidth:0 }}>
          <div className="t-sm mono" style={{ overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>{email}</div>
        </div>
        <button className="btn ghost sm" onClick={() => go("email")}>{tr("password.change","Изменить")}</button>
      </div>
      <form onSubmit={submit}>
        <div className="field" style={{ marginBottom:14 }}>
          <div style={{ display:"flex", alignItems:"baseline", justifyContent:"space-between" }}>
            <label>Пароль</label>
            {typeof LA().forgotPassword === "function" && <button type="button" className="btn ghost sm" style={{ padding:0, color:"var(--accent)", fontWeight:600 }} onClick={() => go("forgot")}>{tr("password.forgot","Забыли?")}</button>}
          </div>
          <div className={shake ? "shake" : ""}>
            <input ref={ref} className="input" type="password" autoComplete="current-password" placeholder="••••••••"
              value={pw} onChange={e => setPw(e.target.value)} disabled={busy} />
          </div>
        </div>
        <button type="submit" className="btn accent block" disabled={busy} style={{ height:"calc(var(--ctrl-h) + 6px)" }}>
          {busy ? tr("password.checking","Проверка…") : <>{tr("password.signin","Войти")}<Icon name="arrowR" size={15} /></>}
        </button>
        {err && <p className="t-xs" style={{ color:"var(--danger)", marginTop:8 }}>{err.message}</p>}
      </form>
      <p className="t-xs faint" style={{ textAlign:"center", marginTop:16, lineHeight:1.5 }}>
        Проблемы со входом? <a href="#" onClick={e=>e.preventDefault()} style={{ color:"var(--accent)" }}>Обратиться в поддержку</a>
      </p>
    </div>
  );
}

/* ---- state: SSO/PASSKEY REDIRECT ---- */
function RedirectPane({ go, provider, org }) {
  // метаданные провайдера — из гидрации (произвольные id), старые имена — fallback
  const provMeta = ((window.LOGIN_HYDRATE || {}).providers || []).find(x => x.id === provider);
  const meta = { passkey:"Passkey / WebAuthn", google:"Google", okta:"Okta SSO" }[provider]
    || (provMeta && provMeta.label ? provMeta.label.replace(/^Войти через\s+/i, "") : provider);
  const iconHint = (provMeta && provMeta.iconHint) || provider;
  /* исход (redirect или canonical next) полностью принадлежит runtime-action
     selectProvider — никакого таймерного успеха здесь нет */
  return (
    <div className="ln-wrap ln-enter">
      <div className="ln-state">
        <div className="ln-spinner" />
        <div>
          <h2 className="display" style={{ fontSize:"calc(var(--u) + 8px)", margin:"0 0 8px" }}>
            {provider === "passkey" ? <>Ожидание <b>Passkey</b>…</> : <>Переход в <b>{meta}</b>…</>}
          </h2>
          <p className="dim t-sm" style={{ margin:0, lineHeight:1.5 }}>
            {provider === "passkey"
              ? "Подтвердите вход отпечатком пальца, лицом или ключом безопасности."
              : `${meta} проверит вашу личность и вернёт вас для подтверждения доступа.`}
          </p>
        </div>
        <div className="handoff">
          <div className="hf-node">
            <div className="hf-blob idp">A</div>
            <span className="t-xs faint">{AUTH.BRAND.org}</span>
          </div>
          <div className="hf-line" />
          <div className="hf-node">
            <div className="hf-blob">{iconHint === "google" ? <GoogleIcon size={20} /> : provider === "passkey" ? <Icon name="key" size={20} /> : <Icon name="shield" size={20} />}</div>
            <span className="t-xs faint">{meta}</span>
          </div>
          <div className="hf-line" style={{ transform:"scaleX(-1)" }} />
          <div className="hf-node">
            <div className="hf-blob" style={{ background:"var(--accent-soft)", color:"var(--accent)", border:"1px solid var(--accent-line)" }}>{RP.initial}</div>
            <span className="t-xs faint">{RP.name.split(" ")[1] || RP.name}</span>
          </div>
        </div>
        <button className="btn ghost sm" onClick={() => go("main")}>Отменить</button>
      </div>
    </div>
  );
}

/* ---- state: CHOOSE ORG — мультиорг-выбор (Google-style account/org chooser).
   Выбор ОТПРАВЛЯЕТСЯ на сервер тем же образцом, что пароль: канонический action
   LOGIN_ACTIONS.selectOrganization({ orgId }) → resolve {next?} решает, какой шаг
   дальше (жёсткого перехода на consent здесь НЕТ). Пока промис не разрешён —
   pending и блокировка повторных нажатий; reject — инлайн-ошибка на этом же
   экране (это не «неверный пароль», на шаг пароля не возвращаем).
   Список приходит готовым: элементы гарантированно имеют id, name, role; вход
   возможен в каждый — состояний «истекла»/«требуется вход» в системе нет. ---- */
function ChooseOrgPane({ go, setOrg }) {
  const orgs = loginOrgs();
  const [busyId, setBusyId] = useState(null);
  const [err, setErr] = useState(null); // { code, message } из reject selectOrganization
  async function pick(o) {
    if (busyId) return;
    if (!LA().selectOrganization) { setErr({ message: tr("chooseorg.unavailable", "Выбор организации недоступен: адаптер не подключён") }); return; }
    setBusyId(o.id); setErr(null);
    try {
      const r = await LA().selectOrganization({ orgId: o.id });
      if (setOrg) setOrg(o);
      /* следующий шаг — из ответа сервера; в проде адаптер уводит со страницы
         реальным редиректом (resolve без next) и UI остаётся в pending */
      const n = nextState(r, o);
      if (n) go(n);
      else setBusyId(null);
    } catch (e2) {
      setBusyId(null);
      setErr(e2 && e2.message ? e2 : { message: tr("chooseorg.fail", "Не удалось выбрать организацию. Попробуйте ещё раз.") });
    }
  }
  return (
    <div className="ln-wrap ln-enter">
      <RPStrip />
      <div style={{ marginBottom:16 }}>
        <div className="eyebrow" style={{ marginBottom:6 }}>{tr("chooseorg.eyebrow","Выбор организации")}</div>
        <h1 className="display" style={{ fontSize:"clamp(20px,3vw,28px)", lineHeight:1.1, margin:0 }}>{tr("chooseorg.title","В какой организации продолжить?")}</h1>
        <p className="dim t-sm" style={{ margin:"7px 0 0", lineHeight:1.5 }}><b>{RP.name}</b> {tr("chooseorg.sub","получит доступ в выбранной организации.")}</p>
      </div>
      <div className="who-strip" style={{ marginBottom:14 }}>
        <div className="who-av">{AUTH.USER.initials}</div>
        <div style={{ flex:1, minWidth:0 }}>
          <div style={{ fontWeight:600, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>{AUTH.USER.name}</div>
          <div className="mono t-xs dim" style={{ overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>{AUTH.USER.email}</div>
        </div>
        <button className="btn ghost sm" disabled={!!busyId} onClick={() => go("main")}>{tr("chooseorg.switch","Сменить")}</button>
      </div>
      <div className="org-choose-list">
        {orgs.map(o => {
          const busy = busyId === o.id;
          return (
            <button key={o.id} className="org-choice" onClick={() => pick(o)} disabled={!!busyId} aria-busy={busy || undefined}
              style={busyId && !busy ? { opacity:.55 } : busy ? { borderColor:"var(--accent)" } : null}>
              <span className="org-choice-av" data-state="active">{o.initials || (o.name || "").slice(0, 2).toUpperCase()}</span>
              <span className="org-choice-main">
                <span className="org-choice-name">{o.name}</span>
                <span className="org-choice-detail dim" style={{ fontWeight:600 }}>{o.role}</span>
              </span>
              <span className="org-choice-cta">
                {busy
                  ? <span className="t-xs" style={{ color:"var(--accent)", fontWeight:600 }}>{tr("chooseorg.selecting","Выбираем…")}</span>
                  : <Icon name="arrowR" size={16} style={{ color:"var(--ink-3)" }} />}
              </span>
            </button>
          );
        })}
        {!orgs.length && <p className="t-sm dim" style={{ textAlign:"center", padding:"8px 0" }}>{tr("chooseorg.empty","Нет доступных организаций.")}</p>}
      </div>
      {err && <p role="alert" className="t-xs" style={{ color:"var(--danger)", margin:"-4px 0 10px" }}>{err.message}</p>}
      <button className="btn block" disabled={!!busyId} onClick={() => go("main")} style={{ justifyContent:"center" }}>
        <Icon name="user" size={15} />{tr("chooseorg.other","Другой аккаунт")}
      </button>
    </div>
  );
}

/* ---- state: CONSENT ---- */
function ConsentPane({ go, twofa, org }) {
  const ROWS = AUTH.CONSENT_SCOPES;
  const [busy, setBusy] = useState(null); // "allow" | "deny" | null
  const [err, setErr] = useState(null);
  const hasDecision = !!LA().consentDecision;
  async function decide(approved, sel) {
    if (busy) return;
    if (!hasDecision) { setErr({ message: tr("consent.unavailable", "Решение по согласию недоступно: адаптер не подключён") }); return; }
    setBusy(approved ? "allow" : "deny"); setErr(null);
    try {
      const r = await LA().consentDecision({ approved, scopes: Object.keys(sel || {}).filter(k => sel[k]) });
      const n = nextState(r, org);
      if (n) go(n === "twofa" && twofa === false ? "success" : n);
      else setBusy(null);
    } catch (e) {
      setErr(e && e.message ? e : { message: tr("consent.fail", "Не удалось обработать решение") });
      setBusy(null);
    }
  }
  const [sel, setSel] = useState({ openid:true, profile:true, email:true, "projects:read":true });
  const toggle = k => { if (ROWS.find(r=>r.k===k)?.required) return; setSel(s => ({...s, [k]:!s[k]})); };
  function CheckMark() { return <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>; }
  /* scope `mail` запрошен, но email_verified=false (из гидрации) — вместо
     финального access_denied ведём на подтверждение адреса (проверка НЕ обходится:
     без подтверждения авторизация дальше не идёт) */
  const needMailVerify = ((window.LOGIN_HYDRATE || {}).emailVerified === false) && ROWS.some(r => r.k === "mail");
  return (
    <div className="ln-wrap ln-enter">
      <div style={{ marginBottom:18 }}>
        <div className="eyebrow" style={{ marginBottom:6 }}>{tr("consent.eyebrow","Запрос доступа")}</div>
        <h1 className="display" style={{ fontSize:"clamp(20px,3vw,28px)", lineHeight:1.1, margin:0 }}><b>{RP.name}</b> запрашивает доступ</h1>
        <p className="dim t-sm" style={{ margin:"7px 0 0" }}>Вы всегда можете отозвать разрешения в настройках аккаунта.</p>
      </div>
      <div className="who-strip">
        <div className="who-av">{AUTH.USER.initials}</div>
        <div style={{ flex:1, minWidth:0 }}>
          <div style={{ fontWeight:600 }}>{AUTH.USER.name}</div>
          <div className="mono t-xs dim">{AUTH.USER.email}</div>
        </div>
        <button className="btn ghost sm" onClick={() => go("main")}>Сменить</button>
      </div>
      {org && (
        <div className="org-context">
          <span className="org-choice-av" data-state="active" style={{ width:32, height:32, borderRadius:9, fontSize:"calc(var(--u) - 4px)" }}>{org.initials}</span>
          <div style={{ flex:1, minWidth:0 }} className="t-sm">
            <span className="dim">{tr("consent.org","Организация")} · </span><b>{org.name}</b>
          </div>
          {loginOrgs().length > 1 && <button className="btn ghost sm" onClick={() => go("chooseorg")}>{tr("consent.changeOrg","Изменить")}</button>}
        </div>
      )}
      <div className="scope-list">
        {ROWS.map(r => (
          <div key={r.k} className={"scope-row" + (sel[r.k]?" on":"") + (r.required?" locked":"")} onClick={() => toggle(r.k)}>
            <div className="scope-box">{sel[r.k] && <CheckMark />}</div>
            <div style={{ flex:1, minWidth:0 }}>
              <div style={{ fontWeight:600, fontSize:"calc(var(--u) - 1px)" }}>{r.n}</div>
              <div className="t-xs dim" style={{ marginTop:2 }}>{r.d}</div>
            </div>
            {r.required && <span className="pill t-xs" style={{ flexShrink:0 }}>required</span>}
          </div>
        ))}
      </div>
      <div aria-live="polite" style={{ minHeight: err ? 20 : 0 }}>
        {err && <p role="alert" className="t-xs" style={{ color:"var(--danger)", margin:"0 0 8px" }}>{err.message}</p>}
      </div>
      <div style={{ display:"flex", gap:8 }}>
        <button className="btn" style={{ flex:1 }} disabled={!!busy || !hasDecision} title={hasDecision ? "" : "Недоступно: нет действия consentDecision"} onClick={() => decide(false, sel)}>{busy === "deny" ? "…" : tr("consent.deny","Отклонить")}</button>
        <button className="btn accent" style={{ flex:2 }} disabled={!!busy || (!needMailVerify && !hasDecision)} title={needMailVerify || hasDecision ? "" : "Недоступно: нет действия consentDecision"}
          onClick={() => needMailVerify ? go("mailverify") : decide(true, sel)}>
          {busy === "allow" ? tr("consent.processing","Обрабатываем…") : tr("consent.allow","Разрешить")} <Icon name="arrowR" size={14} /></button>
      </div>
    </div>
  );
}

/* ---- state: 2FA ---- */
function TwoFAPane({ go }) {
  const [code, setCode] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const ref = useRef(null);
  const avail = !!LA().submitTwoFactor;
  useEffect(() => { ref.current?.focus(); }, []);
  useEffect(() => {
    if (!avail || busy || code.length !== 6) return;
    let cancelled = false;
    (async () => {
      setBusy(true); setErr(null);
      try {
        const r = await LA().submitTwoFactor({ code });
        if (cancelled) return;
        const n = nextState(r, null);
        if (n) go(n); else setBusy(false);
      } catch (e) {
        if (cancelled) return;
        setErr(e && e.message ? e.message : tr("twofa.fail", "Неверный код. Попробуйте ещё раз."));
        setCode(""); setBusy(false);
        ref.current && ref.current.focus();
      }
    })();
    return () => { cancelled = true; };
  }, [code, avail]);
  if (!avail) return <FlowUnavailable go={go} back="consent" label={tr("twofa.unavailable","Проверка второго фактора недоступна: адаптер не подключён.")} />;
  const cells = Array.from({ length:6 }, (_, i) => code[i] || "");
  return (
    <div className="ln-wrap ln-enter">
      <div style={{ marginBottom:20 }}>
        <div className="eyebrow" style={{ marginBottom:6 }}>{tr("twofa.eyebrow","Двухфакторная аутентификация")}</div>
        <h1 className="display" style={{ fontSize:"clamp(20px,3vw,28px)", lineHeight:1.06, margin:0 }}>Код <b>подтверждения</b></h1>
        <p className="dim t-sm" style={{ margin:"7px 0 0", lineHeight:1.5 }}>
          Введите 6-значный код из приложения-аутентификатора.</p>
      </div>
      <div className="otp-row" onClick={() => ref.current?.focus()} style={{ cursor:"text" }}>
        {cells.map((c, i) => (
          <div key={i} className={"otp-cell" + (c ? " filled" : "") + (code.length === i ? " active" : "")}>
            {c || (code.length === i ? <span style={{ borderLeft:"2px solid var(--accent)", height:22, animation:"caretBlink 1s steps(1) infinite" }} /> : "")}
          </div>
        ))}
      </div>
      <input ref={ref} value={code} onChange={e => setCode(e.target.value.replace(/\D/g,"").slice(0,6))}
        inputMode="numeric" style={{ position:"absolute", left:-9999, opacity:0 }} aria-label="код" />
      <div aria-live="polite" style={{ minHeight:18, marginTop:8, textAlign:"center" }}>
        {busy && <span className="t-xs faint">{tr("twofa.checking","Проверяем…")}</span>}
        {err && <span role="alert" className="t-xs" style={{ color:"var(--danger)" }}>{err}</span>}
      </div>
      <div style={{ display:"flex", justifyContent:"space-between", marginTop:14 }}>
        <button className="btn ghost sm" onClick={() => go("consent")} disabled={busy}><Icon name="back" size={14} />Назад</button>
        <button className="btn ghost sm" onClick={() => go("main")} disabled={busy}>Другой способ</button>
      </div>
    </div>
  );
}

/* ---- avatar with ok badge ---- */
function AvatarOk({ size = 76, name = AUTH.USER.name }) {
  const initials = name.split(" ").map(w => w[0]).join("").slice(0,2).toUpperCase();
  const r = size * 0.22;
  return (
    <div style={{ position:"relative", width:size, height:size, flex:"none" }}>
      <div style={{ width:size, height:size, borderRadius:size*0.29, background:"var(--accent-soft)",
        border:"2px solid var(--accent-line)", display:"grid", placeItems:"center",
        fontFamily:"var(--f-display)", fontWeight:800, fontSize:size*0.32, color:"var(--accent)", letterSpacing:"-.02em" }}>
        {initials}
      </div>
      <div style={{ position:"absolute", bottom:-3, right:-3, width:r*2, height:r*2,
        borderRadius:"50%", background:"var(--ok)", border:"2px solid var(--paper)",
        display:"grid", placeItems:"center" }}>
        <svg width={r*1.1} height={r*1.1} viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round">
          <polyline points="20 6 9 17 4 12"/>
        </svg>
      </div>
    </div>
  );
}

/* ---- state: SUCCESS ---- */
function SuccessPane() {
  /* целевые URL — ТОЛЬКО из CONSOLE_URLS (runtime); пусто → нет авто-перехода.
     Никаких локальных признаков авторизации (localStorage) страница не пишет. */
  const URLS = window.CONSOLE_URLS || {};
  const appUrl = URLS.app || "";
  const accountUrl = URLS.account || "";
  const [c, setC] = useState(appUrl ? 3 : -1);
  const [fly, setFly] = useState(null); // {top,left,size,tx,ty,phase}
  const avRef = useRef(null);

  useEffect(() => {
    if (!appUrl || c < 0) return;
    if (c <= 0) { launch(); return; }
    const t = setTimeout(() => setC(c - 1), 1000);
    return () => clearTimeout(t);
  }, [c]);

  function launch() {
    if (fly || !appUrl) return;
    const el = avRef.current;
    if (!el) { window.location.href = appUrl; return; }
    const r = el.getBoundingClientRect();
    const size = r.width;
    const cx = r.left + size / 2, cy = r.top + size / 2;
    // target = top-right corner (where Atlas profile lives)
    const tx = (window.innerWidth - 48) - cx;
    const ty = 28 - cy;
    try { sessionStorage.setItem('atlas_entrance', 'corner'); } catch (e) {} // косметика анимации, не признак авторизации
    setFly({ top: r.top, left: r.left, size, tx, ty, phase: 'placed' });
  }

  // after clone mounted (placed) → next frame trigger flight
  useEffect(() => {
    if (!fly || fly.phase !== 'placed') return;
    const id = requestAnimationFrame(() => requestAnimationFrame(() => {
      setFly(f => ({ ...f, phase: 'flying' }));
      setTimeout(() => { if (appUrl) window.location.href = appUrl; }, 640);
    }));
    return () => cancelAnimationFrame(id);
  }, [fly]);

  const flying = !!fly;

  return (
    <>
    <div className={"ln-wrap success-pane" + (flying ? " is-going" : "")}
      style={{ cursor: c === 0 && !flying ? "pointer" : "default" }}
      onClick={c === 0 && !flying ? launch : undefined}>
      <div className="success-inner">

        <div className="success-av-row">
          <div className="success-av" ref={avRef} style={{ visibility: flying ? "hidden" : "visible" }}>
            <AvatarOk size={76} name={AUTH.USER.name} />
          </div>
          <div className="success-title">
            <div className="eyebrow" style={{ marginBottom:5, color:"var(--ok)" }}>Вход выполнен</div>
            <h1 className="display" style={{ fontSize:"clamp(26px,4vw,34px)", lineHeight:1.06, margin:0 }}>
              С возвращением, <b>{AUTH.USER.firstName}</b></h1>
          </div>
        </div>

        <div className="card" style={{ padding:"14px 16px", display:"flex", alignItems:"center", gap:14 }}>
          <div style={{ width:44, height:44, flex:"none", borderRadius:12, background:"var(--accent-soft)",
            border:"1px solid var(--accent-line)", display:"grid", placeItems:"center", color:"var(--accent)",
            fontFamily:"var(--f-display)", fontWeight:800, fontSize:18 }}>{RP.initial}</div>
          <div style={{ flex:1, minWidth:0 }}>
            <div style={{ fontWeight:700 }}>{RP.name}</div>
            <div className="t-xs dim" style={{ marginTop:2 }}>Авторизация выдана · {RP.scopes.length} scope</div>
          </div>
          {appUrl ? (
            <div style={{ textAlign:"right", flex:"none" }}>
              <div className="t-xs faint">переход через</div>
              <div style={{ fontFamily:"var(--f-display)", fontWeight:800, fontSize:28, color:"var(--ink)", lineHeight:1 }}>{Math.max(c,0)}</div>
            </div>
          ) : null}
        </div>

        <div style={{ display:"flex", flexWrap:"wrap", gap:6 }}>
          {RP.scopes.map(s => (
            <span key={s} style={{ display:"inline-flex", alignItems:"center", gap:5, padding:"3px 9px",
              background:"var(--card-2)", border:"1px solid var(--line)", borderRadius:99,
              fontSize:"calc(var(--u) - 3px)", fontFamily:"var(--f-mono)", fontWeight:500, color:"var(--ink-2)" }}>
              <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="var(--ok)" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
              {s}
            </span>
          ))}
        </div>

        {(accountUrl || appUrl) && (
          <div style={{ display:"flex", gap:8 }}>
            {accountUrl && <a href={accountUrl} className="btn sm" style={{ textDecoration:"none", flex:1, justifyContent:"center" }}>Мой аккаунт</a>}
            {appUrl && <a href={appUrl} className="btn accent sm" style={{ textDecoration:"none", flex:2, justifyContent:"center" }}>Перейти в {RP.name} →</a>}
          </div>
        )}
      </div>
    </div>
    {fly && ReactDOM.createPortal(
      <div style={{
        position:"fixed", top:fly.top, left:fly.left, width:fly.size, height:fly.size, zIndex:9999,
        pointerEvents:"none", transformOrigin:"center center",
        transition: fly.phase === 'flying' ? "transform .62s cubic-bezier(.4,0,.2,1), opacity .62s ease" : "none",
        transform: fly.phase === 'flying' ? `translate(${fly.tx}px, ${fly.ty}px) scale(.22)` : "none",
        opacity: fly.phase === 'flying' ? 0 : 1,
      }}>
        <AvatarOk size={fly.size} name={AUTH.USER.name} />
      </div>,
      document.body
    )}
    </>
  );
}


/* ---- state: ERROR ---- */
function ErrorPane({ go }) {
  return (
    <div className="ln-wrap ln-enter">
      <div className="ln-state">
        <div className="ln-ic err" style={{ width:64, height:64, borderRadius:20 }}>
          <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <circle cx="12" cy="12" r="10"/><path d="M12 8v5M12 16.5v.4"/>
          </svg>
        </div>
        <div>
          <h2 className="display" style={{ fontSize:"calc(var(--u) + 10px)", margin:"0 0 8px" }}>Доступ <b>запрещён</b></h2>
          <p className="dim t-sm" style={{ margin:0, lineHeight:1.5 }}>Запрос авторизации отменён или не может быть выполнен.</p>
        </div>
        {(() => {
          /* контекст ошибки приходит ТОЛЬКО из гидрации (адаптер/runtime);
             локальных error/request ID страница не генерирует */
          const er = (window.LOGIN_HYDRATE || {}).error;
          if (!er || (!er.code && !er.description && !er.requestId)) return null;
          return (
            <div className="card" style={{ width:"100%", padding:"12px 14px" }}>
              <div className="mono t-xs" style={{ display:"flex", flexDirection:"column", gap:5 }}>
                {er.code && <div><span className="faint">error</span>{"  "}<span style={{ color:"var(--danger)" }}>{er.code}</span></div>}
                {er.description && <div><span className="faint">desc{"   "}</span><span>{er.description}</span></div>}
                {er.requestId && <div><span className="faint">req_id{"  "}</span><span>{er.requestId}</span></div>}
              </div>
            </div>
          );
        })()}
        <div style={{ display:"flex", gap:8, width:"100%" }}>
          <button className="btn" style={{ flex:1 }} onClick={() => go("main")}>В приложение</button>
          <button className="btn accent" style={{ flex:1 }} onClick={() => go("main")}>Повторить</button>
        </div>
      </div>
    </div>
  );
}

/* ---- state: FORGOT ---- */
function ForgotPane({ go, email }) {
  const [sent, setSent] = useState(false);
  const [em, setEm] = useState(email || "");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const avail = typeof LA().forgotPassword === "function";
  async function send() {
    if (!em || busy || !avail) return;
    setBusy(true); setErr(null);
    try {
      await LA().forgotPassword(em);
      setSent(true); // строго после await — не оптимистично
    } catch (e2) {
      setErr(e2 && e2.message ? e2 : { message: tr("forgot.fail", "Не удалось отправить ссылку") });
    } finally {
      setBusy(false);
    }
  }
  if (!avail) return <FlowUnavailable go={go} back="password" label={tr("forgot.unavailable","Восстановление пароля не настроено для этой организации.")} />;
  if (sent) return (
    <div className="ln-wrap ln-enter">
      <div className="ln-state">
        <div className="ln-ic ok" style={{ width:56, height:56, borderRadius:16 }}>
          <Icon name="mail" size={24} />
        </div>
        <div>
          <h2 className="display" style={{ fontSize:"calc(var(--u) + 8px)", margin:"0 0 8px" }}>Письмо отправлено</h2>
          <p className="dim t-sm" style={{ margin:0, lineHeight:1.5 }}>Проверьте <span className="mono" style={{ color:"var(--accent)" }}>{em}</span>. Ссылка действует 15 минут.</p>
        </div>
        <button className="btn ghost sm" onClick={() => go("main")}>← На главную</button>
        {AUTH.DEV_FLOWS && <button className="btn accent sm" title="dev: переход к форме сброса (в проде — по ссылке из письма)" onClick={() => go("reset")}>Сбросить пароль →</button>}
      </div>
    </div>
  );
  return (
    <div className="ln-wrap ln-enter">
      <div style={{ marginBottom:20 }}>
        <button className="btn ghost sm" style={{ marginBottom:12, padding:0 }} onClick={() => go("password")}><Icon name="back" size={14} />Назад</button>
        <div className="eyebrow" style={{ marginBottom:6 }}>Восстановление доступа</div>
        <h1 className="display" style={{ fontSize:"clamp(20px,3vw,28px)", lineHeight:1.06, margin:0 }}>Забыли <b>пароль?</b></h1>
        <p className="dim t-sm" style={{ margin:"7px 0 0", lineHeight:1.5 }}>Отправим ссылку для сброса на ваш e-mail.</p>
      </div>
      <div className="field" style={{ marginBottom:14 }}>
        <label>E-mail</label>
        <input className="input" type="email" value={em} onChange={e => setEm(e.target.value)} placeholder="you@company.com" autoFocus />
      </div>
      <button className="btn accent block" style={{ height:"calc(var(--ctrl-h) + 6px)" }} disabled={busy}
        onClick={send}>
        <Icon name="mail" size={15} />{busy ? tr("forgot.sending","Отправка…") : tr("forgot.send","Отправить ссылку")}
      </button>
      {err && <p className="t-xs" style={{ color:"var(--danger)", marginTop:8 }}>{err.message}</p>}
    </div>
  );
}

/* ---- state: REGISTER ---- */
function RegisterPane({ go, email, setEmail }) {
  const [form, setForm] = useState({ name:"", email: email||"", pw:"", pw2:"" });
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState({});
  const avail = !!LA().register;
  function set(k,v){ setForm(p=>({...p,[k]:v})); setErr(e=>({...e,[k]:undefined})); }
  async function submit(e){
    e.preventDefault();
    if (busy) return;
    const er={};
    if(!form.name.trim()) er.name="Введите имя";
    if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) er.email="Некорректный e-mail";
    if(form.pw.length < pwMin()) er.pw="Минимум " + pwMin() + " символов";
    if(form.pw !== form.pw2) er.pw2="Пароли не совпадают";
    if(Object.keys(er).length){ setErr(er); return; }
    if(!avail){ setErr({ form: "Регистрация недоступна: адаптер не подключён" }); return; }
    setBusy(true); setErr({});
    try {
      const r = await LA().register({ name: form.name.trim(), email: form.email, password: form.pw });
      setEmail(form.email);
      const n = nextState(r, null);
      if (n) go(n); else setBusy(false);
    } catch (e2) {
      setErr({ form: (e2 && e2.message) || "Не удалось создать аккаунт" });
      setBusy(false);
    }
  }
  if (!avail) return <FlowUnavailable go={go} label={tr("register.unavailable","Саморегистрация не включена для этой организации.")} />;
  const pwStrength = form.pw.length === 0 ? 0 : form.pw.length < pwMin() ? 1 : form.pw.length < pwMin() + 4 ? 2 : 3;
  const pwColors = ["","var(--danger)","var(--warn)","var(--ok)"];
  const pwLabels = ["","Слабый","Хороший","Надёжный"];
  return (
    <div className="ln-wrap ln-enter">
      <RPStrip />
      <div style={{ marginBottom:18 }}>
        <button className="btn ghost sm" style={{ marginBottom:12,padding:0 }} onClick={()=>go("main")}><Icon name="back" size={14}/>Назад</button>
        <div className="eyebrow" style={{ marginBottom:6 }}>Новый аккаунт</div>
        <h1 className="display" style={{ fontSize:"clamp(22px,3.5vw,30px)",lineHeight:1.06,margin:0 }}>Создать <b>аккаунт</b></h1>
        <p className="dim t-sm" style={{ margin:"7px 0 0",lineHeight:1.5 }}>Заполните данные для регистрации в организации.</p>
      </div>
      <form onSubmit={submit} style={{ display:"flex",flexDirection:"column",gap:12 }}>
        <div className="field">
          <label>Имя и фамилия</label>
          <input className="input" placeholder="Иван Петров" autoFocus value={form.name} onChange={e=>set("name",e.target.value)} disabled={busy} style={err.name?{borderColor:"var(--danger)"}:{}} />
          {err.name && <span className="t-xs" style={{ color:"var(--danger)",marginTop:3,display:"block" }}>{err.name}</span>}
        </div>
        <div className="field">
          <label>E-mail</label>
          <input className="input" type="email" placeholder="you@company.com" value={form.email} onChange={e=>set("email",e.target.value)} disabled={busy} style={err.email?{borderColor:"var(--danger)"}:{}} />
          {err.email && <span className="t-xs" style={{ color:"var(--danger)",marginTop:3,display:"block" }}>{err.email}</span>}
        </div>
        <div className="field">
          <label>Пароль</label>
          <input className="input" type="password" placeholder={"Минимум " + pwMin() + " символов"} value={form.pw} onChange={e=>set("pw",e.target.value)} disabled={busy} style={err.pw?{borderColor:"var(--danger)"}:{}} />
          {form.pw && <div style={{ display:"flex",alignItems:"center",gap:8,marginTop:6 }}>
            <div style={{ flex:1,height:3,borderRadius:4,background:"var(--line-2)" }}><div style={{ width:(pwStrength/3*100)+"%",height:"100%",borderRadius:4,background:pwColors[pwStrength],transition:"width .3s" }}/></div>
            <span className="t-xs" style={{ color:pwColors[pwStrength],fontWeight:600 }}>{pwLabels[pwStrength]}</span>
          </div>}
          {err.pw && <span className="t-xs" style={{ color:"var(--danger)",marginTop:3,display:"block" }}>{err.pw}</span>}
        </div>
        <div className="field">
          <label>Повторите пароль</label>
          <input className="input" type="password" placeholder="••••••••" value={form.pw2} onChange={e=>set("pw2",e.target.value)} disabled={busy} style={err.pw2?{borderColor:"var(--danger)"}:{}} />
          {err.pw2 && <span className="t-xs" style={{ color:"var(--danger)",marginTop:3,display:"block" }}>{err.pw2}</span>}
        </div>
        {err.form && <p role="alert" className="t-xs" style={{ color:"var(--danger)", margin:0 }}>{err.form}</p>}
        <button type="submit" className="btn accent block" disabled={busy} style={{ height:"calc(var(--ctrl-h) + 6px)",marginTop:4 }}>
          {busy ? tr("register.busy","Создание…") : <>{tr("register.submit","Создать аккаунт")}<Icon name="arrowR" size={15}/></>}
        </button>
      </form>
      <p className="t-xs faint" style={{ textAlign:"center",marginTop:16,lineHeight:1.5 }}>
        {tr("register.have","Уже есть аккаунт?")} <button className="btn ghost sm" style={{ padding:0,color:"var(--accent)",fontWeight:600 }} onClick={()=>go("email")}>{tr("register.signin","Войти")}</button>
      </p>
    </div>
  );
}

/* ---- state: VERIFY EMAIL ---- */
function VerifyPane({ go, email, org }) {
  const [code, setCode] = useState("");
  const [err, setErr] = useState(null);
  const [busy, setBusy] = useState(false);
  const [resent, setResent] = useState(false);
  const [resendBusy, setResendBusy] = useState(false);
  const ref = useRef(null);
  const avail = !!LA().confirmRegistrationEmail;
  useEffect(()=>{ ref.current?.focus(); },[]);
  useEffect(()=>{
    if (!avail || busy || code.length !== 6) { if (code.length !== 6) setErr(null); return; }
    let cancelled = false;
    (async () => {
      setBusy(true); setErr(null);
      try {
        const r = await LA().confirmRegistrationEmail({ code });
        if (cancelled) return;
        const n = nextState(r, org);
        if (n) go(n); else setBusy(false);
      } catch (e) {
        if (cancelled) return;
        setErr((e && e.message) || "Неверный код. Попробуйте ещё раз.");
        setCode(""); setBusy(false);
        ref.current && ref.current.focus();
      }
    })();
    return () => { cancelled = true; };
  },[code, avail]);
  async function resend() {
    if (resendBusy || resent || !LA().resendRegistrationEmail) return;
    setResendBusy(true);
    try { await LA().resendRegistrationEmail(); setResent(true); }
    catch (e) { setErr((e && e.message) || "Не удалось отправить письмо"); }
    finally { setResendBusy(false); }
  }
  if (!avail) return <FlowUnavailable go={go} back="register" label={tr("verify.unavailable","Подтверждение адреса недоступно: адаптер не подключён.")} />;
  const cells = Array.from({length:6},(_,i)=>code[i]||"");
  return (
    <div className="ln-wrap ln-enter">
      <div style={{ marginBottom:20 }}>
        <div className="ln-ic ok" style={{ width:52,height:52,borderRadius:14,marginBottom:14 }}><Icon name="mail" size={22}/></div>
        <div className="eyebrow" style={{ marginBottom:6 }}>Подтверждение e-mail</div>
        <h1 className="display" style={{ fontSize:"clamp(20px,3vw,28px)",lineHeight:1.06,margin:0 }}>Проверьте <b>почту</b></h1>
        <p className="dim t-sm" style={{ margin:"7px 0 0",lineHeight:1.5 }}>
          Отправили код на <span className="mono" style={{ color:"var(--accent)" }}>{email||"your@email.com"}</span>. Введите его ниже.</p>
      </div>
      <div className="otp-row" onClick={()=>ref.current?.focus()} style={{ cursor:"text",marginBottom:err?8:16 }}>
        {cells.map((c,i)=>(
          <div key={i} className={"otp-cell"+(c?" filled":"")+(code.length===i?" active":"")+(err?" shake":"")} style={err?{borderColor:"var(--danger)"}:{}}>
            {c||(code.length===i?<span style={{ borderLeft:"2px solid var(--accent)",height:22,animation:"caretBlink 1s steps(1) infinite" }}/>:"")}
          </div>
        ))}
      </div>
      <input ref={ref} value={code} onChange={e=>setCode(e.target.value.replace(/\D/g,"").slice(0,6))}
        inputMode="numeric" style={{ position:"absolute",left:-9999,opacity:0 }} aria-label="код" />
      <div aria-live="polite" style={{ minHeight:18, textAlign:"center", marginBottom:8 }}>
        {busy && <span className="t-xs faint">{tr("verify.checking","Проверяем…")}</span>}
        {err && <span role="alert" className="t-xs" style={{ color:"var(--danger)" }}>{err}</span>}
      </div>
      <div style={{ display:"flex",justifyContent:"space-between",marginTop:14 }}>
        <button className="btn ghost sm" onClick={()=>go("register")} disabled={busy}><Icon name="back" size={14}/>Назад</button>
        <button className="btn ghost sm" onClick={resend} disabled={resent || resendBusy || !LA().resendRegistrationEmail}>
          {resent ? <><Icon name="ok" size={13}/>Отправлено</> : resendBusy ? "Отправляем…" : "Отправить ещё раз"}
        </button>
      </div>
    </div>
  );
}

/* ---- state: RESET PASSWORD ---- */
function ResetPane({ go }) {
  const [pw, setPw] = useState("");
  const [pw2, setPw2] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState({});
  const pwStrength = pw.length === 0 ? 0 : pw.length < pwMin() ? 1 : pw.length < pwMin() + 4 ? 2 : 3;
  const pwColors = ["","var(--danger)","var(--warn)","var(--ok)"];
  const pwLabels = ["","Слабый","Хороший","Надёжный"];
  const avail = !!LA().resetPassword;
  async function submit(e){
    e.preventDefault();
    if (busy) return;
    const er={};
    if(pw.length < pwMin()) er.pw="Минимум " + pwMin() + " символов";
    if(pw !== pw2) er.pw2="Пароли не совпадают";
    if(Object.keys(er).length){ setErr(er); return; }
    if(!avail){ setErr({ form: "Сброс пароля недоступен: адаптер не подключён" }); return; }
    setBusy(true); setErr({});
    try {
      const r = await LA().resetPassword({ password: pw });
      const n = nextState(r, null);
      if (n) go(n); else setBusy(false);
    } catch (e2) {
      setErr({ form: (e2 && e2.message) || "Не удалось сохранить пароль" });
      setBusy(false);
    }
  }
  return (
    <div className="ln-wrap ln-enter">
      <div style={{ marginBottom:20 }}>
        <div className="eyebrow" style={{ marginBottom:6 }}>Восстановление доступа</div>
        <h1 className="display" style={{ fontSize:"clamp(22px,3.5vw,30px)",lineHeight:1.06,margin:0 }}>Новый <b>пароль</b></h1>
        <p className="dim t-sm" style={{ margin:"7px 0 0",lineHeight:1.5 }}>Придумайте надёжный пароль для вашего аккаунта.</p>
      </div>
      <form onSubmit={submit} style={{ display:"flex",flexDirection:"column",gap:12 }}>
        <div className="field">
          <label>Новый пароль</label>
          <input className="input" type="password" autoFocus placeholder={"Минимум " + pwMin() + " символов"}
            value={pw} onChange={e=>{ setPw(e.target.value); setErr(r=>({...r,pw:undefined})); }} disabled={busy}
            style={err.pw?{borderColor:"var(--danger)"}:{}} />
          {pw && <div style={{ display:"flex",alignItems:"center",gap:8,marginTop:6 }}>
            <div style={{ flex:1,height:3,borderRadius:4,background:"var(--line-2)" }}><div style={{ width:(pwStrength/3*100)+"%",height:"100%",borderRadius:4,background:pwColors[pwStrength],transition:"width .3s" }}/></div>
            <span className="t-xs" style={{ color:pwColors[pwStrength],fontWeight:600 }}>{pwLabels[pwStrength]}</span>
          </div>}
          {err.pw && <span className="t-xs" style={{ color:"var(--danger)",marginTop:3,display:"block" }}>{err.pw}</span>}
        </div>
        <div className="field">
          <label>Повторите пароль</label>
          <input className="input" type="password" placeholder="••••••••"
            value={pw2} onChange={e=>{ setPw2(e.target.value); setErr(r=>({...r,pw2:undefined})); }} disabled={busy}
            style={err.pw2?{borderColor:"var(--danger)"}:{}} />
          {err.pw2 && <span className="t-xs" style={{ color:"var(--danger)",marginTop:3,display:"block" }}>{err.pw2}</span>}
        </div>
        <div className="card" style={{ padding:"10px 12px",background:"var(--card-2)",display:"flex",gap:8,alignItems:"center" }}>
          <Icon name="check" size={14} style={{ color:"var(--ok)" }}/>
          <span className="t-xs dim">Ссылка для сброса верифицирована и действительна.</span>
        </div>
        {err.form && <p role="alert" className="t-xs" style={{ color:"var(--danger)", margin:0 }}>{err.form}</p>}
        <button type="submit" className="btn accent block" disabled={busy} style={{ height:"calc(var(--ctrl-h) + 6px)",marginTop:4 }}>
          {busy ? "Сохранение…" : <>Сохранить пароль<Icon name="arrowR" size={15}/></>}
        </button>
      </form>
    </div>
  );
}

/* ---- state: MAGIC — вход без пароля: одноразовый код на почту ---- */
function MagicPane({ go, email, setEmail, org }) {
  const [phase, setPhase] = useState("email"); // email | code
  const [em, setEm] = useState(email || "");
  const [code, setCode] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const ref = useRef(null);
  const avail = !!(LA().requestMagicCode && LA().confirmMagicCode);
  useEffect(() => { ref.current?.focus(); }, [phase]);
  useEffect(() => {
    if (phase !== "code" || busy || code.length !== 6) return;
    let cancelled = false;
    (async () => {
      setBusy(true); setErr(null);
      try {
        const r = await LA().confirmMagicCode({ code });
        if (cancelled) return;
        const n = nextState(r, org);
        if (n) go(n); else setBusy(false);
      } catch (e) {
        if (cancelled) return;
        setErr((e && e.message) || "Код недействителен или истёк.");
        setCode(""); setBusy(false);
        ref.current && ref.current.focus();
      }
    })();
    return () => { cancelled = true; };
  }, [code, phase]);
  async function requestCode(e) {
    e.preventDefault();
    if (!valid || busy) return;
    setBusy(true); setErr(null);
    try { await LA().requestMagicCode({ email: em }); setEmail(em); setPhase("code"); }
    catch (e2) { setErr((e2 && e2.message) || "Не удалось отправить код"); }
    finally { setBusy(false); }
  }
  const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(em);
  if (!avail) return <FlowUnavailable go={go} label={tr("magic.unavailable","Одноразовый код на почту не настроен для этой организации.")} />;
  if (phase === "code") {
    const cells = Array.from({ length: 6 }, (_, i) => code[i] || "");
    return (
      <div className="ln-wrap ln-enter">
        <div style={{ marginBottom:20 }}>
          <div className="ln-ic ok" style={{ width:52, height:52, borderRadius:14, marginBottom:14 }}><Icon name="mail" size={22} /></div>
          <div className="eyebrow" style={{ marginBottom:6 }}>{tr("magic.sent.title","Проверьте почту")}</div>
          <h1 className="display" style={{ fontSize:"clamp(20px,3vw,28px)", lineHeight:1.06, margin:0 }}>Код из <b>письма</b></h1>
          <p className="dim t-sm" style={{ margin:"7px 0 0", lineHeight:1.5 }}>
            Отправили 6-значный код на <span className="mono" style={{ color:"var(--accent)" }}>{em}</span>. Ссылка и код действуют 15 минут.</p>
        </div>
        <div className="otp-row" onClick={() => ref.current?.focus()} style={{ cursor:"text" }}>
          {cells.map((c, i) => (
            <div key={i} className={"otp-cell" + (c ? " filled" : "") + (code.length === i ? " active" : "")}>
              {c || (code.length === i ? <span style={{ borderLeft:"2px solid var(--accent)", height:22, animation:"caretBlink 1s steps(1) infinite" }} /> : "")}
            </div>
          ))}
        </div>
        <input ref={ref} value={code} onChange={e => setCode(e.target.value.replace(/\D/g,"").slice(0,6))}
          inputMode="numeric" style={{ position:"absolute", left:-9999, opacity:0 }} aria-label="код" />
        <div aria-live="polite" style={{ minHeight:18, textAlign:"center", marginTop:8 }}>
          {busy && <span className="t-xs faint">{tr("magic.checking","Проверяем…")}</span>}
          {err && <span role="alert" className="t-xs" style={{ color:"var(--danger)" }}>{err}</span>}
        </div>
        <div style={{ display:"flex", justifyContent:"space-between", marginTop:14 }}>
          <button className="btn ghost sm" onClick={() => setPhase("email")} disabled={busy}><Icon name="back" size={14} />{tr("common.back","Назад")}</button>
          <button className="btn ghost sm" onClick={() => go("main")} disabled={busy}>{tr("common.cancel","Отмена")}</button>
        </div>
      </div>
    );
  }
  return (
    <div className="ln-wrap ln-enter">
      <RPStrip />
      <div style={{ marginBottom:20 }}>
        <button className="btn ghost sm" style={{ marginBottom:12, padding:0 }} onClick={() => go("main")}><Icon name="back" size={14} />{tr("common.back","Назад")}</button>
        <div className="eyebrow" style={{ marginBottom:6 }}>{tr("magic.eyebrow","Вход без пароля")}</div>
        <h1 className="display" style={{ fontSize:"clamp(22px,3.5vw,30px)", lineHeight:1.06, margin:0 }}>Код на <b>почту</b></h1>
        <p className="dim t-sm" style={{ margin:"7px 0 0", lineHeight:1.5 }}>Отправим одноразовый код — пароль не нужен.</p>
      </div>
      <form onSubmit={requestCode}>
        <div className="field" style={{ marginBottom:14 }}>
          <label>{tr("magic.label","E-mail")}</label>
          <input ref={ref} className="input" type="email" autoComplete="email" placeholder="you@company.com"
            value={em} onChange={e => setEm(e.target.value)} />
        </div>
        {err && <p role="alert" className="t-xs" style={{ color:"var(--danger)", margin:"0 0 8px" }}>{err}</p>}
        <button type="submit" className="btn accent block" disabled={!valid || busy} style={{ height:"calc(var(--ctrl-h) + 6px)" }}>
          <Icon name="bolt" size={15} />{busy ? tr("magic.sending","Отправляем…") : tr("magic.send","Отправить одноразовый код")}
        </button>
      </form>
    </div>
  );
}

/* ---- state: DEVICE — вход с устройства без браузера (RFC 8628, user_code) ---- */
function DevicePane({ go, org }) {
  const [code, setCode] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const ref = useRef(null);
  const avail = !!LA().approveDeviceCode;
  useEffect(() => { ref.current?.focus(); }, []);
  const clean = code.replace(/[^A-Z0-9]/g, "");
  const shown = clean.length > 4 ? clean.slice(0, 4) + "-" + clean.slice(4, 8) : clean;
  const full = clean.length === 8;
  async function approve() {
    if (!full || busy || !avail) return;
    setBusy(true); setErr(null);
    try {
      const r = await LA().approveDeviceCode({ user_code: clean });
      const n = nextState(r, org);
      if (n) go(n); else setBusy(false);
    } catch (e) {
      setErr((e && e.message) || "Код устройства не распознан");
      setBusy(false);
    }
  }
  if (!avail) return <FlowUnavailable go={go} label={tr("device.unavailable","Device authorization не настроен для этой организации.")} />;
  return (
    <div className="ln-wrap ln-enter">
      <div style={{ marginBottom:20 }}>
        <button className="btn ghost sm" style={{ marginBottom:12, padding:0 }} onClick={() => go("main")}><Icon name="back" size={14} />{tr("common.back","Назад")}</button>
        <div className="eyebrow" style={{ marginBottom:6 }}>{tr("device.eyebrow","Вход на устройстве")}</div>
        <h1 className="display" style={{ fontSize:"clamp(22px,3.5vw,30px)", lineHeight:1.06, margin:0 }}>Код <b>устройства</b></h1>
        <p className="dim t-sm" style={{ margin:"7px 0 0", lineHeight:1.5 }}>{tr("device.hint","Введите код, показанный на устройстве (ТВ, консоль, CLI).")}</p>
      </div>
      <div className="field" style={{ marginBottom:14 }}>
        <label>user_code</label>
        <input ref={ref} className="input mono" value={shown} disabled={busy}
          onChange={e => setCode(e.target.value.toUpperCase())}
          placeholder="XXXX-XXXX" maxLength={9}
          style={{ letterSpacing:".22em", fontSize:"calc(var(--u) + 4px)", textAlign:"center", height:"calc(var(--ctrl-h) + 10px)" }}
          onKeyDown={e => { if (e.key === "Enter") approve(); }} />
      </div>
      {err && <p role="alert" className="t-xs" style={{ color:"var(--danger)", margin:"0 0 8px" }}>{err}</p>}
      <button className="btn accent block" disabled={!full || busy} onClick={approve} style={{ height:"calc(var(--ctrl-h) + 6px)" }}>
        {busy ? tr("password.checking","Проверка…") : <>{tr("device.approve","Подтвердить устройство")}<Icon name="arrowR" size={15} /></>}
      </button>
      <p className="t-xs faint" style={{ textAlign:"center", marginTop:14, lineHeight:1.5 }}>RFC 8628 · device authorization grant</p>
    </div>
  );
}

/* ---- переключатель языка: дропдаун со списком локалей (N локалей) ---- */
function LangSwitch() {
  const [open, setOpen] = useState(false);
  const L = window.I18N;
  if (!L || (L.locales || []).length < 2) return null;
  return (
    <div style={{ position:"relative" }}>
      <button className="skin-btn" onClick={()=>setOpen(o=>!o)} onBlur={()=>setTimeout(()=>setOpen(false),150)}
        style={{ display:"inline-flex", alignItems:"center", gap:6 }}>
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/></svg>
        {L.label()}
        <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ transform:open?"rotate(180deg)":"none", transition:"transform .16s" }}><path d="M6 9l6 6 6-6"/></svg>
      </button>
      {open && (
        <div style={{ position:"absolute", top:"calc(100% + 7px)", right:0, minWidth:160, background:"var(--card)", border:"1px solid var(--line)", borderRadius:"var(--radius-sm)", boxShadow:"var(--shadow-3)", padding:5, zIndex:80 }}>
          {L.locales.map(l => {
            const on = l.code === L.locale;
            return (
              <button key={l.code} onMouseDown={e=>e.preventDefault()} onClick={()=>{ L.setLocale(l.code); setOpen(false); }}
                style={{ width:"100%", display:"flex", alignItems:"center", gap:8, padding:"8px 10px", border:0, borderRadius:7, cursor:"pointer", fontWeight:600, fontSize:"calc(var(--u) - 1.5px)", textAlign:"left",
                  background:on?"var(--accent-soft)":"transparent", color:on?"var(--accent)":"var(--ink-2)" }}>
                <span style={{ flex:1 }}>{l.label}</span>
                <span className="mono t-xs" style={{ opacity:.6, textTransform:"uppercase" }}>{l.code}</span>
                {on && <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>}
              </button>
            );
          })}
        </div>
      )}
    </div>
  );
}

/* ---- state: MAILVERIFY — подтверждение e-mail внутри OAuth-journey.
   Сценарий: пользователь вошёл, приложение запросило scope `mail`, но
   email_verified=false — вместо финального access_denied даём подтвердить адрес
   и безопасно продолжить исходную авторизацию (проверка НЕ ослабляется).
   Vendor JSX без сетевой/доменной логики, mock-таймеров и своих security-состояний:
   только LOGIN_ACTIONS.requestEmailVerification() / confirmEmailVerification({code}) /
   resendEmailVerification() / continueAuthorization(). Challenge, token, user_id,
   redirect URI и другие OAuth-параметры UI не получает и не передаёт — runtime
   берёт их из доверенного текущего контекста. Ошибки — только нормализованные
   машинные коды (raw-текст backend не рендерится); backend намеренно не различает
   wrong account / already used / invalid / expired — единое состояние
   invalid_or_expired («Код недействителен или истёк»). ---- */
const MV_ERRORS = {
  invalid_or_expired: "Код недействителен или истёк. Запросите новый ниже.",
  rate_limited: "Слишком много попыток.",
  verification_unavailable: "Сервис подтверждения временно недоступен. Попробуйте ещё раз чуть позже.",
  challenge_expired: "Сессия авторизации истекла. Начните вход заново.",
};
/* легаси-коды схлопываются в единое безопасное состояние */
function mvNorm(c) {
  if (c === "invalid_code" || c === "expired_code" || c === "code_already_used" || c === "wrong_account") return "invalid_or_expired";
  return (MV_ERRORS[c] || c === "already_verified") ? c : "verification_unavailable";
}
function mvMask(em) {
  const parts = String(em || "").split("@");
  const u = parts[0] || "", d = parts[1] || "";
  if (!d) return "•••";
  const mu = u ? u[0] + "•••" + (u.length > 1 ? u[u.length - 1] : "") : "•••";
  const dp = d.split(".");
  const md = (dp[0] ? dp[0][0] + "•••" : "•••") + (dp.length > 1 ? "." + dp.slice(1).join(".") : "");
  return mu + "@" + md;
}
function MailVerifyPane({ go }) {
  const A = window.LOGIN_ACTIONS || {};
  const H = window.LOGIN_HYDRATE || {};
  /* точный контракт (fail-closed): все три действия обязаны существовать;
     неполный контракт → честный unavailable state, никаких await(a && a()) */
  const mvAvailable = typeof A.requestEmailVerification === "function"
    && typeof A.confirmEmailVerification === "function"
    && typeof A.resendEmailVerification === "function";
  const [phase, setPhase] = useState("initial"); // initial|requesting|reqfail|sent|confirming|verified|fatal
  const [target, setTarget] = useState(H.maskedEmail || mvMask((AUTH.USER || {}).email)); // masked, готовит сервер
  const [code, setCode] = useState("");
  const [err, setErr] = useState(null);      // нормализованный машинный код
  const [notice, setNotice] = useState("");  // resend success
  const [cool, setCool] = useState(0);       // cooldown повторной отправки, с
  const [wait, setWait] = useState(0);       // rate-limit retry-after, с
  const [resendBusy, setResendBusy] = useState(false);
  const [contBusy, setContBusy] = useState(false); // continueAuthorization в полёте
  const [contErr, setContErr] = useState(false);
  const inRef = useRef(null); const okRef = useRef(null);

  useEffect(() => { if (cool <= 0) return; const t = setInterval(() => setCool(c => c - 1), 1000); return () => clearInterval(t); }, [cool > 0]);
  useEffect(() => {
    if (wait <= 0) return;
    const t = setInterval(() => setWait(w => {
      if (w <= 1) { setErr(e => e === "rate_limited" ? null : e); return 0; }
      return w - 1;
    }), 1000);
    return () => clearInterval(t);
  }, [wait > 0]);
  useEffect(() => { if (phase === "sent") inRef.current && inRef.current.focus(); }, [phase]);
  useEffect(() => { if (phase === "verified") okRef.current && okRef.current.focus(); }, [phase]);

  async function request() {
    if (!mvAvailable) return;
    setPhase("requesting"); setErr(null); setNotice(""); setCode("");
    try {
      const r = (await A.requestEmailVerification()) || {};
      if (r.maskedTarget) setTarget(r.maskedTarget);
      setCool(r.cooldown || 30);
      setPhase("sent");
    } catch (e) {
      const c = mvNorm((e && e.code) || "verification_unavailable");
      if (c === "already_verified") { setPhase("verified"); return; }
      if (c === "challenge_expired") { setErr(c); setPhase("fatal"); return; }
      if (c === "rate_limited") setWait((e && e.retryAfter) || 30);
      setErr(c); setPhase("reqfail");
    }
  }
  async function confirm(ev) {
    if (ev) ev.preventDefault();
    if (phase !== "sent" || code.length < 8 || wait > 0 || !mvAvailable) return;
    setPhase("confirming"); setErr(null); setNotice("");
    try {
      await A.confirmEmailVerification({ code });
      setPhase("verified");
    } catch (e) {
      const c = mvNorm((e && e.code) || "verification_unavailable");
      if (c === "already_verified") { setPhase("verified"); return; }
      if (c === "challenge_expired") { setErr(c); setPhase("fatal"); return; }
      if (c === "rate_limited") setWait((e && e.retryAfter) || 30);
      setErr(c); setCode(""); setPhase("sent");
      if (inRef.current) inRef.current.focus();
    }
  }
  async function resend() {
    if (cool > 0 || resendBusy || phase === "confirming" || !mvAvailable) return;
    setResendBusy(true); setErr(null); setNotice("");
    try {
      const r = (await A.resendEmailVerification()) || {};
      if (r.maskedTarget) setTarget(r.maskedTarget);
      setCool(r.cooldown || 30);
      setNotice(tr("mailverify.resent", "Код отправлен повторно."));
    } catch (e) {
      const c = mvNorm((e && e.code) || "verification_unavailable");
      if (c === "challenge_expired") { setErr(c); setPhase("fatal"); setResendBusy(false); return; }
      if (c === "rate_limited") setCool((e && e.retryAfter) || 30);
      setErr(c);
    }
    setResendBusy(false);
  }
  /* безопасное продолжение исходной авторизации: runtime сам знает challenge и
     redirect (в проде уводит на authorize-редирект); произвольного redirect-input
     нет. В dev-journey после resolve — следующий шаг цепочки. */
  async function proceed() {
    if (contBusy) return;
    setContBusy(true); setContErr(false);
    try {
      const r = A.continueAuthorization ? await A.continueAuthorization() : null;
      /* prod: runtime уводит authorize-редиректом (resolve без next);
         standalone: canonical next продолжает journey */
      const n = nextState(r, null);
      if (n) go(n); else setContBusy(false);
    } catch (e) { setContErr(true); setContBusy(false); }
  }

  const busy = phase === "confirming" || phase === "requesting";
  const secs = wait > 0 ? wait : 0;

  if (!mvAvailable) return (
    <FlowUnavailable go={go} back="consent" label={tr("mailverify.unavailable","Подтверждение адреса недоступно: адаптер не предоставил контракт подтверждения e-mail.")} />
  );
  if (phase === "verified") return (
    <div className="ln-wrap ln-enter">
      <RPStrip />
      <div role="status" aria-live="polite" style={{ display:"flex", flexDirection:"column", alignItems:"center", textAlign:"center", gap:14, padding:"18px 0" }}>
        <span style={{ width:54, height:54, borderRadius:99, background:"var(--ok-soft)", color:"var(--ok)", border:"1px solid color-mix(in srgb, var(--ok) 35%, transparent)", display:"grid", placeItems:"center" }}>
          <Icon name="ok" size={26} />
        </span>
        <div>
          <h1 ref={okRef} tabIndex={-1} className="display" style={{ fontSize:"clamp(20px,3vw,26px)", margin:"0 0 6px", outline:"none" }}>Адрес <b>подтверждён</b></h1>
          <p className="dim t-sm" style={{ margin:0, lineHeight:1.55 }}>
            <span className="mono">{target}</span> подтверждён. Можно безопасно продолжить авторизацию <b>{RP.name}</b>.
          </p>
        </div>
        <div aria-live="polite" style={{ minHeight:18, alignSelf:"stretch" }}>
          {contErr && <p role="alert" className="t-xs" style={{ color:"var(--danger)", margin:0 }}>{MV_ERRORS.verification_unavailable}</p>}
        </div>
        <button className="btn block accent" style={{ height:"calc(var(--ctrl-h) + 6px)" }} onClick={proceed} disabled={contBusy}>
          {contBusy
            ? <><span style={{ width:14, height:14, border:"2px solid rgba(255,255,255,.4)", borderTopColor:"#fff", borderRadius:99, display:"inline-block", animation:"ln-spin .8s linear infinite" }} />{tr("mailverify.continuing","Продолжаем…")}</>
            : <>{tr("mailverify.continue","Продолжить авторизацию")} <Icon name="arrowR" size={14} /></>}
        </button>
      </div>
    </div>
  );

  if (phase === "fatal") return (
    <div className="ln-wrap ln-enter">
      <RPStrip />
      <div role="alert" style={{ display:"flex", flexDirection:"column", alignItems:"center", textAlign:"center", gap:14, padding:"18px 0" }}>
        <span style={{ width:54, height:54, borderRadius:99, background:"var(--danger-soft)", color:"var(--danger)", display:"grid", placeItems:"center" }}><Icon name="alert" size={24} /></span>
        <p className="t-sm" style={{ margin:0, lineHeight:1.55, maxWidth:340 }}>{MV_ERRORS.challenge_expired}</p>
        <button className="btn block accent" onClick={() => go("main")}>{tr("mailverify.restart","Войти заново")}</button>
      </div>
    </div>
  );

  return (
    <div className="ln-wrap ln-enter">
      <RPStrip />
      <div style={{ marginBottom:18 }}>
        <div className="eyebrow" style={{ marginBottom:6 }}>{tr("mailverify.eyebrow","Доступ к почте · подтверждение адреса")}</div>
        <h1 className="display" style={{ fontSize:"clamp(22px,3.5vw,30px)", lineHeight:1.06, margin:0 }}>Подтвердите <b>e-mail</b></h1>
        <p className="dim t-sm" style={{ margin:"10px 0 0", lineHeight:1.55 }}>
          <b>{RP.name}</b> запрашивает доступ к вашей почте, но адрес <span className="mono">{target}</span> ещё не подтверждён.
          {(phase === "sent" || phase === "confirming")
            ? <React.Fragment>{" "}Мы отправили на него 8-значный код подтверждения.</React.Fragment>
            : <React.Fragment>{" "}Отправим на него код подтверждения — без этого авторизация не продолжится.</React.Fragment>}
        </p>
      </div>

      {phase === "initial" && (
        <div>
          <button className="btn block accent" style={{ height:"calc(var(--ctrl-h) + 6px)" }} onClick={request}>
            <Icon name="mail" size={15} />{tr("mailverify.send","Отправить код")}
          </button>
          <div style={{ display:"flex", justifyContent:"flex-start", marginTop:12 }}>
            <button type="button" className="btn ghost sm" onClick={() => go("consent")}><Icon name="back" size={14} />{tr("common.back","Назад")}</button>
          </div>
        </div>
      )}

      {phase === "requesting" && (
        <p role="status" aria-live="polite" className="dim t-sm" style={{ display:"flex", alignItems:"center", gap:9, margin:"4px 0 14px" }}>
          <span style={{ width:15, height:15, border:"2px solid var(--line-3)", borderTopColor:"var(--accent)", borderRadius:99, display:"inline-block", animation:"ln-spin .8s linear infinite" }} />
          {tr("mailverify.sending","Отправляем код…")}
        </p>
      )}

      {phase === "reqfail" && (
        <div style={{ marginBottom:14 }} aria-live="polite">
          <p role="alert" className="t-sm" style={{ color:"var(--danger)", margin:"0 0 10px", lineHeight:1.5 }}>
            {MV_ERRORS[err] || MV_ERRORS.verification_unavailable}{err === "rate_limited" && secs > 0 ? " Повторите через " + secs + " с." : ""}
          </p>
          <button className="btn block" onClick={request} disabled={err === "rate_limited" && secs > 0}>
            <Icon name="mail" size={15} />{tr("mailverify.retry","Отправить код ещё раз")}
          </button>
        </div>
      )}

      {(phase === "sent" || phase === "confirming") && (
        <form onSubmit={confirm}>
          <div className="field" style={{ marginBottom:10 }}>
            <label htmlFor="mv-code">{tr("mailverify.codeLabel","Код из письма — 8 цифр")}</label>
            <input id="mv-code" ref={inRef} className="input mono" value={code}
              onChange={e => setCode(e.target.value.replace(/[^0-9]/g, "").slice(0, 8))}
              inputMode="numeric" autoComplete="one-time-code" placeholder="········"
              disabled={busy || secs > 0}
              aria-describedby="mv-msg"
              style={{ letterSpacing:".28em", fontSize:"calc(var(--u) + 4px)", textAlign:"center" }} />
          </div>
          <div id="mv-msg" aria-live="polite" style={{ minHeight:20, marginBottom:10 }}>
            {err && (
              <p role="alert" className="t-xs" style={{ color:"var(--danger)", margin:0, lineHeight:1.5 }}>
                {MV_ERRORS[err] || MV_ERRORS.verification_unavailable}{err === "rate_limited" && secs > 0 ? " Повторите через " + secs + " с." : ""}
              </p>
            )}
            {!err && notice && <p role="status" className="t-xs" style={{ color:"var(--ok)", margin:0 }}><Icon name="ok" size={12} /> {notice}</p>}
          </div>
          <button type="submit" className="btn block accent" style={{ height:"calc(var(--ctrl-h) + 6px)" }}
            disabled={busy || code.length < 8 || secs > 0}>
            {phase === "confirming"
              ? <><span style={{ width:14, height:14, border:"2px solid rgba(255,255,255,.4)", borderTopColor:"#fff", borderRadius:99, display:"inline-block", animation:"ln-spin .8s linear infinite" }} />{tr("mailverify.checking","Проверяем…")}</>
              : tr("mailverify.confirm","Подтвердить")}
          </button>
          <div style={{ display:"flex", alignItems:"center", justifyContent:"space-between", marginTop:12 }}>
            <button type="button" className="btn ghost sm" onClick={() => go("consent")}>{tr("common.cancel","Отмена")}</button>
            <span aria-live="polite">
              <button type="button" className="btn ghost sm" onClick={resend} disabled={cool > 0 || resendBusy}>
                {resendBusy ? tr("mailverify.resending","Отправляем…")
                  : cool > 0 ? tr("mailverify.resendIn","Отправить ещё раз") + " · " + cool + " с"
                  : tr("mailverify.resend","Отправить ещё раз")}
              </button>
            </span>
          </div>
        </form>
      )}
      {phase !== "initial" && (
        <p className="t-xs faint" style={{ marginTop:16, lineHeight:1.5 }}>
          {tr("mailverify.hint","Код действует 15 минут. Письмо не пришло — проверьте папку «Спам».")}
        </p>
      )}
    </div>
  );
}

/* ---- dev-переключатель потоков (виден только при AUTH.DEV_FLOWS — mock/standalone) ---- */
function FlowPicker({ go, cur }) {
  const FLOWS = [
    ["email", tr("picker.password","Пароль"), () => go("email")],
    ["passkey", tr("picker.passkey","Passkey"), () => go("redirect","passkey")],
    ["google", tr("picker.google","Google SSO"), () => go("redirect","google")],
    ["okta", tr("picker.okta","Okta SSO"), () => go("redirect","okta")],
    ["magic", tr("picker.magic","Код на почту"), () => go("magic")],
    ["device", tr("picker.device","Device code"), () => go("device")],
    ["chooseorg", tr("picker.chooseorg","Выбор организации"), () => go("chooseorg")],
    ["register", tr("picker.register","Регистрация"), () => go("register")],
    ["forgot", tr("picker.forgot","Восстановление"), () => go("forgot")],
    ["twofa", tr("picker.twofa","2FA"), () => go("twofa")],
    ["consent", tr("picker.consent","Согласие"), () => go("consent")],
    ["mailverify", tr("picker.mailverify","Подтверждение e-mail"), () => go("mailverify")],
    ["success", tr("picker.success","Успех"), () => go("success")],
    ["error", tr("picker.error","Ошибка"), () => go("error")],
    ["locked", tr("picker.locked","Блокировка"), () => { window.location.href = "Login locked.html"; }],
  ];
  return (
    <div style={{ position:"fixed", bottom:14, left:"50%", transform:"translateX(-50%)", zIndex:50, display:"flex", gap:5, flexWrap:"wrap", justifyContent:"center", alignItems:"center", maxWidth:"min(94vw, 780px)", padding:"7px 12px", background:"var(--card)", border:"1px solid var(--line)", borderRadius:18, boxShadow:"var(--shadow-2)" }}>
      <span className="t-xs faint" style={{ fontWeight:700, letterSpacing:".06em", textTransform:"uppercase", marginRight:3 }}>{tr("picker.title","Потоки")}</span>
      {FLOWS.map(([id, label, fn]) => (
        <button key={id} onClick={fn} className="t-xs" style={{ border:"1px solid " + (cur === id ? "var(--accent)" : "var(--line)"), background: cur === id ? "var(--accent-soft)" : "transparent", color: cur === id ? "var(--accent)" : "var(--ink-3)", borderRadius:99, padding:"4px 10px", cursor:"pointer", fontWeight:600 }}>{label}</button>
      ))}
    </div>
  );
}

/* direction map */
const FWD_MAP = { main:["email","register","redirect","magic","device","chooseorg"], email:["password","redirect"], password:["chooseorg","consent","forgot"], forgot:["reset"], register:["verify"], verify:["chooseorg","consent"], redirect:["chooseorg","consent"], magic:["chooseorg","consent"], device:["chooseorg","consent"], chooseorg:["consent","twofa","mailverify","success","redirect"], consent:["twofa","success","error","mailverify"], mailverify:["twofa","success"], twofa:["success","error"] };
const UP_STATES = ["success","error"];
function getDir(from, to) {
  if (UP_STATES.includes(to)) return "up";
  if (FWD_MAP[from]?.includes(to)) return "fwd";
  return "back";
}

function App() {
  /* гидрация (login-hydrate v1): адаптер кладёт window.LOGIN_HYDRATE до загрузки приложения;
     providers читает MainPane напрямую — через стейт идут только entryState/email */
  const H = window.LOGIN_HYDRATE || {};
  const [cur,  setCur]  = useState(H.entryState || "main");
  const [dir,  setDir]  = useState("fwd");
  const [email, setEmail] = useState(H.email || "");
  const [provider, setProvider] = useState(null);
  const [org, setOrg] = useState(null); // выбранная организация (мультиорг-чузер)
  const [provErr, setProvErr] = useState(null); // отказ selectProvider — баннер на MainPane
  // мост i18n: смена локали перерисовывает UI
  const [, _force] = useState(0);
  useEffect(() => { window.__loginBump = () => _force(n => n + 1); return () => { if (window.__loginBump) delete window.__loginBump; }; }, []);

  const go = useCallback((s, p) => {
    setDir(getDir(cur, s));
    setCur(s);
    if (s === "main") setOrg(null); // «Сменить/другой аккаунт» — сбрасываем выбранную организацию
    if (p !== undefined) setProvider(p);
  }, [cur]);

  const comps = { main:MainPane, email:EmailPane, password:PasswordPane, redirect:RedirectPane,
    chooseorg:ChooseOrgPane, consent:ConsentPane, twofa:TwoFAPane, success:SuccessPane, error:ErrorPane, forgot:ForgotPane,
    register:RegisterPane, verify:VerifyPane, reset:ResetPane, magic:MagicPane, device:DevicePane, mailverify:MailVerifyPane };

  const enterClass = dir==="up" ? "ln-enter-up" : dir==="back" ? "ln-enter-back" : "ln-enter-fwd";
  const CurComp  = comps[cur];

  return (
    <div className="login-grid">
      <LoginAside />
      <section className="login-form">
        <div style={{ position:"absolute", top:18, right:22, zIndex:10, display:"flex", gap:8 }}><LangSwitch /><SkinToggle /></div>
        <div style={{ position:"relative", width:"100%", display:"flex", justifyContent:"center" }}>
          {CurComp && (
            <div key={"cur-"+cur} className={"ln-wrap " + enterClass} style={{ zIndex:2, width:"100%" }}>
              <CurComp go={go} email={email} setEmail={setEmail} provider={provider} org={org} setOrg={setOrg} twofa={true} provErr={provErr} setProvErr={setProvErr} />
            </div>
          )}
        </div>
      </section>
      {AUTH.DEV_FLOWS && <FlowPicker go={go} cur={cur} />}
    </div>
  );
}

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