/* lk-common.jsx — общий слой 4 личных кабинетов:
   Ic (lucide), форматтеры, Shell (sidebar+topbar+blob), KPI, кольцо прогресса,
   smart-queue строка, секция-карточка. Визуальный каркас — lk-shell.css (из Ops). */
(function () {
  const DS = window.DesignSystem_9e0a09 || (window.DesignSystem_9e0a09 = {});
  if (typeof DS.IconTile !== 'function') {
    DS.IconTile = function IconTile({ size, bg, color, children, style, variant = 'glass' }) {
      const d = size === 'sm' ? 36 : size === 'lg' ? 56 : 44;
      const soft = variant === 'soft';
      return React.createElement('span', {
        style: {
          display: 'inline-grid', placeItems: 'center', width: d, height: d, borderRadius: size === 'lg' ? 16 : 12,
          background: bg || (soft ? 'var(--primary-soft)' : 'var(--glass-bg-strong)'),
          color: color || (soft ? 'var(--accent-foreground)' : 'var(--foreground)'),
          border: soft ? 'none' : '1px solid var(--glass-border)',
          flexShrink: 0, ...(style || {}),
        },
      }, children);
    };
  }

  /* ---------- icon ---------- */
  function Ic({ n, s = 16, sw = 2, c = 'currentColor', style }) {
    const ref = React.useRef();
    React.useEffect(() => {
      const host = ref.current;
      if (host && window.lucide) {
        host.innerHTML = '';
        const i = document.createElement('i');
        i.setAttribute('data-lucide', n);
        host.appendChild(i);
        try { window.lucide.createIcons({ attrs: { width: s, height: s, 'stroke-width': sw, stroke: c }, nameAttr: 'data-lucide' }); } catch (e) {}
      }
    }, [n, s, sw, c]);
    return <span ref={ref} style={{ display: 'inline-flex', lineHeight: 0, ...style }} />;
  }

  /* ---------- format ---------- */
  const fmtRub = (n) => (n || 0).toLocaleString('ru-RU');
  function fmtCompact(n) {
    n = n || 0;
    if (Math.abs(n) >= 1e6) return (n / 1e6).toFixed(1).replace('.0', '') + ' млн';
    if (Math.abs(n) >= 1e3) return Math.round(n / 1e3) + 'к';
    return String(n);
  }

  /* Контекст перемещаемых блоков создаёт тот, кто загрузился первым:
     в ЛК это lk-common, в операционном центре — lk-arrange.jsx. */
  const ArrangeCtx = window.LKArrangeCtx || (window.LKArrangeCtx = React.createContext(null));

  /* ---------- section card (DS Card + head) ---------- */
  function SectionCard({ title, sub, action, children, pad, style, anchor }) {
    const { Card } = DS;
    return (
      <Card variant="glass" padding={pad} style={style} data-comment-anchor={anchor}>
        {title &&
          <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, marginBottom: 16 }}>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div className="t-h3" style={{ fontSize: 16.5, fontWeight: 800, letterSpacing: '-0.01em' }}>{title}</div>
              {sub && <div style={{ fontSize: 12.5, color: 'var(--text-tertiary)', marginTop: 2 }}>{sub}</div>}
            </div>
            {action}
          </div>}
        {children}
      </Card>);
  }

  /* Внутри зоны секция получает обвязку (ручка, ширина, цвет, поп-ап, скрытие);
     вне зоны — рендерится как раньше. Содержимое секции не меняется. */
  function Section(props) {
    const arrange = React.useContext(ArrangeCtx);
    const { icon, title, pad = 'lg' } = props;
    const card = <SectionCard {...props} pad={pad} />;
    if (!arrange || !title || !window.LKArrangeItem) return card;
    const id = String(title).trim().toLowerCase().replace(/\s+/g, '-');
    return (
      <window.LKArrangeItem arrange={arrange} id={id} title={title} icon={icon}>
        {card}
      </window.LKArrangeItem>
    );
  }

  /* ---------- KPI card ---------- */
  /* Спарклайн 8 недель — мини-контекст динамики прямо в KPI */
  function Spark({ data, w = 64, h = 18, good = true }) {
    if (!data || data.length < 2) return null;
    const min = Math.min(...data), max = Math.max(...data), span = max - min || 1;
    const pts = data.map((v, i) => `${(i / (data.length - 1)) * w},${h - 2 - ((v - min) / span) * (h - 4)}`).join(' ');
    return (
      <svg width={w} height={h} style={{ display: 'block', opacity: .8 }}>
        <polyline points={pts} fill="none" stroke={good ? 'var(--success-strong, #3f8a52)' : 'var(--destructive, #b91c1c)'} strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round" />
      </svg>
    );
  }

  function Kpi({ icon, value, unit, label, sub, tone = 'ink', onClick, delta, target, spark }) {
    const map = {
      ink: ['var(--secondary)', 'var(--foreground)'],
      primary: ['color-mix(in srgb, var(--primary) 14%, var(--card))', 'var(--primary)'],
      success: ['var(--success-soft)', 'var(--success-strong)'],
      warning: ['var(--warning-soft)', 'var(--warning-strong)'],
      danger: ['#fdecec', 'var(--destructive)'],
      info: ['color-mix(in srgb, var(--info) 14%, var(--card))', 'var(--info)'],
    };
    const [, fg] = map[tone] || map.ink;
    /* «—» = нет данных: не показываем стойкие подписи/план, чтобы значение и
       контекст не противоречили (Nielsen #1). */
    const noData = value === '—' || value === '' || value == null;
    return (
      <button className="lk-kpi" onClick={onClick}>
        <div className="lk-kpi-top">
          <span className="lk-kpi-title" style={{ color: fg }}>{label}</span>
          <Ic n="arrow-up-right" s={15} c="var(--text-tertiary)" />
        </div>
        <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 8 }}>
          <div className="lk-kpi-v t-num">{value}{unit && <small> {unit}</small>}</div>
          {spark && <Spark data={spark.data || spark} good={spark.good !== false} />}
        </div>
        {delta && (
          <div style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 11.5, fontWeight: 700, fontVariantNumeric: 'tabular-nums', color: delta.good === false ? 'var(--destructive)' : 'var(--success-strong)' }}>
            <Ic n={delta.dir === 'down' ? 'trending-down' : 'trending-up'} s={13} sw={2.2} />
            {delta.v}
            <span style={{ fontWeight: 500, color: 'var(--text-tertiary)' }}>за 7 дн</span>
          </div>
        )}
        {!noData && (sub || target) && (
          <div>
            {sub && <div className="lk-kpi-sub">{sub}</div>}
            {target && <div className="lk-kpi-sub" style={{ color: 'var(--text-tertiary)' }}>план: {target}</div>}
          </div>
        )}
      </button>);
  }

  /* KPI в сетке зоны — с настройками ширины/высоты через lk-arrange (⋯). */
  function ArrangeKpi(props) {
    const Ctx = window.LKArrangeCtx;
    const arrange = Ctx ? React.useContext(Ctx) : null;
    const { label, ...kpiProps } = props;
    const slug = (label || 'kpi').toLowerCase().replace(/[^a-zа-яё0-9]+/gi, '-').replace(/^-+|-+$/g, '');
    const id = 'kpi-' + slug;
    if (!arrange || !window.LKArrangeItem) return <Kpi label={label} {...kpiProps} />;
    return (
      <window.LKArrangeItem arrange={arrange} id={id} title={label} variant="kpi">
        <Kpi label={label} {...kpiProps} />
      </window.LKArrangeItem>
    );
  }

  /* ---------- progress ring ---------- */
  function Ring({ pct, size = 116, stroke = 10, color = 'var(--primary)', label = 'готово' }) {
    const r = (size - stroke) / 2;
    const c = 2 * Math.PI * r;
    return (
      <div className="lk-ring" style={{ width: size, height: size }}>
        <svg width={size} height={size}>
          <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="var(--muted)" strokeWidth={stroke} />
          <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke={color} strokeWidth={stroke} strokeLinecap="round" strokeDasharray={c} strokeDashoffset={c * (1 - pct / 100)} />
        </svg>
        <div className="lk-ring-c"><span className="lk-ring-v t-num">{pct}%</span><span className="lk-ring-l">{label}</span></div>
      </div>);
  }

  /* ---------- progress bar ---------- */
  function Prog({ pct, color = 'var(--primary)' }) {
    return <div className="lk-prog"><span style={{ width: pct + '%', background: color }} /></div>;
  }

  /* ---------- smart-queue row ---------- */
  /* P1-3 (аудит ЛК): money = ₽ под риском, actions = решение в один клик */
  function QRowActions({ actions }) {
    if (!actions || !actions.length) return null;
    return (
      <span style={{ display: 'inline-flex', gap: 6, flexShrink: 0 }} onClick={(e) => e.stopPropagation()}>
        {actions.map((a, i) => a.href
          ? <a key={i} className="chip chip--control" href={a.href} target={a.blank ? '_blank' : undefined} rel="noopener" title={a.title}>{a.label}</a>
          : <button key={i} className={'chip ' + (a.primary ? 'chip--money-strong' : 'chip--control')} onClick={a.onClick} title={a.title}>{a.label}</button>)}
      </span>
    );
  }

  function QueueRow({ bar, icon, iconBg, iconFg, title, meta, right, money, actions, onClick }) {
    const Tag = onClick ? 'button' : 'div';
    return (
      <Tag className="lk-q-row" {...(onClick ? { onClick } : {})}>
        {bar && <span className="lk-q-bar" style={{ background: bar }} />}
        {icon && <span className="lk-q-ic" style={{ background: iconBg || 'var(--secondary)', color: iconFg || 'var(--text-secondary)' }}><Ic n={icon} s={16} sw={2} /></span>}
        <div className="lk-q-main">
          <div className="lk-q-t">{title}</div>
          {meta && <div className="lk-q-s">{meta}</div>}
        </div>
        {money && <span style={{ fontSize: 13, fontWeight: 800, fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap', color: 'var(--foreground)', flexShrink: 0 }}>{money}</span>}
        <QRowActions actions={actions} />
        {right}
        {onClick && <Ic n="chevron-right" s={16} c="var(--text-tertiary)" />}
      </Tag>);
  }

  /* ---------- notifications (action-required queue ≠ activity feed) ---------- */
  // cat: action (требует действия) | info (информационное); to: zone id; pinned: Ops→LK баннер (L-14)
  const NOTIFY = {};

  function getQuery() {
    try { return new URLSearchParams(window.location.search); } catch (e) { return new URLSearchParams(''); }
  }

  /* ---------- command palette (⌘K) ---------- */
  function CmdK({ role, nav, onNav, onClose }) {
    const [q, setQ] = React.useState('');
    const inputRef = React.useRef(null);
    React.useEffect(() => { inputRef.current && inputRef.current.focus(); const k = (e) => e.key === 'Escape' && onClose(); document.addEventListener('keydown', k); return () => document.removeEventListener('keydown', k); }, []);
    const entries = [];
    nav.forEach((g) => g.items.forEach((it) => entries.push({ id: it.id, label: it.label, group: g.label, icon: it.icon, soon: it.soon })));
    const ql = q.trim().toLowerCase();
    const filtered = ql ? entries.filter((e) => e.label.toLowerCase().includes(ql) || (e.group || '').toLowerCase().includes(ql)) : entries;
    const pick = (e) => { if (e.soon) return; onNav(e.id); onClose(); };
    return (
      <div className="cmdk-ov" onClick={onClose}>
        <div className="cmdk-modal" onClick={(e) => e.stopPropagation()}>
          <div className="cmdk-input-row">
            <Ic n="search" s={18} c="var(--text-tertiary)" />
            <input ref={inputRef} value={q} onChange={(e) => setQ(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && filtered[0]) pick(filtered[0]); }} placeholder={'Поиск разделов · ' + role.brand} />
            <span className="cmdk-esc">esc</span>
          </div>
          <div className="cmdk-results">
            {filtered.length === 0 && <div className="cmdk-hint">Ничего не найдено</div>}
            {filtered.map((e) =>
              <button key={e.id} className="cmdk-res" onClick={() => pick(e)} style={e.soon ? { opacity: .5 } : null}>
                <Ic n={e.icon} s={16} c="var(--text-secondary)" /><span>{e.label}</span>
                <span className="r-sub">{e.soon ? 'скоро' : e.group}</span>
              </button>)}
          </div>
          <div className="cmdk-foot"><span>↵ перейти</span><span>esc закрыть</span></div>
        </div>
      </div>);
  }

  /* ---------- notifications panel ---------- */
  function NotifPanel({ items, onNav, onRead, onReadAll, onClose }) {
    React.useEffect(() => { const k = (e) => e.key === 'Escape' && onClose(); document.addEventListener('keydown', k); return () => document.removeEventListener('keydown', k); }, []);
    const actions = items.filter((n) => n.cat === 'action');
    const infos = items.filter((n) => n.cat === 'info');
    const unread = items.filter((n) => n.unread).length;
    const Row = ({ n }) => (
      <button className={'lk-notif' + (n.unread ? ' unread' : '')} onClick={() => { onRead(n.id); onNav(n.to); onClose(); }}>
        <span className="lk-notif-ic" style={{ background: n.cat === 'action' ? 'color-mix(in srgb, var(--primary) 13%, var(--card))' : 'var(--secondary)', color: n.cat === 'action' ? 'var(--primary)' : 'var(--text-secondary)' }}><Ic n={n.icon} s={16} /></span>
        <div className="lk-notif-main"><div className="lk-notif-t">{n.t}</div><div className="lk-notif-m">{n.from && <b style={{ color: 'var(--primary)', fontWeight: 700 }}>{n.from} · </b>}{n.m}{n.m && ' · '}{n.when}</div></div>
        {n.unread && <span className="lk-notif-dot" />}
      </button>);
    return (
      <React.Fragment>
        <div className="lk-notif-ov" onClick={onClose} />
        <div className="lk-notif-panel">
          <div className="lk-notif-head">
            <span style={{ fontWeight: 800, fontSize: 14 }}>Уведомления</span>
            {unread > 0 && <span className="lk-notif-badge">{unread}</span>}
            <button className="lk-notif-readall" onClick={onReadAll}>Прочитать все</button>
          </div>
          <div className="lk-notif-body">
            {actions.length > 0 && <div className="lk-notif-sec">Требуют действия</div>}
            {actions.map((n) => <Row key={n.id} n={n} />)}
            {infos.length > 0 && <div className="lk-notif-sec">Информация</div>}
            {infos.map((n) => <Row key={n.id} n={n} />)}
            {items.length === 0 && <div style={{ padding: 28, textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 13 }}>Нет уведомлений</div>}
          </div>
        </div>
      </React.Fragment>);
  }

  /* ---------- замены материалов: блок решения для роли в цепочке (дизайнер → менеджер → клиент) ---------- */
  function ReplacementDecisions({ role, title }) {
    const { Button, Badge } = DS;
    const P = window.ProjectStore;
    const [, tick] = React.useState(0);
    React.useEffect(() => (P ? P.subscribe(() => tick((x) => x + 1)) : undefined), []);
    if (!P || !P.views.pendingReplacements) return null;
    const rows = P.views.pendingReplacements(role);
    const acc = window.AccessStore && window.AccessStore.currentAccount ? window.AccessStore.currentAccount() : null;
    const actor = { role, name: (acc && acc.name) || ({ designer: 'Дизайнер', manager: 'Менеджер', client: 'Клиент' }[role] || role) };
    const decide = (r, d) => {
      let comment = '';
      if (d === 'reject') { comment = window.prompt('Почему не подходит?', '') || ''; if (!comment.trim()) return; }
      try { P.decideReplacement(r.pid, r.id, d, comment.trim(), actor); } catch (e) { alert(e.message); }
    };
    const money = (n) => (Math.round(n) || 0).toLocaleString('ru-RU') + ' ₽';
    return (
      <Section icon="replace" cat="plumb" title={title || 'Замены материалов'} sub={rows.length ? rows.length + ' на вашем решении' : 'заявок от закупок нет'}
        action={<Badge tone={rows.length ? 'warning' : 'neutral'} dot>{rows.length}</Badge>}>
        {rows.length === 0 ? <div style={{ padding: 18, textAlign: 'center', color: 'var(--text-tertiary)', fontSize: 13 }}>Всё согласовано</div> :
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            {rows.map((r) => (
              <div key={r.id} style={{ border: '1px solid var(--border-subtle)', borderRadius: 'var(--radius-lg)', padding: '13px 15px', background: 'var(--card)' }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', fontSize: 12, color: 'var(--text-tertiary)' }}><b style={{ color: 'var(--foreground)' }}>{r.id}</b><span>· {r.obj}</span><span>· {r.room}</span><span style={{ background: 'var(--secondary)', padding: '1px 8px', borderRadius: 999, color: 'var(--text-secondary)' }}>{r.reasonL}</span><span>· {r.created}</span></div>
                <div style={{ display: 'grid', gridTemplateColumns: '1fr auto 1fr', gap: 10, alignItems: 'center', marginTop: 10 }}>
                  <div><div style={{ fontSize: 11, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: '.04em' }}>Было</div><div style={{ fontWeight: 700 }}>{r.orig.name}</div><div style={{ fontSize: 12.5, color: 'var(--text-secondary)' }}>{r.orig.supplier} · {money(r.orig.price)} · {r.orig.lead} дн</div></div>
                  <Ic n="arrow-right" s={18} c="var(--text-tertiary)" />
                  <div><div style={{ fontSize: 11, color: 'var(--chart-3)', textTransform: 'uppercase', letterSpacing: '.04em' }}>Аналог</div><div style={{ fontWeight: 700 }}>{r.prop.name}</div><div style={{ fontSize: 12.5, color: 'var(--text-secondary)' }}>{r.prop.supplier} · {money(r.prop.price)} · {r.prop.lead} дн</div></div>
                </div>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginTop: 10 }}>
                  <Badge tone={r.priceDelta <= 0 ? 'success' : 'danger'} dot>{r.priceDelta <= 0 ? 'цена ' + (r.priceDelta === 0 ? 'без изменений' : money(r.priceDelta)) : '+' + money(r.priceDelta) + (role !== 'client' ? ' · потребует согласия клиента' : '')}</Badge>
                  <Badge tone={r.leadDelta <= 0 ? 'success' : 'warning'} dot>{r.leadDelta <= 0 ? 'срок не хуже' : 'срок +' + r.leadDelta + ' дн'}</Badge>
                  {r.note && <span style={{ fontSize: 12.5, color: 'var(--text-secondary)' }}>{r.note}</span>}
                  <span style={{ marginLeft: 'auto', display: 'inline-flex', gap: 7 }}>
                    <Button variant="ghost" size="sm" onClick={() => decide(r, 'reject')}>Отклонить</Button>
                    <Button variant="primary" size="sm" onClick={() => decide(r, 'approve')} iconLeft={<Ic n="check" s={13} />}>Согласовать</Button>
                  </span>
                </div>
              </div>))}
          </div>}
      </Section>);
  }

  /* ---------- обратная связь тестировщиков: плавающая кнопка «Сообщить» ---------- */
  function FeedbackFab() {
    const FB = window.FeedbackStore;
    const { Button } = DS; // DS грузится с defer — читаем при рендере, не при загрузке модуля
    const [open, setOpen] = React.useState(false);
    const [kind, setKind] = React.useState('bug');
    const [text, setText] = React.useState('');
    const [done, setDone] = React.useState(false);
    if (!FB) return null;
    const close = () => { setOpen(false); setDone(false); setText(''); };
    const send = () => {
      try { FB.add({ kind, text }); setDone(true); setTimeout(close, 1400); } catch (e) { alert(e.message); }
    };
    const ctx = open ? FB.context() : null;
    return (
      <React.Fragment>
        <button type="button" className="lk-fb-fab" onClick={() => setOpen(true)} title="Сообщить о проблеме или предложении" aria-label="Сообщить">
          <Ic n="message-square-warning" s={17} sw={2.2} /><span>Сообщить</span>
        </button>
        {open && (
          <React.Fragment>
            <div className="lk-modal-ov" onClick={close} />
            <div className="lk-modal lk-fb-modal" role="dialog" aria-modal="true" aria-label="Обратная связь">
              <div className="lk-modal-head">
                <div className="lk-modal-head-text">
                  <div className="lk-modal-title">Что случилось?</div>
                  <div className="lk-modal-sub">{ctx.title}{ctx.account ? ' · ' + ctx.account.name : ''}{ctx.project ? ' · ' + ctx.project : ''} — контекст прикрепится сам</div>
                </div>
                <button className="lk-modal-x" onClick={close} aria-label="Закрыть"><Ic n="x" s={16} /></button>
              </div>
              <div className="lk-modal-body">
                {done ? <div className="nlk-success" style={{ padding: '28px 12px' }}><span className="nlk-success-ic"><Ic n="check" s={24} sw={2.6} c="#fff" /></span><div className="nlk-success-t">Записали</div><div className="nlk-success-s">Спасибо — увидим в Super · Обратная связь.</div></div> : (
                  <React.Fragment>
                    <div className="lk-fb-kinds">
                      {Object.entries(FB.KINDS).map(([k, l]) => (
                        <button key={k} type="button" className={'lk-fb-kind' + (kind === k ? ' on' : '')} onClick={() => setKind(k)}>
                          <Ic n={k === 'bug' ? 'bug' : k === 'unclear' ? 'help-circle' : 'lightbulb'} s={15} sw={2.2} />{l}
                        </button>))}
                    </div>
                    <textarea className="lk-fb-text" rows={4} autoFocus value={text} onChange={(e) => setText(e.target.value)} placeholder={kind === 'bug' ? 'Что нажали и что произошло вместо ожидаемого' : kind === 'unclear' ? 'Что непонятно или где запутались' : 'Что бы сделало работу проще'} />
                    <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 12 }}>
                      <Button variant="ghost" size="sm" onClick={close}>Отмена</Button>
                      <Button variant="primary" size="sm" onClick={send} disabled={!text.trim()} iconLeft={<Ic n="send" s={13} />}>Отправить</Button>
                    </div>
                  </React.Fragment>)}
              </div>
            </div>
          </React.Fragment>)}
      </React.Fragment>);
  }

  /* ---------- app shell ---------- */
  function Shell({ role, nav, active, onNav, crumbs, superview, children, anchorNav, autoHide, tabNav }) {
    /* tabNav — плавающее меню на вкладочных страницах (смета, профиль): тот же
       floatnav-хром, но без scroll-spy по #zone-*. Активность из active, клик → onNav. */
    const floatNav = anchorNav || tabNav;
    /* W2 ACCESS-MODEL: identity из учётной записи (?user=) поверх демо-константы роли */
    const acc = (window.AccessStore && window.AccessStore.currentAccount()) || null;
    const idn = acc ? {
      ...role,
      name: acc.name || role.name,
      email: acc.email || role.email,
      ava: (acc.name || '').split(/\s+/).map((w) => w[0]).slice(0, 2).join('').toUpperCase() || role.ava,
    } : role;
    const [showUser, setShowUser] = React.useState(false);
    const roleKey = document.body.dataset.lkRole || '';
    const profileHref = roleKey ? roleKey + '-profile.html' + (window.location.search || '') : null;
    const logoutHref = (() => {
      try { const qq = new URLSearchParams(window.location.search); qq.delete('user'); qq.delete('t'); const qs = qq.toString();
        const zd = (window.LK_ZONES || {})[roleKey] || {}; return roleKey + '-' + (zd.defaultZone || 'soon') + '.html' + (qs ? '?' + qs : ''); }
      catch (e) { return null; }
    })();
    const q = getQuery();
    const svName = q.get('name');
    const isImpersonate = q.get('impersonate') === '1' || q.get('impersonate') === 'true';
    const isSuperView = !superview && !isImpersonate && (q.has('superView') || q.has('superview')) && svName;
    const banner = superview ? { mode: 'super', name: superview } : isImpersonate ? { mode: 'imp', name: svName || role.name } : isSuperView ? { mode: 'super', name: svName } : null;

    const [notifs, setNotifs] = React.useState(() => (NOTIFY[role.brand] || []).map((n) => ({ ...n, unread: true })));
    const [showNotif, setShowNotif] = React.useState(false);
    const [showCmd, setShowCmd] = React.useState(false);
    const [pinnedClosed, setPinnedClosed] = React.useState(false);
    const unread = notifs.filter((n) => n.unread).length;
    const pinned = notifs.find((n) => n.pinned && n.unread);

    React.useEffect(() => {
      const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) { e.preventDefault(); setShowCmd((v) => !v); } };
      document.addEventListener('keydown', onKey);
      return () => document.removeEventListener('keydown', onKey);
    }, []);
    const readOne = (id) => setNotifs((ns) => ns.map((n) => n.id === id ? { ...n, unread: false } : n));
    const readAll = () => setNotifs((ns) => ns.map((n) => ({ ...n, unread: false })));

    /* HUB: scroll-spy — активный пункт якорного меню по видимой секции */
    const [spyActive, setSpyActive] = React.useState(active);
    React.useEffect(() => {
      if (!anchorNav) return undefined;
      const content = document.querySelector('.content');
      if (!content) return undefined;
      const compute = () => {
        const secs = Array.from(document.querySelectorAll('[data-zone-anchor]')).filter((s) => s.getClientRects().length > 0); /* пропускаем скрытые (свёрнутый MoreFold) */
        if (!secs.length) return;
        let cur;
        if (content.scrollTop + content.clientHeight >= content.scrollHeight - 4) { /* низ прокрутки → последняя секция */
          cur = secs[secs.length - 1].getAttribute('data-zone-anchor');
        } else {
          const refY = content.getBoundingClientRect().top + 56; /* линия под топбаром */
          cur = secs[0].getAttribute('data-zone-anchor');
          let bestTop = -Infinity; /* секция ближе всего сверху к линии; в одном ряду (равный top) — первая (левая) */
          for (const s of secs) {
            const top = s.getBoundingClientRect().top;
            if (top - refY <= 1 && top > bestTop) { bestTop = top; cur = s.getAttribute('data-zone-anchor'); }
          }
        }
        setSpyActive(cur);
        secs.forEach((s) => s.classList.toggle('is-active-sec', s.getAttribute('data-zone-anchor') === cur));
      };
      compute();
      content.addEventListener('scroll', compute, { passive: true });
      window.addEventListener('resize', compute);
      /* зоны (object/docs/chat) догружаются асинхронно — пересчитать после раскладки */
      const timers = [300, 900, 1600].map((ms) => setTimeout(compute, ms));
      return () => { content.removeEventListener('scroll', compute); window.removeEventListener('resize', compute); timers.forEach(clearTimeout); };
    }, [anchorNav]);
    const navActive = anchorNav ? spyActive : active;

    return (
      <React.Fragment>
        <div className="bgfield" aria-hidden="true"><span className="blob bl-amber" /><span className="blob bl-violet" /><span className="blob bl-mint" /></div>
        <div className={'hub' + (floatNav ? ' hub-floatnav' : '')}>
          <aside className="side">
            <div className="brand">
              <span className="brand-mark" style={role.markBg ? { background: role.markBg, boxShadow: 'none' } : null}><Ic n={role.mark || 'hard-hat'} s={18} c="#fff" /></span>
              <div><div className="brand-name">{role.brand}</div><div className="brand-sub">{role.brandSub}</div></div>
            </div>
            {/* P2 F5: активные пункты — в потоке; soon вынесены вниз в свёрнутую группу */}
            {nav.map((g, gi) => {
              const live = g.items.filter((it) => !it.soon);
              if (!live.length) return null;
              return (
                <div className="nav-group" key={gi}>
                  {g.label && <div className="nav-label">{g.label}</div>}
                  {live.map((it) => {
                    const cls = 'nav-item' + (it.id === navActive ? ' active' : '');
                    const inner = <React.Fragment>{it.icon && <span className="nav-ic"><Ic n={it.icon} s={19} sw={1.9} /></span>}<span className="nav-lbl">{it.label}</span>{it.badge != null && <span className="nav-count">{it.badge}</span>}</React.Fragment>;
                    if (anchorNav) {
                      const navHref = it.href || ('#' + it.id);
                      return <a key={it.id} href={navHref} className={cls} style={{ textDecoration: 'none', color: 'inherit' }}
                        onClick={(e) => {
                          if (hubZoneMounted(it.id)) {
                            e.preventDefault();
                            scrollToHubZone(it.id);
                            try { history.replaceState(null, '', '#' + it.id); } catch (x) {}
                            if (onNav) onNav(it.id);
                          }
                        }}>{inner}</a>;
                    }
                    return it.href
                      ? <a key={it.id} href={it.href} className={cls} style={{ textDecoration: 'none', color: 'inherit' }}>{inner}</a>
                      : <button key={it.id} className={cls} onClick={() => onNav(it.id)}>{inner}</button>;
                  })}
                </div>
              );
            })}
            {(() => {
              const soon = nav.flatMap((g) => g.items.filter((it) => it.soon));
              if (!soon.length) return null;
              return (
                <details className="nav-soon-group">
                  <summary className="nav-label" style={{ cursor: 'pointer', listStyle: 'none', display: 'flex', alignItems: 'center', gap: 6 }}>
                    <Ic n="chevron-right" s={12} /> Скоро <span className="nav-count">{soon.length}</span>
                  </summary>
                  {soon.map((it) => (
                    <div key={it.id} className="nav-item soon" style={{ cursor: 'default' }}><span>{it.label}</span></div>
                  ))}
                </details>
              );
            })()}
            <div className="side-foot">
              <span className="side-ava" style={{ background: role.avaC }}>{idn.ava}</span>
              <div style={{ minWidth: 0 }}><div className="side-foot-name">{idn.name}</div><div className="side-foot-role">{idn.email}</div></div>
            </div>
          </aside>
          <div className="main">
            {banner &&
              <div className={'lk-sv' + (banner.mode === 'imp' ? ' impersonate' : '')}>
                <Ic n={banner.mode === 'imp' ? 'user-round' : 'eye'} s={15} />
                {banner.mode === 'imp'
                  ? <React.Fragment>Вход от имени клиента · <b style={{ fontWeight: 800 }}>{banner.name}</b> · действия от его имени фиксируются</React.Fragment>
                  : <React.Fragment>Просмотр от имени Super · <b style={{ fontWeight: 800 }}>{banner.name}</b> · {role.brand} · только чтение</React.Fragment>}
                <span className="lk-sv-audit"><Ic n="lock" s={12} />сессия в аудите</span>
                <a href="../Операционный центр.html" style={{ marginLeft: 'auto', color: 'inherit', fontWeight: 700, display: 'inline-flex', alignItems: 'center', gap: 5 }}><Ic n="arrow-left" s={14} />К операционному центру</a>
              </div>}
            <header className="topbar">
              <div className="crumbs">
                <a href="../Личные кабинеты.html" style={{ color: 'inherit', textDecoration: 'none' }}>Кабинеты</a>
                {crumbs.map((c, i) => <React.Fragment key={i}><span className="sep">/</span><span className={i === crumbs.length - 1 ? 'cur' : ''}>{c}</span></React.Fragment>)}
              </div>
              <div style={{ flex: 1 }} />
              <button className="lk-search" onClick={() => setShowCmd(true)}><Ic n="search" s={15} /><span>Поиск</span><kbd>⌘K</kbd></button>
              <div style={{ position: 'relative' }}>
                <button className="lk-bell" onClick={() => setShowNotif((v) => !v)}><Ic n="bell" s={17} />{unread > 0 && <span className="lk-bell-count">{unread}</span>}</button>
                {showNotif && <NotifPanel items={notifs} onNav={onNav} onRead={readOne} onReadAll={readAll} onClose={() => setShowNotif(false)} />}
              </div>
              <div style={{ position: 'relative' }}>
                <button className="topbar-user" onClick={() => setShowUser((v) => !v)} style={{ border: 'none', background: 'transparent', cursor: 'pointer', fontFamily: 'inherit' }}>
                  <span className="topbar-ava" style={{ background: role.avaC }}>{idn.ava}</span><span className="topbar-user-name">{idn.name}</span><Ic n="chevron-down" s={14} sw={2.2} c="var(--text-tertiary)" />
                </button>
                {showUser && (
                  <div style={{ position: 'absolute', top: 'calc(100% + 6px)', right: 0, zIndex: 60, background: 'var(--card, #fff)', border: '1px solid var(--border-subtle, #e7e2d9)', borderRadius: 12, boxShadow: 'var(--shadow-pop, 0 8px 24px rgba(0,0,0,.12))', padding: 6, minWidth: 200 }}>
                    <div style={{ padding: '8px 10px 6px', borderBottom: '1px solid var(--border-subtle, #eee)', marginBottom: 4 }}>
                      <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--foreground)' }}>{idn.name}</div>
                      <div style={{ fontSize: 11.5, color: 'var(--text-tertiary)', overflow: 'hidden', textOverflow: 'ellipsis' }}>{idn.email}</div>
                    </div>
                    {profileHref && <a href={profileHref} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px', borderRadius: 8, fontSize: 13, fontWeight: 600, color: 'var(--foreground)', textDecoration: 'none' }}><Ic n="user-round" s={15} />Мой профиль</a>}
                    {acc && logoutHref && <a href={logoutHref} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px', borderRadius: 8, fontSize: 13, fontWeight: 600, color: 'var(--destructive, #b91c1c)', textDecoration: 'none' }}><Ic n="log-out" s={15} />Выйти из аккаунта</a>}
                  </div>
                )}
              </div>
            </header>
            {pinned && !pinnedClosed &&
              <div className="lk-pinned">
                <span className="lk-pinned-ic"><Ic n={pinned.icon} s={17} c="#fff" /></span>
                <div style={{ flex: 1, minWidth: 0 }}><div className="lk-pinned-t">{pinned.from && pinned.from + ' · '}{pinned.t}</div><div className="lk-pinned-m">{pinned.m} · {pinned.when} назад</div></div>
                <button className="lk-pinned-cta" onClick={() => { readOne(pinned.id); onNav(pinned.to); }}>Открыть</button>
                <button className="lk-pinned-x" onClick={() => setPinnedClosed(true)}><Ic n="x" s={15} /></button>
              </div>}
            <div className="content"><div className="content-inner">
              {children}
            </div></div>
            {window.LKArrangePanel ? <window.LKArrangePanel /> : null}
          </div>
          {!floatNav && (
            /* телефон (≤640px): сайдбар скрыт, навигация — плавающая пилюля снизу с иконками (CSS показывает только в мобильном медиа) */
            <nav className="lk-floatnav lk-floatnav--mobile" aria-label="Разделы">
              {nav.flatMap((g) => g.items).filter((it) => !it.soon).map((it) => {
                const inner = <React.Fragment>{it.icon && <Ic n={it.icon} s={17} sw={2} />}<span className="lk-floatnav-lbl">{it.label}</span>{it.badge != null && <span className="lk-floatnav-badge">{it.badge}</span>}</React.Fragment>;
                const cls = 'lk-floatnav-item' + (it.id === navActive ? ' active' : '');
                return it.href
                  ? <a key={it.id} href={it.href} className={cls}>{inner}</a>
                  : <button key={it.id} type="button" className={cls} onClick={() => onNav && onNav(it.id)}>{inner}</button>;
              })}
            </nav>
          )}
          {floatNav && (
            <nav className="lk-floatnav" aria-label="Разделы">
              {nav.flatMap((g) => g.items).filter((it) => !it.soon).map((it) => (
                <a key={it.id} href={it.href || ('#' + it.id)} className={'lk-floatnav-item' + (it.id === navActive ? ' active' : '')}
                  onClick={(e) => {
                    if (tabNav) { e.preventDefault(); if (onNav) onNav(it.id); return; }
                    if (hubZoneMounted(it.id)) {
                      e.preventDefault();
                      scrollToHubZone(it.id);
                      try { history.replaceState(null, '', '#' + it.id); } catch (x) {}
                      if (onNav) onNav(it.id);
                    }
                  }}>
                  {it.label}{it.badge != null && <span className="lk-floatnav-badge">{it.badge}</span>}
                </a>
              ))}
            </nav>
          )}
        </div>
        {showCmd && <CmdK role={role} nav={nav} onNav={onNav} onClose={() => setShowCmd(false)} />}
        <FeedbackFab />
      </React.Fragment>);
  }

  /* page head */
  function PageHead({ eyebrow, h1, lede }) {
    const ref = React.useRef(null);
    const [inFieldZone, setInFieldZone] = React.useState(false);
    const [hideDupTitle, setHideDupTitle] = React.useState(false);
    React.useLayoutEffect(() => {
      const root = ref.current;
      const inZone = !!(root && root.closest('.lk-zone-embed'));
      setInFieldZone(inZone);
      if (!root || !inZone || !h1) { setHideDupTitle(false); return; }
      const blockName = root.closest('[data-zone-anchor]')?.querySelector('.of-block-name')?.value
        || root.closest('.of-node')?.querySelector('.of-block-name')?.value;
      setHideDupTitle(!!(blockName && blockName.trim() === String(h1).trim()));
    }, [h1]);
    /* В едином поле (OpsField) заголовок зоны уже в шапке блока — внутри embed
       оставляем только lede, без повторного eyebrow/h1. */
    if (inFieldZone) {
      return lede ? (
        <div className="ph ph--embed" ref={ref}>
          <p className="ph-lede">{lede}</p>
        </div>
      ) : null;
    }
    const H = 'h1';
    return (
      <div className="ph" ref={ref}>
        {eyebrow && <div className="ph-eyebrow">{eyebrow}</div>}
        {h1 && !hideDupTitle && <H className="ph-h1">{h1}</H>}
        {lede && <p className="ph-lede">{lede}</p>}
      </div>);
  }

  /* mount guard helper */
  function mountWhenReady(render) {
    const REQ = ['Button', 'Card', 'Badge', 'IconTile'];
    const ready = () => { const ns = window.DesignSystem_9e0a09 || {}; return REQ.every((k) => typeof ns[k] === 'function') && window.lucide; };
    let root = window.__lkRoot;
    if (!root) {
      const el = document.getElementById('root');
      if (!el) return;
      root = ReactDOM.createRoot(el);
      window.__lkRoot = root;
    }
    let t = 0;
    (function go() {
      if (ready()) { root.render(typeof render === 'function' ? render() : render); return; }
      if (t++ < 60) { setTimeout(go, 80); return; }
      const el = document.getElementById('root');
      if (el && !el.innerHTML) {
        el.innerHTML = '<div style="padding:32px 24px;font-family:system-ui,sans-serif;max-width:520px;margin:40px auto;color:#444"><p style="font-weight:600;margin:0 0 8px">Не удалось загрузить дизайн-систему</p><p style="margin:0 0 16px;font-size:14px;line-height:1.5">Компоненты DesignSystem не зарегистрировались (проверьте, что <code>_ds_bundle.js</code> подключён с <code>defer</code> и React загружен раньше него).</p><p style="margin:0;font-size:13px;color:#888">Откройте консоль браузера (F12) и обновите страницу Ctrl+F5.</p></div>';
      }
    })();
  }



  /* Сегменты внутри зоны (консолидация страниц): deep-link ?seg=
     Стили в .lk-segs — чуть явнее track + активный таб, чтобы не терялись на canvas. */
  function LKSegs({ segs, active, onChange }) {
    return (
      <div className="lk-segs" role="tablist" aria-label="Сегменты раздела">
        {segs.map((sg) => {
          const on = sg.id === active;
          return (
            <button
              key={sg.id}
              type="button"
              role="tab"
              aria-selected={on}
              className={'lk-segs-btn' + (on ? ' is-on' : '')}
              onClick={() => onChange(sg.id)}
            >
              {sg.label}
              {sg.badge != null && <span className="lk-segs-badge">{sg.badge}</span>}
            </button>
          );
        })}
      </div>
    );
  }

  function useSeg(defaultSeg) {
    const [seg, setSeg] = React.useState(() => {
      try { return new URLSearchParams(window.location.search).get('seg') || defaultSeg; } catch (e) { return defaultSeg; }
    });
    const change = (v) => {
      setSeg(v);
      try { const q = new URLSearchParams(window.location.search); q.set('seg', v); history.replaceState(null, '', '?' + q.toString()); } catch (e) {}
    };
    return [seg, change];
  }

  /* ---------- W2 ACCESS-MODEL: страница «Мой профиль» ---------- */
  const PROFILE_BRAND = {
    client:      { brand: 'Клиент',       brandSub: 'ОБИТЕЛЬ · кабинет',         mark: 'home',         markBg: 'var(--info)',    avaC: 'var(--info)' },
    executor:    { brand: 'Исполнитель',     brandSub: 'ОБИТЕЛЬ · работа',        mark: 'paint-roller', markBg: 'var(--chart-4)', avaC: 'var(--chart-4)' },
    foreman:     { brand: 'Прораб',          brandSub: 'ОБИТЕЛЬ · объекты',       mark: 'hard-hat',     markBg: 'var(--chart-2)', avaC: 'var(--chart-2)' },
    designer:    { brand: 'Дизайнер',        brandSub: 'ОБИТЕЛЬ · интерьеры',     mark: 'pen-tool',     markBg: 'var(--chart-5)', avaC: 'var(--chart-5)' },
    manager:     { brand: 'Менеджер',        brandSub: 'ОБИТЕЛЬ · продажи',       mark: 'briefcase',    markBg: 'var(--chart-3)', avaC: 'var(--chart-3)' },
    accountant:  { brand: 'Бухгалтерия',     brandSub: 'ОБИТЕЛЬ · финконтроль',   mark: 'landmark',     markBg: 'var(--chart-2)', avaC: 'var(--chart-2)' },
    procurement: { brand: 'Закупки',         brandSub: 'ОБИТЕЛЬ · комплектация',  mark: 'package',      markBg: 'var(--chart-3)', avaC: 'var(--chart-3)' },
    people:      { brand: 'People Ops',      brandSub: 'ОБИТЕЛЬ · crew management', mark: 'users-round', markBg: 'var(--chart-5)', avaC: 'var(--chart-5)' },
    super:       { brand: 'ОБИТЕЛЬ · Super', brandSub: 'Платформа',               mark: 'shield',       markBg: 'var(--chart-4)', avaC: 'var(--chart-4)' },
  };

  function LKProfilePage({ nav, go, crumbs }) {
    const roleKey = document.body.dataset.lkRole || '';
    const acc = (window.AccessStore && window.AccessStore.currentAccount()) || null;
    const brand = PROFILE_BRAND[roleKey] || { brand: roleKey, brandSub: 'кабинет', mark: 'user-round', markBg: 'var(--secondary)', avaC: 'var(--secondary)' };
    const name = acc ? (acc.name || acc.email || 'Пользователь') : 'Демо-режим';
    const email = acc ? (acc.email || '—') : 'страница открыта без учётной записи (?user=)';
    const ava = (acc && acc.name ? acc.name.split(/\s+/).map((w) => w[0]).slice(0, 2).join('').toUpperCase() : '·');
    const role = { ...brand, name, email, ava };
    const zdict = ((window.LK_ZONES || {})[roleKey] || {}).zones || {};
    const [email2, setEmail2] = React.useState(acc ? acc.email || '' : '');
    const [phone, setPhone] = React.useState(acc ? acc.phone || '' : '');
    const [saved, setSaved] = React.useState(false);
    const saveContacts = () => {
      if (!acc) return;
      window.AccessStore.upsertAccount(acc.profileId, { email: email2, phone: phone }, 'profile_updated');
      setSaved(true); setTimeout(() => setSaved(false), 1800);
    };
    const statusMeta = {
      active: ['В системе', 'var(--success-strong)', 'var(--success-soft)'],
      invited: ['Приглашён', 'var(--warning-strong)', 'var(--warning-soft)'],
      paused: ['Пауза', 'var(--warning-strong)', 'var(--warning-soft)'],
      revoked: ['Доступ отозван', 'var(--destructive)', 'color-mix(in srgb, var(--destructive) 10%, transparent)'],
    }[acc && acc.status] || ['Демо', 'var(--text-tertiary)', 'var(--secondary)'];

    return (
      <Shell role={role} nav={nav} active="profile" onNav={go} crumbs={crumbs} tabNav>
        <div style={{ display: 'grid', gap: 16, maxWidth: 720 }}>
          <Section icon="user-round" title="Мой профиль" sub="Учётная запись и контакты">
            <div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 18 }}>
              <span style={{ width: 52, height: 52, borderRadius: '50%', background: brand.avaC, color: '#fff', fontSize: 17, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>{ava}</span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 17, fontWeight: 800, letterSpacing: '-.015em' }}>{name}</div>
                <div style={{ fontSize: 12.5, color: 'var(--text-tertiary)' }}>{acc ? (acc.roleLabel || roleKey) : brand.brand}</div>
              </div>
              <span style={{ fontSize: 11.5, fontWeight: 700, color: statusMeta[1], background: statusMeta[2], padding: '3px 11px', borderRadius: 'var(--radius-pill, 9999px)' }}>{statusMeta[0]}</span>
            </div>
            {acc ? (
              <div style={{ display: 'grid', gap: 12 }}>
                <label style={{ display: 'grid', gap: 5 }}>
                  <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)' }}>Email</span>
                  <input value={email2} onChange={(e) => setEmail2(e.target.value)} style={{ fontFamily: 'inherit', fontSize: 14, padding: '9px 12px', borderRadius: 10, border: '1px solid var(--border-subtle)', background: 'var(--card)', color: 'var(--foreground)', outline: 'none' }} />
                </label>
                <label style={{ display: 'grid', gap: 5 }}>
                  <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)' }}>Телефон</span>
                  <input value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="+7 …" style={{ fontFamily: 'inherit', fontSize: 14, padding: '9px 12px', borderRadius: 10, border: '1px solid var(--border-subtle)', background: 'var(--card)', color: 'var(--foreground)', outline: 'none' }} />
                </label>
                <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                  <button onClick={saveContacts} style={{ fontFamily: 'inherit', fontSize: 13, fontWeight: 700, padding: '9px 16px', borderRadius: 'var(--radius-pill, 9999px)', border: 'none', background: 'var(--ink, #1a1714)', color: '#fff', cursor: 'pointer' }}>Сохранить контакты</button>
                  {saved && <span style={{ fontSize: 12.5, color: 'var(--success-strong)', fontWeight: 600 }}>Сохранено · записано в аудит</span>}
                </div>
              </div>
            ) : (
              <div style={{ fontSize: 13, color: 'var(--text-tertiary)', lineHeight: 1.55 }}>
                Страница открыта в демо-режиме. Реальный профиль появляется после приглашения из Конструктора (Команда → «Выдать доступ») и входа по ссылке с <code>?user=</code>.
              </div>
            )}
          </Section>

          <Section icon="shield-check" title="Мои права" sub="Разделы кабинета, открытые администратором · read-only">
            <div style={{ display: 'grid', gap: 2 }}>
              {Object.entries(zdict).map(([z, label]) => {
                const ok = !acc || (window.AccessStore && window.AccessStore.zoneAllowed(acc, z));
                return (
                  <div key={z} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 4px', borderBottom: '1px solid var(--border-subtle)', opacity: ok ? 1 : 0.55 }}>
                    <Ic n={ok ? 'check' : 'minus'} s={15} c={ok ? 'var(--success-strong)' : 'var(--text-tertiary)'} />
                    <span style={{ flex: 1, fontSize: 13.5, fontWeight: 600 }}>{label}</span>
                    {!ok && <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>закрыто</span>}
                  </div>
                );
              })}
            </div>
            {acc && acc.caps && Object.keys(acc.caps).some((k) => acc.caps[k]) && (
              <div style={{ marginTop: 14 }}>
                <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '.05em', color: 'var(--text-tertiary)', marginBottom: 6 }}>Дополнительные возможности</div>
                {Object.entries(acc.caps).filter(([, v]) => v).map(([k]) => (
                  <code key={k} style={{ display: 'inline-block', fontSize: 11, color: 'var(--text-secondary)', background: 'var(--secondary)', padding: '2px 8px', borderRadius: 'var(--radius-pill, 9999px)', margin: '0 6px 6px 0' }}>{k}</code>
                ))}
              </div>
            )}
            <div style={{ fontSize: 11.5, color: 'var(--text-tertiary)', marginTop: 12 }}>Изменить набор прав может администратор: Конструктор → Команда → «Выдать доступ».</div>
          </Section>
        </div>
      </Shell>
    );
  }

  /* Hub primitives.
     Правило страницы (2026-07-08): рабочие зоны встраиваются в поток 100% ширины
     (HubZone), без teaser-карточек и без ↗→модалка для основного контента.
     Wgt/HubModal оставляем для редких вторичных панелей; вторичное прячем
     в MoreFold («Ещё»). Сайдбар скроллит к #zone-{id} (anchorNav). */
  /* Зона — 6-колоночный грид; секции внутри управляют своим order и span.
     Остальные дети (PageHead, KPI-строка) по умолчанию занимают всю ширину. */
  /* Контекст lk-arrange для зоны, встроенной в OpsField (of-zone-embed).
     BlockArrange в ops-field.jsx дублирует это на уровне блока; обёртка здесь
     гарантирует ↗/⋯/grip у HubWgtGrid/OpsWgt даже при глубокой вложенности. */
  function LKZoneArrange({ zoneId, children }) {
    const Ctx = window.LKArrangeCtx;
    const arrange = window.useArrangeZone && zoneId ? window.useArrangeZone(zoneId) : null;
    if (!arrange || !Ctx) return children;
    return <Ctx.Provider value={arrange}>{children}</Ctx.Provider>;
  }

  function HubZone({ id, children, className, active }) {
    if (active === false) {
      return (
        <section
          id={id ? 'zone-' + id : undefined}
          data-zone-anchor={id || undefined}
          className="lk-hub-zone lk-hub-zone--deferred"
          aria-hidden="true"
        />
      );
    }
    const arrange = window.useArrangeZone ? window.useArrangeZone(id) : null;
    return (
      <section
        ref={arrange ? arrange.gridRef : undefined}
        id={id ? 'zone-' + id : undefined}
        data-zone-anchor={id || undefined}
        className={'lk-hub-zone' + (arrange ? ' lk-arr-grid' : '') + (className ? ' ' + className : '')}
      >
        <ArrangeCtx.Provider value={arrange}>{children}</ArrangeCtx.Provider>
      </section>
    );
  }

  function MoreFold({ label, children, defaultOpen }) {
    return (
      <details className="lk-more" open={!!defaultOpen}>
        <summary className="lk-more-sum">
          <Ic n="chevron-down" s={14} />
          <span>{label || 'Ещё'}</span>
        </summary>
        <div className="lk-more-body">{children}</div>
      </details>
    );
  }

  function Wgt({ id, title, badge, col, accent, prio, onExpand, children }) {
    return (
      <section id={id ? 'zone-' + id : undefined} data-zone-anchor={id || undefined} className={'lk-wgt' + (col ? ' ' + col : '') + (accent ? ' lk-wgt--' + accent : '')}>
        <div className="lk-wgt-head">
          <div className="lk-wgt-title">{title}{badge != null && <span className="lk-wgt-badge">{badge}</span>}{prio && <span className="lk-wgt-prio">приоритет</span>}</div>
          {onExpand && <button type="button" className="lk-wgt-exp" onClick={onExpand} title="Открыть полностью" aria-label="Открыть полностью"><Ic n="arrow-up-right" s={15} /></button>}
        </div>
        <div className="lk-wgt-body">{children}</div>
      </section>
    );
  }

  function HubModal({ title, onClose, children }) {
    React.useEffect(() => {
      const k = (e) => e.key === 'Escape' && onClose();
      document.addEventListener('keydown', k);
      document.body.dataset.lkModal = '1';
      return () => { document.removeEventListener('keydown', k); delete document.body.dataset.lkModal; };
    }, [onClose]);
    const node = (
      <React.Fragment>
        <div className="lk-modal-ov" onClick={onClose} />
        <div className="lk-modal" role="dialog" aria-modal="true" aria-label={title}>
          <div className="lk-modal-head">
            <div className="lk-modal-head-text">
              <div className="lk-modal-title">{title}</div>
            </div>
            <button type="button" className="lk-modal-x" onClick={onClose} aria-label="Закрыть"><Ic n="x" s={18} /></button>
          </div>
          <div className="lk-modal-body">{children}</div>
        </div>
      </React.Fragment>
    );
    return window.ReactDOM && window.ReactDOM.createPortal ? window.ReactDOM.createPortal(node, document.body) : node;
  }

  function hubScrollContainer() {
    return document.querySelector('.hub .content') || document.querySelector('.main .content') || document.querySelector('.content');
  }

  function scrollHubZoneIntoView(el, opts) {
    if (!el) return;
    const scroller = hubScrollContainer();
    const reduce = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    const smooth = !(opts && opts.instant) && !reduce;
    const behavior = smooth ? 'smooth' : 'auto';
    if (!scroller || scroller === document.documentElement || scroller === document.body) {
      el.scrollIntoView({ behavior, block: 'start' });
      return;
    }
    const pad = 12;
    const sRect = scroller.getBoundingClientRect();
    const eRect = el.getBoundingClientRect();
    const top = scroller.scrollTop + (eRect.top - sRect.top) - pad;
    scroller.scrollTo({ top: Math.max(0, top), behavior });
  }

  function hubZoneMounted(id) {
    const el = document.getElementById('zone-' + id);
    return el && !el.classList.contains('lk-hub-zone--deferred');
  }

  function scrollToHubZone(id, opts) {
    const el = document.getElementById('zone-' + id);
    if (!hubZoneMounted(id)) return false;
    const det = el.closest('details.lk-more');
    if (det && !det.open) {
      det.open = true;
      requestAnimationFrame(() => scrollHubZoneIntoView(el, opts));
      return true;
    }
    scrollHubZoneIntoView(el, opts);
    return true;
  }

  function useHubScroll(zone, opts) {
    const homeZone = (opts && opts.homeZone) || 'today';
    React.useEffect(() => {
      const hash = (typeof location !== 'undefined' && location.hash) ? location.hash.slice(1) : '';
      let target = null;
      if (zone && zone !== homeZone && zone !== 'soon' && hubZoneMounted(zone)) target = zone;
      else if (hash && hubZoneMounted(hash)) target = hash;
      if (target) {
        const t = setTimeout(() => scrollToHubZone(target), 80);
        return () => clearTimeout(t);
      }
      const c = hubScrollContainer();
      const ts = [80, 400].map((ms) => setTimeout(() => { if (c) c.scrollTop = 0; }, ms));
      return () => ts.forEach(clearTimeout);
    }, [zone, homeZone]);
  }

  function makeHubNav(go, external) {
    return function hubNav(id) {
      if (external && external[id]) { if (typeof go === 'function') go(id); return; }
      if (scrollToHubZone(id)) return;
      if (typeof go === 'function') go(id);
    };
  }

  /* ── проект из ProjectStore с перерисовкой по подписке ── */
  function useProject(role) {
    const [, tick] = React.useState(0);
    React.useEffect(() => (window.ProjectStore ? window.ProjectStore.subscribe(() => tick((x) => x + 1)) : undefined), []);
    return window.ProjectStore ? window.ProjectStore.current(role) : null;
  }
  function AssigneeAvatars({ assignees, size = 22 }) {
    if (!assignees || !assignees.length) return <span className="lk-asg-none">не назначена</span>;
    return (
      <span className="lk-asg">
        {assignees.map((a) => <span key={a.accountId} className="mini-ava" title={a.name + (a.auto ? ' · авто' : '')} style={{ width: size, height: size, fontSize: size * 0.42, background: a.auto ? 'var(--chart-2)' : 'var(--primary)' }}>{(a.name || '—').split(' ').map((w) => w[0]).join('').slice(0, 2).toUpperCase()}</span>)}
      </span>
    );
  }

  Object.assign(window, {
    useProject, AssigneeAvatars, ReplacementDecisions,
    Ic, fmtRub, fmtCompact, LKSection: Section, Kpi, ArrangeKpi, Ring, Prog, QueueRow, QRowActions,
    Shell, PageHead, mountWhenReady, LKProfilePage, Spark, LKSegs, useSeg,
    HubZone, LKZoneArrange, MoreFold, Wgt, HubModal, useHubScroll, makeHubNav, hubZoneMounted, scrollToHubZone,
  });
  try { window.dispatchEvent(new CustomEvent('remontpro:lk-ready', { detail: { part: 'common' } })); } catch (e) { /* ignore */ }
})();
