Files
KSP_project/demo/ksp_admin_hifi.html
T
ArmorandClaude Opus 4.7 e1d601a774 chore: add reference docs, scripts, tests, demo, prototypes
- Reference docs: known issues, user journey, location redesign, mission merge plan
- Scripts: fill_location_state, merge_missions, migrate_location, migrate_state
- Tests: e2e test suite, asset entries unit tests
- Demo: location board, hifi prototypes, test screenshots
- Data backup: asset log entries and state nodes
- Update .gitignore

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 15:29:47 +08:00

735 lines
66 KiB
HTML
Raw 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="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KSP管理后台</title>
<style>
:root{--bg:#0B0B10;--sfc:#131318;--sfc2:#1a1a22;--brd:rgba(248,250,252,.06);--txt:#F8FAFC;--mut:#94A3B8;--acc:#3B82F6;--ag:rgba(59,130,246,.3);--grn:#22c55e;--amb:#eab308;--red:#ef4444;--cyn:#06b6d4;--pur:#a855f7;--rad:6px;--sw:224px;--th:42px;--fd:'SF Mono','Cascadia Code',Consolas,monospace;--fu:system-ui,-apple-system,'Segoe UI',sans-serif}
[data-theme="light"]{--bg:#F1F5F9;--sfc:#FFFFFF;--sfc2:#F8FAFC;--brd:rgba(0,0,0,.06);--txt:#0F172A;--mut:#64748B;--acc:#2563EB;--ag:rgba(37,99,235,.15)}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--txt);font-family:var(--fu);height:100vh;overflow:hidden;display:flex;flex-direction:column}
::-webkit-scrollbar{width:4px;height:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--brd);border-radius:2px}
/* Topbar */
#tb{height:var(--th);display:flex;align-items:center;gap:12px;padding:0 14px;background:var(--sfc);border-bottom:1px solid var(--brd);z-index:30;flex-shrink:0}
#tb .br{font-family:var(--fd);font-size:.78rem;font-weight:600;letter-spacing:-.02em}#tb .br b{color:var(--acc);font-weight:700}
#tb .tb-sp{flex:1}
.tb-time{display:flex;align-items:center;gap:5px}
.tb-time .l{font-size:.62rem;color:var(--mut);text-transform:uppercase;letter-spacing:.05em}
.tb-time #td{font-family:var(--fd);font-size:.74rem;color:var(--acc);background:rgba(59,130,246,.06);padding:3px 8px;border-radius:4px;cursor:pointer;border:1px solid transparent;transition:.15s}
.tb-time #td:hover{background:rgba(59,130,246,.12);border-color:var(--ag)}
.b{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:4px;font-size:.7rem;cursor:pointer;border:1px solid var(--brd);background:transparent;color:var(--mut);transition:.15s;white-space:nowrap}
.b:hover{background:rgba(128,128,128,.08);color:var(--txt)}.b.ac{background:var(--acc);color:#fff;border-color:var(--acc)}.b.ac:hover{box-shadow:0 0 10px var(--ag)}
.b.sm{padding:2px 6px;font-size:.64rem}.b.dg{border-color:var(--red);color:var(--red)}.b.dg:hover{background:rgba(239,68,68,.08)}
/* Layout */
#ly{display:flex;flex:1;overflow:hidden}
#sb{width:var(--sw);flex-shrink:0;background:var(--sfc);border-right:1px solid var(--brd);overflow-y:auto;padding:6px 0;user-select:none}
.nst{display:flex;align-items:center;gap:6px;padding:8px 14px 2px;font-family:var(--fd);font-size:.6rem;font-weight:600;color:var(--mut);text-transform:uppercase;letter-spacing:.08em;cursor:pointer}.nst .ar{font-size:.5rem;transition:transform .15s;opacity:.4}.nst.on .ar{transform:rotate(-90deg)}.nst.on+.niw{display:none}
.niw{padding-bottom:2px}
.ni{display:flex;align-items:center;gap:8px;padding:6px 14px;cursor:pointer;font-size:.76rem;color:var(--mut);transition:.1s;border-left:2px solid transparent}
.ni:hover{color:var(--txt);background:rgba(128,128,128,.03)}.ni.act{color:var(--txt);background:rgba(59,130,246,.05);border-left-color:var(--acc)}
.ni .ic{width:16px;height:16px;display:flex;align-items:center;justify-content:center;font-size:.8rem;flex-shrink:0;opacity:.6}.ni.su{padding-left:36px;font-size:.72rem}
#ct{flex:1;overflow-y:auto;padding:20px 24px;position:relative}
.pg{display:none}.pg.act{display:block}.pg.loc{display:none;position:absolute;inset:0;padding:0 !important;z-index:20;background:var(--bg)}.pg.loc.act{display:flex;flex-direction:column}
/* Page Header */
.ph{margin-bottom:18px}.ph .ey{font-family:var(--fd);font-size:.6rem;color:var(--mut);text-transform:uppercase;letter-spacing:.08em;margin-bottom:2px}.ph h1{font-family:var(--fd);font-size:1.1rem;font-weight:600}.ph p{color:var(--mut);font-size:.72rem;margin-top:1px}
.ph-row{display:flex;align-items:center;justify-content:space-between}
/* Metrics */
.mg{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:18px}
.mc{background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);padding:14px 16px;transition:border-color .15s}.mc:hover{border-color:rgba(248,250,252,.1)}
.mc .l{font-family:var(--fd);font-size:.6rem;color:var(--mut);text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px}.mc .v{font-family:var(--fd);font-size:1.6rem;font-weight:600}.mc .s{font-size:.66rem;color:var(--mut);margin-top:2px}
.mc.ac .v{color:var(--acc)}.mc.gn .v{color:var(--grn)}.mc.am .v{color:var(--amb)}
/* Status Board */
.stl{font-family:var(--fd);font-size:.68rem;font-weight:600;color:var(--mut);text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px}
.sgr{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}
.sgi{background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);overflow:hidden}
.sgh{display:flex;align-items:center;gap:8px;padding:8px 14px;border-bottom:1px solid var(--brd);font-family:var(--fd);font-size:.66rem;font-weight:500;cursor:pointer;user-select:none}
.sdt{width:6px;height:6px;border-radius:50%;flex-shrink:0}.sdt.tf{background:var(--amb);box-shadow:0 0 5px var(--amb)}.sdt.ex{background:var(--acc);box-shadow:0 0 5px var(--ag)}.sdt.mt{background:var(--pur);box-shadow:0 0 5px var(--pur)}.sdt.dk{background:var(--grn);box-shadow:0 0 5px var(--grn)}
.scc{color:var(--mut);font-size:.64rem;margin-left:auto}
.srw{display:flex;align-items:center;gap:10px;padding:5px 14px 5px 30px;font-size:.74rem;transition:background .1s}.srw:hover{background:rgba(128,128,128,.02)}
.srw .an{color:var(--acc);cursor:pointer;flex:1;font-weight:500}.srw .an:hover{text-decoration:underline}
.srw .am{color:var(--mut);font-size:.66rem}.srw .al{color:var(--mut);font-size:.66rem;font-family:var(--fd)}
/* Events */
.evl{display:flex;flex-direction:column;gap:3px;margin-bottom:18px}
.evr{display:flex;align-items:center;gap:10px;padding:7px 10px;background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);font-size:.74rem}
.evr .ed{font-family:var(--fd);font-size:.66rem;color:var(--amb);min-width:76px}.evr .eb{font-family:var(--fd);font-size:.6rem;padding:2px 6px;border-radius:3px;background:rgba(234,179,8,.08);color:var(--amb)}
/* Filter Bar */
.fb{display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap}
.fb input[type=text],.fb select{padding:5px 8px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.72rem}
.fb input[type=text]{width:180px}.fb input[type=text]:focus,.fb select:focus{outline:none;border-color:var(--acc)}
.fb label{display:flex;align-items:center;gap:4px;font-size:.7rem;color:var(--mut);cursor:pointer}
/* Table */
table{width:100%;border-collapse:collapse;font-size:.74rem}
th,td{padding:6px 10px;text-align:left;border-bottom:1px solid var(--brd)}
th{font-family:var(--fd);font-size:.62rem;font-weight:600;color:var(--mut);text-transform:uppercase;letter-spacing:.04em;cursor:pointer;user-select:none;white-space:nowrap}
th:hover{color:var(--txt)}.th-sa{color:var(--acc);margin-left:2px}
td{color:var(--txt)}td a{color:var(--acc);text-decoration:none}td a:hover{text-decoration:underline}td.mt{color:var(--mut);font-size:.68rem}
tr:hover td{background:rgba(128,128,128,.015)}
.act-btn{display:inline-flex;gap:4px;font-size:.68rem}.act-btn a{color:var(--mut);text-decoration:none;padding:2px 5px;border-radius:3px;transition:.1s}.act-btn a:hover{background:var(--brd);color:var(--txt)}
/* Pagination */
.pg-bar{display:flex;align-items:center;gap:3px;margin-top:12px;font-size:.7rem}
.pg-bar span,.pg-bar a{padding:3px 7px;border-radius:4px;color:var(--mut);text-decoration:none}.pg-bar a:hover{background:var(--brd);color:var(--txt)}.pg-bar .cur{background:var(--acc);color:#fff}
/* Timeline */
.tl-r{display:flex;align-items:center;gap:8px;padding:5px 8px;margin-bottom:2px;background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);font-size:.72rem}
.tl-bar{flex:1;height:5px;border-radius:3px;background:var(--brd);position:relative;overflow:hidden}
.tl-fl{position:absolute;top:0;height:100%;border-radius:3px;opacity:.75}.tl-fl.tf{background:var(--amb)}.tl-fl.ex{background:var(--acc)}.tl-fl.mt{background:var(--pur)}.tl-fl.dk{background:var(--grn)}
.tl-scale{display:flex;margin-bottom:6px;font-size:.58rem;color:var(--mut);text-align:center;border-bottom:1px solid var(--brd);padding-bottom:4px}
.tl-scale>div{flex:1}
/* Fuel Converter */
.cvw{display:flex;gap:16px}.cvw form{display:flex;flex-direction:column;gap:8px;width:260px}.cvw form label{font-size:.72rem;color:var(--mut)}.cvw form input,.cvw form select{padding:5px 8px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd)}.cvw .res{flex:1}
/* CRUD Modal */
.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:100;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}.modal-bg.open{opacity:1;pointer-events:auto}
.modal{background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);padding:20px;width:400px;max-height:80vh;overflow-y:auto}
.modal h2{font-family:var(--fd);font-size:.85rem;margin-bottom:14px}
.modal .fg{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}
.modal .fg label{font-size:.72rem;color:var(--mut)}
.modal .fg input,.modal .fg select,.modal .fg textarea{padding:6px 8px;border-radius:4px;background:var(--bg);color:var(--txt);border:1px solid var(--brd);font-size:.74rem}
.modal .fg textarea{min-height:60px;resize:vertical}
.modal .fg input:focus,.modal .fg select:focus,.modal .fg textarea:focus{outline:none;border-color:var(--acc)}
.modal .btn-row{display:flex;gap:6px;justify-content:flex-end}
.modal .btn-row .b:first-child{margin-right:auto}
/* Location Board */
#map-ct{flex:1;position:relative;display:flex;align-items:center;justify-content:center;background:var(--bg);cursor:grab;overflow:hidden}
#map-cv{display:block}
#bk-btn{position:absolute;top:10px;left:10px;z-index:5;display:none;background:var(--sfc);border:1px solid var(--brd);color:var(--txt);padding:4px 12px;border-radius:var(--rad);cursor:pointer;font-size:.74rem}#bk-btn.vis{display:block}
#scope-ind{position:absolute;top:10px;right:10px;z-index:5;background:rgba(19,19,24,.85);border:1px solid var(--brd);color:var(--mut);padding:3px 10px;border-radius:4px;font-size:.68rem;font-family:var(--fd)}
#map-info{width:280px;background:var(--sfc);border-left:1px solid var(--brd);padding:16px;overflow-y:auto;display:flex;flex-direction:column;gap:14px;flex-shrink:0}
#map-info h2{font-family:var(--fd);font-size:.85rem;font-weight:600;padding-bottom:5px;border-bottom:1px solid var(--brd)}
.mi-sec h3{font-family:var(--fd);font-size:.6rem;text-transform:uppercase;color:var(--mut);letter-spacing:.05em;margin-bottom:3px}
.mi-val{font-family:var(--fd);font-size:1.3rem;font-weight:600;color:var(--acc)}
.mi-arow{display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid var(--brd);font-size:.76rem}.mi-arow .n{color:var(--acc);cursor:pointer}.mi-arow .n:hover{text-decoration:underline}.mi-arow .loc{color:var(--mut);font-size:.68rem;font-family:var(--fd)}
#tl-layout{display:flex;flex:1;overflow:hidden}
</style>
</head>
<body>
<div id="tb">
<div class="br"><b>KSP</b>&nbsp;管理后台</div>
<div class="tb-sp"></div>
<div class="tb-time"><span class="l">Sim Time</span><span id="td" onclick="openTM()">2060-08-11 10:00</span></div>
<button class="b" onclick="shT(-1)">1d</button><button class="b" onclick="shT(1)">+1d</button><button class="b" onclick="shT(7)">+7d</button><button class="b" onclick="rsT()">Today</button>
<button class="b" onclick="toggleTheme()" title="Toggle theme" style="margin-left:4px"></button>
</div>
<div id="ly">
<nav id="sb">
<a class="ni act" data-pg="overview" href="#"><span class="ic"></span>任务总览</a>
<div class="nst" onclick="this.classList.toggle('on')"><span class="ar"></span>任务运营</div>
<div class="niw">
<a class="ni su" data-pg="assets" href="#"><span class="ic"></span>任务资产</a>
<a class="ni su" data-pg="missions" href="#"><span class="ic"></span>任务列表</a>
<a class="ni su" data-pg="timeline" href="#"><span class="ic"></span>时间线</a>
<a class="ni su" data-pg="location" href="#"><span class="ic"></span>地点板</a>
</div>
<div class="nst" onclick="this.classList.toggle('on')"><span class="ar"></span>数据中心</div>
<div class="niw">
<a class="ni su" data-pg="engines" href="#"><span class="ic"></span>引擎目录</a>
<a class="ni su" data-pg="comms" href="#"><span class="ic"></span>通信部件</a>
<a class="ni su" data-pg="tanks" href="#"><span class="ic"></span>燃料箱规格</a>
<a class="ni su" data-pg="vehicles" href="#"><span class="ic"></span>载具成本</a>
</div>
<a class="ni" data-pg="converter" href="#"><span class="ic"></span>燃料换算</a>
</nav>
<main id="ct">
<!-- ═══ OVERVIEW ═══ -->
<div class="pg act" id="pg-overview">
<div class="ph"><div class="ey">Mission Operations</div><h1>任务总览</h1><p>Simulation: <strong id="ht"></strong> · All metrics derived from asset logs</p></div>
<div class="mg" id="metrics"></div>
<div class="stl">Status Board</div><div class="sgr" id="sgr"></div>
<div class="stl">Upcoming Events</div><div class="evl" id="evl"></div>
</div>
<!-- ═══ ASSETS ═══ -->
<div class="pg" id="pg-assets">
<div class="ph-row"><div class="ph"><div class="ey">Mission Operations</div><h1>任务资产</h1></div><div><button class="b ac" onclick="openCRUD('asset')">+ New Asset</button></div></div>
<div class="fb"><input type="text" id="aq" placeholder="Search..." oninput="rA()"><select id="at" onchange="rA()"><option value="">All Types</option><option>Exploration Mothership</option><option>Space Shuttle</option><option>Station</option><option>Base</option><option>Cargo Ship</option></select><select id="al" onchange="rA()"><option value="">All Locations</option><option>Earth System</option><option>Mars System</option><option>Jupiter System</option><option>Transfer</option></select><label><input type="checkbox" id="ah" onchange="rA()" checked> Hide retired</label></div>
<table id="atbl"><thead><tr>
<th onclick="sA('n')">Name <span class="th-sa"></span></th>
<th onclick="sA('t')">Type <span class="th-sa"></span></th>
<th onclick="sA('l')">Location <span class="th-sa"></span></th>
<th>Current Event</th><th onclick="sA('c')">Entries <span class="th-sa"></span></th><th>Action</th>
</tr></thead><tbody></tbody></table>
<div class="pg-bar" id="apg"></div>
</div>
<!-- ═══ ASSET DETAIL ═══ -->
<div class="pg" id="pg-asset-detail">
<div class="ph-row"><div class="ph"><div class="ey">Mission Asset</div><h1 id="ad-name"></h1><p id="ad-sub"></p></div><div style="display:flex;gap:6px"><button class="b" onclick="openCRUD('asset',adIdx)">Edit Asset</button><button class="b" onclick="swPg('assets')">← Back</button></div></div>
<div style="display:grid;grid-template-columns:1fr 280px;gap:16px">
<div>
<!-- Info cards -->
<div class="mg" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px">
<div class="mc"><div class="l">Type</div><div class="v" id="ad-type" style="font-size:.95rem"></div></div>
<div class="mc"><div class="l">Location</div><div class="v" id="ad-loc" style="font-size:.95rem"></div></div>
<div class="mc"><div class="l">Mission</div><div class="v" id="ad-miss" style="font-size:.85rem"></div></div>
<div class="mc"><div class="l">Entries</div><div class="v" id="ad-entries" style="font-size:.95rem">0</div></div>
</div>
<!-- Docking section: target + vehicles -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
<div>
<div class="stl">Docking Target</div>
<div id="ad-target" style="background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);padding:10px 12px;font-size:.78rem"></div>
</div>
<div>
<div class="stl" style="display:flex;align-items:center;justify-content:space-between">
<span>Docked Vehicles</span>
<button class="b sm ac" onclick="openDockModal()" style="font-size:.64rem">+ Dock</button>
</div>
<div id="ad-docked" style="background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);padding:6px 0;font-size:.76rem;max-height:120px;overflow-y:auto"></div>
</div>
</div>
<!-- Log entries -->
<div class="stl" style="display:flex;align-items:center;justify-content:space-between">
<span>Log Entries</span>
<div style="display:flex;gap:4px">
<button class="b sm ac" onclick="openNewLog('state')">+ State Interval</button>
<button class="b sm" onclick="openNewLog('event')">+ Event Point</button>
</div>
</div>
<div class="fb"><input type="text" id="ad-lq" placeholder="Search logs..." oninput="rADLogs()"><select id="ad-lf" onchange="rADLogs()"><option value="">All Types</option><option value="state">State Interval</option><option value="event">Event Point</option></select></div>
<table><thead><tr><th>Date</th><th>Type</th><th>Title</th><th>State</th><th>Location</th><th>Action</th></tr></thead><tbody id="ad-log-tbl"></tbody></table>
<div class="pg-bar" id="ad-log-pg"></div>
</div>
<div>
<!-- Upcoming events -->
<div class="stl">Upcoming Events</div>
<div class="evl" id="ad-events" style="margin-bottom:14px"></div>
<!-- Quick info -->
<div class="stl">Quick Info</div>
<div style="background:var(--sfc);border:1px solid var(--brd);border-radius:var(--rad);padding:10px 12px;font-size:.72rem;color:var(--mut)">
<div style="margin-bottom:4px"><strong style="color:var(--txt)">Retired:</strong> <span id="ad-retired">No</span></div>
<div style="margin-bottom:4px"><strong style="color:var(--txt)">Created:</strong> <span id="ad-created"></span></div>
<div><strong style="color:var(--txt)">Updated:</strong> <span id="ad-updated"></span></div>
</div>
</div>
</div>
</div>
<!-- ═══ MISSIONS ═══ -->
<div class="pg" id="pg-missions">
<div class="ph"><div class="ey">Mission Operations</div><h1>任务列表</h1></div>
<div class="fb"><input type="text" id="mq" placeholder="Search..." oninput="rM()"><select id="ml" onchange="rM()"><option value="">All Locations</option><option>Earth System</option><option>Mars System</option><option>Jupiter System</option><option>Transfer</option></select></div>
<div class="sgr" id="msg"></div>
</div>
<!-- ═══ TIMELINE ═══ -->
<div class="pg" id="pg-timeline">
<div class="ph"><div class="ey">Mission Operations</div><h1>时间线</h1></div>
<!-- Filter bar row 1: search + presets + scale -->
<div class="fb"><input type="text" id="tl-q" placeholder="Search assets..." oninput="rT()" style="width:160px">
<div style="display:flex;gap:3px;margin-left:4px">
<button class="b sm tl-pre" data-f="active" onclick="tlPreset('active')">Active</button>
<button class="b sm tl-pre" data-f="month" onclick="tlPreset('month')">This Month</button>
<button class="b sm tl-pre" data-f="90d" onclick="tlPreset('90d')">Next 90d</button>
<button class="b sm tl-pre" data-f="all" onclick="tlPreset('all')">All</button>
</div>
<div style="margin-left:auto;display:flex;gap:4px;align-items:center">
<label style="font-size:.68rem;color:var(--mut)">Scale:</label>
<select id="tls" onchange="rT()" style="padding:3px 6px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.68rem"><option value="year">Year</option><option value="month" selected>Month</option><option value="day">Day</option></select>
</div>
</div>
<!-- Filter bar row 2: Type + Location + State + Date range -->
<div class="fb">
<label style="font-size:.68rem;color:var(--mut)">Type:</label><select id="tl-type" onchange="rT()" style="padding:3px 6px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.68rem"><option value="">All</option><option>Exploration Mothership</option><option>Space Shuttle</option><option>Station</option><option>Base</option><option>Cargo Ship</option></select>
<label style="font-size:.68rem;color:var(--mut)">Location:</label><select id="tl-loc" onchange="rT()" style="padding:3px 6px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.68rem"><option value="">All</option><option>Earth System</option><option>Mars System</option><option>Jupiter System</option><option>Transfer</option></select>
<label style="font-size:.68rem;color:var(--mut)">State:</label><select id="tl-state" onchange="rT()" style="padding:3px 6px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.68rem"><option value="">All</option><option value="transfer">Transfer</option><option value="exploration">Exploration</option><option value="maintenance">Maintenance</option><option value="docked">Docked</option></select>
<div style="margin-left:auto;display:flex;gap:4px;align-items:center">
<label style="font-size:.68rem;color:var(--mut)">From:</label><input type="date" id="tl-from" onchange="rT()" style="padding:3px 6px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.68rem">
<label style="font-size:.68rem;color:var(--mut)">To:</label><input type="date" id="tl-to" onchange="rT()" style="padding:3px 6px;border-radius:4px;background:var(--sfc2);color:var(--txt);border:1px solid var(--brd);font-size:.68rem">
</div>
</div>
<!-- Asset pill tags -->
<div class="fb" id="tl-pills" style="gap:4px"></div>
<div id="tlr"></div>
</div>
<!-- ═══ LOCATION BOARD ═══ -->
<div class="pg loc" id="pg-location">
<div id="tl-layout" style="flex:1;display:flex">
<div id="map-ct"><canvas id="map-cv"></canvas><button id="bk-btn" onclick="mapGoBack()">← Back</button><span id="scope-ind">Solar System</span></div>
<div id="map-info"><h2 id="mi-title">Solar System</h2>
<div class="mi-sec"><h3>Scope</h3><div id="mi-scope" style="font-size:.88rem;font-weight:500"></div></div>
<div class="mi-sec"><h3>Assets in Scope</h3><div id="mi-assets"></div></div>
<div class="mi-sec"><h3>Ongoing Missions</h3><div class="mi-val" id="mi-miss">0</div></div>
<div class="mi-sec"><h3>Upcoming Events</h3><div id="mi-events"></div></div>
</div>
</div>
</div>
<!-- ═══ ENGINES ═══ -->
<div class="pg" id="pg-engines">
<div class="ph-row"><div class="ph"><div class="ey">Reference Data</div><h1>引擎目录</h1></div><div><button class="b ac" onclick="openCRUD('engine')">+ New Engine</button></div></div>
<div class="fb"><input type="text" id="eq" placeholder="Search..." oninput="rE()"><select id="ef" onchange="rE()"><option value="">All Fuel</option><option>LF/O</option><option>Nuclear</option><option>Electric</option></select></div>
<table><thead><tr><th onclick="sortT('eq-tbl',0)">Name</th><th onclick="sortT('eq-tbl',1)">Cycle</th><th onclick="sortT('eq-tbl',2)">Mass</th><th onclick="sortT('eq-tbl',3)">Vac Thrust</th><th onclick="sortT('eq-tbl',4)">Vac ISP</th><th>Fuel</th><th>Mod</th><th>Action</th></tr></thead><tbody id="eq-tbl"></tbody></table>
</div>
<!-- ═══ COMMS ═══ -->
<div class="pg" id="pg-comms">
<div class="ph-row"><div class="ph"><div class="ey">Reference Data</div><h1>通信部件</h1></div><div><button class="b ac" onclick="openCRUD('comm')">+ New</button></div></div>
<table><thead><tr><th onclick="sortT('cm-tbl',0)">Name</th><th onclick="sortT('cm-tbl',1)">Range</th><th onclick="sortT('cm-tbl',2)">Mass</th><th>Type</th><th>Source</th><th>Action</th></tr></thead><tbody id="cm-tbl"></tbody></table>
</div>
<!-- ═══ TANKS ═══ -->
<div class="pg" id="pg-tanks">
<div class="ph-row"><div class="ph"><div class="ey">Reference Data</div><h1>燃料箱规格</h1></div><div><button class="b ac" onclick="openCRUD('tank')">+ New</button></div></div>
<table><thead><tr><th onclick="sortT('tk-tbl',0)">Name</th><th onclick="sortT('tk-tbl',1)">Volume</th><th onclick="sortT('tk-tbl',2)">Dry Mass</th><th>Fuel</th><th>Source</th><th>Action</th></tr></thead><tbody id="tk-tbl"></tbody></table>
</div>
<!-- ═══ VEHICLES ═══ -->
<div class="pg" id="pg-vehicles">
<div class="ph-row"><div class="ph"><div class="ey">Reference Data</div><h1>载具成本</h1></div><div><button class="b ac" onclick="openCRUD('vehicle')">+ New</button></div></div>
<table><thead><tr><th onclick="sortT('vh-tbl',0)">Name</th><th onclick="sortT('vh-tbl',1)">Launch Cost</th><th>Source</th><th>Action</th></tr></thead><tbody id="vh-tbl"></tbody></table>
</div>
<!-- ═══ FUEL CONVERTER ═══ -->
<div class="pg" id="pg-converter">
<div class="ph"><div class="ey">Tools</div><h1>燃料换算</h1></div>
<div class="cvw"><form id="cvf"><label>Value<input type="number" id="cvv" value="1000" step="1"></label><label>Mode<select id="cvm"><option value="L2t">Liters → Tons</option><option value="t2L">Tons → Liters</option></select></label><label>Fuel<select id="cvf-sel"></select></label><button type="submit" class="b ac">Convert</button></form><div class="res"><table><thead><tr><th>Fuel</th><th>Density (kg/L)</th><th>Result</th></tr></thead><tbody id="cvr"></tbody></table></div></div>
</div>
</main>
</div>
<!-- ═══ MODALS ═══ -->
<div class="modal-bg" id="tmodal"><div class="modal"><h2>Set Simulation Time</h2><div class="fg"><label>Time<input type="datetime-local" id="tminp" step="60"></label></div><div class="btn-row"><button class="b" onclick="clTM()">Cancel</button><button class="b ac" onclick="apT()">Apply</button></div></div></div>
<div class="modal-bg" id="crud-modal"><div class="modal"><h2 id="crud-title">New Entry</h2><div class="fg" id="crud-fields"></div><div class="btn-row"><button class="b dg" id="crud-del" style="display:none" onclick="crudDel()">Delete</button><button class="b" onclick="clCRUD()">Cancel</button><button class="b ac" onclick="crudSave()">Save</button></div></div></div>
<div class="modal-bg" id="dock-modal"><div class="modal"><h2>Dock Vehicle</h2><div class="fg"><label>Vehicle<select id="dock-vehicle"><option value="">Select existing asset...</option><option>回声号</option><option>秃鹫 VS-01</option><option>擎天号</option><option value="_custom">Custom name...</option></select></label><input type="text" id="dock-custom" placeholder="Enter vehicle name..." style="display:none"><label>Note<textarea id="dock-note" rows="2" placeholder="Optional"></textarea></label></div><div class="btn-row"><button class="b" onclick="clDock()">Cancel</button><button class="b ac" onclick="saveDock()">Dock</button></div></div></div>
<div class="modal-bg" id="log-modal"><div class="modal" style="width:420px;max-height:80vh;overflow-y:auto"><h2 id="log-modal-title">New Log Entry</h2><div class="fg"><input type="hidden" id="log-type" value="state"><label>Title<input type="text" id="log-title" placeholder="Entry title"></label><div id="log-end-wrap"><label>Start At<input type="datetime-local" id="log-start" step="60"></label><label>End At<input type="datetime-local" id="log-end" step="60"></label></div><div id="log-state-wrap"><label>State<select id="log-state"><option value=""></option><option>Transfer</option><option>Exploration</option><option>Maintenance</option><option>Docked</option><option>In Transit</option></select></label></div><label>Location<input type="text" id="log-location" placeholder="LEO / Mars Orbit / etc."></label><label>Summary<textarea id="log-summary" rows="2" placeholder="Optional"></textarea></label><div id="log-nodes-wrap"><hr style="border:none;border-top:1px solid var(--brd);margin:6px 0"><div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px"><span style="font-size:.7rem;color:var(--mut)">State Nodes (0-N)</span><button class="b sm" type="button" onclick="addNodeRow()" style="font-size:.62rem">+ Add Node</button></div><div id="nodes-list"></div></div></div><div class="btn-row"><button class="b" onclick="clLog()">Cancel</button><button class="b ac" onclick="saveLog()">Save</button></div></div></div>
<script>
// ═══ STATE ═══
const ST=new Date('2060-08-11T10:00:00Z');
let cPg='overview',asS={k:'n',d:1};
let mapVS=[{tp:'solar',ck:'sun'}],mapHv=null,mapDg=false,mapDS=null,mapDL=null,mapDD=false,mapVO={x:0,y:0},mapZm=1;
const ASSETS=[
{n:'ST-01 万星源号',t:'Exploration Mothership',l:'Transfer',s:'transfer',m:'OSSEM-1 → Neptune',e:'Arrival 2060-07-25',c:12,r:false},
{n:'ST-02 克里斯滕号',t:'Exploration Mothership',l:'Transfer',s:'transfer',m:'EE-2 → Earth',e:'Arrival 2060-08-26',c:14,r:false},
{n:'ST-03 Muelsyse',t:'Exploration Mothership',l:'LEO',s:'maintenance',m:'Post-Trial Maintenance',e:'Ends 2060-10-25',c:8,r:false},
{n:'XH-01 羲和号',t:'Exploration Mothership',l:'Jupiter System',s:'exploration',m:'Europa Expedition 3',e:'On station',c:22,r:false},
{n:'XH-02 太白号',t:'Exploration Mothership',l:'Mars Orbit',s:'docked',m:'Mars Expedition 4',e:'Surface ops',c:16,r:false},
{n:'XH-03 常曦号',t:'Exploration Mothership',l:'LEO',s:'maintenance',m:'Commissioning',e:'Ends 2060-06-10',c:5,r:false},
{n:'JW-01 金乌号',t:'Cargo Ship',l:'LEO',s:'docked',m:'Station Logistics',e:'Resupply',c:6,r:false},
{n:'星港空间站',t:'Station',l:'LEO',s:'docked',m:'Hub Operations',e:'XH-03 docked',c:18,r:false},
{n:'火星一号基地',t:'Base',l:'Mars Surface',s:'exploration',m:'Habitation',e:'XH-02 crew',c:8,r:false},
{n:'欧罗巴前哨站',t:'Base',l:'Europa Surface',s:'exploration',m:'Ice Drilling',e:'XH-01 crew',c:5,r:false},
{n:'回声号',t:'Space Shuttle',l:'LEO',s:'docked',m:'Station Ferry',e:'Next flight',c:3,r:false},
{n:'秃鹫 VS-01',t:'Space Shuttle',l:'Earth Surface',s:'maintenance',m:'Overhaul',e:'Retired',c:180,r:true},
{n:'擎天号',t:'Cargo Ship',l:'LEO',s:'docked',m:'Cargo Run',e:'Undock 2060-09',c:4,r:false},
{n:'凌霄空间站',t:'Station',l:'LEO',s:'maintenance',m:'Decommissioning',e:'Crew transfer',c:10,r:true},
];
const EVENTS=[{d:'2060-06-10',t:'XH-03 Maintenance Complete'},{d:'2060-06-15',t:'ST-03 Deep Space Test Begins'},{d:'2060-06-28',t:'ST-02 Returns to Earth'},{d:'2060-07-25',t:'ST-01 Arrives at Neptune'},{d:'2060-10-25',t:'ST-03 Commissioning Ends'},{d:'2060-09-01',t:'ST-02 Upgrade Begins'}];
const MISSIONS=[{n:'OSSEM-1',a:'ST-01',l:'Transfer',d:'Neptune',st:'2060-02-11',et:'—',s:'Active'},{n:'EE-2 Return',a:'ST-02',l:'Transfer',d:'Earth',st:'2060-06-28',et:'2060-08-26',s:'Active'},{n:'Post-Trial',a:'ST-03',l:'LEO',d:'LEO',st:'2060-03-07',et:'2060-10-25',s:'Active'},{n:'Europa Exp 3',a:'XH-01',l:'Jupiter',d:'Europa',st:'2060-02-01',et:'—',s:'Active'},{n:'Mars Exp 4',a:'XH-02',l:'Mars Orbit',d:'Mars Surface',st:'2059-06-01',et:'—',s:'Active'},{n:'Commissioning',a:'XH-03',l:'LEO',d:'LEO',st:'2059-12-09',et:'2060-06-10',s:'Active'},{n:'Station Ops',a:'星港',l:'LEO',d:'LEO',st:'2055-01-01',et:'—',s:'Active'},{n:'Habitation',a:'火星一号',l:'Mars Surface',d:'Mars',st:'2055-06-01',et:'—',s:'Active'}];
let ENGINES=[{n:'KS-25 Vector',cy:'Staged',m:4.0,vt:1000,vi:315,f:'LF/O',md:'Stock'},{n:'RE-L10 Poodle',cy:'Expander',m:1.75,vt:250,vi:350,f:'LF/O',md:'Stock'},{n:'LV-N Nerv',cy:'NTR',m:3.0,vt:60,vi:800,f:'Nuclear',md:'Stock'},{n:'IX-6315 Dawn',cy:'Ion',m:0.25,vt:2,vi:4200,f:'Electric',md:'Stock'},{n:'RE-M3 Mainsail',cy:'Staged',m:6.0,vt:1500,vi:310,f:'LF/O',md:'Stock'},{n:'CR-7 RAPIER',cy:'Combined',m:2.0,vt:180,vi:320,f:'LF/O',md:'Stock'}];
let COMMS=[{n:'Communotron 16',rng:'500M',mass:0.015,tp:'Direct',src:'Stock'},{n:'HG-5 High Gain',rng:'5M',mass:0.075,tp:'Relay',src:'Stock'},{n:'RA-2 Relay',rng:'2G',mass:0.15,tp:'Relay',src:'Stock'},{n:'RA-15 Relay',rng:'15G',mass:0.3,tp:'Relay',src:'Stock'}];
let TANKS=[{n:'Oscar-B',vol:40,mass:0.025,f:'LF/O',src:'Stock'},{n:'FL-T400',vol:400,mass:0.25,f:'LF/O',src:'Stock'},{n:'FL-T800',vol:800,mass:0.5,f:'LF/O',src:'Stock'},{n:'Jumbo-64',vol:6400,mass:4.0,f:'LF/O',src:'Stock'}];
let VEHICLES=[{n:'CERV Shuttle',cost:45000,src:'KSP'},{n:'Vulture B1',cost:38000,src:'KSP'},{n:'Echo Shuttle',cost:62000,src:'KSP'},{n:'Enterprise',cost:75000,src:'KSP'},{n:'Qingtian Cargo',cost:28000,src:'KSP'}];
const FUELS=[{n:'LF',d:.005},{n:'Oxidizer',d:.005},{n:'LF/O Mixed',d:.005},{n:'Monopropellant',d:.004},{n:'XenonGas',d:.0001},{n:'LH2',d:.00007085}];
const CRUD_DEFS={
asset:{title:'Asset',fields:[{k:'n',l:'Name'},{k:'t',l:'Type',type:'select',opts:['Exploration Mothership','Space Shuttle','Station','Base','Cargo Ship']},{k:'l',l:'Location'},{k:'s',l:'State',type:'select',opts:['transfer','exploration','maintenance','docked']},{k:'m',l:'Mission'},{k:'e',l:'Event'}],store:ASSETS},
engine:{title:'Engine',fields:[{k:'n',l:'Name'},{k:'cy',l:'Cycle'},{k:'m',l:'Mass (t)',type:'number'},{k:'vt',l:'Vac Thrust (kN)',type:'number'},{k:'vi',l:'Vac ISP (s)',type:'number'},{k:'f',l:'Fuel',type:'select',opts:['LF/O','Nuclear','Electric']},{k:'md',l:'Mod'}],store:ENGINES},
comm:{title:'Communication',fields:[{k:'n',l:'Name'},{k:'rng',l:'Range'},{k:'mass',l:'Mass (t)',type:'number'},{k:'tp',l:'Type',type:'select',opts:['Direct','Relay']},{k:'src',l:'Source'}],store:COMMS},
tank:{title:'Tank',fields:[{k:'n',l:'Name'},{k:'vol',l:'Volume (L)',type:'number'},{k:'mass',l:'Dry Mass (t)',type:'number'},{k:'f',l:'Fuel'},{k:'src',l:'Source'}],store:TANKS},
vehicle:{title:'Vehicle',fields:[{k:'n',l:'Name'},{k:'cost',l:'Launch Cost',type:'number'},{k:'src',l:'Source'}],store:VEHICLES},
};
// ═══ HELPERS ═══
function fm(d){return`${d.getUTCFullYear()}-${String(d.getUTCMonth()+1).padStart(2,'0')}-${String(d.getUTCDate()).padStart(2,'0')} ${String(d.getUTCHours()).padStart(2,'0')}:${String(d.getUTCMinutes()).padStart(2,'0')}`}
function uT(){const s=fm(ST);$('td').textContent=s;$('ht').textContent=s}
// ═══ NAV ═══
function $(s){return document.querySelector(s.startsWith('#')||s.startsWith('.')||s.startsWith('[')?s:'#'+s)}
function swPg(p){
document.querySelectorAll('.pg').forEach(e=>e.classList.remove('act'));
document.querySelectorAll('.ni').forEach(e=>e.classList.remove('act'));
const pg=$(`#pg-${p}`);if(pg)pg.classList.add('act');
const nav=$(`[data-pg="${p}"]`);if(nav)nav.classList.add('act');
cPg=p;
if(p==='overview'){rO();rEts()}else if(p==='assets')rA();else if(p==='asset-detail'&&adIdx>=0){}else if(p==='missions')rM();else if(p==='timeline'){rT();initTlPills()}else if(p==='engines')rE();else if(p==='comms')rCm();else if(p==='tanks')rTk();else if(p==='vehicles')rVh();else if(p==='converter')rCv();
if(p==='location'){initMap();setTimeout(()=>rMapInfo(),100)}
}
document.querySelectorAll('.ni').forEach(e=>{e.addEventListener('click',function(ev){ev.preventDefault();swPg(this.dataset.pg)})});
// ═══ OVERVIEW ═══
function rO(){
const act=ASSETS.filter(a=>!a.r).length,ms=MISSIONS.filter(m=>m.s==='Active').length,up=EVENTS.filter(e=>e.d>=ST.toISOString().slice(0,10)).length;
$('metrics').innerHTML=`<div class="mc ac"><div class="l">Tracked Assets</div><div class="v">${act}</div><div class="s">6 Motherships · 2 Stations · 2 Cargo · 2 Shuttles</div></div><div class="mc gn"><div class="l">Active Missions</div><div class="v">${ms}</div><div class="s">2 Deep Space · 1 Transfer · 2 Orbit · 1 Surface</div></div><div class="mc am"><div class="l">Upcoming (30d)</div><div class="v">${up}</div><div class="s">${up>0?EVENTS.filter(e=>e.d>=ST.toISOString().slice(0,10))[0].t:'None imminent'}</div></div>`;
const gr={transfer:{l:'In Transit',c:'tf',i:[]},exploration:{l:'Exploration',c:'ex',i:[]},maintenance:{l:'Maintenance',c:'mt',i:[]},docked:{l:'Docked / Surface',c:'dk',i:[]}};
for(const a of ASSETS){if(a.r)continue;gr[a.s].i.push(a)}
$('sgr').innerHTML=Object.values(gr).filter(g=>g.i.length).map(g=>`<div class="sgi"><div class="sgh"><span class="sdt ${g.c}"></span>${g.l}<span class="scc">${g.i.length}</span></div>${g.i.map(a=>`<div class="srw"><span class="an">${a.n}</span><span class="am">${a.m}</span><span class="al">${a.l}</span></div>`).join('')}</div>`).join('');
}
function rEts(){$('evl').innerHTML=EVENTS.filter(e=>e.d>=ST.toISOString().slice(0,10)).slice(0,6).map(e=>`<div class="evr"><span class="ed">${e.d}</span>${e.t}<span class="eb">${new Date(e.d+'T00:00Z')<new Date(ST.getTime()+30*86400000)?'< 30d':''}</span></div>`).join('')}
// ═══ ASSETS ═══
function rA(){
const q=($('aq').value||'').toLowerCase(),tp=$('at').value,loc=$('al').value,hd=$('ah').checked;
let rows=ASSETS.filter(a=>{if(hd&&a.r)return false;if(tp&&a.t!==tp)return false;if(loc&&!a.l.includes(loc))return false;if(q&&!a.n.toLowerCase().includes(q)&&!a.t.toLowerCase().includes(q))return false;return true});
rows.sort((a,b)=>{let va=a[asS.k]||'',vb=b[asS.k]||'';if(typeof va==='string'){va=va.toLowerCase();vb=vb.toLowerCase()}return va>vb?asS.d:va<vb?-asS.d:0});
const pp=10,tot=Math.ceil(rows.length/pp),pg=Math.min(Math.max(1,1),tot||1),pgd=rows.slice((pg-1)*pp,pg*pp);
$('atbl').querySelector('tbody').innerHTML=pgd.map(a=>`<tr><td><a href="#" onclick="openAssetDetail(${ASSETS.indexOf(a)});return false">${a.n}</a></td><td>${a.t}</td><td class="mt">${a.l}</td><td>${a.e||'—'}</td><td>${a.c}</td><td><div class="act-btn"><a href="#" onclick="openCRUD('asset',${ASSETS.indexOf(a)});return false">Edit</a><a href="#" onclick="delItem(ASSETS,${ASSETS.indexOf(a)});rA();return false">Del</a></div></td></tr>`).join('');
}
function sA(k){if(asS.k===k)asS.d*=-1;else{asS.k=k;asS.d=1}rA()}
// ═══ MISSIONS ═══
function rM(){
const q=($('mq').value||'').toLowerCase(),lc=$('ml').value;
let rows=MISSIONS.filter(m=>{if(lc&&!m.l.includes(lc)&&!m.d.includes(lc))return false;if(q&&!m.n.toLowerCase().includes(q)&&!m.a.toLowerCase().includes(q))return false;return true});
const g={};for(const m of rows){if(!g[m.l])g[m.l]=[];g[m.l].push(m)}
$('msg').innerHTML=Object.entries(g).map(([k,i])=>`<div class="sgi"><div class="sgh">${k}<span class="scc">${i.length}</span></div>${i.map(m=>`<div class="srw"><span class="an">${m.a}: ${m.n}</span><span class="am">${m.st}${m.d}</span><span class="al">${m.s}</span></div>`).join('')}</div>`).join('');
}
// ═══ TIMELINE ═══
let tlPresetVal='active',tlPills=[];
function tlPreset_set(f){tlPresetVal=f;document.querySelectorAll('.tl-pre').forEach(b=>b.classList.toggle('ac',b.dataset.f===f));rT()}
function tlPreset(f){tlPreset_set(f)}
function rT(){
const sc=$('tls').value,q=($('tl-q').value||'').toLowerCase(),tp=$('tl-type').value,loc=$('tl-loc').value,st=$('tl-state').value;
const from=$('tl-from').value,to=$('tl-to').value;
// Filter assets
let rows=ASSETS.filter(a=>{
if(tlPresetVal==='active'&&a.r)return false;
if(tp&&a.t!==tp)return false;
if(loc&&!a.l.includes(loc))return false;
if(st&&a.s!==st)return false;
if(q&&!a.n.toLowerCase().includes(q))return false;
if(tlPills.length&&!tlPills.some(p=>a.n.toLowerCase().includes(p.toLowerCase())))return false;
return true;
});
// Render scale
const ms=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
let h='<div class="tl-scale">';
for(let i=0;i<12;i++)h+=`<div>${ms[i]}</div>`;
h+='</div>';
for(const a of rows){
const w=Math.max(5,20+Math.random()*70);
h+=`<div class="tl-r"><span style="width:110px;font-size:.68rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${a.n}">${a.n}</span><div class="tl-bar"><div class="tl-fl ${a.s}" style="left:${5+Math.random()*25}%;width:${w}%"></div></div></div>`;
}
$('tlr').innerHTML=h;
// Render pills
renderTlPills(rows);
}
function renderTlPills(rows){
const all=tlPresetVal==='active'?ASSETS.filter(a=>!a.r):ASSETS;
$('tl-pills').innerHTML=all.map(a=>{
const sel=tlPills.some(p=>p.toLowerCase()===a.n.toLowerCase());
return`<button class="b sm${sel?' ac':''}" onclick="tlTogglePill('${a.n}')" style="font-size:.64rem">${a.n}</button>`;
}).join('')+`<button class="b sm" onclick="tlPills=[];rT()" style="font-size:.64rem;color:var(--mut)">Clear</button>`;
}
function tlTogglePill(name){
const idx=tlPills.findIndex(p=>p.toLowerCase()===name.toLowerCase());
if(idx>=0)tlPills.splice(idx,1);else tlPills.push(name);
rT();
}
function initTlPills(){
$('tl-pills').innerHTML=ASSETS.filter(a=>!a.r).map(a=>
`<button class="b sm" onclick="tlTogglePill('${a.n}')" style="font-size:.64rem">${a.n}</button>`
).join('')+`<button class="b sm" onclick="tlPills=[];rT()" style="font-size:.64rem;color:var(--mut)">Clear</button>`;
}
// ═══ ASSET DETAIL ═══
let adIdx=-1;
const MOCK_LOGS=[
{t:'2060-02-11',k:'state',title:'Transfer to Neptune',state:'Transfer',loc:'Solar Orbit',nodes:[
{title:'Departed Earth orbit',at:'2060-02-11',detail:'Lightspeed engine burn'},
{title:'Mid-course correction',at:'2060-03-15',detail:'Delta-v 12 km/s'}
]},
{t:'2060-07-25',k:'state',title:'Neptune System Operations',state:'Exploration',loc:'Neptune Orbit',nodes:[]},
{t:'2059-11-25',k:'state',title:'Star Port Full Upgrade',state:'Maintenance',loc:'LEO',nodes:[
{title:'Central extension installed',at:'2059-12-10',detail:''},
{title:'Cryo-sleep chambers deployed',at:'2060-01-15',detail:'12 chambers'}
]},
{t:'2059-11-24',k:'event',title:'Arrived at Earth from Saturn',state:'—',loc:'LEO',nodes:[]},
{t:'2058-06-01',k:'state',title:'Saturn Exploration Mission 1',state:'Exploration',loc:'Saturn System',nodes:[]},
{t:'2058-01-05',k:'state',title:'Maintenance and Upgrades',state:'Maintenance',loc:'LEO',nodes:[]},
{t:'2057-12-20',k:'event',title:'Returned from Europa',state:'—',loc:'LEO',nodes:[]},
{t:'2056-12-15',k:'state',title:'Europa Outpost Construction 1',state:'Exploration',loc:'Jupiter System',nodes:[]},
{t:'2056-11-03',k:'event',title:'Returned from Mars',state:'—',loc:'LEO',nodes:[]},
{t:'2056-06-02',k:'state',title:'Mars One Construction Mission 2',state:'Exploration',loc:'Mars Orbit',nodes:[]},
];
// Docking: parent=asset idx this is docked to, vehicles=[{name,since,until}] (until=null=still docked)
const DOCK_DATA={
0:{parent:-1,vehicles:[]},1:{parent:-1,vehicles:[]},2:{parent:7,vehicles:[]},3:{parent:-1,vehicles:[]},
4:{parent:8,vehicles:[{name:'JW-01 金乌号',since:'2059-08-01',until:null}]},
5:{parent:7,vehicles:[]},6:{parent:7,vehicles:[]},
7:{parent:-1,vehicles:[{name:'XH-03 常曦号',since:'2060-03-07',until:null},{name:'JW-01 金乌号',since:'2060-06-01',until:null},{name:'回声号',since:'2060-01-15',until:null}]},
8:{parent:-1,vehicles:[{name:'XH-02 太白号',since:'2059-08-01',until:null}]},
9:{parent:-1,vehicles:[{name:'XH-01 羲和号',since:'2060-02-01',until:null}]},
10:{parent:7,vehicles:[]},11:{parent:-1,vehicles:[]},12:{parent:7,vehicles:[]},13:{parent:-1,vehicles:[]},
};
function openAssetDetail(idx){
adIdx=idx;const a=ASSETS[idx],dk=DOCK_DATA[idx]||{parent:-1,vehicles:[]};
$('ad-name').textContent=a.n;
$('ad-sub').textContent=`${a.t} · ${a.r?'Retired':'Active'}`;
$('ad-type').textContent=a.t;
$('ad-loc').textContent=a.l;
$('ad-miss').textContent=a.m;
$('ad-entries').textContent=MOCK_LOGS.length;
$('ad-retired').textContent=a.r?'Yes':'No';
$('ad-created').textContent='2050-03-15';
$('ad-updated').textContent='2060-02-10';
// Docking target (with undock)
if(dk.parent>=0){const p=ASSETS[dk.parent];
$('ad-target').innerHTML=`<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 10px"><span><span style="color:var(--acc);cursor:pointer" onclick="openAssetDetail(${dk.parent})">${p.n}</span> <span class="mt">${p.l}</span></span><a href="#" onclick="undockTarget();return false" style="font-size:.68rem;color:var(--red)">分离</a></div>`;
}else{
$('ad-target').innerHTML='<div style="padding:6px 10px;color:var(--mut);font-size:.72rem">Not docked — <a href="#" onclick="dockToAsset();return false" style="color:var(--accent)">Dock to asset</a></div>';
}
// Docked vehicles (multiple, each with undock)
const active=dk.vehicles.filter(v=>!v.until);
$('ad-docked').innerHTML=active.length?active.map(v=>{
const ri=dk.vehicles.indexOf(v);
return`<div style="display:flex;align-items:center;justify-content:space-between;padding:5px 10px;border-bottom:1px solid var(--brd)"><span><strong style="color:var(--txt)">${v.name}</strong> <span class="mt">since ${v.since}</span></span><a href="#" onclick="undockVehicle(${ri});return false" style="font-size:.68rem;color:var(--red)">分离</a></div>`;
}).join(''):'<div style="padding:6px 10px;color:var(--mut);font-size:.72rem">No vehicles docked</div>';
// Events
const evts=EVENTS.filter(e=>e.d>=ST.toISOString().slice(0,10));
$('ad-events').innerHTML=evts.length?evts.map(e=>`<div class="evr"><span class="ed">${e.d}</span>${e.t}</div>`).join(''):'<div style="color:var(--mut);font-size:.72rem;padding:6px 0">No upcoming events</div>';
rADLogs();
swPg('asset-detail');
}
function undockTarget(){const dk=DOCK_DATA[adIdx];if(dk)dk.parent=-1;openAssetDetail(adIdx)}
function dockToAsset(){openDockModal()}
function undockVehicle(i){const dk=DOCK_DATA[adIdx];if(dk&&dk.vehicles[i]){dk.vehicles[i].until=ST.toISOString().slice(0,10);openAssetDetail(adIdx)}}
function rADLogs(){
const q=($('ad-lq')?.value||'').toLowerCase(),ft=$('ad-lf')?.value||'';
let logs=MOCK_LOGS.filter(l=>{
if(ft&&l.k!==ft)return false;
if(q&&!l.title.toLowerCase().includes(q))return false;
return true;
});
$('ad-log-tbl').innerHTML=logs.map(l=>{
const ri=MOCK_LOGS.indexOf(l);
const nodes=l.nodes&&l.nodes.length?l.nodes.map(n=>`<div class="mt" style="font-size:.62rem">→ ${n.title} (${n.at}${n.detail?' · '+n.detail:''})</div>`).join(''):'';
return`<tr><td class="mt">${l.t}${l.end?` → ${l.end}`:''}</td><td><span style="font-size:.64rem;padding:2px 5px;border-radius:3px;background:${l.k==='state'?'rgba(59,130,246,.1);color:var(--acc)':'rgba(234,179,8,.08);color:var(--amb)'}">${l.k==='state'?'State':'Event'}</span></td><td>${l.title}${nodes}</td><td class="mt">${l.state}</td><td class="mt">${l.loc}</td><td><div class="act-btn"><a href="#" onclick="openEditLog(${ri});return false" style="font-size:.66rem">Edit</a><a href="#" onclick="delLog(${ri});return false" style="font-size:.66rem;color:var(--red)">Del</a></div></td></tr>`;
}).join('');
}
// ═══ DATA CENTER ═══
function rE(){const q=($('eq').value||'').toLowerCase(),ft=$('ef').value;let r=ENGINES.filter(e=>{if(ft&&e.f!==ft)return false;if(q&&!e.n.toLowerCase().includes(q)&&!e.f.toLowerCase().includes(q))return false;return true});$('eq-tbl').innerHTML=r.map((e,i)=>`<tr><td><a href="#">${e.n}</a></td><td>${e.cy}</td><td>${e.m}t</td><td>${e.vt}kN</td><td>${e.vi}s</td><td>${e.f}</td><td>${e.md}</td><td><div class="act-btn"><a href="#" onclick="openCRUD('engine',${i});return false">Edit</a><a href="#" onclick="delItem(ENGINES,${i});rE();return false">Del</a></div></td></tr>`).join('')}
function rCm(){$('cm-tbl').innerHTML=COMMS.map((c,i)=>`<tr><td>${c.n}</td><td>${c.rng}</td><td>${c.mass}t</td><td>${c.tp}</td><td>${c.src}</td><td><div class="act-btn"><a href="#" onclick="openCRUD('comm',${i});return false">Edit</a><a href="#" onclick="delItem(COMMS,${i});rCm();return false">Del</a></div></td></tr>`).join('')}
function rTk(){$('tk-tbl').innerHTML=TANKS.map((t,i)=>`<tr><td>${t.n}</td><td>${t.vol}L</td><td>${t.mass}t</td><td>${t.f}</td><td>${t.src}</td><td><div class="act-btn"><a href="#" onclick="openCRUD('tank',${i});return false">Edit</a><a href="#" onclick="delItem(TANKS,${i});rTk();return false">Del</a></div></td></tr>`).join('')}
function rVh(){$('vh-tbl').innerHTML=VEHICLES.map((v,i)=>`<tr><td>${v.n}</td><td>${v.cost.toLocaleString()}</td><td>${v.src}</td><td><div class="act-btn"><a href="#" onclick="openCRUD('vehicle',${i});return false">Edit</a><a href="#" onclick="delItem(VEHICLES,${i});rVh();return false">Del</a></div></td></tr>`).join('')}
// ═══ FUEL CONVERTER ═══
function rCv(){$('cvf-sel').innerHTML=FUELS.map(f=>`<option>${f.n}</option>`).join('');$('cvr').innerHTML=FUELS.map(f=>`<tr><td>${f.n}</td><td>${f.d}</td><td>—</td></tr>`).join('')}
$('cvf').addEventListener('submit',function(e){e.preventDefault();const v=parseFloat($('cvv').value)||0,mode=$('cvm').value;$('cvr').innerHTML=FUELS.map(f=>{const r=mode==='L2t'?(v*f.d/1000).toFixed(4)+' t':(v*1000/f.d).toFixed(0)+' L';return`<tr><td>${f.n}</td><td>${f.d}</td><td style="font-family:var(--fd);color:var(--acc)">${r}</td></tr>`}).join('')});
// ═══ CRUD ═══
let crudType='',crudIdx=-1;
function openCRUD(type,idx){
crudType=type;crudIdx=idx!==undefined?idx:-1;
const def=CRUD_DEFS[type];$('crud-title').textContent=(idx!==undefined?'Edit ':'New ')+def.title;
$('crud-del').style.display=idx!==undefined?'inline-flex':'none';
$('crud-fields').innerHTML=def.fields.map(f=>{
const v=idx!==undefined?(def.store[idx][f.k]||''):'';
if(f.type==='select')return`<label>${f.l}<select data-k="${f.k}">${f.opts.map(o=>`<option${o===v?' selected':''}>${o}</option>`).join('')}</select></label>`;
if(f.type==='number')return`<label>${f.l}<input type="number" data-k="${f.k}" value="${v}" step="any"></label>`;
return`<label>${f.l}<input type="text" data-k="${f.k}" value="${v}"></label>`;
}).join('');
$('#crud-modal').classList.add('open');
}
function clCRUD(){$('#crud-modal').classList.remove('open')}
function crudSave(){
const def=CRUD_DEFS[crudType],obj={};
def.fields.forEach(f=>{const el=$(`#crud-modal [data-k="${f.k}"]`);obj[f.k]=f.type==='number'?parseFloat(el.value)||0:el.value});
if(crudIdx>=0)Object.assign(def.store[crudIdx],obj);else def.store.push(obj);
clCRUD();
if(cPg==='assets')rA();else if(cPg==='engines')rE();else if(cPg==='comms')rCm();else if(cPg==='tanks')rTk();else if(cPg==='vehicles')rVh();
}
function crudDel(){const def=CRUD_DEFS[crudType];if(crudIdx>=0){def.store.splice(crudIdx,1);clCRUD();if(cPg==='assets')rA();else if(cPg==='engines')rE();else if(cPg==='comms')rCm();else if(cPg==='tanks')rTk();else if(cPg==='vehicles')rVh()}}
// === DOCKING ===
function openDockModal(){$('dock-custom').style.display='none';$('dock-vehicle').value='';$('dock-note').value='';$('dock-modal').classList.add('open')}
function clDock(){$('dock-modal').classList.remove('open')}
function saveDock(){
const v=$('dock-vehicle').value;if(!v)return;
const name=v==='_custom'?$('dock-custom').value:v;if(!name)return;
const dk=DOCK_DATA[adIdx]||{parent:-1,vehicles:[]};DOCK_DATA[adIdx]=dk;
dk.vehicles.push({name,since:ST.toISOString().slice(0,10),until:null});
clDock();openAssetDetail(adIdx);
}
$('dock-vehicle').addEventListener('change',function(){$('dock-custom').style.display=this.value==='_custom'?'block':'none'});
// === LOG MODAL (multi-node) ===
let logNodeData=[];
function addNodeRow(title='',at='',detail=''){
logNodeData.push({title,at,detail});
renderNodeRows();
}
function removeNodeRow(i){logNodeData.splice(i,1);renderNodeRows()}
function renderNodeRows(){
$('nodes-list').innerHTML=logNodeData.length?logNodeData.map((n,i)=>`
<div style="background:var(--sfc2);border:1px solid var(--brd);border-radius:4px;padding:6px 8px;margin-bottom:4px">
<div style="display:flex;gap:6px;align-items:center">
<input type="text" value="${n.title}" placeholder="Node title" oninput="logNodeData[${i}].title=this.value" style="flex:1;padding:3px 6px;border-radius:3px;background:var(--bg);color:var(--txt);border:1px solid var(--brd);font-size:.7rem">
<input type="datetime-local" value="${n.at}" oninput="logNodeData[${i}].at=this.value" step="60" style="width:140px;padding:3px 6px;border-radius:3px;background:var(--bg);color:var(--txt);border:1px solid var(--brd);font-size:.7rem">
<button class="b sm" type="button" onclick="removeNodeRow(${i})" style="color:var(--red);font-size:.6rem">✕</button>
</div>
<input type="text" value="${n.detail}" placeholder="Detail (optional)" oninput="logNodeData[${i}].detail=this.value" style="width:100%;padding:3px 6px;border-radius:3px;background:var(--bg);color:var(--txt);border:1px solid var(--brd);font-size:.7rem;margin-top:3px">
</div>
`).join(''):'<div style="font-size:.68rem;color:var(--mut);padding:4px 0">No nodes. Click "+ Add Node" to add.</div>';
}
function openNewLog(type){
logNodeData=[];
$('log-type').value=type;$('log-title').value='';$('log-start').value='';$('log-end').value='';
$('log-state').value='';$('log-location').value='';$('log-summary').value='';
$('log-end-wrap').style.display=type==='state'?'block':'none';
$('log-state-wrap').style.display=type==='state'?'block':'none';
$('log-nodes-wrap').style.display=type==='state'?'block':'none';
renderNodeRows();$('log-modal-title').textContent='New Log Entry';
$('log-modal').classList.add('open');
}
function openEditLog(i){
editLogIdx=i;const l=MOCK_LOGS[i];
$('log-type').value=l.k;$('log-title').value=l.title;$('log-start').value=l.t;$('log-end').value=l.end||'';
$('log-state').value=l.state==='—'?'':l.state;$('log-location').value=l.loc==='—'?'':l.loc;$('log-summary').value=l.summary||'';
logNodeData=(l.nodes||[]).map(n=>({title:n.title,at:n.at,detail:n.detail||''}));
$('log-end-wrap').style.display=l.k==='state'?'block':'none';
$('log-state-wrap').style.display=l.k==='state'?'block':'none';
$('log-nodes-wrap').style.display=l.k==='state'?'block':'none';
renderNodeRows();$('log-modal-title').textContent='Edit Log Entry';
$('log-modal').classList.add('open');
}
function clLog(){$('log-modal').classList.remove('open')}
function saveLog(){
const type=$('log-type').value,title=$('log-title').value;if(!title)return;
const entry={t:($('log-start').value||'').slice(0,10)||ST.toISOString().slice(0,10),end:$('log-end').value?$('log-end').value.slice(0,10):null,k:type,title,state:$('log-state').value||'—',loc:$('log-location').value||'—',summary:$('log-summary').value,nodes:logNodeData.filter(n=>n.title).map(n=>({title:n.title,at:n.at||entry?.t||'',detail:n.detail}))};
if(editLogIdx>=0)MOCK_LOGS[editLogIdx]=entry;else MOCK_LOGS.unshift(entry);
editLogIdx=-1;rADLogs();clLog();$('ad-entries').textContent=MOCK_LOGS.length;
}
function delLog(i){if(confirm('Delete?')){MOCK_LOGS.splice(i,1);rADLogs();$('ad-entries').textContent=MOCK_LOGS.length}}
function delItem(arr,i){if(confirm('Delete this item?'))arr.splice(i,1)}
function sortT(id,col){const tb=document.getElementById(id).querySelector('tbody');const rows=Array.from(tb.rows);rows.sort((a,b)=>a.cells[col].textContent.localeCompare(b.cells[col].textContent));tb.append(...rows)}
// ═══ LOCATION BOARD ═══
const LOC_ASSETS=ASSETS;
const LOC_EVENTS=EVENTS;
const BODIES={sun:{n:'Sun',t:'star',r:20,fr:52,c:'#FDB813',c2:'#ff6b00'},mercury:{n:'Mercury',t:'p',r:5.5,fr:38,c:'#b5b5b5',c2:'#8a8a8a'},venus:{n:'Venus',t:'p',r:8,fr:44,c:'#e6c229',c2:'#c4952a'},earth:{n:'Earth',t:'p',r:9,fr:46,c:'#4da6ff',c2:'#1a56db',mo:{moon:{n:'Moon',r:2.8,c:'#ccc',c2:'#999',orb:40,P:27.3}}},mars:{n:'Mars',t:'p',r:7,fr:40,c:'#c1440e',c2:'#7a1f0a',mo:{phobos:{n:'Phobos',r:1.6,c:'#998',c2:'#665',orb:26,P:.32},deimos:{n:'Deimos',r:1.4,c:'#887',c2:'#554',orb:35,P:1.26}}},ceres:{n:'Ceres',t:'d',r:4,fr:32,c:'#8b8b8b',c2:'#6b6b6b'},vesta:{n:'Vesta',t:'d',r:3,fr:28,c:'#a0a0a0',c2:'#808080'},jupiter:{n:'Jupiter',t:'p',r:16,fr:60,c:'#c9b07c',c2:'#8b6914',mo:{io:{n:'Io',r:2.8,c:'#e8d44d',c2:'#b8a420',orb:50,P:1.77},europa:{n:'Europa',r:2.5,c:'#d4c9a8',c2:'#a09060',orb:62,P:3.55},ganymede:{n:'Ganymede',r:3.2,c:'#aaa',c2:'#777',orb:76,P:7.15},callisto:{n:'Callisto',r:2.8,c:'#666',c2:'#444',orb:90,P:16.7}}},saturn:{n:'Saturn',t:'p',r:13.5,fr:54,c:'#e0c97f',c2:'#b8952a',mo:{mimas:{n:'Mimas',r:1.8,c:'#bbb',c2:'#999',orb:44,P:.94},enceladus:{n:'Enceladus',r:2,c:'#dde',c2:'#bbc',orb:56,P:1.37},titan:{n:'Titan',r:3.5,c:'#d4a85c',c2:'#a07830',orb:90,P:15.95},iapetus:{n:'Iapetus',r:2.7,c:'#999',c2:'#666',orb:112,P:79.3}}},uranus:{n:'Uranus',t:'p',r:11,fr:46,c:'#7ec8e3',c2:'#4a90b0',mo:{miranda:{n:'Miranda',r:1.6,c:'#ccc',c2:'#aaa',orb:36,P:1.41},titania:{n:'Titania',r:3,c:'#bbb',c2:'#999',orb:64,P:8.71}}},neptune:{n:'Neptune',t:'p',r:10,fr:44,c:'#3f54ba',c2:'#1a2a80',mo:{triton:{n:'Triton',r:2.8,c:'#b8d4e3',c2:'#80a0b0',orb:44,P:5.88}}},pluto:{n:'Pluto',t:'d',r:3,fr:28,c:'#d4c9a8',c2:'#a09070',mo:{charon:{n:'Charon',r:2.2,c:'#bbb',c2:'#999',orb:28,P:6.39}}}};
const SO=['mercury','venus','earth','mars','ceres','vesta','jupiter','saturn','uranus','neptune','pluto'];
for(const bk of Object.keys(BODIES)){const b=BODIES[bk];if(b.mo)for(const[mk,mb]of Object.entries(b.mo))BODIES[mk]=mb}
const J2000_JD=2451545.0,DAY_MS=86400000;
const ORB={mercury:{a:.387098,e:.205630,i:7.005,O:48.331,p:77.456,L:252.251,P:87.969},venus:{a:.723332,e:.006772,i:3.395,O:76.680,p:131.563,L:181.980,P:224.701},earth:{a:1.000003,e:.016709,i:.000,O:.000,p:102.938,L:100.464,P:365.256},mars:{a:1.523679,e:.093401,i:1.850,O:49.558,p:336.060,L:355.453,P:686.980},ceres:{a:2.767500,e:.075823,i:10.593,O:80.306,p:72.716,L:73.598,P:1680},vesta:{a:2.361000,e:.088740,i:7.141,O:103.852,p:150.047,L:18.900,P:1325},jupiter:{a:5.203363,e:.048393,i:1.305,O:100.556,p:14.754,L:34.404,P:4332.589},saturn:{a:9.537070,e:.054151,i:2.485,O:113.715,p:92.432,L:49.944,P:10759.22},uranus:{a:19.19126,e:.047168,i:.770,O:74.230,p:170.964,L:313.232,P:30685.4},neptune:{a:30.06904,e:.008606,i:1.770,O:131.723,p:44.971,L:304.880,P:60189},pluto:{a:39.48212,e:.248828,i:17.140,O:110.304,p:224.069,L:238.929,P:90560}};
function jd(d){return d.getTime()/DAY_MS+2440587.5}
function kepl(M,e,eps=1e-8){let E=M+e*Math.sin(M)*(1+e*Math.cos(M));for(let i=0;i<20;i++){let d=(M-E+e*Math.sin(E))/(1-e*Math.cos(E));E+=d;if(Math.abs(d)<eps)break}return E}
function helio(k,jv){const o=ORB[k];if(!o)return{x:0,y:0};const D=Math.PI/180,n=2*Math.PI/o.P,ds=jv-J2000_JD;let M=(o.L-o.p)*D+n*ds;M=((M%(2*Math.PI))+2*Math.PI)%(2*Math.PI);const E=kepl(M,o.e),s=Math.sin(E),c=Math.cos(E),r=o.a*(1-o.e*c),sv=Math.sqrt(1-o.e*o.e)*s/(1-o.e*c),cv=(c-o.e)/(1-o.e*c);const w=(o.p-o.O)*D,O=o.O*D,I=o.i*D,cO=Math.cos(O),sO=Math.sin(O),cI=Math.cos(I),sI=Math.sin(I);const xo=r*cv,yo=r*sv,cw=Math.cos(w),sw=Math.sin(w);const xe=xo*(cO*cw-sO*sw*cI)-yo*(cO*sw+sO*cw*cI),ye=xo*(sO*cw+cO*sw*cI)+yo*(cO*cw*cI-sO*sw);const dist=Math.sqrt(xe*xe+ye*ye),dd=Math.pow(dist,.4)*100,sc=dist>.001?dd/dist:0;return{x:xe*sc,y:ye*sc}}
let mapCtx,mapCv,mapStars=null;
function initMap(){
if(mapCv)return;
mapCv=document.getElementById('map-cv');mapCtx=mapCv.getContext('2d');
function rsz(){const r=document.getElementById('map-ct').getBoundingClientRect(),d=devicePixelRatio||1;if(r.width===0||r.height===0)return;mapCv.width=r.width*d;mapCv.height=r.height*d;mapCv.style.width=r.width+'px';mapCv.style.height=r.height+'px';mapStars=null;renderMap()}
window.addEventListener('resize',rsz);
setTimeout(rsz,100);
// Events
mapCv.addEventListener('mousedown',e=>{mapDg=true;mapDS={x:e.clientX,y:e.clientY};mapDL={x:e.clientX,y:e.clientY};mapDD=false});
window.addEventListener('mousemove',e=>{if(!mapDg||!mapDL)return;if(Math.abs(e.clientX-mapDS.x)>6||Math.abs(e.clientY-mapDS.y)>6)mapDD=true;if(mapDD){mapVO.x+=e.clientX-mapDL.x;mapVO.y+=e.clientY-mapDL.y;mapDL={x:e.clientX,y:e.clientY};renderMap()}});
window.addEventListener('mouseup',()=>{mapDg=false;mapDS=null;mapDL=null});
mapCv.addEventListener('wheel',e=>{e.preventDefault();mapZm=Math.max(.3,Math.min(4,mapZm*(e.deltaY>0?.9:1.1)));renderMap()},{passive:false});
mapCv.addEventListener('click',e=>{if(mapDD)return;const r=mapCv.getBoundingClientRect(),t=mapHit(e.clientX-r.left,e.clientY-r.top);if(!t)return;const v=mapVS[mapVS.length-1],b=BODIES[t];if(v.tp==='solar')mapVS.push({tp:b&&b.mo?'planet_system':'single',ck:t});else if(v.tp==='planet_system')mapVS.push({tp:'single',ck:t});mapVO={x:0,y:0};mapZm=1;updateMapBack();renderMap();rMapInfo()});
mapCv.addEventListener('mousemove',e=>{if(mapDD)return;const r=mapCv.getBoundingClientRect(),t=mapHit(e.clientX-r.left,e.clientY-r.top);if(t!==mapHv){mapHv=t;mapCv.style.cursor=t?'pointer':'grab';renderMap()}});
}
function mapFV(k){if(BODIES[k])return BODIES[k];for(const bk of Object.keys(BODIES)){const b=BODIES[bk];if(b.mo&&b.mo[k])return b.mo[k]}return null}
function mapHit(mx,my){
const v=mapVS[mapVS.length-1],cs=[];
if(v.tp==='solar'){cs.push({k:'sun'});for(const k of SO)cs.push({k})}
else if(v.tp==='planet_system'){const c=BODIES[v.ck];if(c){cs.push({k:v.ck});if(c.mo)for(const mk of Object.keys(c.mo))cs.push({k:mk})}}
else{const b=BODIES[v.ck];if(b)cs.push({k:v.ck})}
for(const{k}of cs){const b=mapFV(k);if(!b||b._x==null)continue;const dx=mx-b._x,dy=my-b._y,hr=(b._r||4)+6;if(dx*dx+dy*dy<=hr*hr)return k}return null
}
function mapDraw(ox,oy,rad,key,lb){
const b=BODIES[key]||mapFV(key);if(!b)return;
const isStar=b.t==='star',bc=b.c||'#888';
mapCtx.save();mapCtx.beginPath();mapCtx.arc(ox,oy,rad,0,Math.PI*2);mapCtx.clip();
// Gradient sphere
const g=mapCtx.createRadialGradient(ox-rad*.25,oy-rad*.3,0,ox,oy,rad);
if(isStar){g.addColorStop(0,'#fffff8');g.addColorStop(.3,'#ffcc44');g.addColorStop(.7,'#ff6600');g.addColorStop(1,'#330800')}
else{g.addColorStop(0,'#ffffff');g.addColorStop(.2,bc);g.addColorStop(.8,b.c2||'#333');g.addColorStop(1,'#111')}
mapCtx.fillStyle=g;mapCtx.beginPath();mapCtx.arc(ox,oy,rad,0,Math.PI*2);mapCtx.fill();mapCtx.restore();
// Shade
if(!isStar){mapCtx.save();mapCtx.beginPath();mapCtx.arc(ox,oy,rad,0,Math.PI*2);mapCtx.clip();
const sh=mapCtx.createRadialGradient(ox-rad*.3,oy-rad*.3,rad*.05,ox+rad*.15,oy+rad*.15,rad*1.1);sh.addColorStop(0,'rgba(255,255,255,.1)');sh.addColorStop(.4,'rgba(0,0,0,0)');sh.addColorStop(.8,'rgba(0,0,0,.5)');mapCtx.fillStyle=sh;mapCtx.fillRect(ox-rad,oy-rad,rad*2,rad*2);mapCtx.restore()}
mapCtx.fillStyle='#e2e8f0';mapCtx.font=`${Math.max(8,rad*.45)}px system-ui`;mapCtx.textAlign='center';mapCtx.fillText(lb,ox,oy-rad-4);
b._x=ox;b._y=oy;b._r=rad;
}
function renderMap(){
if(!mapCv||mapCv.width===0||mapCv.height===0)return;
const dpr=devicePixelRatio||1,W=mapCv.width/dpr,H=mapCv.height/dpr;
mapCtx.save();mapCtx.setTransform(dpr,0,0,dpr,0,0);
if(!mapStars||mapStars.width!==mapCv.width){
const sc=document.createElement('canvas');sc.width=W;sc.height=H;
const sx=sc.getContext('2d');sx.fillStyle=document.documentElement.getAttribute('data-theme')==='light'?'#1a1a2e':'#0B0B10';sx.fillRect(0,0,W,H);
for(let i=0;i<600;i++){sx.fillStyle=`rgba(255,255,255,${.2+Math.random()*.6})`;sx.beginPath();sx.arc(Math.random()*W,Math.random()*H,Math.random()*1.2+.1,0,Math.PI*2);sx.fill()}
mapStars=sc;
}
mapCtx.drawImage(mapStars,0,0,W,H);
const v=mapVS[mapVS.length-1],cx=W/2+mapVO.x,cy=H/2+mapVO.y,jv=jd(new Date(ST.getTime()));
if(v.tp==='solar'){
for(const k of SO){mapCtx.beginPath();mapCtx.arc(cx,cy,Math.pow(ORB[k].a,.4)*100*mapZm,0,Math.PI*2);mapCtx.strokeStyle='rgba(148,163,184,.1)';mapCtx.lineWidth=.5;mapCtx.setLineDash([3,6]);mapCtx.stroke();mapCtx.setLineDash([])}
mapDraw(cx,cy,BODIES.sun.r*mapZm,'sun','Sun');
for(const k of SO){const b=BODIES[k],p=helio(k,jv);mapDraw(cx+p.x*mapZm,cy-p.y*mapZm,b.r*mapZm,k,b.n)}
}else if(v.tp==='planet_system'){
const c=BODIES[v.ck];if(!c)return;
mapDraw(cx,cy,c.fr*mapZm,v.ck,c.n);
if(c.mo)for(const[mk,mb]of Object.entries(c.mo)){const od=mb.orb*mapZm*1.8;mapCtx.beginPath();mapCtx.arc(cx,cy,od,0,Math.PI*2);mapCtx.strokeStyle='rgba(148,163,184,.1)';mapCtx.lineWidth=.5;mapCtx.setLineDash([3,6]);mapCtx.stroke();mapCtx.setLineDash([]);const an=(2*Math.PI*((jv-J2000_JD)%mb.P))/mb.P;mapDraw(cx+od*Math.cos(an),cy-od*Math.sin(an),mb.r*2.2*mapZm,mk,mb.n)}
}else{
const b=BODIES[v.ck];if(!b)return;const mx=Math.min(cx,cy)*.5,rr=Math.min((b.fr?b.fr*3:(b.r||4)*15)*mapZm,mx);
mapDraw(cx,cy,rr,v.ck,b.n);
// Saturn rings
if(v.ck==='saturn'&&rr>6){mapCtx.save();mapCtx.strokeStyle='rgba(200,180,140,.3)';mapCtx.lineWidth=Math.max(1,rr*.06);mapCtx.beginPath();mapCtx.ellipse(cx,cy,rr*1.5,rr*.18,0,0,Math.PI*2);mapCtx.stroke();mapCtx.strokeStyle='rgba(200,180,140,.15)';mapCtx.lineWidth=Math.max(1,rr*.04);mapCtx.beginPath();mapCtx.ellipse(cx,cy,rr*1.8,rr*.28,0,0,Math.PI*2);mapCtx.stroke();mapCtx.restore()}
}
// Hover
if(mapHv){const b=mapFV(mapHv);if(b&&b._x!=null){mapCtx.beginPath();mapCtx.arc(b._x,b._y,(b._r||4)+5,0,Math.PI*2);mapCtx.strokeStyle='rgba(59,130,246,.7)';mapCtx.lineWidth=2;mapCtx.stroke()}}
mapCtx.restore();
}
function rMapInfo(){
const v=mapVS[mapVS.length-1];let sk,sn;
if(v.tp==='solar'){sk='solar_system';sn='Solar System'}else{sk=v.ck;const b=BODIES[v.ck];sn=b?b.n+(v.tp==='planet_system'?' System':''):v.ck}
$('mi-title').textContent=sn;$('mi-scope').textContent=sn;
const as=LOC_ASSETS.filter(a=>{
if(sk==='solar_system')return true;
const loc=a.l.toLowerCase();
if(sk==='earth')return loc.includes('leo')||loc.includes('earth');
if(sk==='mars')return loc.includes('mars');
if(sk==='jupiter')return loc.includes('jupiter')||loc.includes('europa');
if(sk==='saturn')return false;if(sk==='uranus')return false;if(sk==='neptune')return false;
return loc.includes(sk);
});
$('mi-assets').innerHTML=as.length?as.map(a=>`<div class="mi-arow"><span class="n">${a.n}</span><span class="loc">${a.l}</span></div>`).join(''):'<div style="color:var(--mut);font-size:.72rem">No assets</div>';
$('mi-miss').textContent=as.filter(a=>a.s==='transfer'||a.s==='exploration').length;
const ns=ST.toISOString().slice(0,10);
$('mi-events').innerHTML=LOC_EVENTS.filter(e=>e.d>=ns).slice(0,4).map(e=>`<div style="padding:2px 0;font-size:.7rem;border-bottom:1px solid var(--brd)"><span style="color:var(--amb);font-family:var(--fd);font-size:.64rem">${e.d}</span> ${e.t}</div>`).join('')||'<div style="color:var(--mut);font-size:.72rem">None</div>';
}
function updateMapBack(){$('bk-btn').classList.toggle('vis',mapVS.length>1)}
function mapGoBack(){if(mapVS.length>1)mapVS.pop();mapVO={x:0,y:0};mapZm=1;updateMapBack();renderMap();rMapInfo()}
// ═══ THEME ═══
function toggleTheme(){const cur=document.documentElement.getAttribute('data-theme'),next=cur==='dark'?'light':'dark';document.documentElement.setAttribute('data-theme',next);localStorage.setItem('ksp-theme',next);mapStars=null;if(mapCv)renderMap()}
(function(){const t=localStorage.getItem('ksp-theme');if(t)document.documentElement.setAttribute('data-theme',t)})();
// ═══ TIME ═══
function openTM(){$('tminp').value=`${ST.getUTCFullYear()}-${String(ST.getUTCMonth()+1).padStart(2,'0')}-${String(ST.getUTCDate()).padStart(2,'0')}T${String(ST.getUTCHours()).padStart(2,'0')}:${String(ST.getUTCMinutes()).padStart(2,'0')}`;$('tmodal').classList.add('open')}
function clTM(){$('tmodal').classList.remove('open')}
function apT(){const v=$('tminp').value;if(!v)return;const d=new Date(v+'Z');if(isNaN(d.getTime()))return;ST.setTime(d.getTime());uT();rO();rEts();clTM()}
function shT(d){ST.setTime(ST.getTime()+d*86400000);uT();rO();rEts()}
function rsT(){ST.setTime(new Date('2060-08-11T10:00:00Z').getTime());uT();rO();rEts()}
$('tmodal').addEventListener('click',function(e){if(e.target===this)clTM()});
document.addEventListener('keydown',e=>{if(e.key==='Escape'){clTM();clCRUD()}if(e.key==='t'&&e.ctrlKey){e.preventDefault();openTM()}});
// ═══ INIT ═══
uT();rO();rEts();rCv();
</script>
</body>
</html>