Files

216 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Штурман — роадмапа</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3.6.0/dist/tabler-icons.min.css">
<style>
:root{
--bg0:#ffffff; --bg1:#f6f6f4; --bg2:#ececea; --tx0:#1a1a18; --tx1:#67675f; --tx2:#9a9a92;
--bd0:rgba(0,0,0,.13); --bd1:rgba(0,0,0,.24); --rmd:8px; --rlg:12px;
--v0:#6b7280; --v1:#2f6fd0; --v2b:#c2790a; --v3:#0d8a80; --v4:#7c54d0;
--font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
@media(prefers-color-scheme:dark){:root{
--bg0:#1f1f1d; --bg1:#272724; --bg2:#161614; --tx0:#ececea; --tx1:#a6a69f; --tx2:#74746e;
--bd0:rgba(255,255,255,.14); --bd1:rgba(255,255,255,.26);
--v0:#9aa3b2; --v1:#6ea2f0; --v2b:#e0a64a; --v3:#3bb9ad; --v4:#a98ae8;
}}
*{box-sizing:border-box}
body{margin:0;background:var(--bg2);color:var(--tx0);font-family:var(--font);line-height:1.55;font-size:15px;
-webkit-font-smoothing:antialiased}
.wrap{max-width:1080px;margin:0 auto;padding:28px 20px 64px}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}
h1{font-size:22px;font-weight:600;margin:0 0 2px}
.sub{color:var(--tx1);font-size:14px;margin:0 0 18px}
.tabs{display:flex;gap:8px;margin:0 0 18px;flex-wrap:wrap}
.tab{appearance:none;border:.5px solid var(--bd1);background:var(--bg0);color:var(--tx0);font:inherit;font-size:14px;
padding:7px 14px;border-radius:var(--rmd);cursor:pointer;display:flex;align-items:center;gap:7px;transition:.12s}
.tab:hover{background:var(--bg1)}
.tab.on{background:var(--tx0);color:var(--bg0);border-color:var(--tx0)}
.view{display:none} .view.on{display:block}
.legend{display:flex;gap:14px;flex-wrap:wrap;color:var(--tx1);font-size:13px;margin:0 0 16px}
.legend i{font-style:normal;display:inline-flex;align-items:center;gap:5px}
.dot{width:10px;height:10px;border-radius:3px;display:inline-block}
.phase{background:var(--bg0);border:.5px solid var(--bd0);border-left:3px solid var(--ac);border-radius:var(--rlg);
margin:0 0 12px;overflow:hidden}
.phead{display:flex;align-items:center;gap:12px;padding:14px 16px;cursor:pointer;user-select:none}
.phead:hover{background:var(--bg1)}
.badge{font-weight:600;font-size:13px;color:var(--bg0);background:var(--ac);border-radius:6px;padding:3px 9px;flex:none}
.ptitle{font-weight:500;font-size:16px} .pdemo{color:var(--tx1);font-size:13px;margin-top:2px}
.pmeta{flex:1;min-width:0} .chev{color:var(--tx2);transition:.15s;flex:none}
.phase.open .chev{transform:rotate(90deg)}
.crit{font-size:12px;color:var(--ac);border:.5px solid var(--ac);border-radius:20px;padding:2px 9px;flex:none;white-space:nowrap}
.body{display:none;padding:4px 16px 16px;border-top:.5px solid var(--bd0)}
.phase.open .body{display:block}
.ms{border:.5px solid var(--bd0);border-radius:var(--rmd);margin-top:10px;background:var(--bg1)}
.mhead{display:flex;align-items:center;gap:10px;padding:10px 12px;cursor:pointer}
.mhead:hover{background:var(--bg2)}
.mid{font-weight:600;font-size:13px;color:var(--ac);flex:none;font-variant-numeric:tabular-nums}
.mname{font-weight:500;font-size:14px;flex:1}
.killer{font-size:11px;font-weight:600;background:var(--v2b);color:#000;border-radius:5px;padding:2px 7px;flex:none}
.mdet{display:none;padding:2px 12px 13px 12px;font-size:13.5px}
.ms.open .mdet{display:block}
.row{display:flex;gap:8px;padding:5px 0;border-top:.5px solid var(--bd0)}
.row:first-child{border-top:none}
.k{color:var(--tx2);flex:none;width:104px;font-size:12.5px;padding-top:1px}
.v{color:var(--tx0)}
.ids{display:flex;flex-wrap:wrap;gap:4px}
.id{font-family:ui-monospace,Menlo,monospace;font-size:12px;background:var(--bg0);border:.5px solid var(--bd0);
border-radius:5px;padding:1px 6px;color:var(--tx0)}
.gates{margin-top:12px;font-size:13px;color:var(--tx1)}
.gates b{color:var(--tx0);font-weight:500}
.flow{display:flex;flex-direction:column;gap:6px}
.fstep{display:flex;align-items:center;gap:12px;background:var(--bg0);border:.5px solid var(--bd0);border-left:3px solid var(--ac);
border-radius:var(--rmd);padding:11px 14px}
.fb{font-weight:600;font-size:13px;color:var(--bg0);background:var(--ac);border-radius:6px;padding:3px 9px;flex:none}
.fchain{font-size:13.5px;color:var(--tx0)} .fchain b{color:var(--ac);font-weight:600}
.arrow{color:var(--tx2);text-align:center;font-size:18px;line-height:.7}
.note{color:var(--tx1);font-size:13px;margin-top:14px;padding:12px 14px;background:var(--bg0);border:.5px solid var(--bd0);border-radius:var(--rmd)}
.rk{width:100%;border-collapse:collapse;font-size:13.5px}
.rk th{text-align:left;font-weight:500;color:var(--tx1);font-size:12.5px;padding:8px 10px;border-bottom:.5px solid var(--bd1)}
.rk td{padding:9px 10px;border-bottom:.5px solid var(--bd0);vertical-align:top}
.rk tr:hover td{background:var(--bg1)}
.rg{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--tx0)}
.by{font-weight:600;color:var(--ac2);white-space:nowrap}
.hg{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:12px}
.hcard{background:var(--bg0);border:.5px solid var(--bd0);border-radius:var(--rlg);padding:14px 16px}
.hcard h3{font-size:14px;font-weight:500;margin:0 0 8px;color:var(--tx1)}
.hitem{display:flex;gap:8px;align-items:baseline;padding:4px 0;font-size:13.5px}
.hitem .id{flex:none}
</style>
</head>
<body>
<div class="wrap">
<h2 class="sr">Интерактивная роадмапа проекта Штурман: пять фаз v0–v4, разложенных на вехи с зависимостями, тестом без машины и критериями готовности, критический путь и реестр рисков.</h2>
<h1>Штурман — роадмапа</h1>
<p class="sub">Лестница v0–v4: каждая фаза → вехи с зависимостями, тестом без машины и критерием готовности. 170 функций, источник правды — <span class="rg">docs/roadmap.md</span>.</p>
<div class="tabs" id="tabs">
<button class="tab on" data-v="ladder"><i class="ti ti-stairs-up" aria-hidden="true"></i>Лестница</button>
<button class="tab" data-v="crit"><i class="ti ti-route" aria-hidden="true"></i>Критпуть</button>
<button class="tab" data-v="risk"><i class="ti ti-alert-triangle" aria-hidden="true"></i>Риски</button>
<button class="tab" data-v="horizon"><i class="ti ti-telescope" aria-hidden="true"></i>Горизонт</button>
</div>
<div class="view on" id="ladder">
<div class="legend" id="legend"></div>
<div id="phases"></div>
</div>
<div class="view" id="crit"><div class="flow" id="flow"></div>
<div class="note">Параллельно (не на критпути): телефон/медиа/камера (v2.4v2.6), companion (v3.4), расширения (v3.5); нав- и OTA-ветки v4 идут одновременно. Сквозной enabling-трек — dev-environment.</div>
</div>
<div class="view" id="risk"><table class="rk"><thead><tr><th>Гейт</th><th>Выбор между</th><th>Решить к</th><th>Док</th></tr></thead><tbody id="risks"></tbody></table></div>
<div class="view" id="horizon"><p class="sub" style="margin-bottom:14px">За горизонтом v4 — доразвитие из тех же доменов по мере спроса (20 функций, вне лестницы v0–v4).</p><div class="hg" id="hgrid"></div></div>
</div>
<script>
const AC={v0:'var(--v0)',v1:'var(--v1)',v2:'var(--v2b)',v3:'var(--v3)',v4:'var(--v4)'};
const P=[
{id:'v0',t:'База + Shell',demo:'вкл/выкл в машине → мгновенный красивый UI, переживает срыв питания',crit:'образ → power-safe → shell',
gates:'A01 (Armbian/Debian vs Yocto) · A02 (f2fs vs ext4) · B08/B09 (MCU vs supercap)',
ms:[
{id:'v0.1',n:'Образ-болванка',ids:'A01 A02 A06 A17',dep:'hardware/BSP',test:'Lima-VM',done:'init поднялся; RO-rootfs A/B + overlay + /data монтируются'},
{id:'v0.2',n:'Boot-конвейер',ids:'A04 A05 A15',dep:'v0.1',test:'VM boot',done:'Stage 0/1/2 разделены; splash мгновенно'},
{id:'v0.3',n:'Power-safe ядро',ids:'B01 B03 B04 B02 B06 B07 A14 B05',dep:'v0.2, hold-up (hw §3)',test:'fake-ACC + power-cut в VM',done:'N циклов зажигания без потери /data; abort до PONR'},
{id:'v0.4',n:'MCU/thermal fail-safe',ids:'B08 B09 B10 A12',dep:'v0.3, мок-MCU',test:'мок-MCU/sensor',done:'thermal-trip → graceful; MCU-таймер режет питание при зависании SoC'},
{id:'v0.5',n:'Shell первый кадр',ids:'C03 C04 C01 C05 C02 C07 C09 C10',dep:'v0.2, design-system',test:'нативный Slint + VM',done:'до интерактива < бюджет (perf §3); авто день/ночь'},
{id:'v0.6',n:'База-доводка + dev-харнесс',ids:'A09 A10 A11 A07 A16 F01 F02 F03 F04 J06',dep:'параллельно',test:'сам harness',done:'dev-итерация без железа; память/лог/eMMC в бюджете'}]},
{id:'v1',t:'Ассистент онлайн + связь + аудио + Location',demo:'«Штурман, …» → устный RU-ответ; аудио с ducking; distraction по GPS',crit:'ассистент-онлайн',
gates:'K05 (маппинг руля/ADC) · B08/B09 (если MCU не закрыт) · distraction-числа (safety §4)',
ms:[
{id:'v1.1',n:'Аудио-плоскость',ids:'H01 H02 H05 H04',dep:'v0 (PipeWire/WirePlumber)',test:'fake-аудио (H §12)',done:'ducking-лестница; громкость с руля; ducking ≤150 ms'},
{id:'v1.2',n:'Location / датчики',ids:'K01 K02 K03 K04 A08',dep:'v0, fake-GPS',test:'NMEA-реплей',done:'Location публикует zero-clamp-скорость; time-sync от GPS'},
{id:'v1.3',n:'Связь-core',ids:'G01 G02 G03',dep:'v0, NM/MM',test:'мок-сеть',done:'portal/limited не врёт «online»; статус в баре'},
{id:'v1.4',n:'Ассистент-пайплайн (офлайн-узлы)',ids:'D01 D02 D03 D04 D05 H03',dep:'v1.1, mic',test:'моки STT(текст)/TTS(лог)',done:'wake→STT→TTS офлайн; AEC держит wake во время медиа; wake ≤400 ms'},
{id:'v1.5',n:'Интенты + онлайн-LLM',ids:'D06 D07 D08 D09 D11',dep:'v1.4, v1.3',test:'мок-LLM (canned) + мок-сеть',done:'голос→ответ; «нет сети» graceful; локальный интент ≤300 ms без LLM'},
{id:'v1.6',n:'Distraction + руль',ids:'C11 C12 D12 C13 K05',dep:'v1.2, v1.4, safety §4',test:'fake-GPS speed + мок-руль',done:'distraction по порогам safety §4; навигация/громкость рулём'},
{id:'v1.7',n:'База-доводка v1',ids:'A13 B11 B12 B13',dep:'v0.4',test:'мок',done:'wake-word гейтится состояниями; sleep/wake базово'}]},
{id:'v2',t:'Контекст машины (killer) + телефон + медиа + камера',demo:'«прочитай ошибки / что значит лампочка» → live OBD+DTC по-человечески',crit:'контекст машины (killer)',
gates:'E03 (DTC-база своя RU vs готовая) · H06 (декодер/AAC-патент) · J01 (DRM-handoff Stage 0→1)',
ms:[
{id:'v2.1',n:'CAN-транспорт',ids:'E01 E04 E06 E05',dep:'v0/v1, data-model, hw(CAN), ISO-TP',test:'Vehicle Simulator (ELM327-emu + vcan)',done:'live-сигналы rate-cap ~1020 Гц; engine_running (не дублирует Power)'},
{id:'v2.2',n:'DTC + диагностика',ids:'E02 E03 E07 E08',dep:'v2.1, DTC-база',test:'симулятор инжектит P0420',done:'читает реальные DTC + RU-расшифровка'},
{id:'v2.3',n:'Vehicle-context',killer:1,ids:'D10 C08',dep:'v2.2, v1-ассистент (D06/D08)',test:'симулятор P0420 → объяснение по-русски',done:'killer-demo: голос «что за ошибка» → live OBD+DTC → человеческое объяснение'},
{id:'v2.4',n:'Телефон',ids:'G04 G05 G06 G07 G08 G09 G10',dep:'v1.1, v1.6, BT, D §6',test:'fake-BT-стек (G §12)',done:'звонок руль/тач; контакты PBAP; входящий-оверлей vs реверс'},
{id:'v2.5',n:'Медиа',ids:'H06 H07 H08 H09 H10 C06',dep:'v2.4, v1.1, storage',test:'синтет-медиа + fake-A2DP',done:'локальный трек + BT-музыка; now-playing — первая Wayland-поверхность'},
{id:'v2.6',n:'Камера',ids:'A18 J01 J02 J03 J04 J05 K06 B14',dep:'v2.1, C06, A §4/B §7',test:'fake-камера (паттерн/no-signal/реверс)',done:'реверс→камера приоритетно; парктроник; fail-safe «нет сигнала»'},
{id:'v2.7',n:'Телеметрия-задел',ids:'L07',dep:'security-privacy §7, consent',test:'мок-телеметрия-sink',done:'по умолчанию ноль egress; consent-гейт'}]},
{id:'v3',t:'Офлайн-фолбэк + Plugin API + companion',demo:'ответ без сети + установка стороннего плагина + companion-телефон',crit:'офлайн + Plugin API',
gates:'D13 (офлайн-модель) · D14 (память/consent) · F11 (подпись) · L03 (моб-стек) · L06 (push) · H11 (FM-тюнер)',
ms:[
{id:'v3.1',n:'Plugin host',ids:'F05 F06 F07 F08 F09 F10 C14',dep:'v0 (App-Host, a-base §3), plugin-sdk',test:'plugin-host-харнесс (F)',done:'плагин install/update/remove в песочнице; ревью разрешений (perm-UI)'},
{id:'v3.2',n:'Plugin-интенты + подпись',ids:'D15 F11',dep:'v3.1, v1-ассистент',test:'тест-плагин с intent',done:'плагин регистрирует интент; подпись манифеста'},
{id:'v3.3',n:'Офлайн-ассистент',ids:'D13 D14',dep:'v1-пайплайн, storage',test:'мок-сеть off + локальная модель',done:'сеть выключена → локальный ответ; память водителя (.md)'},
{id:'v3.4',n:'Companion',ids:'L01 L02 L03 L04 L05 L06 L08',dep:'v1.3, v2 (E/trip-плагин)',test:'фейк-companion-peer + мок-облако',done:'паринг; синк настроек/памяти/поездок local-first; бэкап'},
{id:'v3.5',n:'Связь + медиа-расширения',ids:'G11 G12 H11 H12 H13 H14',dep:'v1.3, v3.1, network',test:'моки сети/медиа',done:'WiFi-hotspot; SMS-чтение; media-source через плагин'}]},
{id:'v4',t:'Навигация + OTA + прод-образ + ретрофит',demo:'офлайн-навигация + OTA + документированный ретрофит на реальное авто',crit:'первый «продукт»',
gates:'I03 (Valhalla vs OSRM) · I01 (карты + ODbL + размещение) · G13 (проекция scope/legal)',
ms:[
{id:'v4.1',n:'Карты + рендер',ids:'I01 I02 I04 I08',dep:'tech-stack, хранилище, C §4, G §2',test:'нав-сим (тест-регион PMTiles)',done:'карта рендерится офлайн (GPU); детект «вне региона»; геокодер'},
{id:'v4.2',n:'Роутинг + ведение',ids:'I03 I05 I06 I07 I09 I10 I11 I12 I13 K09',dep:'v4.1, v1.2, v1.1, D §6',test:'мок-маршрут (rerouting/maneuver)',done:'turn-by-turn-ведение; map-matching; назначение голосом; resume'},
{id:'v4.3',n:'Прод-образ + secure boot',ids:'A03 A20 A21 A22',dep:'v0-образ, hardware',test:'флеш на железо (HW-in-the-loop)',done:'релиз-образ прошивается; verified boot; /data шифрован; factory reset'},
{id:'v4.4',n:'OTA-канал',ids:'A19 L09 L10 L11 L12 L13',dep:'v4.3 (trust-anchor), v3.4, F11',test:'мок-облако/OTA (подписанные/битые/downgrade)',done:'OTA доставляет/применяет/откатывает; anti-rollback; каналы stable/beta'},
{id:'v4.5',n:'Ретрофит + опц.',ids:'G13',dep:'всё v0–v4 на реальном авто',test:'реальная установка',done:'документированный ретрофит на одну машину (BSP/калибровка/гайд)'}]}
];
const CRIT=[
{p:'v0',c:'<b>v0.1</b> образ → <b>v0.2</b> boot → <b>v0.3</b> power-safe → <b>v0.5</b> shell'},
{p:'v1',c:'<b>v1.1</b> аудио + <b>v1.2</b> Location + <b>v1.3</b> сеть → <b>v1.4</b> пайплайн → <b>v1.5</b> онлайн-LLM'},
{p:'v2',c:'<b>v2.1</b> CAN → <b>v2.2</b> DTC → <b>v2.3</b> контекст (KILLER)'},
{p:'v3',c:'<b>v3.1</b> plugin-host → <b>v3.3</b> офлайн-LLM'},
{p:'v4',c:'<b>v4.1</b> карты → <b>v4.2</b> роутинг ‖ <b>v4.3</b> secure boot → <b>v4.4</b> OTA → <b>v4.5</b> ретрофит'}
];
const RISK=[
['A01','Armbian/Debian vs Yocto','v0','a-base §2'],['A02','f2fs vs ext4','v0','a-base §3'],
['B08/B09','MCU-копилот vs supercap-only','v0','b-power §5, hw §3'],
['distraction','числа км/ч + список блокируемого','v1','safety §4'],
['perf','латентные числа (кадр/голос/ввод)','по фазам','performance §3'],
['K05','маппинг кнопок / ADC-калибровка','v1/v2','k-sensors §3'],
['E03','своя RU DTC-база vs готовая','v2','e-vehicle-data §5'],
['H06','symphonia + AAC-патент (юр)','v2','h-media §5'],
['J01','DRM-master handoff Stage 0→1','v2','j-cameras §3'],
['D13','YandexGPT Lite / T-lite / Qwen','v3','d-assistant §5'],
['D14','что авто-запоминать / схема / consent','v3','d-assistant §7'],
['F11','формат / keyring / ревокация','v3','f-plugin §3, sec-priv §6'],
['L03','Flutter vs Rust-core+native','v3','l-cloud §3'],
['L06','self-relay vs APNs/FCM vs локально','v3','l-cloud §5'],
['H11','FM-тюнер: добавить vs отказаться','v3','h-media §7, hw §4'],
['A19','RAUC vs Mender/swupdate/OSTree','v4','a-base §5'],
['I03','Valhalla vs OSRM','v4','i-nav §4'],
['I01','ODbL-атрибуция + размещение данных','v4','i-nav §2'],
['G13','not-first-party / плагин / вне скоупа','v4','g-conn §8']
];
const esc=s=>s.replace(/&/g,'&amp;').replace(/</g,'&lt;');
function ph(p){
const m=p.ms.map(x=>`<div class="ms" style="--ac:${AC[p.id]}"><div class="mhead"><span class="mid">${x.id}</span><span class="mname">${esc(x.n)}</span>${x.killer?'<span class="killer">killer</span>':''}<i class="ti ti-chevron-down chev" aria-hidden="true"></i></div><div class="mdet"><div class="row"><span class="k">ID каталога</span><span class="v ids">${x.ids.split(' ').map(i=>`<span class="id">${i}</span>`).join('')}</span></div><div class="row"><span class="k">Зависит от</span><span class="v">${esc(x.dep)}</span></div><div class="row"><span class="k">Тест без машины</span><span class="v">${esc(x.test)}</span></div><div class="row"><span class="k">Готово, когда</span><span class="v">${esc(x.done)}</span></div></div></div>`).join('');
return `<div class="phase" style="--ac:${AC[p.id]}"><div class="phead"><span class="badge">${p.id}</span><div class="pmeta"><div class="ptitle">${esc(p.t)}</div><div class="pdemo">${esc(p.demo)}</div></div><span class="crit">${esc(p.crit)}</span><i class="ti ti-chevron-right chev" aria-hidden="true"></i></div><div class="body">${m}<div class="gates"><b><i class="ti ti-alert-triangle" aria-hidden="true"></i> Гейты фазы:</b> ${esc(p.gates)}</div></div></div>`;
}
document.getElementById('phases').innerHTML=P.map(ph).join('');
document.getElementById('legend').innerHTML=P.map(p=>`<i><span class="dot" style="background:${AC[p.id]}"></span>${p.id}${esc(p.crit)}</i>`).join('');
document.getElementById('flow').innerHTML=CRIT.map((s,i)=>`<div class="fstep" style="--ac:${AC[s.p]}"><span class="fb">${s.p}</span><span class="fchain">${s.c}</span></div>${i<CRIT.length-1?'<div class="arrow">↓</div>':''}`).join('');
const acFor=s=>{const m=(s||'').match(/v[0-4]/);return AC[m?m[0]:'v0'];};
document.getElementById('risks').innerHTML=RISK.map(r=>`<tr style="--ac2:${acFor(r[2])}"><td class="rg">${esc(r[0])}</td><td>${esc(r[1])}</td><td class="by">${esc(r[2])}</td><td class="rg">${esc(r[3])}</td></tr>`).join('');
const HORIZON=[
{g:'База',items:[['A23','мульти-BSP'],['A24','kernel/dtb-тюнинг (ongoing)']]},
{g:'Shell / ассистент',items:[['C15','мультидисплей / профили / виджеты'],['D16','barge-in']]},
{g:'Vehicle-Data',items:[['E09','trip-производные (у плагина)'],['E10','fuel-trim'],['E11','VIN'],['E12','DBC-сниффинг'],['E13','vendor-DTC'],['E14','лог поездок']]},
{g:'Экосистема / медиа',items:[['F12','курируемый стор'],['H15','мульти-зона / EQ / A2DP-source']]},
{g:'Навигация',items:[['I14','трафик TMC/RDS'],['I15','онлайн-трафик / поиск'],['I16','DR в тоннелях']]},
{g:'Камеры / датчики',items:[['J07','0..N источников'],['J08','dashcam'],['J09','surround / 360°'],['K07','IMU'],['K08','выделенные не-CAN датчики']]}
];
document.getElementById('hgrid').innerHTML=HORIZON.map(g=>`<div class="hcard"><h3>${esc(g.g)}</h3>${g.items.map(it=>`<div class="hitem"><span class="id">${it[0]}</span><span>${esc(it[1])}</span></div>`).join('')}</div>`).join('');
document.addEventListener('click',e=>{
const t=e.target.closest('.tab'); if(t){document.querySelectorAll('.tab').forEach(x=>x.classList.toggle('on',x===t));document.querySelectorAll('.view').forEach(v=>v.classList.toggle('on',v.id===t.dataset.v));return;}
const mh=e.target.closest('.mhead'); if(mh){mh.parentElement.classList.toggle('open');return;}
const phd=e.target.closest('.phead'); if(phd){phd.parentElement.classList.toggle('open');}
});
document.querySelector('.phase').classList.add('open');
</script>
</body>
</html>