Files
gacha-event-tracker/design/mockups/round6/a-inline.html
T
Lucas WintherandClaude Opus 5.5 50c6e91b00 feat: the redesigned Event Clock
The new reader app replaces the old UI: Checklist with a rail (focus
pills, Next to expire, today's dailies, pinned routines), Timeline in a
new and a classic look, Routines, Archive and a Settings area, in a
Structured (default) and a Relaxed interface. It follows the approved
Round 6 B and Round 7 mockups, corrected screen by screen with the owner.

Reader data moves to a v2 store (gacha-tracker:v2:*). Existing v1 data
is migrated on first load, a recovery copy is written first, and the v1
keys are never removed, so a rollback finds them unchanged. Redeem codes
for Genshin arrive as their own feed.

Also: live's urgency ramp, editable per theme; completion and
section-complete fireworks; a returning-user tour; the design mockups,
specs, plans, reviews and handoff record.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 01:55:16 +02:00

1969 lines
289 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="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Event Clock · Design A</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Barlow:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
<style>
:root{
--ground:#0e1117; --panel:#141923; --raised:#1c2230; --rule:#232a38; --rule-strong:#323b4e;
--ink:#e4e7ef; --ink-strong:#fff; --muted:#8b93a7; --faint:#5d6579;
--calm:#55607f; --near:#5b8def; --soon:#e8b33c; --critical:#ff4d6a; --fill:#c9cedb;
--chrome-bg:#e9e4d4; --chrome-ink:#2a2a2a;
}
:root[data-theme="light"]{
--ground:#eef0f5; --panel:#ffffff; --raised:#e2e6ef; --rule:#d3d8e3; --rule-strong:#b9c0cf;
--ink:#151824; --ink-strong:#000; --muted:#525a70; --faint:#757d92;
--calm:#8a92a8; --near:#2b62c6; --soon:#8a6008; --critical:#c81e3c; --fill:#3a4152;
}
*{box-sizing:border-box}
html,body{margin:0}
body{background:var(--ground);color:var(--ink);font:400 14px/1.5 Barlow,system-ui,sans-serif;font-variant-numeric:tabular-nums;padding-top:40px;min-height:100vh;overflow-x:hidden}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;padding:0}
input,select,textarea{font:inherit;color:var(--ink);background:var(--ground);border:1px solid var(--rule-strong);border-radius:3px;padding:6px 8px;min-width:0}
:focus-visible{outline:2px solid var(--near);outline-offset:2px}
.cond{font-family:"Barlow Condensed",Barlow,sans-serif}
/* ---------- prototype chrome ---------- */
#chrome{position:fixed;inset:0 0 auto 0;height:40px;z-index:50;background:var(--chrome-bg);color:var(--chrome-ink);display:flex;align-items:center;gap:5px;padding:0 12px;font:500 12px/1 system-ui,sans-serif;border-bottom:2px dashed #b9b29a;overflow-x:auto;white-space:nowrap}
#chrome b{font-weight:700;margin-right:6px}
#chrome button{padding:5px 7px;border-radius:3px;border:1px solid #c4bda4;background:#f6f2e6;color:#2a2a2a;flex:none}
#chrome button[aria-pressed="true"]{background:#2a2a2a;color:#f6f2e6;border-color:#2a2a2a}
#chrome .sep{width:1px;height:20px;background:#c4bda4;margin:0 4px;flex:none}
#chrome .lbl{color:#6b6450;flex:none}
/* ---------- app shell ---------- */
.shell{max-width:1320px;margin:0 auto;border-inline:1px solid var(--rule);min-height:calc(100vh - 40px);display:flex;flex-direction:column}
.shell>.body{flex:1}
header.top{display:flex;align-items:flex-end;gap:28px;padding:18px 24px 0;border-bottom:1px solid var(--rule)}
.mark{font:700 20px/1 "Barlow Condensed";letter-spacing:.04em;padding-bottom:14px}
.mark span{color:var(--near)}
nav.tabs{display:flex;gap:2px;flex:1;min-width:0}
nav.tabs button{padding:10px 14px 12px;font:600 15px/1 "Barlow Condensed";letter-spacing:.03em;color:var(--faint);border-bottom:2px solid transparent;margin-bottom:-1px;display:flex;gap:6px;align-items:baseline;white-space:nowrap}
nav.tabs button:hover{color:var(--muted)}
nav.tabs button[aria-current="page"]{color:var(--ink);border-color:var(--ink)}
nav.tabs .n{font:500 12px Barlow;color:var(--faint)}
nav.tabs .gear{margin-left:auto}
nav.tabs svg{width:15px;height:15px;align-self:center}
footer.foot{border-top:1px solid var(--rule);padding:16px 24px;display:flex;gap:18px;flex-wrap:wrap;font-size:12px;color:var(--faint)}
footer.foot button{color:var(--muted);text-decoration:underline;text-underline-offset:3px;text-decoration-color:var(--rule-strong)}
footer.foot button:hover{color:var(--ink)}
/* ---------- what's new ---------- */
.wn{border-bottom:1px solid var(--rule);padding:16px 24px 14px;background:color-mix(in srgb,var(--near) 6%,transparent)}
.wn-h{display:flex;align-items:baseline;gap:10px;margin-bottom:8px;flex-wrap:wrap}
.wn-h .k{font:600 13px "Barlow Condensed";letter-spacing:.1em;text-transform:uppercase;color:var(--near)}
.wn-h .d{font-size:12px;color:var(--faint)}
.wn-h h3{font:600 19px/1.15 "Barlow Condensed";margin:0;flex-basis:100%;letter-spacing:.01em}
.wn ul{list-style:none;margin:0 0 10px;padding:0}
.wn li{display:grid;grid-template-columns:88px 1fr;gap:10px;padding:3px 0;font-size:13px;color:var(--muted)}
.wn li .kind{color:var(--faint);font-size:12px}
.wn .acts{display:flex;gap:6px;align-items:center}
.pillbtn{font-size:12px;font-weight:600;padding:5px 12px;border-radius:3px;border:1px solid color-mix(in srgb,var(--near) 55%,transparent);color:var(--near);background:color-mix(in srgb,var(--near) 12%,transparent)}
.pillbtn:hover{background:color-mix(in srgb,var(--near) 20%,transparent)}
/* ---------- checklist ---------- */
.ev{display:grid;grid-template-columns:340px 1fr;min-height:calc(100vh - 160px)}
.rail{border-right:1px solid var(--rule);min-width:0}
.head{padding:22px 24px 20px;border-bottom:1px solid var(--rule)}
.legend{font-size:12px;color:var(--faint);margin:0 0 6px}
.head h2{font:600 30px/1.05 "Barlow Condensed";margin:0 0 6px;letter-spacing:.005em}
.head .game{font-weight:500;font-size:14px}
.big{font:700 64px/1 "Barlow Condensed";letter-spacing:-.01em;margin:16px 0 4px}
.big small{font-size:18px;font-weight:600;color:var(--muted);margin-left:6px;letter-spacing:0}
.exact{font-size:12px;color:var(--faint);margin-bottom:12px}
.meter{display:flex;gap:2px;height:9px}
.meter i{flex:1;background:var(--rule);border-radius:1px}
.meter i.on{background:var(--h)}
.meter.unknown{background:repeating-linear-gradient(135deg,var(--rule) 0 3px,transparent 3px 6px)}
.meter.unknown i{background:transparent}
.then{list-style:none;margin:16px 0 0;padding:12px 0 0;border-top:1px solid var(--rule)}
.then li{display:flex;gap:8px;align-items:baseline;font-size:13px;padding:3px 0;color:var(--muted)}
.then .dot{width:7px;height:7px;border-radius:50%;background:var(--g);flex:none;transform:translateY(-1px)}
.then .t{flex:1}
.then .r{font:600 14px "Barlow Condensed"}
.pinned{padding:18px 0 8px}
.pinned-h{display:flex;justify-content:space-between;align-items:baseline;padding:0 24px 8px;gap:10px}
.pinned-h .lbl{font:600 16px "Barlow Condensed";letter-spacing:.02em}
.pinned-h .lbl small{font:500 12px Barlow;color:var(--faint);margin-left:6px;letter-spacing:0}
.pinned-h button{font-size:12px;color:var(--near)}
.pinned .hint{padding:0 24px 8px;font-size:12px;color:var(--faint);margin:0}
.gblock{border-top:1px solid var(--rule)}
.gblock-h{display:grid;grid-template-columns:3px 1fr auto;gap:0 12px;align-items:center;width:100%;text-align:left;padding:10px 24px}
.gblock-h:hover{background:var(--panel)}
.gblock-h .rl{width:3px;height:30px;border-radius:2px;background:var(--g)}
.gblock-h .nm{font:600 15px/1.2 "Barlow Condensed";letter-spacing:.02em;color:var(--g)}
.gblock-h .nm .srv{color:var(--muted);font-weight:500}
.gblock-h .sm{font-size:12px;color:var(--muted);line-height:1.35}
.gblock-h .sm b{font-weight:600;color:var(--soon)}
.gblock-h .chev{color:var(--faint);transition:transform .14s}
.gblock.open .gblock-h .chev{transform:rotate(180deg)}
.gitems{padding:0 24px 10px 39px}
.gitem{display:flex;align-items:center;gap:10px;padding:6px 0;border-top:1px solid var(--rule);font-size:13px}
.gitem:first-child{border-top:0}
.gitem .t{flex:1;min-width:0;line-height:1.3}
.gitem .t small{display:block;color:var(--faint);font-size:11.5px}
.gitem.met .t{color:var(--faint)}
.mini-amt{display:flex;align-items:center;gap:5px;font:600 13px "Barlow Condensed";color:var(--faint);white-space:nowrap}
.mini-amt input{width:74px;padding:3px 6px;text-align:right;font:600 13px "Barlow Condensed"}
.rail-foot{display:flex;justify-content:space-between;padding:12px 24px 6px;font-size:12px;color:var(--faint);border-top:1px solid var(--rule)}
.rail-foot button{color:var(--near)}
.pinned .empty{padding:4px 24px 12px;font-size:13px;color:var(--muted);margin:0}
.main{min-width:0}
.btn{padding:7px 12px;border:1px solid var(--rule-strong);border-radius:3px;font-weight:500;font-size:13px;color:var(--muted);white-space:nowrap}
.btn:hover{color:var(--ink);border-color:var(--muted)}
.btn.solid{background:var(--ink);color:var(--ground);border-color:var(--ink)}
.btn.solid:hover{background:var(--ink-strong)}
.btn.link{border:0;padding:7px 4px;color:var(--near)}
.btn.sm{padding:3px 8px;font-size:12px}
.btn[disabled]{opacity:.4;cursor:default}
.ledger-h{display:flex;justify-content:space-between;align-items:baseline;padding:20px 24px 6px;gap:12px;flex-wrap:wrap}
.ledger-h h2{font:600 22px "Barlow Condensed";margin:0;letter-spacing:.02em}
.ledger-h .sub{font-size:12px;color:var(--faint)}
.focusrow{display:flex;gap:2px 16px;flex-wrap:wrap;padding:6px 24px 0;border-bottom:1px solid var(--rule-strong)}
.focusrow button{font:600 14px "Barlow Condensed";letter-spacing:.03em;color:var(--faint);padding:8px 0 9px;border-bottom:2px solid transparent;margin-bottom:-1px}
.focusrow button:hover{color:var(--muted)}
.focusrow button[aria-pressed="true"]{color:var(--ink);border-color:var(--fh,var(--ink))}
.focusrow .n{font:500 11px Barlow;color:var(--faint);margin-left:4px}
.seg{display:inline-flex;border:1px solid var(--rule-strong);border-radius:3px;flex:none}
.seg button{padding:5px 10px;font-size:12px;color:var(--faint);white-space:nowrap}
.seg button[aria-pressed="true"]{background:var(--raised);color:var(--ink)}
table.led{width:100%;border-collapse:collapse}
table.led th{font:500 12px Barlow;color:var(--faint);text-align:left;padding:6px 12px;border-bottom:1px solid var(--rule-strong);white-space:nowrap}
table.led td{padding:12px 12px;border-bottom:1px solid var(--rule);vertical-align:middle}
table.led th:first-child,table.led td:first-child{padding-left:24px}
table.led th:last-child,table.led td:last-child{padding-right:24px}
table.led tbody tr{cursor:pointer}
table.led tbody tr:hover{background:var(--panel)}
.gcell{display:flex;align-items:center;gap:10px;white-space:nowrap;font-weight:600;font-size:13px;color:var(--g)}
.gcell::before{content:"";width:3px;height:26px;border-radius:2px;background:var(--g);flex:none}
.gcell small{display:block;color:var(--muted);font-weight:500;font-size:11.5px;line-height:1}
.tcell{font-weight:500;font-size:15px}
.tag{font-size:11px;color:var(--faint);border:1px solid var(--rule-strong);border-radius:2px;padding:0 5px;margin-left:8px;font-weight:500;vertical-align:2px;white-space:nowrap}
.tag.own{color:var(--g);border-color:color-mix(in srgb,var(--g) 45%,transparent)}
.mcell{width:34%}
.cd{font:600 20px "Barlow Condensed";text-align:right;white-space:nowrap}
.cd.calm{color:var(--calm)}.cd.near{color:var(--near)}.cd.soon{color:var(--soon)}.cd.critical{color:var(--critical)}.cd.unk{color:var(--faint);font-size:14px}
.more{padding:12px 24px;font-size:13px;color:var(--muted)}
.more button{color:var(--near)}
/* ---------- routines ---------- */
.rt-top{display:flex;align-items:flex-end;gap:16px;padding:22px 24px 16px;border-bottom:1px solid var(--rule);flex-wrap:wrap}
.rt-top h1{font:700 34px/1 "Barlow Condensed";margin:0;letter-spacing:.01em}
.rt-top .sum{font-size:13px;color:var(--muted);flex:1;min-width:200px}
.rt-top .sum b{color:var(--ink);font-weight:600}
.hintbar{display:flex;gap:16px;align-items:center;flex-wrap:wrap;padding:14px 24px;border-bottom:1px solid var(--rule);background:color-mix(in srgb,var(--near) 7%,transparent);font-size:13px}
.hintbar p{margin:0;flex:1;min-width:240px;color:var(--muted)}
.hintbar p b{color:var(--ink);font-weight:600}
.hintbar svg{width:14px;height:14px;vertical-align:-2px;color:var(--near)}
.band{border-bottom:1px solid var(--rule)}
.band-h{display:grid;grid-template-columns:1fr auto;align-items:end;padding:22px 24px 10px;gap:12px;border-bottom:1px solid var(--rule-strong)}
.band-h h2{font:700 26px/1 "Barlow Condensed";margin:0;letter-spacing:.02em}
.band-h .meta{font-size:12px;color:var(--faint);margin-top:4px}
.bulkpins{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap}
.bulkpins button{font-size:12px;color:var(--muted);border:1px solid var(--rule-strong);border-radius:2px;padding:2px 8px}
.bulkpins button:hover{color:var(--ink);border-color:var(--muted)}
.band-h .reset{text-align:right}
.band-h .reset .k{font-size:12px;color:var(--faint)}
.band-h .reset .v{font:700 34px/1 "Barlow Condensed"}
.subsrv{padding:10px 24px 2px;font:600 13px "Barlow Condensed";letter-spacing:.08em;text-transform:uppercase;color:var(--faint)}
table.rt{width:100%;border-collapse:collapse;table-layout:fixed}
table.rt th{font:500 11px Barlow;color:var(--faint);text-align:left;padding:6px 12px 4px;letter-spacing:.02em}
table.rt td{padding:10px 12px;border-bottom:1px solid var(--rule);vertical-align:middle}
table.rt td:first-child,table.rt th:first-child{padding-left:24px;width:140px}
table.rt .c-cyc{width:130px;text-align:right}
table.rt .c-pin{width:78px;text-align:center}
table.rt .c-k{width:52px;text-align:right;padding-right:24px}
table.rt tr:last-child td{border-bottom:0}
.rtitle{font-weight:500;font-size:15px}
.prog{width:30%}
.amt{display:flex;align-items:center;gap:12px;width:100%;text-align:left}
.amt .bar{flex:1;height:6px;background:var(--rule);position:relative;border-radius:1px;overflow:hidden}
.amt .bar i{position:absolute;inset:0 auto 0 0;background:var(--fill)}
.amt .bar.full i{background:var(--g)}
.amt .num{font:600 15px "Barlow Condensed";min-width:110px;text-align:right;color:var(--muted)}
.amt .num b{color:var(--ink);font-weight:600}
.amt input{width:90px;padding:3px 6px}
.tick{display:inline-flex;align-items:center;gap:8px;font-size:13px;color:var(--muted)}
.tick .box{width:18px;height:18px;border:1px solid var(--rule-strong);border-radius:3px;display:grid;place-items:center;flex:none}
.tick[aria-pressed="true"]{color:var(--ink)}
.tick[aria-pressed="true"] .box{background:var(--g);border-color:var(--g)}
.tick svg{width:12px;height:12px;stroke:var(--ground);stroke-width:3;fill:none;opacity:0}
.tick[aria-pressed="true"] svg{opacity:1}
.tick .part{font:600 12px "Barlow Condensed";color:var(--muted)}
.cyc{font:600 14px "Barlow Condensed";color:var(--muted);white-space:nowrap}
.pin{width:34px;height:28px;border-radius:3px;display:inline-grid;place-items:center;color:var(--faint);border:1px solid transparent}
.pin svg{width:16px;height:16px}
.pin:hover{color:var(--ink);border-color:var(--rule-strong)}
.pin[aria-pressed="true"]{color:var(--near);background:color-mix(in srgb,var(--near) 15%,transparent);border-color:color-mix(in srgb,var(--near) 45%,transparent)}
.pinlbl{display:inline-flex;align-items:center;gap:4px}
.pinlbl svg{width:12px;height:12px}
.kebab{width:28px;height:28px;border-radius:3px;display:inline-grid;place-items:center;color:var(--faint);position:relative}
.kebab:hover{background:var(--raised);color:var(--ink)}
.menu{position:absolute;right:0;top:30px;z-index:20;background:var(--panel);border:1px solid var(--rule-strong);border-radius:4px;min-width:200px;text-align:left;padding:4px;box-shadow:0 8px 24px rgb(0 0 0 / .35)}
.menu span{display:block;width:100%;text-align:left;padding:7px 10px;font-size:13px;color:var(--muted);border-radius:3px;cursor:pointer}
.menu span:hover{background:var(--raised);color:var(--ink)}
.menu span.danger:hover{color:var(--critical)}
tr.met .rtitle{color:var(--muted)}
.empty-band{padding:14px 24px;font-size:13px;color:var(--faint)}
.empty-state{padding:72px 24px;max-width:560px}
.empty-state h2{font:700 30px/1.1 "Barlow Condensed";margin:0 0 10px}
.empty-state p{color:var(--muted);margin:0 0 20px;max-width:46ch}
/* ---------- drawer: library + create ---------- */
.scrim{position:fixed;inset:40px 0 0 0;background:rgb(8 10 14 / .6);z-index:30}
:root[data-theme="light"] .scrim{background:rgb(26 31 48 / .35)}
.drawer{position:fixed;top:40px;right:0;bottom:0;width:min(580px,100vw);background:var(--panel);border-left:1px solid var(--rule-strong);z-index:31;display:flex;flex-direction:column}
.drawer-h{padding:20px 24px 14px;border-bottom:1px solid var(--rule)}
.drawer-h .r1{display:flex;justify-content:space-between;align-items:center;gap:12px}
.drawer-h h2{font:700 28px/1 "Barlow Condensed";margin:0}
.drawer-h p{margin:6px 0 12px;font-size:13px;color:var(--muted)}
.drawer-h input[type=search]{width:100%}
.drawer-h .newpin{display:flex;gap:14px;align-items:center;font-size:12px;color:var(--faint);margin-top:10px;flex-wrap:wrap}
.drawer-h .newpin label{display:flex;gap:6px;align-items:center;color:var(--muted)}
.drawer-h input[type=checkbox]{accent-color:var(--near)}
.back{font-size:13px;color:var(--muted);display:inline-flex;gap:4px;align-items:center}
.back:hover{color:var(--ink)}
.close{width:32px;height:32px;display:grid;place-items:center;border-radius:3px;color:var(--muted);flex:none}
.close:hover{background:var(--raised);color:var(--ink)}
.drawer-b{overflow:auto;flex:1}
.mkown{display:grid;grid-template-columns:auto 1fr auto;gap:14px;align-items:center;width:100%;text-align:left;margin:14px 0 0;padding:14px 16px;border:1px dashed color-mix(in srgb,var(--near) 60%,transparent);border-radius:4px;background:color-mix(in srgb,var(--near) 7%,transparent)}
.mkown:hover{background:color-mix(in srgb,var(--near) 13%,transparent);border-style:solid}
.mkown .plus{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;background:var(--near);color:var(--ground)}
.mkown .plus svg{width:16px;height:16px}
.mkown b{display:block;font:700 18px/1.1 "Barlow Condensed";letter-spacing:.02em}
.mkown small{display:block;font-size:12px;color:var(--muted)}
.mkown .go{color:var(--near)}
.lg{border-bottom:1px solid var(--rule)}
.lg-h{display:flex;align-items:center;gap:10px;padding:16px 24px 8px;flex-wrap:wrap}
.lg-h .nm{font:700 18px "Barlow Condensed";color:var(--g);letter-spacing:.02em;flex:1}
.lg-h .cnt{font-size:12px;color:var(--faint)}
.bulk{display:flex;gap:6px;padding:0 24px 10px;flex-wrap:wrap}
.bulk button{font-size:12px;color:var(--muted);border:1px solid var(--rule-strong);border-radius:2px;padding:3px 8px}
.bulk button:hover{color:var(--ink);border-color:var(--muted)}
.li{display:grid;grid-template-columns:1fr auto;gap:12px;padding:9px 24px;border-top:1px solid var(--rule);align-items:center}
.li .t{font-weight:500}
.li .d{font-size:12px;color:var(--faint)}
.fols{display:flex;gap:4px}
.fol{font-size:12px;font-weight:600;padding:5px 10px;border-radius:3px;border:1px solid var(--rule-strong);color:var(--muted);min-width:92px;white-space:nowrap}
.fols .fol{min-width:0}
.fol:hover{color:var(--ink);border-color:var(--muted)}
.fol[aria-pressed="true"]{background:color-mix(in srgb,var(--g) 16%,transparent);border-color:color-mix(in srgb,var(--g) 60%,transparent);color:var(--ink)}
.others summary{padding:14px 24px;cursor:pointer;color:var(--muted);font-size:13px}
.cf{padding:18px 24px 32px;display:grid;grid-template-columns:1fr 1fr;gap:16px 14px}
.cf .full{grid-column:1/-1}
.cf .fld{display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--faint);min-width:0}
.cf .fld>span:first-child{font:600 13px "Barlow Condensed";letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.cf .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;color:var(--muted);font-size:13px}
.cf .row input[type=number]{width:84px}
.cf .row input[type=date]{width:150px}
.cf .row input.unit{width:120px}
.cf textarea{min-height:60px;resize:vertical}
.cf .help{font-size:12px;color:var(--faint)}
.pills{display:flex;gap:4px;flex-wrap:wrap}
.pills button{font-size:12px;padding:5px 9px;border:1px solid var(--rule-strong);border-radius:2px;color:var(--muted)}
.pills button:hover{color:var(--ink)}
.pills button[aria-pressed="true"]{border-color:var(--near);color:var(--ink);background:color-mix(in srgb,var(--near) 14%,transparent)}
.preview{grid-column:1/-1;border:1px solid var(--rule-strong);border-left:3px solid var(--g);border-radius:3px;padding:12px 14px;background:var(--ground)}
.preview .k{font-size:11px;color:var(--faint);letter-spacing:.06em;text-transform:uppercase}
.preview .t{font:600 18px/1.2 "Barlow Condensed";letter-spacing:.01em;margin:2px 0}
.preview .l{font-size:13px;color:var(--muted)}
.preview .l b{color:var(--ink);font-weight:600}
.cf-foot{grid-column:1/-1;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.chk{display:flex;gap:8px;align-items:center;cursor:pointer;color:var(--muted);font-size:13px}
.chk input{accent-color:var(--near)}
.chk.block{align-items:flex-start;padding:6px 0}
.chk.block input{margin-top:3px}
.chk small{display:block;color:var(--faint);font-size:12px}
/* ---------- settings ---------- */
.settings{display:grid;grid-template-columns:300px 1fr;min-height:calc(100vh - 160px)}
.slist{border-right:1px solid var(--rule);padding:10px 0 30px}
.slist h4{font:600 13px/1 "Barlow Condensed";letter-spacing:.08em;text-transform:uppercase;color:var(--faint);margin:18px 24px 6px}
.srow{display:grid;grid-template-columns:3px 1fr auto;gap:0 12px;align-items:center;width:100%;text-align:left;padding:9px 24px}
.srow:hover{background:var(--panel)}
.srow[aria-current="true"]{background:var(--raised)}
.srow .r3{width:3px;height:30px;border-radius:2px;background:var(--g,transparent)}
.srow.gen .r3{height:18px}
.srow .nm{font-size:14px;font-weight:600;line-height:1.3;display:block}
.srow .what{font-size:12px;color:var(--faint);line-height:1.3;display:block}
.srow.off .nm{color:var(--muted)}
.srow.off .r3{opacity:.25}
.srow svg{width:14px;height:14px;color:var(--faint)}
.slist .moreg{font-size:12px;color:var(--faint);padding:8px 24px}
.slist .moreg:hover{color:var(--ink)}
.profile{padding:22px 32px 64px;max-width:820px;min-width:0}
.ptitle{display:flex;align-items:flex-end;gap:14px;flex-wrap:wrap;padding-bottom:14px;border-bottom:1px solid var(--rule-strong)}
.ptitle h2{font:700 34px/1 "Barlow Condensed";margin:0;color:var(--g,var(--ink));letter-spacing:.01em}
.ptitle .studio{font-size:12px;color:var(--faint);padding-bottom:3px}
.switch{margin-left:auto;display:flex;align-items:center;gap:10px;font-size:13px;color:var(--muted)}
.sw{width:34px;height:20px;border-radius:10px;background:var(--rule-strong);position:relative;transition:background .14s;flex:none}
.sw::after{content:"";position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:var(--muted);transition:transform .14s,background .14s}
.sw[aria-checked="true"]{background:color-mix(in srgb,var(--g,var(--near)) 70%,var(--ground))}
.sw[aria-checked="true"]::after{transform:translateX(14px);background:var(--ink-strong)}
.pdesc{color:var(--muted);font-size:13.5px;margin:14px 0 0;max-width:62ch}
.comp{margin:14px 0 8px;display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
.comp button{padding:12px 12px 11px;text-align:left;display:flex;flex-direction:column;align-items:flex-start;justify-content:flex-start;border:1px solid var(--rule-strong);border-radius:3px;position:relative}
.comp button:hover{border-color:var(--muted)}
.comp button[aria-pressed="true"]{background:color-mix(in srgb,var(--g) 14%,transparent);border-color:color-mix(in srgb,var(--g) 70%,transparent)}
.comp button[data-st="some"]{border-style:dashed;border-color:color-mix(in srgb,var(--g) 70%,transparent)}
.comp button[disabled]{opacity:.4;cursor:default}
.comp .cbx{position:absolute;top:12px;right:12px;width:16px;height:16px;border-radius:3px;border:1px solid var(--rule-strong);display:grid;place-items:center}
.comp button[aria-pressed="true"] .cbx{background:var(--g);border-color:var(--g)}
.comp button[data-st="some"] .cbx::after{content:"";width:8px;height:2px;background:var(--g)}
.comp .cbx svg{width:11px;height:11px;stroke:var(--ground);stroke-width:3;fill:none}
.comp .ln{font:600 17px/1.1 "Barlow Condensed";letter-spacing:.02em;display:block;padding-right:22px}
.comp .ls{font-size:11.5px;color:var(--faint);display:block;margin-top:3px;line-height:1.3}
.compnote{font-size:12px;color:var(--faint);display:flex;gap:14px;align-items:center;flex-wrap:wrap}
.compnote button{color:var(--near)}
.grp{margin-top:30px}
.grp h3{display:flex;gap:10px;align-items:baseline;font:700 18px "Barlow Condensed";letter-spacing:.02em;margin:0;padding-bottom:6px;border-bottom:1px solid var(--rule-strong);flex-wrap:wrap}
.grp h3 span{font:400 12px Barlow;color:var(--faint);letter-spacing:0}
.trow{display:grid;grid-template-columns:1fr auto;gap:12px;align-items:center;padding:10px 0;border-bottom:1px solid var(--rule)}
.trow .lbl{font-weight:500;line-height:1.3;min-width:0}
.trow .lbl small{display:block;color:var(--faint);font-size:12px;font-weight:400}
.srvrow{display:grid;grid-template-columns:auto 1fr auto;gap:14px;align-items:center;padding:12px 0;border-bottom:1px solid var(--rule)}
.srvrow .tagn{font:600 12px "Barlow Condensed";letter-spacing:.06em;color:var(--faint);text-transform:uppercase;width:58px}
.srvrow .clock{font-size:13px;color:var(--muted);line-height:1.45}
.srvrow .clock b{color:var(--ink);font-weight:600}
.srvrow .clock .nx{font:600 14px "Barlow Condensed";color:var(--ink)}
.srvrow .r{display:flex;gap:8px;align-items:center}
.about{display:grid;grid-template-columns:1fr 1fr;gap:10px 24px;margin-top:10px;font-size:13px}
.about .k{color:var(--faint);font-size:12px}
.about a{color:var(--near)}
.soon-note{margin-top:12px;font-size:12px;color:var(--faint);border:1px dashed var(--rule-strong);border-radius:3px;padding:10px 12px}
.viewpick{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:14px}
.vcard{border:1px solid var(--rule-strong);border-radius:4px;padding:14px;text-align:left}
.vcard:hover{border-color:var(--muted)}
.vcard[aria-pressed="true"]{border-color:var(--near);background:color-mix(in srgb,var(--near) 8%,transparent)}
.vcard b{display:block;font:700 20px "Barlow Condensed";letter-spacing:.02em;margin-top:10px}
.vcard small{color:var(--faint);font-size:12px}
.vprev{height:62px;display:flex;flex-direction:column;gap:6px;justify-content:center}
.vprev.list i{display:block;height:7px;border-radius:1px;background:linear-gradient(90deg,var(--soon) 0 18%,var(--rule) 18%)}
.vprev.list i:nth-child(2){background:linear-gradient(90deg,var(--near) 0 46%,var(--rule) 46%);width:86%}
.vprev.list i:nth-child(3){background:linear-gradient(90deg,var(--calm) 0 70%,var(--rule) 70%);width:72%}
.vprev.tl{position:relative}
.vprev.tl i{position:absolute;height:12px;border-radius:2px;border-left:3px solid var(--g);background:color-mix(in srgb,var(--g) 30%,transparent)}
.vprev.tl em{position:absolute;top:0;bottom:0;left:46%;width:1px;background:var(--critical)}
/* ---------- archive (three variants) ---------- */
.filters{display:flex;gap:12px;padding:14px 24px;align-items:center;flex-wrap:wrap;border-bottom:1px solid var(--rule-strong)}
.month{padding:18px 24px 6px;font:700 20px "Barlow Condensed";letter-spacing:.02em;border-bottom:1px solid var(--rule-strong);display:flex;gap:10px;align-items:baseline;flex-wrap:wrap}
.month span{font:400 12px Barlow;color:var(--faint)}
.st{font-size:12px;font-weight:500;padding:1px 7px;border-radius:2px;white-space:nowrap}
.st.done{color:var(--near);background:color-mix(in srgb,var(--near) 15%,transparent)}
.st.ignored{color:var(--muted);border:1px dashed var(--rule-strong)}
.st.expired{color:var(--critical);background:color-mix(in srgb,var(--critical) 12%,transparent)}
.amark{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;flex:none}
.amark svg{width:13px;height:13px}
.amark.done{background:var(--g);color:var(--ground)}
.amark.ignored{border:1px dashed var(--faint);color:var(--faint)}
.amark.expired{border:1.5px solid var(--critical);color:var(--critical)}
.ach{font-size:12.5px;color:var(--muted)}
.ach.done{color:var(--ink)}
.ach.expired{color:var(--critical)}
.ach.ignored{color:var(--faint)}
.stack{display:flex;height:6px;border-radius:1px;overflow:hidden;background:var(--rule);flex:1;min-width:40px}
.stack.mini{flex:none;width:90px;height:4px;align-self:center}
.stack i{display:block;height:100%}
.s-done{background:var(--near)}
.s-expired{background:var(--critical)}
.s-ignored{background:var(--rule-strong)}
/* progress glyphs */
.pips{display:flex;gap:2px;flex-wrap:wrap;max-width:190px}
.pips i{width:6px;height:10px;border-radius:1px;background:var(--rule)}
.pips i.on{background:var(--g)}
.pips.expired i:not(.on){background:transparent;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--critical) 55%,transparent)}
.pips.ignored i.on{background:var(--faint)}
.pips.big{max-width:none;gap:4px}
.pips.big i{width:auto;flex:1;min-width:8px;max-width:30px;height:22px;border-radius:2px}
.pips.stages.big i{max-width:44px}
.pips-ax{display:flex;justify-content:space-between;gap:8px;font-size:11.5px;color:var(--faint);margin-top:6px}
.abar{width:150px;height:8px;border-radius:1px;background:var(--rule);position:relative;overflow:hidden}
.abar i{position:absolute;inset:0 auto 0 0;width:var(--f);background:var(--g)}
.abar.expired{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--critical) 55%,transparent)}
.abar.big{width:100%;height:20px;border-radius:2px}
.keepnote{font-size:12px;color:var(--faint);margin:10px 0 0}
.sidep-viz{margin:14px 0 4px}
/* 1 · ledger */
.v1-stats{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr);border-bottom:1px solid var(--rule-strong)}
.v1-figs{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border-right:1px solid var(--rule)}
.v1-figs>div{padding:16px 20px;border-right:1px solid var(--rule);display:flex;flex-direction:column}
.v1-figs>div:last-child{border-right:0}
.v1-figs b{font:700 34px/1 "Barlow Condensed";color:var(--ink)}
.v1-figs b small{font-size:20px;color:var(--muted);margin-left:1px}
.v1-figs span{font-size:12.5px;color:var(--muted);margin-top:4px}
.v1-figs em{font-style:normal;font-size:11.5px;color:var(--faint);margin-top:2px}
.v1-figs .warn b{color:var(--critical)}
.v1-games{padding:12px 20px;display:flex;flex-direction:column;gap:5px}
.v1-g{display:grid;grid-template-columns:72px 1fr 34px;gap:10px;align-items:center;text-align:left;padding:2px 4px;border-radius:2px}
.v1-g:hover{background:var(--panel)}
.v1-g[aria-pressed="true"]{background:var(--raised)}
.v1-g .nm{font-weight:600;font-size:12.5px;color:var(--g)}
.v1-g .ct{font-size:12px;color:var(--muted);text-align:right}
.v1-key{margin:4px 0 0;font-size:11px;color:var(--faint)}
.v1-key i{display:inline-block;width:8px;height:8px;border-radius:1px;margin:0 3px 0 6px;vertical-align:-1px}
.v1-key i:first-child{margin-left:0}
.v1row{display:grid;grid-template-columns:22px 92px minmax(0,1fr) 210px 110px;gap:14px;align-items:center;width:100%;text-align:left;padding:11px 24px;border-bottom:1px solid var(--rule);position:relative}
.v1row:hover{background:var(--panel)}
.v1row .tt{min-width:0;display:flex;flex-direction:column}
.v1row .ti{font-weight:500;font-size:15px}
.v1row .v1-res{display:flex;align-items:center;gap:10px;justify-content:flex-end}
.v1row .v1-res b{font-size:17px;font-weight:700;min-width:62px;text-align:right}
.v1row .when{font-size:12px;color:var(--faint);white-space:nowrap;text-align:right}
.v1row.done .v1-res b{color:var(--ink)}
.v1row.expired{background:color-mix(in srgb,var(--critical) 5%,transparent);box-shadow:inset 3px 0 0 var(--critical)}
.v1row.expired .v1-res b{color:var(--critical)}
.v1row.ignored .ti{color:var(--muted);text-decoration:line-through;text-decoration-color:var(--faint)}
.v1row.ignored .gcell{opacity:.5}
.v1row.ignored{color:var(--muted)}
.gcell small{font-size:11px}
/* 2 · by game and version */
.v2{display:grid;grid-template-columns:260px minmax(0,1fr);min-height:60vh}
.v2-side{border-right:1px solid var(--rule-strong);padding:10px 0;display:flex;flex-direction:column}
.v2-si{display:grid;gap:3px;text-align:left;padding:10px 20px;border-left:3px solid transparent}
.v2-si:hover{background:var(--panel)}
.v2-si[aria-pressed="true"]{background:var(--raised);border-left-color:var(--g,var(--ink))}
.v2-si .nm{font-weight:600;font-size:14px}
.v2-si small{font-size:12px;color:var(--faint)}
.v2-si .stack.mini{width:100%;margin-top:3px}
.v2-main{min-width:0;padding-bottom:30px}
.v2-gh{font:700 24px "Barlow Condensed";margin:0;padding:20px 24px 4px;color:var(--g)}
.v2-ver{margin:14px 24px 6px;border:1px solid var(--rule-strong);border-radius:4px;overflow:hidden}
.v2-vh{display:flex;gap:14px;align-items:baseline;padding:12px 16px 8px;flex-wrap:wrap}
.v2-vn{font:700 22px/1 "Barlow Condensed";color:var(--g)}
.v2-vs{font-size:13px;color:var(--muted)}
.v2-vs b{color:var(--ink);font-weight:600}
.v2-vd{margin-left:auto;font-size:12px;color:var(--faint)}
.v2-meter{display:flex;gap:3px;padding:0 16px 12px}
.v2-meter i{flex:1;height:10px;border-radius:1px;max-width:40px}
.v2-meter i.done{background:var(--g)}
.v2-meter i.expired{box-shadow:inset 0 0 0 1.5px var(--critical)}
.v2-meter i.ignored{background:repeating-linear-gradient(135deg,var(--rule-strong) 0 2px,transparent 2px 5px)}
.v2row{display:grid;grid-template-columns:22px minmax(0,1fr) auto 104px 16px;gap:12px;align-items:center;width:100%;text-align:left;padding:9px 16px;border-top:1px solid var(--rule)}
.v2row:hover{background:var(--panel)}
.v2row .ti{font-weight:500;font-size:14px}
.v2row.ignored .ti{color:var(--muted);text-decoration:line-through;text-decoration-color:var(--faint)}
.v2row .when{font-size:12px;color:var(--faint);text-align:right;white-space:nowrap}
.v2row .go{color:var(--faint)}
.v2-page{max-width:820px;padding:20px 24px 50px}
.v2-back{display:inline-flex;gap:4px;align-items:center;margin-bottom:16px;color:var(--muted)}
.v2-title{font:700 40px/1.02 "Barlow Condensed";margin:4px 0 6px}
.v2-big{font:600 18px "Barlow Condensed";letter-spacing:.01em;margin:0 0 18px}
.v2-strip{border:1px solid var(--rule-strong);border-radius:4px;padding:12px 14px 10px;margin-bottom:18px}
.v2-axis{display:flex;justify-content:space-between;font-size:12px;color:var(--faint);margin-bottom:8px}
.v2-track{position:relative;height:30px;background:var(--panel);border-radius:2px}
.v2-track .sib{position:absolute;top:21px;height:5px;border-radius:1px;background:color-mix(in srgb,var(--g) 30%,transparent)}
.v2-track .sib.expired{background:color-mix(in srgb,var(--critical) 40%,transparent)}
.v2-track .sib.ignored{background:var(--rule-strong)}
.v2-track .me{position:absolute;top:3px;height:16px;border-radius:2px;background:var(--g);display:flex;align-items:center;overflow:hidden}
.v2-track .me.expired{background:transparent;box-shadow:inset 0 0 0 1.5px var(--critical)}
.v2-track .me.ignored{background:transparent;box-shadow:inset 0 0 0 1px var(--faint)}
.v2-track .me span{font-size:11px;font-weight:600;color:var(--ground);padding:0 6px;white-space:nowrap}
.v2-track .me.expired span,.v2-track .me.ignored span{color:var(--muted)}
.v2-track em{position:absolute;top:-4px;bottom:-4px;width:1px;background:var(--critical)}
.v2-cap{font-size:11.5px;color:var(--faint);margin:8px 0 0}
.v2-h3{font:600 13px "Barlow Condensed";letter-spacing:.1em;text-transform:uppercase;color:var(--faint);margin:0 0 8px}
.v2-viz{margin-bottom:6px}
.v2-page .desc{font-size:15px;line-height:1.6;margin:16px 0}
.v2-page .acts{display:flex;gap:8px;margin-top:18px;flex-wrap:wrap}
/* 3 · history */
.v3-key{font-size:11.5px;color:var(--faint);display:flex;align-items:center;gap:4px;margin-left:auto}
.v3-key .k{display:inline-block;width:18px;height:8px;border-radius:1px;margin-left:8px}
.v3-key .k.done{background:var(--near)}
.v3-key .k.expired{box-shadow:inset 0 0 0 1.5px var(--critical);background:repeating-linear-gradient(135deg,color-mix(in srgb,var(--critical) 30%,transparent) 0 2px,transparent 2px 5px)}
.v3-key .k.ignored{box-shadow:inset 0 0 0 1px var(--faint)}
.v3-board{padding:6px 24px 14px;border-bottom:1px solid var(--rule-strong)}
.v3-axis,.v3-lane,.v3-dens{display:grid;grid-template-columns:76px minmax(0,1fr)}
.v3-ln{font-size:12px;font-weight:600;color:var(--g,var(--faint));padding-top:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.v3-lt{position:relative}
.v3-axis .v3-lt{height:26px;border-bottom:1px solid var(--rule-strong)}
.v3-axis .wk{position:absolute;bottom:0;height:6px;width:1px;background:var(--rule-strong)}
.v3-axis .mo{position:absolute;top:4px;font:600 13px "Barlow Condensed";letter-spacing:.06em;text-transform:uppercase;color:var(--muted);padding-left:4px;border-left:1px solid var(--rule-strong)}
.v3-axis .nowtag{position:absolute;top:2px;transform:translateX(-50%);font-size:10.5px;font-weight:600;color:var(--critical);background:var(--ground);padding:0 3px}
.v3-lanes{position:relative}
.v3-lanes::after{content:"";position:absolute;top:0;bottom:0;left:calc(76px + (100% - 76px) * var(--n) / 100);width:1px;background:color-mix(in srgb,var(--critical) 60%,transparent);pointer-events:none}
.v3-lane{border-bottom:1px solid var(--rule)}
.v3-lane .v3-lt{height:calc(var(--rows) * 20px + 8px);overflow:hidden}
.v3-bar{position:absolute;height:16px;border-radius:2px;overflow:hidden;display:flex;align-items:center;transition:filter .12s,box-shadow .12s}
.v3-bar span{font-size:10.5px;font-weight:600;padding:0 5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.v3-bar.done{background:color-mix(in srgb,var(--g) 78%,transparent);color:var(--ground)}
.v3-bar.expired{background:repeating-linear-gradient(135deg,color-mix(in srgb,var(--critical) 26%,transparent) 0 3px,transparent 3px 7px);box-shadow:inset 0 0 0 1.5px var(--critical);color:var(--ink)}
.v3-bar.ignored{box-shadow:inset 0 0 0 1px var(--faint);color:var(--faint)}
.v3-bar.hl,.v3-bar:hover{filter:brightness(1.25);box-shadow:0 0 0 2px var(--ink);z-index:2}
.v3-dens .v3-ln{color:var(--faint);font-weight:500}
.v3-dens .v3-lt{height:30px;border-top:1px solid var(--rule)}
.v3-dens i{position:absolute;bottom:2px;background:var(--near);opacity:.7;border-radius:1px 1px 0 0}
.v3-list{padding-bottom:20px}
.v3row{display:grid;grid-template-columns:22px 92px minmax(0,1fr) 110px;gap:14px;align-items:center;width:100%;text-align:left;padding:9px 24px;border-bottom:1px solid var(--rule)}
.v3row .tt{display:flex;flex-direction:column;min-width:0}
.v3row .ti{font-weight:500;font-size:14px}
.v3row.ignored .ti{color:var(--muted);text-decoration:line-through;text-decoration-color:var(--faint)}
.v3row .when{font-size:12px;color:var(--faint);text-align:right;white-space:nowrap}
.v3row:hover,.v3row.hl{background:var(--raised)}
.sidep{position:fixed;top:40px;right:0;bottom:0;width:min(440px,100vw);z-index:31;background:var(--panel);border-left:1px solid var(--rule-strong);overflow:auto;padding:22px 24px 40px;box-shadow:-12px 0 30px rgb(0 0 0 / .3)}
.sidep h2{font:700 28px/1.05 "Barlow Condensed";margin:4px 0 8px}
.sidep .x{float:right;color:var(--muted)}
.sidep .acts{display:flex;gap:8px;margin-top:18px;flex-wrap:wrap}
.sidep .pips.big i{height:18px}
@media (max-width:860px){
.v1-stats{grid-template-columns:1fr}
.v1-figs{grid-template-columns:1fr 1fr;border-right:0;border-bottom:1px solid var(--rule)}
.v1-figs>div{padding:12px 16px;border-bottom:1px solid var(--rule)}
.v1-figs>div:nth-child(2n){border-right:0}
.v1-games{padding:12px 16px}
.v1row{grid-template-columns:22px minmax(0,1fr) auto;padding:11px 16px}
.v1row .gc2,.v1row .when{display:none}
.v1row .v1-res .pips,.v1row .v1-res .abar{display:none}
.v1row .v1-res b{min-width:0;font-size:15px}
.v2{grid-template-columns:1fr}
.v2-side{flex-direction:row;overflow-x:auto;border-right:0;border-bottom:1px solid var(--rule-strong);padding:0}
.v2-si{flex:none;border-left:0;border-bottom:3px solid transparent;padding:10px 14px}
.v2-si[aria-pressed="true"]{border-bottom-color:var(--g,var(--ink))}
.v2-si .stack.mini{width:80px}
.v2-ver{margin:12px 16px 4px}
.v2-gh{padding:16px 16px 0}
.v2row{grid-template-columns:22px minmax(0,1fr) 16px;row-gap:2px}
.v2row .ach{grid-column:2;grid-row:2}
.v2row .when{display:none}
.v2row .go{grid-row:1/3;grid-column:3}
.v2-page{padding:16px 16px 40px}
.v2-title{font-size:32px}
.v3-board{padding:6px 16px 12px}
.v3-axis,.v3-lane,.v3-dens{grid-template-columns:56px minmax(0,1fr)}
.v3-lanes::after{left:calc(56px + (100% - 56px) * var(--n) / 100)}
.v3-bar span{display:none}
.v3-key{margin-left:0}
.v3row{grid-template-columns:22px minmax(0,1fr);padding:9px 16px}
.v3row .gc2,.v3row .when{display:none}
}
.dsheet{width:min(560px,calc(100vw - 32px))!important;max-height:calc(100vh - 80px);overflow:auto}
.dsheet .desc{font-size:14px;color:var(--ink);margin:12px 0;line-height:1.55}
.dl2{display:grid;grid-template-columns:110px 1fr;gap:6px 14px;font-size:13px;margin:14px 0;padding:12px 0;border-block:1px solid var(--rule)}
.dl2 dt{color:var(--faint)}
.dl2 dd{margin:0;color:var(--muted)}
.dl2 dd b{color:var(--ink);font-weight:600}
.saved{font-size:12px;color:var(--faint);border-left:2px solid var(--rule-strong);padding:4px 10px;margin-top:8px}
.saved a{color:var(--near);word-break:break-all}
/* ---------- timeline ---------- */
.tl-ctl{display:flex;gap:10px 18px;align-items:center;flex-wrap:wrap;padding:12px 24px;border-bottom:1px solid var(--rule)}
.tl-ctl .chk{font-size:12.5px}
.tlwrap{overflow:auto;max-height:calc(100vh - 230px);position:relative;border-top:1px solid var(--rule-strong)}
.board{position:relative}
.axis{position:sticky;top:0;z-index:6;display:flex;background:var(--ground);border-bottom:1px solid var(--rule-strong)}
.axis .corner{position:sticky;left:0;z-index:7;background:var(--ground);border-right:1px solid var(--rule);flex:none}
.axis .day{flex:none;font-size:11px;color:var(--faint);padding:8px 0 6px 6px;border-left:1px solid var(--rule);line-height:1.2}
.axis .day b{display:block;font:600 14px "Barlow Condensed";color:var(--muted)}
.axis .day.wk b{color:var(--ink)}
.grpl{position:sticky;left:0;z-index:4;display:inline-block;font:600 13px "Barlow Condensed";letter-spacing:.08em;text-transform:uppercase;color:var(--faint);padding:12px 12px 6px 24px}
.grpl small{font:400 12px Barlow;text-transform:none;letter-spacing:0;margin-left:8px}
.lane{display:flex;border-top:1px solid var(--rule);position:relative}
.lane .nm{position:sticky;left:0;z-index:3;flex:none;background:var(--ground);border-right:1px solid var(--rule);padding:0 10px 0 24px;display:flex;align-items:center;font-weight:600;font-size:13px;color:var(--g);line-height:1.2;overflow:hidden}
.lane .nm::before{content:"";width:3px;height:60%;max-height:26px;border-radius:2px;background:var(--g);margin-right:10px;flex:none}
.lane .nm small{display:block;color:var(--muted);font-weight:500;font-size:11.5px}
.lane .tr{position:relative;flex:none}
.resets{background:color-mix(in srgb,var(--panel) 70%,transparent);border-bottom:1px solid var(--rule-strong)}
.resets .lane .nm{background:var(--panel)}
.gridcol{position:absolute;top:0;bottom:0;width:1px;background:var(--rule);opacity:.55}
.gridcol.wk{opacity:1;background:var(--rule-strong)}
.bar{position:absolute;height:30px;border-radius:3px;background:color-mix(in srgb,var(--g) 22%,var(--panel));border-left:3px solid var(--g);font-size:12px;padding:6px 8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;color:var(--ink)}
.bar .ey{font:600 10.5px "Barlow Condensed";letter-spacing:.06em;text-transform:uppercase;color:var(--g);margin-right:6px}
.bar.ns{background:color-mix(in srgb,var(--g) 11%,var(--panel));border-left-style:dashed}
.bar.open{-webkit-mask-image:linear-gradient(90deg,#000 60%,transparent);mask-image:linear-gradient(90deg,#000 60%,transparent)}
.bar.clip{-webkit-mask-image:linear-gradient(90deg,transparent,#000 22px);mask-image:linear-gradient(90deg,transparent,#000 22px);padding-left:14px}
.bar.own{outline:1px dashed color-mix(in srgb,var(--g) 55%,transparent);outline-offset:-1px}
/* routine on the timeline: a slim outlined pill spanning its current period, filled to progress, capped at the reset */
.rbar{position:absolute;height:20px;border-radius:10px;border:1px solid color-mix(in srgb,var(--g) 70%,transparent);background:transparent;overflow:visible;cursor:pointer}
.rbar .fill{position:absolute;inset:0 auto 0 0;border-radius:10px 0 0 10px;background:color-mix(in srgb,var(--g) 30%,transparent)}
.rbar.met .fill{border-radius:10px;background:color-mix(in srgb,var(--g) 18%,transparent)}
.rbar .lab{position:absolute;left:9px;top:0;right:22px;font:600 12px/18px "Barlow Condensed";letter-spacing:.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ink)}
.rbar.met .lab{color:var(--muted)}
.rbar .cap{position:absolute;right:-1px;top:-4px;bottom:-4px;width:2px;background:var(--g);border-radius:1px}
.rbar .rs{position:absolute;right:5px;top:3px;width:12px;height:12px;color:var(--g)}
.rbar.met .rs{color:var(--muted)}
.rbar .out{position:absolute;left:calc(100% + 8px);top:0;font:600 12px/18px "Barlow Condensed";letter-spacing:.02em;white-space:nowrap;color:var(--muted)}
.rbar.clipped{border-left-color:transparent;border-radius:0 10px 10px 0}
.rghost{position:absolute;height:20px;border-radius:10px;border:1px dashed color-mix(in srgb,var(--g) 32%,transparent);pointer-events:none}
.now{position:absolute;top:0;bottom:0;width:1px;background:var(--critical);z-index:2;pointer-events:none}
.nowchip{position:absolute;top:2px;transform:translateX(-50%);background:var(--critical);color:#fff;font:600 11px/1 "Barlow Condensed";padding:3px 5px;border-radius:2px;letter-spacing:.04em;z-index:8}
.tl-legend{display:flex;gap:18px;flex-wrap:wrap;font-size:12px;color:var(--faint);align-items:center;padding:12px 24px}
.tl-legend i{display:inline-block;vertical-align:-3px;margin-right:6px}
/* ---------- onboarding ---------- */
.ob{max-width:820px;margin:0 auto;padding:40px 24px 80px}
.ob .step{font-size:12px;color:var(--faint);margin-bottom:8px}
.ob h1{font:700 44px/1.02 "Barlow Condensed";margin:0 0 10px}
.ob .lede{color:var(--muted);max-width:58ch;margin:0 0 24px}
.gpick{display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--rule)}
.gpick button{display:flex;align-items:center;gap:10px;padding:11px 12px;border-bottom:1px solid var(--rule);text-align:left;color:var(--muted)}
.gpick button:nth-child(odd){border-right:1px solid var(--rule)}
.gpick button .sw2{width:12px;height:12px;border-radius:2px;border:1px solid var(--g);flex:none}
.gpick button[aria-pressed="true"]{color:var(--ink);background:color-mix(in srgb,var(--g) 9%,transparent)}
.gpick button[aria-pressed="true"] .sw2{background:var(--g)}
.gpick button:hover{color:var(--ink)}
.ob-foot{display:flex;gap:12px;align-items:center;margin-top:28px;flex-wrap:wrap}
.ob-foot .cnt{color:var(--faint);font-size:13px;flex:1}
.mtx{width:100%;border-collapse:collapse;margin-top:6px}
.mtx th{font:600 13px "Barlow Condensed";letter-spacing:.06em;text-transform:uppercase;color:var(--faint);text-align:center;padding:6px 4px;border-bottom:1px solid var(--rule-strong)}
.mtx th:first-child{text-align:left}
.mtx td{padding:8px 4px;border-bottom:1px solid var(--rule);text-align:center}
.mtx td:first-child{text-align:left;font:600 15px "Barlow Condensed";letter-spacing:.02em;color:var(--g)}
.mtx tr.all td{background:var(--panel)}
.mtx tr.all td:first-child{color:var(--ink);padding-left:8px}
.mtx td small{display:block;font:400 11.5px Barlow;color:var(--faint);letter-spacing:0}
.tg{width:30px;height:30px;border-radius:3px;border:1px solid var(--rule-strong);display:inline-grid;place-items:center}
.tg svg{width:14px;height:14px;stroke:var(--ground);stroke-width:3;fill:none;opacity:0}
.tg[aria-pressed="true"]{background:var(--g,var(--near));border-color:var(--g,var(--near))}
.tg[aria-pressed="true"] svg{opacity:1}
.tg[disabled]{opacity:.25;cursor:default}
.tg[data-mixed="true"]{border-style:dashed;border-color:var(--muted)}
/* ---------- sheets ---------- */
.sheet{position:fixed;z-index:31;left:50%;top:50%;transform:translate(-50%,-50%);width:min(480px,calc(100vw - 32px));background:var(--panel);border:1px solid var(--rule-strong);border-radius:6px;padding:22px 24px}
.sheet h2{font:700 26px/1.05 "Barlow Condensed";margin:4px 0 8px}
.sheet .acts{display:flex;gap:8px;margin-top:18px;flex-wrap:wrap}
.clog{width:min(620px,calc(100vw - 32px))!important;max-height:calc(100vh - 80px);overflow:auto;padding:0!important}
.clog-h{display:flex;justify-content:space-between;align-items:center;padding:18px 24px 10px;position:sticky;top:0;background:var(--panel);border-bottom:1px solid var(--rule)}
.clog-h h2{margin:0}
.rel{padding:16px 24px 18px;border-bottom:1px solid var(--rule)}
.rel .d{font:600 12px "Barlow Condensed";letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
.rel h3{font:700 19px/1.15 "Barlow Condensed";margin:4px 0 6px;letter-spacing:.01em}
.rel p{margin:0 0 8px;color:var(--muted);font-size:13.5px}
.rel ul{margin:0;padding-left:18px;font-size:13px;color:var(--muted)}
/* ---------- why panel ---------- */
.why{position:fixed;top:40px;left:0;bottom:0;width:min(500px,100vw);z-index:40;background:#f6f2e6;color:#232323;border-right:2px dashed #b9b29a;overflow:auto;padding:22px 24px 40px;font:400 14px/1.55 system-ui,sans-serif}
.why h2{font:700 18px system-ui;margin:0 0 8px}
.why h3{font:700 14px system-ui;margin:20px 0 6px}
.why code,.why pre{font:12px/1.5 ui-monospace,Menlo,monospace;background:#ebe5d3;border-radius:3px}
.why code{padding:1px 4px}
.why pre{padding:10px;overflow:auto;white-space:pre}
.why ul{padding-left:18px;margin:6px 0}
.why .x{float:right;border:1px solid #c4bda4;border-radius:3px;padding:3px 8px;background:#fff}
.why .flag{background:#fbe3c4;border-left:3px solid #c77a12;padding:8px 10px;margin:10px 0;font-size:13px}
.toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:var(--raised);border:1px solid var(--rule-strong);border-radius:4px;padding:10px 14px;z-index:45;font-size:13px;display:flex;gap:12px;box-shadow:0 8px 24px rgb(0 0 0 / .35);max-width:calc(100vw - 32px)}
@media (max-width:860px){
.ev,.settings{grid-template-columns:1fr}
.rail,.slist{border-right:0}
.slist{border-bottom:1px solid var(--rule-strong)}
.mcell,table.led .h-meter{display:none}
table.led .h-game,table.led td.gc{display:none}
.gsm{display:block;font-size:12px;font-weight:600;color:var(--g)}
table.rt{table-layout:auto}
table.rt td:first-child,table.rt th:first-child,table.rt .c-cyc,table.rt .h-prog,table.rt .h-t{display:none}
table.rt td:nth-child(2){padding-left:16px}
table.rt .c-pin{width:auto;padding:10px 2px}
table.rt .c-k{padding-right:12px}
.pinlbl span{display:none}
.prog{width:auto}
.amt .bar{display:none}
.amt .num{min-width:0;font-size:14px}
.gpick{grid-template-columns:1fr}
.gpick button:nth-child(odd){border-right:0}
header.top{gap:12px;padding:12px 16px 0;flex-wrap:wrap}
.mark{padding-bottom:4px}
nav.tabs{overflow-x:auto;width:100%}
nav.tabs button{padding:10px 10px 12px}
.big{font-size:52px}
.band-h .reset .v{font-size:26px}
.ob h1{font-size:34px}
.profile{padding:20px 16px 48px}
.comp{grid-template-columns:1fr 1fr}
.cf{grid-template-columns:1fr;padding:16px}
.focusrow,.ledger-h,.rt-top,.band-h,.head,.wn,.tl-ctl,.hintbar,.filters,.arc-sum,.month,.tl-legend,footer.foot{padding-left:16px;padding-right:16px}
.pinned-h,.pinned .hint,.gblock-h,.rail-foot,.pinned .empty{padding-left:16px;padding-right:16px}
.gitems{padding:0 16px 10px 31px}
.wn li{grid-template-columns:1fr}
.wn li .kind{display:none}
.arow{grid-template-columns:22px 1fr auto;padding:12px 16px}
.arow .gc2,.arow .when{display:none}
.srvrow{grid-template-columns:1fr;gap:6px}
.about{grid-template-columns:1fr}
.viewpick{grid-template-columns:1fr}
.li{padding-left:16px;padding-right:16px}
.lane .nm{padding-left:12px}
.mtx th{font-size:11px}
.rt-top{flex-direction:column;align-items:flex-start;gap:10px}
}
@media (min-width:861px){ .gsm,.gsmi{display:none} }
@media (prefers-reduced-motion:no-preference){
.drawer{animation:slide .18s ease-out}
@keyframes slide{from{transform:translateX(24px);opacity:.6}}
}
/* Round 4: the timetable identity, with room for the reading view. */
:root{--faint:#9ca7b7;--muted:#afb8c7;--success:#6ccbad;--review-accent:#e7dcbb}
:root[data-theme="light"]{--faint:#596271;--muted:#485365;--success:#26705b}
::selection{background:var(--near);color:var(--ground)}
input,textarea{caret-color:var(--near)}
*{scrollbar-color:var(--rule-strong) var(--ground);scrollbar-width:thin}
.shell{max-width:1480px}
.ev{grid-template-columns:300px minmax(0,1fr)}
.ledger-h{padding:30px 32px 22px;gap:20px;flex-wrap:wrap}
.ledger-h h2{font-size:28px}
.list-tools{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.seg button{min-height:36px}
.focusrow{padding-top:16px;padding-bottom:16px;gap:8px}
.standard-events{padding:0 32px}
.event-entry{display:grid;grid-template-columns:minmax(0,1fr) 130px 16px;align-items:center;gap:24px;text-align:left;width:100%;border-bottom:1px solid var(--rule);padding:24px 0;max-height:260px;overflow:hidden;transition:opacity .3s ease,max-height .35s ease,padding .35s ease}
.event-entry:hover{background:var(--surface)}
.event-entry .identity{font-size:12px;color:var(--g);display:block;margin-bottom:7px}
.event-entry .event-title{display:block;font-size:18px;font-weight:500;line-height:1.4;text-wrap:pretty}
.event-entry .event-description{display:block;color:var(--muted);font-size:13px;margin-top:7px;line-height:1.6;max-width:65ch}
.event-entry .deadline{text-align:right;font:600 22px/1.3 "Barlow Condensed",sans-serif}
.event-entry .deadline small{display:block;font:400 12px/1.5 Barlow,sans-serif;color:var(--muted);margin-top:5px}
.deadline.calm,.cd.calm{color:var(--muted)}
.deadline.near{color:var(--near)}
.deadline.soon{color:var(--soon)}
.deadline.critical{color:var(--critical)}
.event-entry .meter{margin-top:12px}
.event-entry svg{width:16px;height:16px;color:var(--muted)}
.archive-confirm{display:block!important;color:var(--success)!important;font-size:13px!important;margin-top:9px}
.archiving{background:var(--surface);pointer-events:none}
.event-entry.fading{opacity:0;max-height:0;padding-top:0;padding-bottom:0;border-bottom:0}
tr.fading{opacity:0;transition:opacity .3s ease}
.code-notice{padding:10px 32px;border-bottom:1px solid var(--rule);font-size:13px;display:flex;gap:16px;align-items:center;flex-wrap:wrap;background:var(--surface)}
.code-notice button{color:var(--near);text-decoration:underline;text-underline-offset:4px;min-height:32px}
.new-code{font-size:11px;color:var(--near);margin-left:5px}
.subtabs{display:flex;gap:24px;border-bottom:1px solid var(--rule);margin:20px 0 28px}
.subtabs button{padding:12px 0;border-bottom:2px solid transparent;color:var(--muted)}
.subtabs button[aria-current="page"]{color:var(--ink);border-color:var(--ink)}
.code-item{padding:24px 0;border-bottom:1px solid var(--rule)}
.code-title{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.code-title code{font-size:17px;overflow-wrap:anywhere}
.code-item p{color:var(--muted);margin:8px 0}
.code-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:12px}
.code-actions button[aria-pressed="true"]{background:var(--raised);color:var(--success)}
.muted-copy{color:var(--muted);line-height:1.65;max-width:70ch}
.source-note{font-size:12px;color:var(--muted);margin:20px 0;line-height:1.6}
.type-guide{margin:24px 0;display:grid;grid-template-columns:1fr 1fr;gap:18px 28px}
.type-guide dt{font-weight:600;margin-bottom:3px}
.type-guide dd{margin:0;color:var(--muted);font-size:13px;line-height:1.6}
.matrix-scroll{overflow-x:auto}
.server-setup{padding:24px;background:var(--surface);margin-top:16px}
.server-setup h3{margin-top:0}
.server-choices{display:flex;gap:10px;flex-wrap:wrap;margin:18px 0}
.server-choices button{padding:10px 14px;border:1px solid var(--rule-strong)}
.server-choices button[aria-pressed="true"]{background:var(--ink);color:var(--ground)}
.archive-view{display:flex;gap:16px;align-items:center;padding:24px 32px 0;flex-wrap:wrap}
.story{margin:24px 32px 30px;padding:24px 0;border-block:1px solid var(--rule);display:grid;grid-template-columns:1fr auto;gap:24px}
.story h2{font-size:24px;margin:0 0 8px}
.story p{margin:0;color:var(--muted);max-width:65ch;line-height:1.65}
.story dl{display:flex;gap:28px;margin:0;align-items:center}
.story dt{font-size:12px;color:var(--muted)}
.story dd{font:600 28px "Barlow Condensed";margin:0 0 4px}
.timeline-scroller{overflow:auto;max-height:72vh;border-block:1px solid var(--rule);margin-top:16px;position:relative}
.timeline-board{position:relative;isolation:isolate}
.time-axis,.timeline-entry,.game-separator{display:grid;grid-template-columns:250px 1fr}
.time-axis{position:sticky;top:0;z-index:8;background:var(--ground);height:42px;border-bottom:1px solid var(--rule)}
.time-axis>strong{padding:12px 20px;position:sticky;left:0;background:var(--ground);z-index:9;font-size:12px}
.time-axis .ticks{position:relative;font-size:12px;color:var(--muted)}
.time-axis .ticks span{position:absolute;top:12px}
.timeline-entry{height:56px;border-bottom:1px solid var(--rule);position:relative}
.timeline-name{position:sticky;left:0;z-index:4;padding:9px 16px;background:var(--ground);font-size:13px;line-height:1.4;display:block;text-align:left;border-right:1px solid var(--rule);overflow:hidden}
.timeline-name small{display:block;color:var(--muted);font-size:11px}
.time-track{position:relative;background-image:linear-gradient(to right,var(--rule) 1px,transparent 1px);background-size:60px 100%}
.period-block{position:absolute;top:12px;height:30px;background:color-mix(in srgb,var(--g) 20%,var(--ground));border:1px solid color-mix(in srgb,var(--g) 65%,var(--rule));text-align:left;padding:5px 8px;overflow:hidden;white-space:nowrap;font-size:12px;max-width:100%}
.period-block.routine{border-style:dashed;background:var(--surface)}
.period-block .fill{position:absolute;inset:0 auto 0 0;background:color-mix(in srgb,var(--g) 20%,transparent);pointer-events:none}
.period-block span{position:relative}
.reset-mark{position:absolute;top:9px;bottom:9px;border-right:2px solid var(--g);pointer-events:none}
.game-separator{border-bottom:1px solid var(--rule-strong);background:var(--surface);height:38px;font-size:13px}
.game-separator strong{position:sticky;left:0;padding:9px 16px;background:var(--surface);z-index:5;color:var(--g)}
.game-separator span{padding:9px 12px;color:var(--muted)}
.reset-strip .timeline-entry{height:48px}
.reset-strip .period-block{top:8px}
.today-line{position:absolute;top:0;bottom:0;border-left:1px solid var(--near);pointer-events:none;z-index:3}
.timeline-help{padding:16px 24px;color:var(--muted);font-size:12px}
.review-note{font-size:12px;color:var(--muted);padding:12px 24px;border-bottom:1px solid var(--rule)}
.toast{display:flex;align-items:center;gap:24px;max-width:min(580px,calc(100vw - 32px));white-space:normal;z-index:60;pointer-events:none}
.toast button{font-weight:600;text-decoration:underline;text-underline-offset:4px;min-height:32px;pointer-events:auto}
.toast:not(.undo-note){bottom:80px}
.undo-note{bottom:20px}
.profile,.main,.v2-main,.body{min-width:0}
.sheet{max-height:calc(100dvh - 80px);overflow:auto}
.sheet svg{width:18px;height:18px}
.wn{margin:20px 24px;padding:20px;border:1px solid var(--rule-strong)}
.wn h3{font-size:22px;margin:0 0 10px}
.wn p{color:var(--muted);max-width:70ch;line-height:1.6}
.wn .acts{display:flex;gap:12px;flex-wrap:wrap}
@media(min-width:1100px){.rail{position:sticky;top:40px;align-self:start;max-height:calc(100vh - 40px);overflow:auto}}
@media(max-width:1050px){.ev{grid-template-columns:240px minmax(0,1fr)}.story{grid-template-columns:1fr}.ledger-h{padding:24px}.standard-events{padding:0 24px}.event-entry{gap:16px;grid-template-columns:minmax(0,1fr) 105px 14px}.event-entry .event-title{font-size:17px}}
@media(max-width:700px){.ev{display:block}.rail{border-right:0;border-bottom:1px solid var(--rule)}.rail .head{padding:22px 20px}.rail .then{display:none}.rail .big{font-size:32px}.rail .head h2{font-size:21px}.rail .pinned{padding-bottom:12px}.ledger-h{padding:24px 20px 16px}.list-tools{gap:8px}.standard-events{padding:0 20px}.event-entry{grid-template-columns:minmax(0,1fr) 90px;gap:16px;padding:22px 0}.event-entry>svg{display:none}.event-entry .event-title{font-size:16px}.event-entry .deadline{font-size:20px}.event-entry .event-description{font-size:12px}.event-entry .meter{display:none}.code-notice{padding:10px 20px;gap:8px 16px}.story{margin:20px}.story dl{gap:24px;flex-wrap:wrap}.archive-view{padding:20px 20px 0}.type-guide{grid-template-columns:1fr;gap:14px}.time-axis,.timeline-entry,.game-separator{grid-template-columns:170px 1fr}.timeline-name{font-size:12px}.profile{padding:20px}.subtabs{gap:20px}.server-setup{padding:16px}.top{padding-inline:12px}.tabs{overflow-x:auto}.tabs button{flex:none}.v2row{padding-block:16px!important}.code-item code{font-size:15px}}
@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition:none!important;animation:none!important}}
/* Round 5: optional arrangements, with the same timetable identity. */
:root{--surface:var(--panel)}
#chrome select{flex:none;width:auto;min-width:120px;height:28px;font:12px system-ui,sans-serif;background:var(--chrome-bg);color:var(--chrome-ink);border:1px solid #b9b29a;padding:3px 6px}
:root[data-scheme="warm"]{--ground:#181512;--panel:#211d18;--raised:#302a22;--rule:#393128;--rule-strong:#55493b;--ink:#f0e8dc;--muted:#c3b7a5;--faint:#b1a593;--near:#9bc4ef;--soon:#edc465;--critical:#ff8e9b}
:root[data-scheme="warm"][data-theme="light"]{--ground:#f5efe3;--panel:#fffbf4;--raised:#e8decc;--rule:#d8cdb9;--rule-strong:#b8aa92;--ink:#282219;--muted:#605544;--faint:#6a5d4a;--near:#315c89;--soon:#805a12;--critical:#ac2741}
:root[data-scheme="plum"]{--ground:#17131c;--panel:#211b29;--raised:#30263d;--rule:#3b3048;--rule-strong:#584768;--ink:#eee7f6;--muted:#c0b3ce;--faint:#b1a1c0;--near:#bea4f2;--soon:#efc46c;--critical:#ff91a7}
:root[data-scheme="plum"][data-theme="light"]{--ground:#f2edf7;--panel:#fdfaff;--raised:#e6dced;--rule:#d6c9df;--rule-strong:#b7a4c5;--ink:#2a2033;--muted:#61516e;--faint:#6d5a7c;--near:#70449e;--soon:#825909;--critical:#ae2549}
.focus-tools{position:sticky;top:40px;z-index:12;background:var(--panel);border-bottom:1px solid var(--rule-strong);padding:16px 24px;display:flex;gap:16px;align-items:center;flex-wrap:wrap}
.focus-tools .focus-label{font-weight:600;font-size:14px;color:var(--ink)}
.focus-buttons{display:flex;gap:8px;flex-wrap:wrap}
.focus-buttons button,.focus-rail button{padding:10px 14px;border:1px solid var(--rule-strong);border-radius:4px;font-size:13px;min-height:40px;color:var(--muted);background:var(--ground)}
.focus-buttons button[aria-pressed="true"],.focus-rail button[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:var(--ground)}
.focus-buttons .n{font-size:11px;margin-left:8px;opacity:.85}
.focus-tools select{min-height:40px;min-width:200px}
.focus-rail{padding:20px;border-bottom:1px solid var(--rule)}
.focus-rail h3{font-size:14px;margin:0 0 12px}
.focus-rail .focus-buttons{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.focus-rail button{text-align:left;padding:10px}
.focus-current{color:var(--muted);font-size:13px}
.daily-panel{padding:20px;border-bottom:1px solid var(--rule)}
.daily-panel h3{font-size:16px;margin:0}
.daily-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}
.daily-heading button,.daily-layout button{font-size:12px;text-decoration:underline;text-underline-offset:3px;color:var(--near);min-height:32px}
.daily-layout{padding:10px 20px;color:var(--muted);font-size:12px;border-bottom:1px solid var(--rule);display:flex;gap:12px;align-items:center}
.daily-row{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:10px 0;border-bottom:1px solid var(--rule)}
.daily-row:last-child{border-bottom:0}
.daily-row strong{font-size:13px;color:var(--g)}
.daily-row small{display:block;font-size:11px;color:var(--muted);margin-top:3px}
.daily-row .tick{min-width:44px;min-height:44px;justify-content:center}
.event-entry.no-description{padding-block:14px}
.event-entry.no-description .meter{display:none}
.availability{margin:16px 0 24px;padding:14px 0;border-block:1px solid var(--rule);font-size:13px;color:var(--muted);line-height:1.65}
.availability p{margin:6px 0}
.preference-grid{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin:28px 0}
.preference-grid label{display:flex;flex-direction:column;gap:8px;font-weight:500}
.preference-grid label small{font-weight:400;color:var(--muted);line-height:1.6}
.preference-grid select{min-height:42px;width:100%}
.preference-grid .chk{flex-direction:row;align-items:flex-start;font-weight:400}
.zone-search{display:block;width:min(100%,450px);margin:14px 0 8px;min-height:40px}
.zone-select{display:block;width:min(100%,450px);min-height:42px;margin:8px 0}
.swatches{display:flex;gap:12px;flex-wrap:wrap;margin:16px 0}
.scheme-option{padding:14px 18px;border:1px solid var(--rule-strong);min-width:120px;text-align:left}
.scheme-option[aria-pressed="true"]{outline:2px solid var(--near);outline-offset:3px}
.scheme-option .swatch{display:flex;height:18px;margin-bottom:10px;border:1px solid var(--rule-strong)}
.scheme-option i{flex:1;background:var(--swatch)}
.game-colour{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:16px 0}
.game-colour input[type="color"]{padding:2px;width:48px;height:40px}
.game-colour input[type="text"]{width:110px;min-height:40px}
.colour-preview{padding:12px 16px;background:var(--raised);font-size:15px;font-weight:600}
.freshness-bar{padding:12px 24px;border-bottom:1px solid var(--rule);background:var(--panel);font-size:12px;color:var(--muted);display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap}
.freshness-bar button{color:var(--near);min-height:32px;text-decoration:underline;text-underline-offset:3px}
.freshness-warning{padding:16px 24px;border-bottom:1px solid var(--rule-strong);background:var(--panel);font-size:13px;line-height:1.6}
.freshness-warning h2{font-size:16px;margin:0 0 6px;color:var(--soon)}
.freshness-warning p{margin:6px 0;color:var(--muted)}
.freshness-warning summary{cursor:pointer;color:var(--near);padding:10px 0;min-height:40px}
.source-row{display:grid;grid-template-columns:160px 1fr;gap:16px;padding:18px 0;border-bottom:1px solid var(--rule);font-size:13px;line-height:1.65}
.source-row strong{color:var(--g)}
.source-row p{margin:3px 0;color:var(--muted)}
.source-row .reason{color:var(--soon)}
.source-row .reason.current{color:var(--success)}
.source-status-label{font-size:11px;color:var(--soon);margin-left:8px}
.small-source-label{display:block;color:var(--soon);font-size:11px;margin-top:4px}
.stats-story{margin:24px 32px;border-block:1px solid var(--rule);padding:24px 0}
.stats-story h2{margin:0 0 8px;font-size:24px}
.stats-story>p{color:var(--muted);max-width:70ch;line-height:1.65}
.stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:28px;margin:24px 0}
.stats-grid strong{display:block;font:600 36px "Barlow Condensed",sans-serif;color:var(--ink)}
.stats-grid span{display:block;font-size:13px;margin:4px 0}
.stats-grid small{color:var(--muted);font-size:12px;line-height:1.5;display:block}
.stats-game{display:grid;grid-template-columns:140px 1fr 180px;gap:16px;align-items:center;width:100%;padding:9px 0;text-align:left;font-size:12px}
.stats-game .name{color:var(--g)}
.stats-game .counts{color:var(--muted);text-align:right}
.backup-policy{display:flex;gap:24px;margin:22px 0;padding-block:20px;border-block:1px solid var(--rule);flex-wrap:wrap}
.backup-policy b{display:block;font-size:20px;margin-bottom:4px}
.backup-policy span{font-size:13px;color:var(--muted)}
.backup-row{display:grid;grid-template-columns:1fr auto;align-items:center;gap:16px;text-align:left;width:100%;padding:16px 0;border-bottom:1px solid var(--rule)}
.backup-row small{display:block;margin-top:4px;color:var(--muted);font-size:12px}
.backup-row .btn{pointer-events:none}
.backup-heading{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin:28px 0 4px}
.backup-heading h3{margin:0}
.backup-heading span{font-size:12px;color:var(--muted)}
.restore-preview{margin-top:24px;background:var(--panel);padding:24px}
.restore-preview h3{margin:0 0 12px}
.restore-preview p{color:var(--muted);line-height:1.65}
.restore-preview dl{display:grid;grid-template-columns:1fr 1fr;gap:12px;font-size:13px}
.restore-preview dd{margin:0;font-weight:600}
.restore-preview .acts{display:flex;gap:12px;flex-wrap:wrap;margin-top:20px}
.about-prose{max-width:70ch;color:var(--muted);line-height:1.75;font-size:14px}
.about-prose h3{margin:28px 0 8px;color:var(--ink)}
.about-links{display:flex;gap:16px;flex-wrap:wrap;margin:16px 0}
button:disabled{cursor:default;opacity:.55}
.about-prose a,.source-row a{color:var(--near);text-underline-offset:3px}
@media(max-width:700px){.focus-tools{padding:14px 20px;position:static;gap:12px}.focus-buttons{gap:8px}.focus-buttons button{padding:9px 11px}.focus-rail .focus-buttons{grid-template-columns:repeat(3,1fr)}.preference-grid{grid-template-columns:1fr;gap:22px}.source-row{grid-template-columns:1fr;gap:4px}.freshness-bar,.freshness-warning{padding:14px 20px}.stats-story{margin:20px}.stats-grid{grid-template-columns:1fr 1fr;gap:24px 16px}.stats-game{grid-template-columns:90px 1fr;gap:10px}.stats-game .counts{grid-column:2;text-align:left}.restore-preview{padding:18px}.backup-policy{gap:20px}.backup-policy b{font-size:18px}.daily-panel{padding-block:16px}.event-entry.no-description{padding-block:12px}}
@media(min-width:1100px){:root:not([data-focus-layout="rail"]) .ev .rail{top:var(--focus-offset,120px);max-height:calc(100vh - var(--focus-offset,120px))}}
/* Quieter revision: controls sit with the content they affect. */
.data-status{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:7px 24px;border-bottom:1px solid var(--rule);color:var(--muted);font-size:12px;line-height:1.5;background:var(--ground)}
.data-status .summary{display:flex;gap:8px 18px;align-items:center;flex-wrap:wrap}
.data-status .state{color:var(--soon)}
.data-status button{font-size:12px;min-height:32px;color:var(--near);text-decoration:underline;text-underline-offset:3px;flex:none}
.list-heading{display:grid;grid-template-columns:auto auto;gap:6px 18px;align-items:center;justify-content:start}
.list-heading .sub{grid-column:1/-1}
.event-focus{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}
.event-focus select{min-height:36px;max-width:220px;font-size:13px;padding:6px 8px;background:var(--ground)}
.inline-game-filter{padding:0 24px 14px;border-bottom:1px solid var(--rule)}
.inline-game-filter .focus-buttons{gap:6px}
.inline-game-filter .focus-buttons button{padding:6px 10px;min-height:32px;font-size:12px}
.timeline-game-filter{margin-left:auto}
.changelog-page .rel{padding:24px 0;border-bottom:1px solid var(--rule)}
.changelog-page .rel h3{font:600 24px "Barlow Condensed",sans-serif;margin:8px 0}
.changelog-page .rel .d{font-size:12px;color:var(--muted)}
.changelog-page .rel p,.changelog-page .rel li{max-width:70ch;line-height:1.7;color:var(--muted)}
.changelog-page .rel ul{padding-left:20px}
@media(max-width:700px){.data-status{padding:6px 20px;gap:12px}.data-status .summary{gap:0 12px}.list-heading{gap:6px 12px;grid-template-columns:minmax(0,1fr) auto;width:100%}.list-heading h2{font-size:25px}.event-focus{gap:6px}.event-focus select{max-width:172px;font-size:12px;min-height:36px}.event-focus label{display:none}.inline-game-filter{padding-inline:20px}.timeline-game-filter{margin-left:0}}
/* Three structures, one visual system. */
.prototype-link{padding:5px 8px;background:var(--chrome-bg);color:var(--chrome-ink);text-decoration:underline;flex:none}
.routine-display{padding:16px 24px;border-bottom:1px solid var(--rule);display:flex;gap:18px 28px;flex-wrap:wrap;align-items:center;background:var(--panel)}
.routine-display label{display:flex;gap:10px;align-items:center;color:var(--muted);font-size:13px}
.routine-display select{min-height:36px;padding:6px 9px}
.routine-display p{width:100%;font-size:12px;color:var(--muted);margin:0}
.gblock-h.noncollapsible{cursor:default}
.gblock-h.noncollapsible:hover{background:transparent}
input[type="number"]{appearance:auto;-moz-appearance:auto}
input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{opacity:1}
.mini-amt input{width:86px;padding-right:4px}
.event-source-state{padding:14px 0;margin:18px 0;border-block:1px solid var(--rule);font-size:13px;line-height:1.6}
.event-source-state strong{color:var(--soon)}
.event-source-state p{color:var(--muted);margin:7px 0}
.rail-status{padding:12px 20px;border-bottom:1px solid var(--rule);font-size:12px;color:var(--muted);line-height:1.6}
.rail-status button{color:var(--near);text-decoration:underline;text-underline-offset:3px;min-height:32px}
.rail-game-picker{padding:20px;border-bottom:1px solid var(--rule)}
.rail-game-picker label{display:block;font-size:13px;font-weight:500;margin-bottom:9px}
.rail-game-picker select{width:100%;min-height:40px}
.rail-game-picker small{display:block;font-size:12px;color:var(--muted);margin-top:8px}
:root[data-variant="b"] .rail .then{display:none}
:root[data-variant="b"] .rail .head{padding:20px}
:root[data-variant="b"] .rail .head h2{font-size:23px}
:root[data-variant="b"] .rail .big{font-size:34px}
:root{--accent:var(--near)}
:focus-visible{outline-color:var(--accent)}
.code-notice button,.about-prose a,.source-row a,.data-status button,.inline-data button,.daily-heading button,.daily-layout button{color:var(--accent)}
.scheme-option[aria-pressed="true"]{outline-color:var(--accent)}
.custom-scheme{padding:18px 0;margin:16px 0;border-block:1px solid var(--rule)}
.custom-colours{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin:20px 0}
.custom-colours label{font-size:13px;color:var(--muted);display:flex;flex-direction:column;gap:8px}
.custom-colours label>span{display:flex;gap:10px}
.custom-colours input[type="color"]{width:44px;height:38px;padding:2px}
.custom-colours input[type="text"]{width:106px;min-height:38px}
.reset-editor{display:flex;gap:14px 20px;align-items:center;flex-wrap:wrap;padding:18px 0;border-bottom:1px solid var(--rule);font-size:13px}
.reset-editor>div{flex:1;min-width:210px}
.reset-editor strong{color:var(--g)}
.reset-editor small{display:block;color:var(--muted);line-height:1.6;margin-top:4px}
.reset-editor label{display:flex;flex-direction:column;gap:6px;color:var(--muted);font-size:12px}
.reset-editor select,.reset-editor input{min-height:36px}
.game-source{padding:12px 0;border-bottom:1px solid var(--rule)}
.game-source a{color:var(--accent);text-underline-offset:3px}
.game-source small{display:block;font-size:12px;color:var(--muted);margin-top:5px}
.returning-intro{padding:20px 24px;border-bottom:1px solid var(--rule-strong);background:var(--panel)}
.returning-intro h2{font-size:21px;margin:0 0 8px}
.returning-intro p{max-width:75ch;color:var(--muted);font-size:13px;line-height:1.65;margin:8px 0}
.returning-intro .tour-step{font-size:12px;color:var(--muted);margin-bottom:8px}
.returning-intro .acts{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}
.upcoming-choice{margin:20px 0;padding:18px 0;border-block:1px solid var(--rule)}
@media(max-width:700px){.custom-colours{grid-template-columns:1fr}.reset-editor{gap:14px}.reset-editor>div{flex-basis:100%}.returning-intro{padding:18px 20px}}
.open-ledger{display:block;max-width:1240px;margin:0 auto}
.open-ledger .main{width:100%}
.next-line{width:calc(100% - 64px);margin:0 32px 18px;border-block:1px solid var(--rule);padding:12px 0;display:grid;grid-template-columns:110px minmax(0,1fr) auto;align-items:center;gap:20px;text-align:left}
.next-line .caption{color:var(--muted);font-size:12px}
.next-line .name{font-size:14px;font-weight:500}
.next-line .name small{display:block;color:var(--g);font-size:11px;margin-top:3px}
.next-line .remaining{font:600 23px "Barlow Condensed",sans-serif;color:var(--soon)}
.quick-dailies{margin:0 32px 18px;padding:0;border-bottom:1px solid var(--rule)}
.quick-dailies .daily-heading{margin-bottom:4px}
.quick-dailies .daily-heading h3{font-size:14px}
.quick-dailies .daily-items{display:grid;grid-template-columns:repeat(auto-fit,minmax(185px,1fr));gap:0 24px}
.quick-dailies .daily-row{border-bottom:0;padding:6px 0}
.quick-dailies .daily-row small{max-width:150px}
.open-ledger .daily-layout{margin:0 32px;padding:4px 0;border-bottom:0}
.routine-drawer{margin:0 32px 18px;border-bottom:1px solid var(--rule)}
.routine-drawer>summary{cursor:pointer;min-height:40px;padding:12px 0;color:var(--muted);font-size:13px}
.routine-drawer>h3{font-size:14px;margin:16px 0 10px}
.routine-drawer .pinned{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:0 20px;border:0;padding-bottom:12px}
.routine-drawer .pinned-h,.routine-drawer .hint,.routine-drawer .rail-foot{grid-column:1/-1}
.routine-drawer .pinned-h{padding:8px 0}
.routine-drawer .gblock-h{padding-inline:0}
.routine-drawer .gitems{padding-inline:0}
.routine-drawer .gblock{align-self:start}
.routine-drawer .rail-foot{padding-inline:0}
.inline-data{font-size:12px;color:var(--muted);display:inline-flex;gap:10px;margin-top:8px;align-items:center;flex-wrap:wrap}
.inline-data button{color:var(--near);text-decoration:underline;text-underline-offset:3px;min-height:28px}
.ledger-h .sub .inline-data{display:flex}
@media(min-width:1100px){:root[data-variant="b"] .ev .rail{top:40px;max-height:calc(100vh - 40px)}}
@media(max-width:700px){.routine-display{padding:16px 20px;gap:14px}.routine-display label{display:grid;grid-template-columns:1fr;gap:6px;width:100%}.routine-display select{width:100%}.next-line{margin:0 20px 14px;width:calc(100% - 40px);grid-template-columns:1fr auto;gap:6px 16px}.next-line .caption{grid-column:1/-1}.quick-dailies{margin:0 20px 14px}.quick-dailies .daily-items{grid-template-columns:1fr 1fr;gap:0 16px}.quick-dailies .daily-row{gap:8px}.quick-dailies .daily-row small{font-size:10px}.quick-dailies .daily-row strong{font-size:12px}.routine-drawer{margin:0 20px 14px}.open-ledger .daily-layout{margin:0 20px}.routine-drawer .pinned{grid-template-columns:1fr}.rail-game-picker{padding:16px 20px}.rail-status{padding:10px 20px}}
</style>
</head>
<body>
<div id="chrome"></div>
<div id="app"></div>
<div id="overlay"></div>
<script>
// ============ real content (feed sample, src/shared/routines.ts, games.ts) ============
const NOW = Date.parse("2026-10-16T12:00:00Z");
const GAMES = [{"id":"genshin","name":"Genshin Impact","short":"Genshin","hue":"#4EA8DE","chores":"Commissions, resin","studio":"HoYoverse"},{"id":"hsr","name":"Honkai: Star Rail","short":"Star Rail","hue":"#7B8CFF","chores":"Daily training, Trailblaze Power","studio":"HoYoverse"},{"id":"zzz","name":"Zenless Zone Zero","short":"ZZZ","hue":"#F2A03D","chores":"Daily missions, battery","studio":"HoYoverse"},{"id":"wuwa","name":"Wuthering Waves","short":"Wuwa","hue":"#3DD6A0","chores":"Daily activity, waveplate","studio":"Kuro Games"},{"id":"arknights","name":"Arknights","short":"Arknights","hue":"#9AA3B8","chores":"Daily missions, sanity","studio":"Hypergryph"},{"id":"endfield","name":"Arknights: Endfield","short":"Endfield","hue":"#E8635A","chores":"Daily missions","studio":"Hypergryph"},{"id":"nte","name":"Neverness to Everness","short":"NTE","hue":"#C77DFF","chores":"Daily tasks","studio":"Hotta Studio"},{"id":"nikki","name":"Infinity Nikki","short":"Nikki","hue":"#F27BB0","chores":"Daily tasks, Vital Energy","studio":"Infold Games"},{"id":"p5x","name":"Persona 5: The Phantom X","short":"P5X","hue":"#D62246","chores":"Daily missions, stamina","studio":"Perfect World"},{"id":"r1999","name":"Reverse: 1999","short":"R1999","hue":"#C9A227","chores":"Daily missions","studio":"Bluepoch"},{"id":"ba","name":"Blue Archive","short":"Blue Archive","hue":"#3FCBDD","chores":"Daily missions, AP","studio":"Nexon Games"},{"id":"fgo","name":"Fate/Grand Order","short":"FGO","hue":"#6F8BE0","chores":"Daily Quests, AP","studio":"Lasengle"},{"id":"gfl2","name":"Girls' Frontline 2: Exilium","short":"GFL2","hue":"#A9C23F","chores":"Daily missions","studio":"Sunborn"},{"id":"stellasora","name":"Stella Sora","short":"Stella Sora","hue":"#2E9E9E","chores":"Daily missions","studio":"Yostar"},{"id":"czn","name":"Chaos Zero Nightmare","short":"CZN","hue":"#B84A9C","chores":"Daily missions","studio":"Smilegate"},{"id":"nikke","name":"Goddess of Victory: Nikke","short":"Nikke","hue":"#E4572E","chores":"Daily missions, outpost","studio":"Shift Up"},{"id":"uma","name":"Umamusume: Pretty Derby","short":"Umamusume","hue":"#6FBF44","chores":"Daily races, missions","studio":"Cygames"},{"id":"hi3","name":"Honkai Impact 3rd","short":"Honkai 3rd","hue":"#8B5CF6","chores":"Daily missions, stamina","studio":"HoYoverse"},{"id":"pgr","name":"Punishing: Gray Raven","short":"PGR","hue":"#CC292B","chores":"Daily missions, serum","studio":"Kuro Games"}];
const G = Object.fromEntries(GAMES.map(g=>[g.id,g]));
const RAW_ROUTINES = [["routine:genshin:weekly-bosses","genshin","Weekly Bosses",{"unit":"week"},{"kind":"amount","target":3,"unit":"runs"}],["routine:genshin:battle-pass-exp","genshin","Battle Pass weekly EXP",{"unit":"week"},{"kind":"amount","target":10000,"unit":"EXP"}],["routine:genshin:bounties","genshin","Reputation Bounties",{"unit":"week"},{"kind":"amount","target":3,"unit":"bounties"}],["routine:genshin:requests","genshin","Reputation Requests",{"unit":"week"},{"kind":"amount","target":3,"unit":"requests"}],["routine:genshin:realm-depot","genshin","Realm Depot: Realm Treasures",{"unit":"week"},{"kind":"days","required":1}],["routine:genshin:paimons-bargains","genshin","Paimon's Bargains: Intertwined Fates",{"unit":"month"},{"kind":"amount","target":5,"unit":"fates"},"premium"],["routine:genshin:paimons-bargains-acquaint","genshin","Paimon's Bargains: Acquaint Fates",{"unit":"month"},{"kind":"amount","target":5,"unit":"fates"},"standard"],["routine:genshin:spiral-abyss","genshin","Spiral Abyss",{"unit":"month","startsOn":16},{"kind":"days","required":1}],["routine:genshin:imaginarium-theater","genshin","Imaginarium Theater",{"unit":"month"},{"kind":"days","required":1}],["routine:hsr:echo-of-war","hsr","Echo of War",{"unit":"week"},{"kind":"amount","target":3,"unit":"runs"}],["routine:hsr:nameless-honor-exp","hsr","Nameless Honor weekly EXP",{"unit":"week"},{"kind":"amount","target":8000,"unit":"EXP"}],["routine:hsr:embers-exchange","hsr","Embers Exchange: Star Rail Special Pass",{"unit":"month"},{"kind":"amount","target":5,"unit":"passes"},"premium"],["routine:hsr:embers-exchange-standard","hsr","Embers Exchange: Star Rail Pass",{"unit":"month"},{"kind":"amount","target":5,"unit":"passes"},"standard"],["routine:hsr:points","hsr","Divergent Universe & Currency Wars points",{"unit":"week"},{"kind":"amount","target":18000,"unit":"points"}],["routine:hsr:pure-fiction","hsr","Pure Fiction",{"unit":"cycle","days":42,"from":"2026-10-19"},{"kind":"days","required":1}],["routine:hsr:memory-of-chaos","hsr","Memory of Chaos",{"unit":"cycle","days":42,"from":"2026-11-02"},{"kind":"days","required":1}],["routine:hsr:apocalyptic-shadow","hsr","Apocalyptic Shadow",{"unit":"cycle","days":42,"from":"2026-11-16"},{"kind":"days","required":1}],["routine:zzz:ridu-weekly","zzz","Ridu Weekly",{"unit":"week"},{"kind":"amount","target":2100,"unit":"points"}],["routine:zzz:bounty-commissions","zzz","Bounty Commissions",{"unit":"week"},{"kind":"amount","target":8000,"unit":"points"}],["routine:zzz:notorious-hunt","zzz","Notorious Hunt",{"unit":"week"},{"kind":"amount","target":3,"unit":"runs"}],["routine:zzz:city-fund-exp","zzz","New Eridu City Fund weekly EXP",{"unit":"week"},{"kind":"amount","target":10000,"unit":"EXP"}],["routine:zzz:signal-shop-tapes","zzz","Signal Shop: Encrypted Master Tapes",{"unit":"month"},{"kind":"amount","target":5,"unit":"tapes"},"premium"],["routine:zzz:signal-shop-master-tapes","zzz","Signal Shop: Master Tapes",{"unit":"month"},{"kind":"amount","target":5,"unit":"tapes"},"standard"],["routine:zzz:signal-shop-boopons","zzz","Signal Shop: Boopons",{"unit":"month"},{"kind":"amount","target":5,"unit":"Boopons"},"premium"],["routine:zzz:shiyu-defense","zzz","Shiyu Defense",{"unit":"cycle","days":14,"from":"2026-10-16"},{"kind":"days","required":1}],["routine:zzz:deadly-assault","zzz","Deadly Assault",{"unit":"cycle","days":14,"from":"2026-10-23"},{"kind":"days","required":1}],["routine:endfield:weekly-routine","endfield","Weekly Routine",{"unit":"week"},{"kind":"amount","target":10,"unit":"points"}],["routine:endfield:focus-commissions","endfield","Etchspace Salvage: Focus Commissions",{"unit":"week"},{"kind":"amount","target":3,"unit":"commissions"}],["routine:wuwa:weekly-challenge","wuwa","Weekly Challenge",{"unit":"week"},{"kind":"amount","target":3,"unit":"claims"}],["routine:wuwa:pioneer-podcast-exp","wuwa","Pioneer Podcast weekly EXP",{"unit":"week"},{"kind":"amount","target":12000,"unit":"EXP"}],["routine:wuwa:weekly-activities","wuwa","Weekly Activities",{"unit":"week"},{"kind":"amount","target":6000,"unit":"points"}],["routine:wuwa:tower-of-adversity","wuwa","Tower of Adversity",{"unit":"cycle","days":28,"from":"2026-11-09"},{"kind":"days","required":1}],["routine:nikki:mira-journey-exp","nikki","Mira Journey weekly EXP",{"unit":"week"},{"kind":"amount","target":8000,"unit":"Journey EXP"}],["routine:nikki:realm-of-breakthrough","nikki","Realm of Breakthrough",{"unit":"week"},{"kind":"amount","target":2,"unit":"trials"}],["routine:nikki:resonance-revelation","nikki","Resonance Store: Revelation Crystals",{"unit":"month"},{"kind":"amount","target":5,"unit":"crystals"},"premium"],["routine:nikki:resonance-resonite","nikki","Resonance Store: Resonite Crystals",{"unit":"month"},{"kind":"amount","target":5,"unit":"crystals"},"standard"],["routine:nte:season-pass-exp","nte","Season Pass weekly EXP",{"unit":"week"},{"kind":"amount","target":12000,"unit":"EXP"}],["routine:nte:anomaly-pilgrimage","nte","Anomaly Pilgrimage",{"unit":"week"},{"kind":"amount","target":3,"unit":"claims"}],["routine:nte:city-stamina","nte","City Stamina",{"unit":"week"},{"kind":"amount","target":700,"unit":"stamina"}],["routine:nte:realm-of-greed","nte","Realm of Greed",{"unit":"week"},{"kind":"days","required":1}],["routine:nte:special-city-commission","nte","Special City Commission",{"unit":"week"},{"kind":"days","required":1}],["routine:nte:beyond-the-rails","nte","Beyond the Rails",{"unit":"cycle","days":14,"from":"2026-10-22"},{"kind":"days","required":1}],["routine:nte:tycoon-fund","nte","Tycoon Incentive Fund",{"unit":"cycle","days":14,"from":"2026-10-26"},{"kind":"amount","target":1000000,"unit":"fons"}]];
const BUILTINS = [
...GAMES.map(g=>({id:`routine:${g.id}:daily`,game:g.id,title:`Dailies: ${g.chores}`,period:{unit:"day"},measure:{kind:"days",required:1},src:"The game's standing chores",builtin:true})),
...RAW_ROUTINES.map(([id,game,title,period,measure,pulls])=>({id,game,title,period,measure,pulls,src:"Community wiki, checked 14 Oct 2026",builtin:true})),
];
// Live events: real titles and windows from the feed; descriptions are mockup copy.
const EVENTS = [["genshin","Anniversary Memories Album","2026-09-28","2026-10-18","Look back through the year's moments in an album and claim Primogems for each page you fill."],["zzz","Clink, Clank, Pinball Knight!","2026-09-10","2026-10-19","A pinball minigame with daily stages; clear them for Polychromes and upgrade materials."],["zzz","Diary of an Orbie Parent","2026-09-30","2026-10-19","Raise an Orbie through daily check-ins and small tasks."],["nte","Stamina Recharge","2026-10-05","2026-10-19","Spend City Stamina twice as fast in select Hethereau Hobby activities."],["zzz","Data Bounty - Combat Simulation","2026-10-14","2026-10-19","Short combat simulations with bonus rewards for the first clear of each."],["zzz","All-New Program","2026-09-09","2026-10-20",""],["genshin","Predictive Victory Dynamics","2026-10-12","2026-10-20","A combat event in stages; each unlocks a day after the last."],["wuwa","Bountiful Crescendo","2026-10-15","2026-10-22",""],["hsr","4.6 Planar Fissure","2026-10-12","2026-10-25","Double rewards from Planar Ornament Extraction, a few times a day."],["wuwa","Artisan's Search","2026-10-08","2026-10-26",""],["wuwa","Waking Moon Fishing","2026-10-15","2026-11-01",""],["genshin","Battle Pass - Moontrace","2026-09-23","2026-11-02",""],["genshin","To Temper Thyself and Journey Far","2026-08-10","2026-11-02",""],["hsr","Love, Ghosts & Robots","2026-09-28","2026-11-11",""],["hsr","Astral Imagea","2026-09-28","2026-11-11",""],["nte","Pukaland Travelogue","2026-09-30","2026-11-11",""],["wuwa","3.7 Moonlit Path","2026-09-30","2026-11-11",""],["nte","Coal Lumps Treasure","2026-10-08","2026-11-11",""],["endfield","Originium Supply Run","2026-10-09","2026-10-23","Mockup event so Endfield has a row."],["hsr","Free Gilgamesh or Archer","2026-07-24",null,""]].map(([game,title,s,e,desc],i)=>({id:"e"+i,game,title,desc,start:Date.parse(s+"T03:00:00Z"),end:e?Date.parse(e+"T03:00:00Z"):null,src:game==="genshin"?"https://game8.co/games/Genshin-Impact/archives/297500":null}));
const OWN_EVENTS_SEED=[{id:"own1",game:"genshin",title:"Photo contest with the guild",desc:"Yours.",start:Date.parse("2026-10-10T00:00:00Z"),end:Date.parse("2026-10-24T20:00:00Z"),own:true}];
// Archive: snapshots taken when an event was archived. Titles, versions, descriptions and progress are mockup text.
const VERSIONS={genshin:[["6.0","2026-08-27","2026-10-08"],["6.1","2026-10-08","2026-11-19"]],hsr:[["4.5","2026-08-13","2026-09-24"],["4.6","2026-09-24","2026-11-05"]],zzz:[["2.2","2026-08-20","2026-10-01"],["2.3","2026-10-01","2026-11-12"]],wuwa:[["3.6","2026-08-21","2026-09-30"],["3.7","2026-09-30","2026-11-11"]],endfield:[["1.2","2026-08-14","2026-09-25"],["1.3","2026-09-25","2026-11-06"]],nte:[["1.1","2026-08-28","2026-10-08"],["1.2","2026-10-08","2026-11-19"]]};
// [id, game, version, title, status, start, end, archivedAt, measure, got, need, unit, description, sourceMoved]
const ARC_ROWS=[
["g1","genshin","6.0","Thunder Gauntlet Trials","done","2026-08-27","2026-09-08","2026-09-07","stages",6,6,"stages","A combat challenge in six stages, one unlocking each day. Each first clear pays Primogems.",0],
["g4","genshin","6.0","Fontinalia Film Festival","done","2026-09-03","2026-09-17","2026-09-16","amount",1200,1200,"Film Points","Shoot short scenes around Fontaine and trade Film Points in the event shop.",0],
["g5","genshin","6.0","Mystic Melody Concert","ignored","2026-09-10","2026-09-24","2026-09-11","days",0,5,"days","A rhythm minigame with daily songs.",0],
["g7","genshin","6.0","Gilded Winds Exploration","expired","2026-09-12","2026-10-06","2026-10-06","stages",4,5,"stages","Explore five wind-swept ruins; each area opens a few days after the last.",0],
["g3","genshin","6.0","Ley Line Overflow","expired","2026-09-15","2026-09-22","2026-09-22","days",1,7,"days","Double Ley Line rewards, three times a day.",0],
["g6","genshin","6.0","Primordial Echoes Web Event","done","2026-09-20","2026-09-27","2026-09-21","days",3,3,"days","A browser event: check in on three days for Primogems.",1],
["g2","genshin","6.0","Overflowing Mastery","done","2026-10-02","2026-10-09","2026-10-08","days",6,7,"days","Double Talent Domain drops, three times a day.",0],
["g8","genshin","6.1","Moonlit Feast Login","done","2026-10-08","2026-10-15","2026-10-14","days",7,7,"days","Log in on seven days for Intertwined Fates.",0],
["h2","hsr","4.5","Starlight Rhapsody","done","2026-08-13","2026-09-02","2026-09-01","stages",8,8,"stages","A story event in eight chapters.",0],
["h3","hsr","4.5","Gift of Odyssey","done","2026-08-13","2026-09-24","2026-08-20","days",7,7,"days","Log in on seven days for Star Rail Special Passes.",0],
["h4","hsr","4.5","Aetherium Wars: Final Bout","ignored","2026-08-20","2026-09-23","2026-08-21","days",0,10,"days","A creature-battling event with a separate shop.",1],
["h5","hsr","4.5","Realm of the Strange","done","2026-09-01","2026-09-08","2026-09-07","days",3,3,"days","Triple relic drops from Caverns of Corrosion, a few times a day.",0],
["h6","hsr","4.5","Fate's Forge Trial","done","2026-09-05","2026-09-19","2026-09-18","amount",3000,3000,"points","Trial characters in staged fights; points unlock rewards.",0],
["h7","hsr","4.5","Boulder Town Market","expired","2026-09-09","2026-09-23","2026-09-23","amount",1800,2400,"coins","Run a market stall; coins buy materials in the event shop.",0],
["h8","hsr","4.6","Planar Fissure","done","2026-09-26","2026-10-03","2026-10-02","days",5,5,"days","Double rewards from Planar Ornament Extraction.",0],
["h1","hsr","4.6","Garden of Plenty","expired","2026-09-29","2026-10-06","2026-10-06","days",3,7,"days","Double rewards from Calyx, until the daily limit.",0],
["z3","zzz","2.2","Sweet Sweet Sweets","done","2026-08-20","2026-09-03","2026-09-02","days",7,7,"days","Bake with Bangboos on seven days for Polychromes.",0],
["z6","zzz","2.2","Ether Tide Login","done","2026-08-20","2026-09-02","2026-08-27","days",7,7,"days","Log in on seven days for Encrypted Master Tapes.",0],
["z4","zzz","2.2","Rally Racer Remix","ignored","2026-08-27","2026-09-10","2026-08-27","stages",0,6,"stages","A racing minigame in six tracks.",0],
["z1","zzz","2.2","Hollow Zero: Lost Void – Season 2","done","2026-08-29","2026-09-28","2026-09-27","stages",9,9,"tiers","A roguelite season with weekly score tiers; each tier pays out once.",0],
["z5","zzz","2.2","Bangboo Brawl","expired","2026-09-03","2026-09-17","2026-09-17","amount",1450,1500,"points","Team Bangboos up in auto-battles for event points.",0],
["z2","zzz","2.2","Hollow Zero: Lost Void – Season 3","done","2026-09-12","2026-10-12","2026-10-11","stages",9,9,"tiers","A roguelite season with weekly score tiers; each tier pays out once.",0],
["z7","zzz","2.3","Phaethon's Errands","done","2026-10-01","2026-10-13","2026-10-12","stages",5,5,"stages","Run five errands across New Eridu, one a day.",0],
["w2","wuwa","3.6","Tidebound Regatta","done","2026-08-21","2026-09-04","2026-09-03","stages",7,7,"races","Boat races along the coast; first clears pay Astrite.",0],
["w3","wuwa","3.6","Echoing Marsh Patrol","expired","2026-08-28","2026-09-11","2026-09-11","days",4,7,"days","Daily patrol missions through the marsh.",0],
["w4","wuwa","3.6","Lollo Logistics","done","2026-09-04","2026-09-18","2026-09-15","amount",2000,2000,"Credits","Deliver parcels around Lahai-Roi for Credits.",0],
["w6","wuwa","3.6","Waveplate Bonus Week","ignored","2026-09-15","2026-09-22","2026-09-15","days",0,7,"days","Double drops from Simulation Challenges.",0],
["w5","wuwa","3.6","Thousand Gateways","done","2026-09-11","2026-09-25","2026-09-24","stages",10,10,"floors","A ten-floor challenge tower with set teams.",0],
["w1","wuwa","3.7","Ascendant Aces","ignored","2026-10-01","2026-10-29","2026-10-03","stages",0,8,"rounds","A card battle event with a separate shop.",0],
["e2","endfield","1.2","Talos-II Survey Grant","done","2026-08-14","2026-08-28","2026-08-27","days",7,7,"days","Log in on seven days for Originium.",0],
["e3","endfield","1.2","Wuling Night Market","expired","2026-08-21","2026-09-11","2026-09-11","amount",4200,6000,"points","Trade at the night market stalls for event points.",1],
["e1","endfield","1.2","Frontier Logistics Drill","done","2026-09-04","2026-09-18","2026-09-16","stages",6,6,"stages","A logistics event in daily stages.",0],
["e4","endfield","1.3","Protocol Overload Exercise","ignored","2026-09-25","2026-10-09","2026-09-26","stages",0,5,"stages","Combat drills with rotating modifiers.",0],
["n2","nte","1.1","Hethereau Cup Kart","done","2026-08-28","2026-09-11","2026-09-10","stages",5,5,"races","Kart races through the city's districts.",0],
["n3","nte","1.1","City Pulse Sign-in","done","2026-08-28","2026-09-10","2026-09-05","days",7,7,"days","Sign in on seven days for Annulith.",0],
["n4","nte","1.1","Anomaly Night Shift","expired","2026-09-17","2026-10-01","2026-10-01","days",6,7,"days","Night-shift anomaly hunts, one a day.",0],
["n1","nte","1.1","Neon Nights Photo Hunt","ignored","2026-09-20","2026-10-04","2026-09-22","days",1,5,"days","Photograph neon signs around the city for event currency.",0],
["n5","nte","1.2","Pukaland Pre-show","done","2026-10-08","2026-10-14","2026-10-13","days",3,3,"days","A three-day warm-up for Pukaland.",0],
];
const daysBetween=(a,b)=>Math.round((Date.parse(b)-Date.parse(a))/DAY_MS);
const DAY_MS=864e5;
/** Which days of the run were ticked, deterministically, for the pips. */
function tickDays(id,span,got,status){
if(!got)return[];
let h=[...id].reduce((s,c)=>s*31+c.charCodeAt(0),7)>>>0;const rnd=()=>(h=(h*1103515245+12345)>>>0)/4294967296;
if(status==="done"&&got>=span*0.5)return Array.from({length:got},(_,i)=>i);
const set=new Set();while(set.size<Math.min(got,span)){set.add(Math.floor(rnd()*span))}
return [...set].sort((a,b)=>a-b);
}
const ARCHIVE_SEED=ARC_ROWS.map(([id,game,ver,title,status,start,end,at,t,got,need,unit,desc,moved])=>{
const span=Math.max(1,daysBetween(start,end));
return{id,game,ver,title,status,start,end,at,desc,moved:!!moved,
prog:{t,got,need,unit,days:t==="days"?tickDays(id,span,got,status):[]},
src:game==="genshin"?"https://game8.co/games/Genshin-Impact/archives/297500":null};
});
const CHANGELOG=[
{date:"2026-10-16",title:"Routines get their own tab",kind:"New",body:"Dailies, weeklies, monthlies and endgame cycles moved out of the left column into a Routines tab. You choose which ones show in the Checklist and on the Timeline, one pin each.",items:["Pin a routine to the Checklist, the Timeline, or both","Each game has its own settings page, with its servers and reset time","Play a game on more than one server, with separate dailies for each","The Archive keeps what you finished or ignored, with the details it had"]},
{date:"2026-10-16",title:"Battle pass windows for Star Rail, ZZZ and Infinity Nikki",kind:"Games & sources",body:"Each battle pass shows the dates its season runs.",items:[]},
{date:"2026-10-16",title:"Endgame modes on their own cycles for Star Rail, ZZZ, Wuthering Waves and NTE",kind:"Games & sources",body:"Pure Fiction, Memory of Chaos, Apocalyptic Shadow, Shiyu Defense, Deadly Assault, Tower of Adversity, Beyond the Rails and the Tycoon Incentive Fund, each showing the days left in its cycle.",items:[]},
{date:"2026-10-16",title:"Neverness to Everness's Europe day rolls at the game's own reset",kind:"Fixed",body:"",items:[]},
{date:"2026-10-14",title:"Choose premium pulls only, or standard-banner pulls too",kind:"New",body:"",items:[]},
];
const BATCH="2026-10-16";
// ============ clocks: per server ============
const DAY=864e5, H=36e5;
// Server offsets for the demo; the real app reads games.ts (resetOffsets, resetHourLocal).
const SV={asia:{name:"Asia",short:"Asia",off:8},america:{name:"America",short:"NA",off:-5},europe:{name:"Europe",short:"EU",off:1}};
const YOUR_OFF=2, YOUR_TZ="Copenhagen, UTC+2";
const rh=r=>r?.resetHour??4;
const resetUTC=(srv,h=4)=>((h-SV[srv].off)%24+24)%24;
function automaticNextDaily(srv,h=4){const d=new Date(NOW);d.setUTCHours(resetUTC(srv,h),0,0,0);let t=+d;if(t<=NOW)t+=DAY;return t}
const localDow=(t,srv)=>new Date(t+SV[srv].off*H).getUTCDay();
function nextWeekly(srv,dow=1,h=4){let t=nextDaily(srv,h);while(localDow(t,srv)!==dow)t+=DAY;return t}
const monthAt=(y,m,d,srv,h)=>Date.UTC(y,m,d,0,Math.round(h*60))-SV[srv].off*H;
function nextMonthly(srv,day=1,h=4){const n=new Date(NOW+SV[srv].off*H);let t=monthAt(n.getUTCFullYear(),n.getUTCMonth(),day,srv,h);if(t<=NOW)t=monthAt(n.getUTCFullYear(),n.getUTCMonth()+1,day,srv,h);return t}
function cycleNext(p,srv,h=4){const a=Date.parse(p.from+"T00:00:00Z")+h*H-SV[srv].off*H,L=p.days*DAY;return a+Math.floor((NOW-a)/L+1)*L}
/** The period a routine is in right now, on one server: [start, end). end is the reset. */
function automaticPeriodOf(r,srv){const p=r.period,h=rh(r);
if(p.unit==="day"){const e=nextDaily(srv,h);return{start:e-DAY,end:e}}
if(p.unit==="week"){const e=nextWeekly(srv,p.dow??1,h);return{start:e-7*DAY,end:e}}
if(p.unit==="month"){const e=nextMonthly(srv,p.startsOn||1,h);const d=new Date(e+SV[srv].off*H);return{start:monthAt(d.getUTCFullYear(),d.getUTCMonth()-1,p.startsOn||1,srv,h),end:e}}
const e=cycleNext(p,srv,h);return{start:e-p.days*DAY,end:e}}
const resetOf=(r,srv)=>periodOf(r,srv).end;
function dur(ms){if(ms<H)return Math.max(1,Math.round(ms/6e4))+"m";const d=Math.floor(ms/DAY),h=Math.floor(ms%DAY/H),m=Math.floor(ms%H/6e4);return d?`${d}d ${h}h`:`${h}h ${String(m).padStart(2,"0")}m`}
function heat(ms){return ms==null?"unk":ms<DAY?"critical":ms<3*DAY?"soon":ms<7*DAY?"near":"calm"}
const HEAT={calm:"var(--calm)",near:"var(--near)",soon:"var(--soon)",critical:"var(--critical)"};
const fmt=n=>Number(n).toLocaleString("en-GB");
const oldYourTime=t=>new Date(t+YOUR_OFF*H).toLocaleString("en-GB",{weekday:"short",hour:"2-digit",minute:"2-digit",timeZone:"UTC"});
const oldDateStr=t=>new Date(t+YOUR_OFF*H).toLocaleString("en-GB",{weekday:"short",day:"numeric",month:"short",hour:"2-digit",minute:"2-digit",timeZone:"UTC"});
const dayStr=s=>new Date(Date.parse(s+"T12:00:00Z")).toLocaleDateString("en-GB",{day:"numeric",month:"short",timeZone:"UTC"});
const PERIOD_LABEL={day:"Daily",week:"Weekly",month:"Monthly",cycle:"Cycle"};
const WEEKDAYS=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
const ord=n=>n+(n%10===1&&n!==11?"st":n%10===2&&n!==12?"nd":n%10===3&&n!==13?"rd":"th");
const hh=h=>String(h).padStart(2,"0")+":00";
function periodWords(p,h=4){const at=` at ${hh(h)}`;return p.unit==="day"?`Every day${at}`:p.unit==="week"?`Every ${WEEKDAYS[p.dow??1]}${at}`:p.unit==="month"?`Every month on the ${ord(p.startsOn||1)}${at}`:`Every ${p.days} days${at}`}
// ============ state ============
const DEFAULT_GAMES=["genshin","hsr","zzz","wuwa","endfield","nte"];
const DEFAULT_FOLLOWED=["routine:genshin:daily","routine:genshin:daily@asia","routine:hsr:daily","routine:zzz:daily","routine:wuwa:daily","routine:genshin:weekly-bosses","routine:genshin:weekly-bosses@asia","routine:genshin:battle-pass-exp","routine:genshin:spiral-abyss","routine:hsr:echo-of-war","routine:hsr:points","routine:hsr:pure-fiction","routine:hsr:memory-of-chaos","routine:zzz:ridu-weekly","routine:zzz:notorious-hunt","routine:zzz:shiyu-defense","routine:wuwa:weekly-activities","routine:wuwa:tower-of-adversity","routine:endfield:weekly-routine","routine:nte:beyond-the-rails","mytask:a1"];
const DEFAULT_PINS={"routine:genshin:daily":{c:1},"routine:genshin:daily@asia":{c:1},"routine:hsr:daily":{c:1},"routine:zzz:daily":{c:1},"routine:wuwa:daily":{c:1},
"routine:genshin:weekly-bosses":{c:1,t:1},"routine:genshin:weekly-bosses@asia":{c:1},"routine:genshin:spiral-abyss":{t:1},
"routine:hsr:echo-of-war":{c:1,t:1},"routine:hsr:points":{c:1},"routine:hsr:pure-fiction":{c:1,t:1},"routine:hsr:memory-of-chaos":{t:1},
"routine:zzz:ridu-weekly":{c:1,t:1},"routine:zzz:shiyu-defense":{c:1,t:1},"routine:wuwa:tower-of-adversity":{t:1},"routine:endfield:weekly-routine":{c:1},"routine:nte:beyond-the-rails":{t:1},"mytask:a1":{c:1}};
const OWN_ROUTINES_SEED=[{id:"mytask:a1",game:"endfield",title:"Protocol Space weekly clear",period:{unit:"week",dow:1},measure:{kind:"days",required:1},src:"Yours",own:true}];
const PROGRESS_SEED={"routine:genshin:daily":1,"routine:hsr:daily":1,"routine:genshin:weekly-bosses":2,"routine:genshin:weekly-bosses@asia":3,"routine:genshin:battle-pass-exp":10000,"routine:hsr:echo-of-war":1,"routine:hsr:points":11200,"routine:zzz:ridu-weekly":1900,"routine:zzz:notorious-hunt":3,"routine:wuwa:weekly-activities":2400,"routine:endfield:weekly-routine":6};
const clone=o=>JSON.parse(JSON.stringify(o));
const BLANK_FORM=()=>({title:"",game:"zzz",srv:null,unit:"week",dow:1,dom:1,days:14,from:"2026-10-16",hourMode:"game",hour:4,done:"once",nDays:3,target:2100,unit2:"points",note:"",link:"",pc:true,pt:false,example:null});
function fresh(){return{
tab:"checklist", arcV:2, defaultView:"checklist", focus:"all", open:{}, group:"horizon", drawer:null, ob:null, why:false,
games:[...DEFAULT_GAMES], servers:Object.fromEntries(GAMES.map(g=>[g.id,g.id==="genshin"?["europe","asia"]:["europe"]])), region:"europe",
hideEvents:new Set(), followed:new Set(DEFAULT_FOLLOWED), pins:clone(DEFAULT_PINS), own:clone(OWN_ROUTINES_SEED),
ownEvents:[...OWN_EVENTS_SEED], progress:{...PROGRESS_SEED}, archive:clone(ARCHIVE_SEED), archived:new Set(),
q:"", editing:null, menu:null, sheet:null, arcSheet:null, clog:false, theme:"dark", showUpcoming:false, standardPulls:false,
arcGame:"all", arcStatus:"all", toast:null, sel:"genshin", allGames:false, newPin:{c:true,t:false},
wnSeen:true, rtHint:false, tlMode:"bygame", tlStrip:false, addSrv:false, form:BLANK_FORM(),
}}
let S=fresh();
const all=()=>[...BUILTINS,...S.own];
const byId=id=>all().find(r=>r.id===id);
const first=g=>S.servers[g][0];
const multi=g=>S.servers[g].length>1;
const keyOf=(id,srv,g)=>srv===first(g)?id:`${id}@${srv}`;
function parseKey(k){const [id,srv]=k.split("@");const r=byId(id);if(!r)return null;return{r,srv:srv||first(r.game),key:k}}
/** Every followed routine on every server it is played on: the one list every view queries. */
const insts=()=>[...S.followed].map(parseKey).filter(i=>i&&S.games.includes(i.r.game)&&S.servers[i.r.game].includes(i.srv));
const pinned=(k,p)=>!!S.pins[k]?.[p];
function setPin(k,p,v){S.pins[k]={...(S.pins[k]||{}),[p]:v?1:0}}
function follow(k,on,pins=S.newPin){if(on){S.followed.add(k);if(!S.pins[k])S.pins[k]={c:pins.c?1:0,t:pins.t?1:0}}else{S.followed.delete(k);delete S.pins[k]}}
const req=r=>r.measure.kind==="days"?r.measure.required:r.measure.target;
const val=i=>S.progress[i.key]||0;
const isMet=i=>val(i)>=req(i.r);
const reset=i=>resetOf(i.r,i.srv);
function counts(){const f=insts();return{total:f.length,left:f.filter(i=>!isMet(i)).length,c:f.filter(i=>pinned(i.key,"c")).length,t:f.filter(i=>pinned(i.key,"t")).length}}
const laneName=(g,srv)=>multi(g)?`${G[g].short} · ${SV[srv].short}`:G[g].short;
const lanes=()=>S.games.flatMap(g=>S.servers[g].map(srv=>({g,srv})));
const round4PinnedIn=(g,srv,p)=>insts().filter(i=>i.r.game===g&&i.srv===srv&&pinned(i.key,p));
const liveEvents=()=>[...EVENTS,...S.ownEvents].filter(e=> (e.end===null||e.end>NOW)&&S.games.includes(e.game)&&!S.hideEvents.has(e.game)&&(!S.archived.has(e.id)||S.pending?.[e.id])&&(S.showUpcoming||e.start<=NOW));
// ============ render helpers ============
const esc=s=>String(s??"").replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
const gv=g=>`--g:${G[g]?.hue||"#9aa3b8"}`;
const ICON={
gear:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>',
dots:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><circle cx="5" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="19" cy="12" r="1.7"/></svg>',
x:'<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 6l12 12M18 6L6 18"/></svg>',
check:'<svg viewBox="0 0 24 24"><path d="M5 12.5l4.5 4.5L19 7"/></svg>',
chev:'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>',
right:'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6"/></svg>',
left:'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 6l-6 6 6 6"/></svg>',
plus:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6"><path d="M12 5v14M5 12h14"/></svg>',
list:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6h11M9 12h11M9 18h11"/><path d="M4 6h.01M4 12h.01M4 18h.01" stroke-width="3" stroke-linecap="round"/></svg>',
bars:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M4 6h9M8 12h12M4 18h7"/></svg>',
reset:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11a8 8 0 1 0-2.3 5.7"/><path d="M20 4v7h-7"/></svg>',
ignore:'<svg viewBox="0 0 24 24"><path d="M6 12h12"/></svg>',
ran:'<svg viewBox="0 0 24 24"><path d="M12 7v6M12 17v.01"/></svg>',
};
function meter(ev,n=24){if(ev.end==null)return `<div class="meter unknown">${"<i></i>".repeat(n)}</div>`;const frac=Math.max(0,(ev.end-NOW)/(ev.end-ev.start));const lit=Math.max(1,Math.round(frac*n));const h=HEAT[heat(ev.end-NOW)];return `<div class="meter" style="--h:${h}">${Array.from({length:n},(_,i)=>`<i class="${i<lit?"on":""}"></i>`).join("")}</div>`}
function focusRow(countFn){return `<div class="focusrow" role="group" aria-label="Focus on a game"><button data-focus="all" aria-pressed="${S.focus==="all"}">All<span class="n">${countFn("all")}</span></button>${S.games.map(g=>`<button data-focus="${g}" aria-pressed="${S.focus===g}" style="--fh:${G[g].hue}">${G[g].short}<span class="n">${countFn(g)}</span></button>`).join("")}</div>`}
const progText=i=>{const r=i.r,v=val(i);return r.measure.kind==="amount"?`${fmt(v)}/${fmt(r.measure.target)}${r.measure.unit?" "+r.measure.unit:""}`:r.measure.required>1?`${Math.min(v,r.measure.required)}/${r.measure.required} days`:isMet(i)?"done":"to do"};
// ============ chrome ============
const JUMPS=[["checklist","Checklist"],["focused","Checklist focused"],["routines","Routines"],["add","Add & choose"],["create","Create routine"],["settings","Settings"],["gamepage","Settings game page"],["onboard","Onboarding"],["returning","Returning user"],["archive","Archive"],["archivedetail","Archive detail"],["timeline","Timeline"]];
let lastJump="checklist";
function round4Chrome(){document.getElementById("chrome").innerHTML=`<b>Round 4 · Combined review</b><span class="lbl">Archive:</span>${[[1,"1 · Ledger with a story"],[2,"2 · By game and version"],[3,"3 · History view"]].map(([v,l])=>`<button data-arcv="${v}" aria-pressed="${S.arcV===v}">${l}</button>`).join("")}<span class="sep"></span>${JUMPS.map(([k,l])=>`<button data-jump="${k}" aria-pressed="${lastJump===k}">${l}</button>`).join("")}<span class="sep"></span><span class="lbl">Default view</span><button data-dv="checklist" aria-pressed="${S.defaultView==="checklist"}">Checklist</button><button data-dv="timeline" aria-pressed="${S.defaultView==="timeline"}">Timeline</button><span class="sep"></span><button data-act="why" aria-pressed="${S.why}">Why / foundation</button><button data-act="reset">Reset demo</button>`}
function setFocus(g){S.focus=g;if(g==="all")S.open={}}
function jump(k){lastJump=k;S.drawer=null;S.ob=null;S.sheet=null;S.arcSheet=null;S.clog=false;S.menu=null;
if(k==="checklist"){S.tab="checklist";setFocus("all")}
if(k==="focused"){S.tab="checklist";setFocus("genshin")}
if(k==="routines")S.tab="routines";
if(k==="add"){S.tab="routines";S.drawer="library"}
if(k==="create"){S.tab="routines";S.drawer="create";S.form=BLANK_FORM()}
if(k==="settings"){S.tab="settings";S.sel="general"}
if(k==="gamepage"){S.tab="settings";S.sel="genshin"}
if(k==="archive"){S.tab="archive";S.arcGame="all";S.arcStatus="all"}
if(k==="archivedetail"){S.tab="archive";S.arcSheet=(S.archive.find(a=>a.id==="n4")||S.archive[0])?.id}
if(k==="timeline")S.tab="timeline";
if(k==="returning"){S.tab=S.defaultView;S.wnSeen=false;setFocus("all")}
if(k==="onboard"){S.ob={step:0,games:[],comp:{},view:"checklist"}}
}
// ============ shell ============
function tabs(){const c=counts();const t=[["checklist","Checklist",liveEvents().length],["timeline","Timeline",""],["routines","Routines",c.total?`${c.left} left`:""],["archive","Archive",S.archive.length]];
return `<header class="top"><div class="mark">EVENT<span>CLOCK</span></div><nav class="tabs" aria-label="Sections">${t.map(([k,l,n])=>`<button data-tab="${k}" ${S.tab===k?'aria-current="page"':""}>${l}${n!==""?` <span class="n">${n}</span>`:""}</button>`).join("")}<button class="gear" data-tab="settings" ${S.tab==="settings"?'aria-current="page"':""}>${ICON.gear}Settings</button></nav></header>`}
const footer=()=>`<footer class="foot"><span>Dates in your time (${YOUR_TZ}). Every event links to its source.</span><button data-act="clog">Changelog</button><button data-act="noop">Source code</button><button data-act="noop">Report a problem</button></footer>`;
/** What's new: once per batch, on whichever of Checklist and Timeline the reader opens. */
function oldWhatsNew(){if(S.wnSeen)return"";const top=CHANGELOG[0];const rest=CHANGELOG.filter(c=>c.date===BATCH);
return `<section class="wn" aria-label="What's new"><div class="wn-h"><span class="k">What's new</span><span class="d">${dayStr(BATCH)} · ${rest.length} changes</span><h3>${esc(top.title)}</h3></div>
<ul>${[...top.items.map(t=>["New",t]),...rest.slice(1).map(c=>[c.kind,c.title])].slice(0,5).map(([k,t])=>`<li><span class="kind">${esc(k)}</span><span>${esc(t)}</span></li>`).join("")}</ul>
<div class="acts"><button class="pillbtn" data-act="wngot">Got it</button><button class="btn link" data-act="clog">See all</button>${S.defaultView==="timeline"&&S.tab==="timeline"?`<span style="font-size:12px;color:var(--faint);margin-left:auto">Shown here too, because Timeline is your default view.</span>`:""}</div></section>`}
// ============ checklist ============
function laneSummary(rs){const bits=[];const by=u=>rs.filter(i=>i.r.period.unit===u);
const day=by("day"),wk=by("week"),mo=by("month"),cy=by("cycle");
if(day.length)bits.push(day.some(i=>!isMet(i))?`<b>dailies to do</b>`:"dailies done");
if(wk.length)bits.push(`${wk.filter(isMet).length} of ${wk.length} this week`);
if(mo.length)bits.push(`${mo.filter(isMet).length}/${mo.length} month`);
if(cy.length){const s=cy.filter(i=>!isMet(i)).sort((a,b)=>reset(a)-reset(b))[0];bits.push(s?`cycle ${Math.ceil((reset(s)-NOW)/DAY)}d`:"cycles done")}
return bits.join(" · ")}
function miniItem(i){const r=i.r,met=isMet(i),v=val(i);
const when=r.period.unit==="cycle"?`${dur(reset(i)-NOW)} left in cycle`:r.period.unit==="day"?`resets in ${dur(reset(i)-NOW)}`:r.period.unit==="week"?"this week":"this month";
const ctl=r.measure.kind==="days"?`<button class="tick" data-tick="${i.key}" aria-pressed="${met}" aria-label="${esc(r.title)} done">${r.measure.required>1?`<span class="part">${Math.min(v,r.measure.required)}/${r.measure.required}</span>`:""}<span class="box">${ICON.check}</span></button>`
:`<span class="mini-amt"><input type="number" min="0" inputmode="numeric" data-mini="${i.key}" value="${v}" aria-label="${esc(r.title)} amount">/ ${fmt(r.measure.target)}</span>`;
return `<div class="gitem ${met?"met":""}"><span class="t">${esc(r.title)}${r.own?'<span class="tag own">yours</span>':""}<small>${when}${r.measure.unit?` · ${esc(r.measure.unit)}`:""}</small></span>${ctl}</div>`}
function round4Rail(){
const ls=lanes().filter(l=>pinnedIn(l.g,l.srv,"c").length&&(S.focus==="all"||S.focus===l.g));const c=counts();
const head=`<div class="pinned-h"><span class="lbl">Routines<small>${S.focus==="all"?"pinned to the checklist":"for "+G[S.focus].short}</small></span><button data-tab="routines">Edit pins</button></div>`;
if(!ls.length)return `<section class="pinned">${head}<p class="empty">${S.focus==="all"?"Nothing pinned to the checklist yet. Pin a routine on the Routines tab and it shows up here.":`No ${G[S.focus].short} routines pinned here. <button class="btn link" data-tab="routines" style="padding:0">Pin some</button>`}</p><div class="rail-foot"><span>${c.total} followed · ${c.left} left</span><button data-tab="routines">Routines tab</button></div></section>`;
return `<section class="pinned">${head}${S.focus==="all"?`<p class="hint">Focus on a game to open its routines.</p>`:""}
${ls.map(({g,srv})=>{const k=g+"@"+srv;const rs=pinnedIn(g,srv,"c").sort((a,b)=>reset(a)-reset(b));const open=S.focus!=="all"||S.open[k];
return `<div class="gblock ${open?"open":""}" style="${gv(g)}"><button class="gblock-h" data-expand="${k}" aria-expanded="${!!open}"><span class="rl"></span><span><span class="nm">${G[g].short}${multi(g)?` <span class="srv">· ${SV[srv].short}</span>`:""}</span><br><span class="sm">${laneSummary(rs)}</span></span><span class="chev">${ICON.chev}</span></button>${open?`<div class="gitems">${rs.map(miniItem).join("")}</div>`:""}</div>`}).join("")}
<div class="rail-foot"><span>${c.c} pinned of ${c.total} followed</span><button data-tab="routines">Routines tab</button></div></section>`}
function compactChecklist(){
const evs=liveEvents().sort((a,b)=>(a.end??9e15)-(b.end??9e15));
const live=evs.filter(e=>S.focus==="all"||e.game===S.focus);
const next=live.find(e=>e.end);const then=live.filter(e=>e!==next).slice(0,3);
const rail=`<aside class="rail">${next?`<section class="head"><p class="legend">Next to expire${S.focus!=="all"?" in "+G[S.focus].short:""}</p><h2>${esc(next.title)}</h2><div class="game" style="color:${G[next.game].hue}">${G[next.game].name}${next.own?" · yours":""}</div>
<div class="big cond" style="color:${HEAT[heat(next.end-NOW)]}">${dur(next.end-NOW)}<small>left</small></div><div class="exact">Ends ${dateStr(next.end)}</div>${meter(next)}
<ul class="then">${then.map(e=>`<li style="${gv(e.game)}"><span class="dot"></span><span class="t">${esc(e.title)}</span><span class="r" style="color:${e.end?HEAT[heat(e.end-NOW)]:"var(--faint)"}">${e.end?dur(e.end-NOW):"no end"}</span></li>`).join("")}</ul></section>`:`<section class="head"><p class="legend">Next to expire</p><p style="color:var(--muted)">Nothing running.</p></section>`}
${railPinned()}</aside>`;
const rows=live.map(e=>{const ms=e.end?e.end-NOW:null;const h=heat(ms);return `<tr data-ev="${e.id}" tabindex="0" role="button" style="${gv(e.game)}"><td class="gc"><span class="gcell">${G[e.game].short}</span></td><td class="tcell"><span class="gsm">${G[e.game].short}</span>${esc(e.title)}${e.own?'<span class="tag own">yours</span>':""}</td><td class="mcell">${meter(e,30)}</td><td class="cd ${h}">${ms==null?"End unknown":dur(ms)}</td></tr>`}).join("");
const cnt=g=>evs.filter(e=>g==="all"||e.game===g).length;
return `<div class="ev">${rail}<section class="main">${whatsNew()}<div class="ledger-h"><div><h2>Running now</h2><div class="sub">${live.length} events${S.focus==="all"?` in ${S.games.length} games`:""} · sorted by what ends first</div></div><div class="seg"><button aria-pressed="true">Ending soonest</button><button aria-pressed="false" data-act="noop">Doing first</button></div></div>
${focusRow(cnt)}
<table class="led"><thead><tr><th class="h-game">Game</th><th>Event</th><th class="h-meter">Time left</th><th style="text-align:right">Ends in</th></tr></thead><tbody>${rows||`<tr><td colspan="4" style="color:var(--faint)">Nothing running for this game.</td></tr>`}</tbody></table>
<div class="more">Done or ignored events leave this list and wait in the <button data-tab="archive">Archive</button>.</div></section></div>`}
// ============ routines ============
function pinBtn(i,p){const on=pinned(i.key,p);const what=p==="c"?"Checklist":"Timeline";return `<button class="pin" data-pin="${i.key}|${p}" aria-pressed="${on}" aria-label="${on?"Unpin from":"Pin to"} ${what}: ${esc(i.r.title)}" title="${on?"Shown in":"Show in"} ${what}">${p==="c"?ICON.list:ICON.bars}</button>`}
function routineRow(i){const r=i.r,met=isMet(i),v=val(i);let prog;
if(r.measure.kind==="days"){prog=`<button class="tick" data-tick="${i.key}" aria-pressed="${met}"><span class="box">${ICON.check}</span>${r.measure.required>1?`${Math.min(v,r.measure.required)} of ${r.measure.required} days`:met?"Done":"To do"}</button>`}
else if(S.editing===i.key){prog=`<form class="amt" data-amtform="${i.key}"><input type="number" min="0" name="v" value="${v}" autofocus aria-label="Amount"><span class="num">/ ${fmt(r.measure.target)}</span><button class="btn sm" type="submit">Save</button></form>`}
else{const f=Math.min(1,v/r.measure.target);prog=`<button class="amt" data-amt="${i.key}" title="Enter an amount"><span class="bar ${f>=1?"full":""}"><i style="width:${f*100}%"></i></span><span class="num"><b>${fmt(v)}</b> / ${fmt(r.measure.target)} ${esc(r.measure.unit||"")}</span></button>`}
const cyc=r.period.unit==="cycle"?`<span class="cyc">${dur(reset(i)-NOW)} left</span>`:S.group==="game"?`<span class="cyc">${PERIOD_LABEL[r.period.unit]}</span>`:"";
const menu=S.menu===i.key?`<div class="menu" role="menu">${r.measure.kind==="amount"?`<span role="menuitem" data-amt="${i.key}">Edit amount</span>`:""}<span role="menuitem" data-act="target">Change target</span>${r.builtin?`<span role="menuitem" data-act="mine" data-id="${i.key}">Make it mine</span>`:`<span role="menuitem" data-act="editown">Edit</span>`}<span role="menuitem" class="danger" data-unfollow="${i.key}">${r.own?"Delete":"Unfollow"}</span></div>`:"";
return `<tr class="${met?"met":""}" style="${gv(r.game)}"><td><span class="gcell"><span>${G[r.game].short}${multi(r.game)?`<small>${SV[i.srv].name}</small>`:""}</span></span></td><td class="rtitle"><span class="gsm">${laneName(r.game,i.srv)}</span>${esc(r.title)}${r.own?'<span class="tag own">yours</span>':""}</td><td class="prog">${prog}</td><td class="c-cyc">${cyc}</td><td class="c-pin">${pinBtn(i,"c")}</td><td class="c-pin">${pinBtn(i,"t")}</td><td class="c-k"><span class="kebab" role="button" tabindex="0" data-menu="${i.key}" aria-label="More for ${esc(r.title)}" aria-expanded="${S.menu===i.key}">${ICON.dots}${menu}</span></td></tr>`}
const rtHead=()=>`<thead><tr><th class="h-g">Game</th><th class="h-t">Routine</th><th class="h-prog">Progress</th><th class="c-cyc"></th><th class="c-pin"><span class="pinlbl">${ICON.list}<span>Checklist</span></span></th><th class="c-pin"><span class="pinlbl">${ICON.bars}<span>Timeline</span></span></th><th class="c-k"></th></tr></thead>`;
function bulkPins(tag,rs){if(!rs.length)return"";const allC=rs.every(i=>pinned(i.key,"c")),allT=rs.every(i=>pinned(i.key,"t"));return `<div class="bulkpins"><button data-bulkpin="${tag}|c|${allC?0:1}">${allC?"Unpin all from":"Pin all to"} checklist</button><button data-bulkpin="${tag}|t|${allT?0:1}">${allT?"Unpin all from":"Pin all to"} timeline</button></div>`}
function baseRoutinesScreen(){
const f=insts();const c=counts();
const top=`<div class="rt-top"><h1>Routines</h1><div class="sum">${f.length?`<b>${c.left}</b> of ${c.total} still to do. <b>${c.c}</b> pinned to the checklist, <b>${c.t}</b> to the timeline. The rest live only here.`:"Things that reset: dailies, weeklies, monthlies and endgame cycles."}</div>${f.length?`<div class="seg"><button data-group="horizon" aria-pressed="${S.group==="horizon"}">By reset</button><button data-group="game" aria-pressed="${S.group==="game"}">By game</button></div>`:""}<button class="btn" data-act="create">Create your own</button><button class="btn solid" data-act="library">Browse the library</button></div>`;
const hint=S.rtHint?`<div class="hintbar"><p><b>Two pins on every row.</b> ${ICON.list} puts a routine in the Checklist's left column, ${ICON.bars} puts it on the Timeline. Unpinned ones live only here. You've seen both views now, so pick what you want in front of you.</p><button class="pillbtn" data-act="rthint">Got it</button></div>`:"";
if(!f.length)return top+hint+`<div class="empty-state"><h2>You're not following any routines yet</h2><p>Pick the dailies, weekly bosses, shop restocks and endgame modes you actually do. Then pin the ones you want to see in the checklist or on the timeline.</p><button class="btn solid" data-act="library">Browse the library</button> <button class="btn link" data-act="create">Create your own</button></div>`;
if(S.group==="game"){
return top+hint+S.games.filter(g=>f.some(i=>i.r.game===g)).map(g=>{const rs=f.filter(i=>i.r.game===g);const left=rs.filter(i=>!isMet(i)).length;
const body=S.servers[g].map(srv=>{const sr=rs.filter(i=>i.srv===srv).sort((a,b)=>reset(a)-reset(b));if(!sr.length)return"";return `${multi(g)?`<div class="subsrv">${SV[srv].name} server · daily reset in ${dur(nextDaily(srv)-NOW)}</div>`:""}<table class="rt">${rtHead()}<tbody>${sr.map(routineRow).join("")}</tbody></table>`}).join("");
return `<section class="band" id="band-${g}"><div class="band-h" style="${gv(g)}"><div><h2 style="color:var(--g)">${G[g].name}</h2><div class="meta">${left} of ${rs.length} to do · ${rs.filter(i=>pinned(i.key,"c")).length} in checklist · ${rs.filter(i=>pinned(i.key,"t")).length} on timeline${multi(g)?` · ${S.servers[g].length} servers`:""}</div>${bulkPins("g:"+g,rs)}</div><div class="reset"><div class="k">Daily reset${multi(g)?` (${SV[first(g)].short})`:""}</div><div class="v cond">${dur(nextDaily(first(g))-NOW)}</div></div></div>${body}</section>`}).join("");
}
const bands=[["day","Today","Daily chores, on each game's own reset"],["week","This week","Rolls with each game's Monday reset"],["month","This month","Shop restocks and monthly modes"],["cycle","Cycles","Endgame modes on their own clocks"]];
const order=i=>S.games.indexOf(i.r.game)*10+S.servers[i.r.game].indexOf(i.srv);
return top+hint+bands.map(([u,name,meta])=>{const rs=f.filter(i=>i.r.period.unit===u).sort((a,b)=>u==="cycle"?reset(a)-reset(b):order(a)-order(b));const left=rs.filter(i=>!isMet(i)).length;
const nxt=rs.length?Math.min(...rs.map(reset)):null;const ms=nxt?nxt-NOW:0;
const col=!rs.length||!left?"var(--faint)":ms<3*H?"var(--critical)":ms<DAY&&u!=="day"?"var(--soon)":"var(--ink)";
return `<section class="band"><div class="band-h"><div><h2>${name}</h2><div class="meta">${meta} · ${rs.length?`${left} of ${rs.length} to do`:"none followed"}</div>${bulkPins("u:"+u,rs)}</div><div class="reset"><div class="k">${u==="cycle"?"Next cycle ends":"Next reset in"}</div><div class="v cond" style="color:${col}">${nxt?dur(ms):"—"}</div></div></div>${rs.length?`<table class="rt">${rtHead()}<tbody>${rs.map(routineRow).join("")}</tbody></table>`:`<div class="empty-band">Nothing here. <button class="btn link" data-act="library">Add from the library</button></div>`}</section>`}).join("");
}
// ============ drawer: library ============
function libraryDrawer(){
const q=S.q.trim().toLowerCase();
const match=r=>!q||r.title.toLowerCase().includes(q)||G[r.game].name.toLowerCase().includes(q);
const vis=r=>S.standardPulls||r.pulls!=="standard";
const group=g=>{const rs=BUILTINS.filter(r=>r.game===g&&match(r)&&vis(r));if(!rs.length)return"";
const srvs=S.games.includes(g)?S.servers[g]:["europe"];const isOn=(r,srv)=>S.followed.has(keyOf(r.id,srv,g));
const n=rs.filter(r=>srvs.some(s=>isOn(r,s))).length;
const weekly=rs.filter(r=>r.period.unit==="week"),cyc=rs.filter(r=>r.period.unit==="cycle");
const allOn=list=>list.every(r=>srvs.every(s=>isOn(r,s)));
return `<section class="lg" style="${gv(g)}"><div class="lg-h"><span class="nm">${G[g].name}</span><span class="cnt">${n} of ${rs.length} followed${srvs.length>1?` · ${srvs.length} servers`:""}</span></div>
<div class="bulk">${weekly.length?`<button data-bulk="${g}|week">${allOn(weekly)?"Unfollow":"Follow"} all weeklies</button>`:""}${cyc.length?`<button data-bulk="${g}|cycle">${allOn(cyc)?"Unfollow":"Follow"} all cycles</button>`:""}<button data-bulk="${g}|all">${allOn(rs)?"Unfollow everything":"Follow everything"}</button></div>
${rs.map(r=>`<div class="li"><div><div class="t">${esc(r.title)}</div><div class="d">${periodWords(r.period)} · ${r.measure.kind==="amount"?`${fmt(r.measure.target)} ${esc(r.measure.unit||"")}`:"tick when done"}${r.pulls==="standard"?" · standard banner":""} · ${esc(r.src)}</div></div>${srvs.length>1?`<div class="fols">${srvs.map(s=>`<button class="fol" data-follow="${keyOf(r.id,s,g)}" aria-pressed="${isOn(r,s)}" aria-label="${isOn(r,s)?"Unfollow":"Follow"} on ${SV[s].name}">${SV[s].short}</button>`).join("")}</div>`:`<button class="fol" data-follow="${keyOf(r.id,srvs[0],g)}" aria-pressed="${isOn(r,srvs[0])}">${isOn(r,srvs[0])?"Following":"Follow"}</button>`}</div>`).join("")}</section>`};
const others=GAMES.map(g=>g.id).filter(g=>!S.games.includes(g));
return `<div class="scrim" data-act="closedrawer"></div><aside class="drawer" role="dialog" aria-label="Routine library"><div class="drawer-h"><div class="r1"><h2>Library</h2><button class="close" data-act="closedrawer" aria-label="Close">${ICON.x}</button></div>
<button class="mkown" data-act="create"><span class="plus">${ICON.plus}</span><span><b>Create your own routine</b><small>Anything the library doesn't have: a guild run, a shop you buy from, a mode on its own clock.</small></span><span class="go">${ICON.right}</span></button>
<p>Or follow built-ins for your games, each from a dated source. What you follow lands on the Routines tab.</p><input type="search" id="libq" placeholder="Search routines or games" value="${esc(S.q)}" aria-label="Search the library">
<div class="newpin"><span>New follows also show in</span><label><input type="checkbox" data-newpin="c" ${S.newPin.c?"checked":""}>Checklist</label><label><input type="checkbox" data-newpin="t" ${S.newPin.t?"checked":""}>Timeline</label></div></div>
<div class="drawer-b">${S.games.map(group).join("")}${others.length?`<details class="others"><summary>${others.length} games you don't follow</summary>${others.map(group).join("")}</details>`:""}</div></aside>`;
}
// ============ drawer: create your own ============
const EXAMPLES=[
{id:"guild",label:"Weekly guild run",f:{title:"Guild co-op clear",unit:"week",dow:5,done:"once",hourMode:"game"},hint:"Ticked once a week, on the day your group plays."},
{id:"login",label:"Daily login",f:{title:"Log in for the daily reward",unit:"day",done:"once"},hint:"A tick a day on the game's reset."},
{id:"shop",label:"Monthly shop restock",f:{title:"Buy the monthly pull bundle",unit:"month",dom:1,done:"amount",target:5,unit2:"pulls"},hint:"Counts what you bought against the stock."},
{id:"cycle",label:"Endgame cycle",f:{title:"Endgame cycle",unit:"cycle",days:14,from:"2026-10-16",done:"once"},hint:"Fill in a day a cycle began; how often it comes back does the rest."},
{id:"stamina",label:"Weekly points target",f:{title:"Weekly activity points",unit:"week",dow:1,done:"amount",target:2100,unit2:"points"},hint:"Enter your number as you go."},
];
function formRoutine(F){const g=F.game,srv=F.srv&&S.servers[g]?.includes(F.srv)?F.srv:first(g);
const period=F.unit==="day"?{unit:"day"}:F.unit==="week"?{unit:"week",dow:+F.dow}:F.unit==="month"?{unit:"month",startsOn:+F.dom}:{unit:"cycle",days:Math.max(2,+F.days||14),from:F.from||"2026-10-16"};
const measure=F.done==="once"?{kind:"days",required:1}:F.done==="ndays"?{kind:"days",required:Math.max(1,+F.nDays||1)}:{kind:"amount",target:Math.max(1,+F.target||1),unit:F.unit2};
return{game:g,srv,title:F.title.trim()||"Untitled routine",period,measure,resetHour:F.hourMode==="custom"?+F.hour:4}}
function previewLine(F){const r=formRoutine(F);const p=periodOf(r,r.srv);
const what=r.measure.kind==="amount"?`reach <b>${fmt(r.measure.target)} ${esc(r.measure.unit||"")}</b>`:r.measure.required>1?`tick on <b>${r.measure.required} days</b>`:`tick it <b>once</b>`;
const per={day:"each day",week:"each week",month:"each month",cycle:"each cycle"}[r.period.unit];
return `Resets <b>${periodWords(r.period,rh(r)).replace(/^E/,"e")} ${SV[r.srv].name} server time</b> · ${yourTime(p.end)} your time · next in <b>${dur(p.end-NOW)}</b><br>Done when you ${what} ${per}.`}
function createDrawer(){const F=S.form;const g=F.game;const ex=EXAMPLES.find(e=>e.id===F.example);
const opt=(v,l,cur)=>`<option value="${v}" ${String(cur)===String(v)?"selected":""}>${l}</option>`;
return `<div class="scrim" data-act="closedrawer"></div><aside class="drawer" role="dialog" aria-label="Create your own routine"><div class="drawer-h"><div class="r1"><button class="back" data-act="library">${ICON.left}Library</button><button class="close" data-act="closedrawer" aria-label="Close">${ICON.x}</button></div>
<h2 style="margin-top:10px">Create your own routine</h2><p>It sits beside the built-ins on the Routines tab, marked as yours, and you can pin it like any other.</p></div>
<div class="drawer-b"><form id="createform" class="cf" style="${gv(g)}">
<div class="fld full"><span>Start from an example</span><div class="pills">${EXAMPLES.map(e=>`<button type="button" data-ex="${e.id}" aria-pressed="${F.example===e.id}">${e.label}</button>`).join("")}</div>${ex?`<span class="help">${esc(ex.hint)}</span>`:""}</div>
<label class="fld full"><span>What is it</span><input name="title" required placeholder="Guild co-op clear" value="${esc(F.title)}"></label>
<label class="fld"><span>Game</span><select name="game">${S.games.map(x=>opt(x,G[x].name,g)).join("")}</select></label>
${multi(g)?`<label class="fld"><span>Server</span><select name="srv">${S.servers[g].map(s=>opt(s,SV[s].name,F.srv||first(g))).join("")}</select></label>`:`<div class="fld"><span>Server</span><span class="help" style="padding-top:6px">${SV[first(g)].name}, the one you play ${G[g].short} on.</span></div>`}
<div class="fld full"><span>Repeats</span><div class="pills">${[["day","Every day"],["week","Every week"],["month","Every month"],["cycle","Every N days"]].map(([u,l])=>`<button type="button" data-fu="${u}" aria-pressed="${F.unit===u}">${l}</button>`).join("")}</div>
<div class="row" style="margin-top:8px">${F.unit==="week"?`on <select name="dow">${[1,2,3,4,5,6,0].map(d=>opt(d,WEEKDAYS[d],F.dow)).join("")}</select>`:F.unit==="month"?`on the <select name="dom">${Array.from({length:28},(_,i)=>opt(i+1,ord(i+1),F.dom)).join("")}</select> of the month`:F.unit==="cycle"?`every <input type="number" name="days" min="2" max="366" value="${F.days}"> days, counted from <input type="date" name="from" value="${F.from}"> <span class="help">any day a cycle began</span>`:`<span class="help">Rolls at the game's daily reset.</span>`}</div></div>
<div class="fld full"><span>Resets at</span><div class="row"><label class="chk"><input type="radio" name="hourMode" value="game" ${F.hourMode==="game"?"checked":""}>The game's reset, 04:00 server time</label><label class="chk"><input type="radio" name="hourMode" value="custom" ${F.hourMode==="custom"?"checked":""}>Another hour</label>${F.hourMode==="custom"?`<select name="hour">${Array.from({length:24},(_,h)=>opt(h,hh(h)+" server time",F.hour)).join("")}</select>`:""}</div></div>
<div class="fld full"><span>Done when</span><div class="pills">${[["once","I tick it once"],["ndays","I tick it on several days"],["amount","I reach a number"]].map(([k,l])=>`<button type="button" data-fd="${k}" aria-pressed="${F.done===k}">${l}</button>`).join("")}</div>
<div class="row" style="margin-top:8px">${F.done==="ndays"?`on <input type="number" name="nDays" min="1" max="31" value="${F.nDays}"> days each ${F.unit==="cycle"?"cycle":F.unit}`:F.done==="amount"?`reach <input type="number" name="target" min="1" value="${F.target}"> <input class="unit" name="unit2" placeholder="points" value="${esc(F.unit2)}" aria-label="Unit">`:`<span class="help">One tick, then it waits for the reset.</span>`}</div></div>
<label class="fld"><span>Note <span style="text-transform:none;letter-spacing:0;font:400 12px Barlow;color:var(--faint)">optional</span></span><textarea name="note" placeholder="Bring a healer">${esc(F.note)}</textarea></label>
<label class="fld"><span>Link <span style="text-transform:none;letter-spacing:0;font:400 12px Barlow;color:var(--faint)">optional</span></span><input name="link" type="url" placeholder="https://" value="${esc(F.link)}"></label>
<div class="fld full"><span>Also show in</span><div class="row"><label class="chk"><input type="checkbox" name="pc" ${F.pc?"checked":""}>${ICON.list.replace("<svg",'<svg width="14" height="14"')} Checklist</label><label class="chk"><input type="checkbox" name="pt" ${F.pt?"checked":""}>${ICON.bars.replace("<svg",'<svg width="14" height="14"')} Timeline</label><span class="help">Always on the Routines tab.</span></div></div>
<div class="preview"><div class="k">Preview</div><div class="t">${esc(F.title.trim()||"Untitled routine")} <span style="color:var(--g);font-size:14px">· ${laneName(g,F.srv&&S.servers[g].includes(F.srv)?F.srv:first(g))}</span></div><div class="l" id="pvline">${previewLine(F)}</div></div>
<div class="cf-foot"><button class="btn solid" type="submit">Create and follow</button><button class="btn link" type="button" data-act="closedrawer">Cancel</button></div>
</form></div></aside>`;
}
// ============ settings ============
const COMP=[["events","Events","This game's events in Checklist and Timeline"],["dailies","Dailies","Chores that reset each game-day, such as daily commissions"],["weekly","Weeklies & monthlies","Weekly goals and monthly restocks, such as bosses and shop pulls"],["cycles","Cycles","Routines on a game-specific schedule, such as a 14-day endgame mode"]];
const kindOf=r=>r.period.unit==="day"?"dailies":r.period.unit==="cycle"?"cycles":"weekly";
const builtinsOf=(g,kind)=>BUILTINS.filter(r=>r.game===g&&kindOf(r)===kind&&(S.standardPulls||r.pulls!=="standard"));
function compState(g,kind){if(kind==="events")return S.hideEvents.has(g)?"off":"on";const rs=builtinsOf(g,kind);if(!rs.length)return"none";
const ks=rs.flatMap(r=>S.servers[g].map(s=>keyOf(r.id,s,g)));const n=ks.filter(k=>S.followed.has(k)).length;return n===0?"off":n===ks.length?"on":"some"}
function applyComp(g,kind,on){if(kind==="events"){on?S.hideEvents.delete(g):S.hideEvents.add(g);return 0}let n=0;
for(const r of builtinsOf(g,kind))for(const s of S.servers[g]){const k=keyOf(r.id,s,g);if(on&&!S.followed.has(k)){follow(k,true);n++}else if(!on&&S.followed.has(k)){follow(k,false);n++}}return n}
function gameLine(g){if(!S.games.includes(g))return"Not followed";const parts=COMP.filter(([k])=>{const s=compState(g,k);return s==="on"||s==="some"}).map(([k,l])=>k==="weekly"?"weeklies":l.toLowerCase());
return `${parts.length?parts.join(", "):"nothing"}${multi(g)?` · ${S.servers[g].map(s=>SV[s].short).join(" + ")}`:""}`}
const GENERAL=[["about","About Event Clock","Credits, sources and project"],["changelog","Changelog","Changes and release notes"],["sources","Data & sources","Freshness and source status"],["general","Region & time","Default server, your time zone"],["appearance","Appearance",""],["view","Default view",""],["lists","Lists",""],["own","Your own events and games",""],["backup","Backup",""]];
function settingsScreen(){
const fl=S.games,nf=GAMES.map(g=>g.id).filter(g=>!S.games.includes(g));
const row=g=>`<button class="srow ${S.games.includes(g)?"":"off"}" data-sel="${g}" aria-current="${S.sel===g}" style="${gv(g)}"><span class="r3"></span><span><span class="nm">${G[g].name}</span><span class="what">${gameLine(g)}</span></span>${ICON.right}</button>`;
const what=k=>k==="changelog"?"Changes and release notes":k==="backup"?"7 days · 4 weeks · 6 months":k==="about"?"Credits and project":k==="sources"?"Checks, ages and reasons":k==="general"?`${SV[S.region].name} · ${displayZone()}`:k==="appearance"?S.theme[0].toUpperCase()+S.theme.slice(1)+" · "+S.scheme:k==="view"?S.defaultView[0].toUpperCase()+S.defaultView.slice(1):k==="own"?`${S.ownEvents.length} event${S.ownEvents.length===1?"":"s"}`:k==="lists"?(S.showUpcoming?"Upcoming shown":"Running only"):"Save to a file";
return `<div class="settings"><nav class="slist" aria-label="Settings"><h4>General</h4>${GENERAL.map(([k,l])=>`<button class="srow gen" data-sel="${k}" aria-current="${S.sel===k}" style="--g:var(--rule-strong)"><span class="r3"></span><span><span class="nm">${l}</span><span class="what">${what(k)}</span></span>${ICON.right}</button>`).join("")}
<h4>Your games</h4>${fl.map(row).join("")}<h4>Other games</h4>${nf.slice(0,S.allGames?99:3).map(row).join("")}${!S.allGames&&nf.length>3?`<button class="moreg" data-act="allgames">Show ${nf.length-3} more games</button>`:""}</nav>
<div class="profile">${G[S.sel]?renderGamePage(S.sel):generalPane(S.sel)}</div></div>`;
}
/** A game's settings page. Swappable in a later round (game info, redeem codes). */
function originalGamePage(g){
const on=S.games.includes(g);
const head=`<div class="ptitle"><h2>${G[g].name}</h2><span class="studio">${G[g].studio}</span><label class="switch">Following<button class="sw" role="switch" data-followgame="${g}" aria-checked="${on}" aria-label="Follow ${G[g].name}"></button></label></div>`;
if(!on)return `<div style="${gv(g)}">${head}<p class="pdesc">You're not following ${G[g].name}, so none of its events or routines show anywhere. Follow it to set it up.</p></div>`;
const comp=COMP.map(([k,l,sub])=>{const st=compState(g,k);const n=k==="events"?liveEvents().filter(e=>e.game===g).length+" running":st==="none"?"none built in":`${builtinsOf(g,k).length} built in`;
return `<button data-comp="${g}|${k}" data-st="${st}" aria-pressed="${st==="on"}" ${st==="none"?"disabled":""}><span class="cbx">${st==="on"?ICON.check.replace("<svg",'<svg stroke="currentColor"'):""}</span><span class="ln">${l}</span><span class="ls">${sub}</span><span class="ls">${st==="some"?"Some, fine-tuned":n}</span></button>`}).join("");
const srvRows=S.servers[g].map((s,ix)=>{const nd=nextDaily(s);return `<div class="srvrow"><span class="tagn">${ix===0?"Main":"Also"}</span><div class="clock"><b>${SV[s].name}</b> · resets 04:00 server time · ${hh(new Date(nd+YOUR_OFF*H).getUTCHours())} your time · next in <span class="nx">${dur(nd-NOW)}</span><br><span style="color:var(--faint);font-size:12px">${ix===0?"Its dailies and routines are the ones you already had.":"Its own dailies and routines, ticked separately."}</span></div><div class="r"><select data-srvset="${g}|${ix}" aria-label="Server">${Object.keys(SV).filter(k=>k===s||!S.servers[g].includes(k)).map(k=>`<option value="${k}" ${k===s?"selected":""}>${SV[k].name}</option>`).join("")}</select>${ix>0?`<button class="btn sm" data-srvdel="${g}|${s}">Remove</button>`:""}</div></div>`}).join("");
const free=Object.keys(SV).filter(k=>!S.servers[g].includes(k));
return `<div style="${gv(g)}">${head}
<div class="grp"><h3>What to track<span>Any combination. Each one follows or unfollows that kind of built-in for ${G[g].short}${multi(g)?", on every server":""}.</span></h3>
<div class="comp" role="group" aria-label="What to track">${comp}</div>
<div class="compnote"><span>Fine-tune single routines, and where they show, on the Routines tab.</span><button data-managein="${g}">Fine-tune in Routines →</button></div></div>
<div class="grp"><h3>Servers<span>Play on more than one server? Add your other server to track both accounts separately.</span></h3>${srvRows}
${free.length?(S.addSrv===g?`<div class="srvrow"><span class="tagn">Add</span><div class="clock">Which server? Start with events only, then choose routines for this server. Nothing is copied.</div><div class="r">${free.map(k=>`<button class="btn sm" data-srvadd="${g}|${k}">${SV[k].name}</button>`).join("")}<button class="btn link" data-act="srvcancel">Cancel</button></div></div>`:`<button class="btn link" data-act="srvopen" data-g="${g}" style="padding-left:0;margin-top:6px">Track another server</button>`):""}</div>
<div class="grp"><h3>About this game</h3><div class="about"><div><div class="k">Studio</div>${G[g].studio}</div><div><div class="k">Event source</div><a href="#" data-act="noop">game8.co</a> · checked twice a day</div><div><div class="k">Daily chores</div>${esc(G[g].chores)}</div><div><div class="k">Links</div><a href="#" data-act="noop">Official site</a> · <a href="#" data-act="noop">Wiki</a></div></div>
<div class="soon-note">Placeholder. Redeem codes, version dates and more about the game come in a later round.</div></div></div>`;
}
function round4GeneralPane(k){
if(k==="general")return `<div><div class="ptitle"><h2>Region & time</h2></div><p class="pdesc">The server a game starts on when you follow it. Each game can differ, and can have more than one, on its own page.</p>
<div class="grp"><h3>Default server</h3><div class="seg" style="margin-top:12px">${Object.keys(SV).map(r=>`<button data-region="${r}" aria-pressed="${S.region===r}">${SV[r].name}</button>`).join("")}</div></div>
<div class="grp"><h3>Your time zone<span>Every date and countdown is shown in it.</span></h3><div class="trow"><span class="lbl">${YOUR_TZ}<small>From this browser.</small></span><span style="color:var(--faint);font-size:12px">Automatic</span></div></div>
<div class="grp"><h3>Resets for your games</h3>${lanes().map(({g,srv})=>`<div class="trow" style="${gv(g)}"><span class="lbl" style="color:var(--g)">${laneName(g,srv)}<small>${SV[srv].name} · 04:00 server time · ${hh(new Date(nextDaily(srv)+YOUR_OFF*H).getUTCHours())} your time</small></span><span class="cond" style="font:600 15px 'Barlow Condensed'">${dur(nextDaily(srv)-NOW)}</span></div>`).join("")}</div></div>`;
if(k==="appearance")return `<div><div class="ptitle"><h2>Appearance</h2></div><div class="grp"><h3>Theme</h3><div class="seg" style="margin-top:12px">${["dark","light"].map(t=>`<button data-theme="${t}" aria-pressed="${S.theme===t}">${t[0].toUpperCase()+t.slice(1)}</button>`).join("")}</div></div></div>`;
if(k==="view")return `<div><div class="ptitle"><h2>Default view</h2></div><p class="pdesc">What opens when you come back. What's new shows in both, so you won't miss it either way.</p><div class="viewpick">${viewCards(S.defaultView,"dv")}</div></div>`;
if(k==="lists")return `<div><div class="ptitle"><h2>Lists</h2></div><p class="pdesc">Choices about the Checklist and Timeline themselves. Which routines show in them is set per routine, by its pins.</p>
<div class="grp"><label class="chk block"><input type="checkbox" data-pref="showUpcoming" ${S.showUpcoming?"checked":""}><span>Show events that haven't started<small>Off keeps the lists to what you can play now.</small></span></label>
<label class="chk block"><input type="checkbox" data-pref="tlStrip" ${S.tlStrip?"checked":""}><span>Timeline: resets in their own strip<small>Off draws pinned routines in each game's lane, beside its events.</small></span></label>
<label class="chk block"><input type="checkbox" data-pref="standardPulls" ${S.standardPulls?"checked":""}><span>Offer standard-banner pulls in the library<small>Premium pulls are always offered.</small></span></label>
</div></div>`;
if(k==="own")return `<div><div class="ptitle"><h2>Your own events and games</h2></div><p class="pdesc">Only what you typed in yourself. Routines, built-in or yours, live on the Routines tab.</p>
<div class="grp"><h3>Your events</h3>${S.ownEvents.map(o=>`<div class="trow" style="${gv(o.game)}"><span class="lbl">${esc(o.title)}<small>${G[o.game].short} · ends ${new Date(o.end).toLocaleDateString("en-GB",{day:"numeric",month:"short"})}</small></span><button class="btn sm" data-act="editown">Edit</button></div>`).join("")}<button class="btn link" data-act="newevent" style="padding-left:0">+ Add an event</button></div>
<div class="grp"><h3>Your games</h3><p class="pdesc" style="margin:10px 0">None yet. Add a game the app doesn't cover, and give it a colour.</p><button class="btn link" data-act="newevent" style="padding-left:0">+ Add a game</button></div></div>`;
return `<div><div class="ptitle"><h2>Backup</h2></div><p class="pdesc">Everything lives in this browser: games and servers, followed routines, pins, ticks and the Archive. Save it to a file to move it to another device.</p><div style="display:flex;gap:8px;margin-top:16px;flex-wrap:wrap"><button class="btn solid" data-act="noop">Save to a file</button><button class="btn" data-act="noop">Load a file</button></div></div>`;
}
function viewCards(cur,attr){return [["checklist","Checklist","A list, closest deadline first."],["timeline","Timeline","Bars per game, today pinned."]].map(([k,l,s])=>`<button class="vcard" data-${attr}="${k}" aria-pressed="${cur===k}"><div class="vprev ${k==="checklist"?"list":"tl"}">${k==="checklist"?"<i></i><i></i><i></i>":`<i style="--g:#4EA8DE;left:4%;width:52%;top:6px"></i><i style="--g:#F2A03D;left:30%;width:44%;top:24px"></i><i style="--g:#3DD6A0;left:50%;width:46%;top:42px"></i><em></em>`}</div><b>${l}</b><small>${s}</small></button>`).join("")}
// ============ archive ============
// Three variants of the same snapshot data; the bar switches S.arcV between them.
const ST_LABEL={done:"Completed",ignored:"Ignored",expired:"Ran out"};
const ST_ICON={done:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7"/></svg>',
ignored:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M6 12h12"/></svg>',
expired:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round"><path d="M12 6.5v7M12 17.5v.01"/></svg>'};
const arcMark=st=>`<span class="amark ${st}" aria-hidden="true">${ST_ICON[st]}</span>`;
const arcFrac=a=>a.prog&&a.prog.need?Math.min(1,a.prog.got/a.prog.need):a.status==="done"?1:0;
const unitOf=(a,n)=>{const u=a.prog.unit;return n===1&&u.endsWith("s")?u.slice(0,-1):u};
/** What the reader got out of it, in their terms: "6/7 days", "missed by 1 day", "1,450/1,500 points". */
function achieved(a){const p=a.prog;if(!p)return{big:"",line:esc(a.progress||"")};
const got=p.t==="amount"?fmt(p.got):p.got,need=p.t==="amount"?fmt(p.need):p.need,u=p.unit;
const big=`${got}/${need}`;
if(a.status==="done")return{big,line:p.got>=p.need?`${big} ${u}`:`${big} ${u} · marked done`};
if(a.status==="expired"){const short=p.need-p.got;return{big,line:p.t==="amount"?`${big} ${u} · short by ${fmt(short)}`:`${big} ${u} · missed by ${short} ${unitOf(a,short)}`}}
return{big:"—",line:`Ignored on ${dayStr(a.at)}${p.got?`, after ${p.got} ${unitOf(a,p.got)}`:""}`};
}
const runDays=a=>Math.max(1,daysBetween(a.start,a.end));
const monthOf=s=>new Date(s+"T12:00:00Z").toLocaleDateString("en-GB",{month:"long",year:"numeric",timeZone:"UTC"});
const verOf=(g,v)=>(VERSIONS[g]||[]).find(x=>x[0]===v);
function arcList(){return S.archive.filter(a=>(S.arcGame==="all"||a.game===S.arcGame)&&(S.arcStatus==="all"||a.status===S.arcStatus)).sort((a,b)=>(b.end||b.at).localeCompare(a.end||a.at))}
const tally=as=>({done:as.filter(a=>a.status==="done").length,expired:as.filter(a=>a.status==="expired").length,ignored:as.filter(a=>a.status==="ignored").length,all:as.length});
/** A stacked bar of statuses — the one stats primitive all three variants share. */
function stack(t,cls=""){const n=t.all||1;return `<span class="stack ${cls}" role="img" aria-label="${t.done} completed, ${t.expired} ran out, ${t.ignored} ignored"><i class="s-done" style="width:${t.done/n*100}%"></i><i class="s-expired" style="width:${t.expired/n*100}%"></i><i class="s-ignored" style="width:${t.ignored/n*100}%"></i></span>`}
const arcIntro=`Finished, ignored, or ran out. Kept in your save and left off the Checklist and Timeline. Open one to see it as it was.`;
function statusSeg(){return `<div class="seg">${[["all","All"],["done","Completed"],["expired","Ran out"],["ignored","Ignored"]].map(([k,l])=>`<button data-arcst="${k}" aria-pressed="${S.arcStatus===k}">${l}</button>`).join("")}</div>`}
function gameSelect(){const games=[...new Set(S.archive.map(a=>a.game))];return `<select id="arcgame" aria-label="Game"><option value="all">All games</option>${games.map(g=>`<option value="${g}" ${S.arcGame===g?"selected":""}>${G[g].name}</option>`).join("")}</select>`}
/** How the reader's progress looked: pips per day, per stage, or a filled bar. */
function progViz(a,big=false){const p=a.prog;if(!p)return"";
if(p.t==="days"&&!big)return `<div class="pips ${a.status}" style="${gv(a.game)}" aria-label="${p.got} of ${p.need} days">${Array.from({length:p.need},(_,i)=>`<i class="${i<p.got?"on":""}"></i>`).join("")}</div>`;
if(p.t==="days"){const span=runDays(a),on=new Set(p.days);
return `<div class="pips ${big?"big":""} ${a.status}" style="${gv(a.game)}" aria-label="${p.got} of ${span} days checked in">${Array.from({length:span},(_,i)=>`<i class="${on.has(i)?"on":""}" title="${dayStr(new Date(Date.parse(a.start)+i*DAY_MS).toISOString().slice(0,10))}"></i>`).join("")}</div>${big?`<div class="pips-ax"><span>${dayStr(a.start)}</span><span>${p.got} of ${span} days ticked · goal ${p.need}</span><span>${(a.end?dayStr(a.end):"End unknown")}</span></div>`:""}`}
if(p.t==="stages")return `<div class="pips stages ${big?"big":""} ${a.status}" style="${gv(a.game)}" aria-label="${p.got} of ${p.need} ${p.unit}">${Array.from({length:p.need},(_,i)=>`<i class="${i<p.got?"on":""}"></i>`).join("")}</div>`;
return `<div class="abar ${big?"big":""} ${a.status}" style="${gv(a.game)};--f:${arcFrac(a)*100}%" aria-label="${fmt(p.got)} of ${fmt(p.need)} ${p.unit}"><i></i></div>`;
}
/** Snapshot facts every variant's detail shows. */
function oldArcFacts(a){const running=Date.parse(a.end+"T03:00:00Z")>NOW;const ac=achieved(a);
return `<p class="desc">${esc(a.desc)||'<span style="color:var(--faint)">The source gave no description.</span>'}</p>
<dl class="dl2"><dt>Ran</dt><dd>${dayStr(a.start)} – ${(a.end?dayStr(a.end):"End unknown")} 2026 · ${runDays(a)} days${running?" · <b>still running</b>":""}</dd>${a.ver?`<dt>Version</dt><dd>${G[a.game].short} ${a.ver}</dd>`:""}<dt>Your progress</dt><dd><b class="ach ${a.status}">${ac.line}</b></dd><dt>Archived</dt><dd>${dayStr(a.at)} 2026, ${a.status==="done"?"when you marked it done":a.status==="ignored"?"when you ignored it":"by the clock, when it ended unfinished"}</dd></dl>
<div class="saved">Source: <a href="${esc(a.src)}" target="_blank" rel="noopener">${esc((a.src||"Reader-authored or sample event").replace("https://",""))}</a><br>${a.moved?`Saved copy from ${dayStr(a.at)} 2026. <b>The live page no longer lists this event</b>; what you see here is what it said then.`:`Saved copy from ${dayStr(a.at)} 2026 — the live page may have moved on.`}</div>
<div class="acts">${running?`<button class="btn solid" data-restore="${a.id}">Restore to the lists</button>`:`<button class="btn" data-restore="${a.id}">Restore</button>`}<button class="btn link" data-act="closesheet">Close</button></div>
<p class="keepnote">Restore takes it out of the Archive${running?" and puts it back in the Checklist and Timeline":"; an event that has ended stays off the lists either way"}. Nothing here is ever deleted from your save.</p>`}
function originalArchive(){return S.arcV===2?arcV2():S.arcV===3?arcV3():arcV1()}
function oldArchiveDetail(id){const a=S.archive.find(x=>x.id===id);if(!a||S.arcV===2)return"";
const head=`<p class="legend" style="color:var(--g)">${G[a.game].name}${a.ver?" "+a.ver:""} · <span class="st ${a.status}">${ST_LABEL[a.status]}</span></p><h2>${esc(a.title)}</h2>`;
if(S.arcV===3)return `<aside class="sidep" role="dialog" aria-label="${esc(a.title)}" style="${gv(a.game)}"><button class="x" data-act="closesheet" aria-label="Close">${ICON.x}</button>${head}<div class="sidep-viz">${progViz(a,true)}</div>${arcFacts(a)}</aside>`;
return `<div class="scrim" data-act="closesheet"></div><div class="sheet dsheet" role="dialog" aria-label="${esc(a.title)}" style="${gv(a.game)}">${head}<div class="sidep-viz">${progViz(a,true)}</div>${arcFacts(a)}</div>`}
// ---------- 1 · Ledger with a story ----------
function arcV1(){const list=arcList();const all=S.archive;
const ended=all.filter(a=>a.status!=="ignored");const rate=ended.length?Math.round(all.filter(a=>a.status==="done").length/ended.length*100):0;
const oct=all.filter(a=>a.end>="2026-10-01");const octEnded=oct.filter(a=>a.status!=="ignored");const octRate=octEnded.length?Math.round(oct.filter(a=>a.status==="done").length/octEnded.length*100):0;
const byEnd=[...all].filter(a=>a.status!=="ignored").sort((a,b)=>(a.end||a.at).localeCompare(b.end||b.at));let run=0,best=0,cur=0;byEnd.forEach(a=>{if(a.status==="done"){run++;best=Math.max(best,run)}else run=0});for(let i=byEnd.length-1;i>=0&&byEnd[i].status==="done";i--)cur++;
const ticks=all.reduce((s,a)=>s+(a.prog?.t==="days"?a.prog.got:0),0);
const games=[...new Set(all.map(a=>a.game))];
const months={};list.forEach(a=>{(months[monthOf(a.end)]??=[]).push(a)});
return `<div class="rt-top"><h1>Archive</h1><div class="sum">${arcIntro}</div></div>
<section class="v1-stats" aria-label="Your record">
<div class="v1-figs">
<div><b>${octRate}<small>%</small></b><span>finished in October</span><em>${rate}% since August</em></div>
<div><b>${cur}</b><span>finished in a row</span><em>best run ${best}</em></div>
<div><b>${ticks}</b><span>check-in days logged</span><em>across ${all.filter(a=>a.prog?.t==="days").length} events</em></div>
<div class="warn"><b>${all.filter(a=>a.status==="expired").length}</b><span>ran out unfinished</span><em>${all.filter(a=>a.status==="expired"&&a.prog&&a.prog.need-a.prog.got<=1).length} by a single day or stage</em></div>
</div>
<div class="v1-games">${games.map(g=>{const t=tally(all.filter(a=>a.game===g));return `<button class="v1-g" data-arcgame="${S.arcGame===g?"all":g}" aria-pressed="${S.arcGame===g}" style="${gv(g)}"><span class="nm">${G[g].short}</span>${stack(t)}<span class="ct">${t.done}/${t.all-t.ignored}</span></button>`}).join("")}<p class="v1-key"><i class="s-done"></i>completed <i class="s-expired"></i>ran out <i class="s-ignored"></i>ignored · count is finished of those you played</p></div>
</section>
<div class="filters">${gameSelect()}${statusSeg()}</div>
${Object.entries(months).map(([m,as])=>{const t=tally(as);return `<div class="month v1-month">${m}<span>${t.done} completed · ${t.expired} ran out · ${t.ignored} ignored</span>${stack(t,"mini")}</div>${as.map(a=>{const ac=achieved(a);return `<button class="v1row ${a.status}" data-arc="${a.id}" style="${gv(a.game)}">${arcMark(a.status)}<span class="gcell gc2">${G[a.game].short}<small>${a.ver||""}</small></span><span class="tt"><span class="ti">${esc(a.title)}</span><small class="ach ${a.status}"><span class="gsmi">${G[a.game].short} · </span>${ac.line}</small></span><span class="v1-res">${a.status==="ignored"?"":`<b class="cond">${ac.big}</b>`}${a.status==="ignored"?`<span class="st ignored">Ignored</span>`:progViz(a)}</span><span class="when">${dayStr(a.start)} – ${(a.end?dayStr(a.end):"End unknown")}</span></button>`}).join("")}`}).join("")||`<div class="empty-band">Nothing matches.</div>`}`;
}
// ---------- 2 · By game and version ----------
function arcV2(){
if(S.arcSheet){const a=S.archive.find(x=>x.id===S.arcSheet);if(a)return arcV2Page(a)}
const games=[...new Set(S.archive.map(a=>a.game))];const sel=S.arcGame;
const side=`<nav class="v2-side" aria-label="Games"><button class="v2-si" data-arcgame="all" aria-pressed="${sel==="all"}"><span class="nm">All games</span><small>${S.archive.length} events</small></button>${games.map(g=>{const t=tally(S.archive.filter(a=>a.game===g));return `<button class="v2-si" data-arcgame="${g}" aria-pressed="${sel===g}" style="${gv(g)}"><span class="nm" style="color:var(--g)">${G[g].name}</span><small>${t.done} of ${t.all} done${t.expired?` · ${t.expired} ran out`:""}</small>${stack(t,"mini")}</button>`}).join("")}</nav>`;
const shown=sel==="all"?games:[sel];
const body=shown.map(g=>{const vers=[...new Set(S.archive.filter(a=>a.game===g).map(a=>(a.ver||"Saved events")))].sort().reverse();
return `<section class="v2-game" style="${gv(g)}">${sel==="all"?`<h2 class="v2-gh">${G[g].name}</h2>`:""}${vers.map(v=>{const as=arcList().filter(a=>a.game===g&&(a.ver||"Saved events")===v);if(!as.length)return"";const all=S.archive.filter(a=>a.game===g&&(a.ver||"Saved events")===v);const t=tally(all);const vr=verOf(g,v);const live=vr&&Date.parse(vr[2]+"T03:00:00Z")>NOW;
return `<div class="v2-ver"><div class="v2-vh"><span class="v2-vn cond">${G[g].short} ${v}</span><span class="v2-vs"><b>${t.done} of ${t.all} done</b>${t.expired?` · ${t.expired} ran out`:""}${t.ignored?` · ${t.ignored} ignored`:""}</span><span class="v2-vd">${vr?`${dayStr(vr[1])} – ${dayStr(vr[2])}${live?" · current version":""}`:""}</span></div>
<div class="v2-meter" aria-hidden="true">${all.sort((a,b)=>(a.end||a.at).localeCompare(b.end||b.at)).map(a=>`<i class="${a.status}" title="${esc(a.title)}"></i>`).join("")}</div>
${as.sort((a,b)=>(b.end||b.at).localeCompare(a.end||a.at)).map(a=>{const ac=achieved(a);return `<button class="v2row ${a.status}" data-arc="${a.id}">${arcMark(a.status)}<span class="tt"><span class="ti">${esc(a.title)}</span></span><span class="ach ${a.status}">${ac.line}</span><span class="when">${dayStr(a.start)} – ${(a.end?dayStr(a.end):"End unknown")}</span><span class="go">${ICON.right}</span></button>`}).join("")}</div>`}).join("")}</section>`}).join("")||`<div class="empty-band">Nothing matches.</div>`;
return `<div class="rt-top"><h1>Archive</h1><div class="sum">${arcIntro}</div>${statusSeg()}</div><div class="v2">${side}<div class="v2-main">${body}</div></div>`;
}
function oldArcPage(a){const vr=verOf(a.game,a.ver);const ac=achieved(a);
const v0=vr?Date.parse(vr[1]):Date.parse(a.start),v1=vr?Math.max(Date.parse(vr[2]),Date.parse(a.end)):Date.parse(a.end),span=v1-v0;
const pos=d=>((Date.parse(d)-v0)/span*100).toFixed(2);const nowPos=Math.min(100,Math.max(0,(NOW-v0)/span*100));
const sibs=S.archive.filter(x=>x.game===a.game&&x.ver===a.ver&&x.id!==a.id).sort((x,y)=>x.start.localeCompare(y.start));
return `<div class="v2-page" style="${gv(a.game)}"><button class="btn link v2-back" data-act="closesheet">${ICON.left} ${G[a.game].short} ${a.ver} archive</button>
<p class="legend" style="color:var(--g)">${G[a.game].name} ${a.ver} · <span class="st ${a.status}">${ST_LABEL[a.status]}</span></p><h1 class="v2-title">${esc(a.title)}</h1><p class="v2-big ach ${a.status}">${ac.line}</p>
<div class="v2-strip" aria-label="When it ran in version ${a.ver}"><div class="v2-axis"><span>${G[a.game].short} ${a.ver} · ${vr?dayStr(vr[1]):""}</span><span>${vr?dayStr(vr[2]):""}</span></div>
<div class="v2-track">${sibs.map(x=>`<i class="sib ${x.status}" style="left:${pos(x.start)}%;width:${(pos(x.end)-pos(x.start)).toFixed(2)}%" title="${esc(x.title)}"></i>`).join("")}<b class="me ${a.status}" style="left:${pos(a.start)}%;width:${(pos(a.end)-pos(a.start)).toFixed(2)}%"><span>${esc(a.title)}</span></b>${nowPos<100?`<em style="left:${nowPos}%" title="Now"></em>`:""}</div>
<p class="v2-cap">The bright bar is this event; the faint ones are the version's other archived events.</p></div>
<h3 class="v2-h3">${a.prog?.t==="days"?"Your check-ins":a.prog?.t==="stages"?`Your ${esc(a.prog.unit)}`:"Your total"}</h3><div class="v2-viz">${progViz(a,true)}</div>
${arcFacts(a)}</div>`}
// ---------- 3 · History view ----------
const H0=Date.parse("2026-08-10T00:00:00Z"),H1=Date.parse("2026-10-20T00:00:00Z");
const hx=d=>((Date.parse(d+"T00:00:00Z")-H0)/(H1-H0)*100);
function arcV3(){const list=arcList();const games=[...new Set(list.map(a=>a.game))].sort((a,b)=>DEFAULT_GAMES.indexOf(a)-DEFAULT_GAMES.indexOf(b));
const weeks=[];for(let t=Date.parse("2026-08-10T00:00:00Z");t<H1;t+=7*DAY_MS)weeks.push(t);
const months=[["Aug","2026-08-10"],["Sep","2026-09-01"],["Oct","2026-10-01"]];
// check-in density per day across everything shown
const nDays=Math.round((H1-H0)/DAY_MS);const dens=new Array(nDays).fill(0);list.forEach(a=>{if(a.prog?.t!=="days")return;a.prog.days.forEach(i=>{const d=Math.round((Date.parse(a.start)+i*DAY_MS-H0)/DAY_MS);if(d>=0&&d<nDays)dens[d]++})});const dmax=Math.max(1,...dens);
const lanes=games.map(g=>{const as=list.filter(a=>a.game===g).sort((a,b)=>a.start.localeCompare(b.start));const rows=[];as.forEach(a=>{let r=rows.findIndex(end=>end<=a.start);if(r<0){r=rows.length;rows.push("")}rows[r]=a.end||a.at;a._row=r});
return `<div class="v3-lane" style="${gv(g)};--rows:${rows.length}"><div class="v3-ln">${G[g].short}</div><div class="v3-lt">${as.map(a=>`<button class="v3-bar ${a.status}" data-arc="${a.id}" data-hl="${a.id}" style="left:${hx(a.start).toFixed(2)}%;width:${Math.max(.8,Math.min(100,hx(a.end||a.at))-hx(a.start)).toFixed(2)}%;top:${a._row*20+4}px${hx(a.end||a.at)>100?";-webkit-mask-image:linear-gradient(90deg,#000 70%,transparent);mask-image:linear-gradient(90deg,#000 70%,transparent)":""}" aria-label="${esc(a.title)}, ${ST_LABEL[a.status]}"><span>${esc(a.title)}</span></button>`).join("")}</div></div>`}).join("");
const nowX=Math.min(100,(NOW-H0)/(H1-H0)*100);
return `<div class="rt-top"><h1>Archive</h1><div class="sum">Your season so far: everything you finished, ignored, or let run out since August. ${arcIntro.split(". ").slice(1).join(". ")}</div></div>
<div class="filters">${gameSelect()}${statusSeg()}<span class="v3-key"><i class="k done"></i>completed <i class="k expired"></i>ran out <i class="k ignored"></i>ignored</span></div>
<section class="v3-board" aria-label="Archived events over time">
<div class="v3-axis"><div class="v3-ln"></div><div class="v3-lt">${weeks.map(t=>`<span class="wk" style="left:${((t-H0)/(H1-H0)*100).toFixed(2)}%"></span>`).join("")}${months.map(([m,d])=>`<span class="mo" style="left:${hx(d).toFixed(2)}%">${m}</span>`).join("")}<span class="nowtag" style="left:${nowX}%">Today</span></div></div>
<div class="v3-lanes" style="--n:${nowX}">${lanes||`<div class="empty-band">Nothing matches.</div>`}</div>
<div class="v3-dens"><div class="v3-ln">Check-ins</div><div class="v3-lt">${dens.map((n,i)=>`<i style="left:${(i/nDays*100).toFixed(2)}%;width:${(100/nDays).toFixed(2)}%;height:${n?Math.max(2,n/dmax*22):0}px" title="${n} check-ins"></i>`).join("")}</div></div>
</section>
<div class="v3-list">${list.map(a=>{const ac=achieved(a);return `<button class="v3row ${a.status}" data-arc="${a.id}" data-hl="${a.id}" style="${gv(a.game)}">${arcMark(a.status)}<span class="gcell gc2">${G[a.game].short}</span><span class="tt"><span class="ti">${esc(a.title)}</span><small class="ach ${a.status}"><span class="gsmi">${G[a.game].short} · </span>${ac.line}</small></span><span class="when">${dayStr(a.start)} – ${(a.end?dayStr(a.end):"End unknown")}</span></button>`}).join("")}</div>`;
}
// ============ timeline ============
/**
* One routine on the board: a slim outlined pill spanning the period it's in now,
* filled to its progress, capped by its reset; the next period is a faint dashed ghost.
* Swappable in a later round that compares drawings.
*/
function drawRoutineOnTimeline(i,x,top,w0,w1,label){const p=periodOf(i.r,i.srv);const met=isMet(i);
const L=x(p.start),R=x(p.end),W=Math.max(14,R-L);const frac=met?1:Math.min(1,val(i)/req(i.r));
const clipped=p.start<w0;const name=label??i.r.title;const txt=`${esc(name)} · ${met?"done":esc(progText(i))}`;
const tip=`${esc(laneName(i.r.game,i.srv))}: ${esc(i.r.title)} · ${esc(progText(i))} · resets ${yourTime(p.end)}`;
const narrow=W<140;
const ghost=p.end<w1?`<div class="rghost" style="left:${R+3}px;width:${Math.max(10,x(Math.min(w1,p.end+(p.end-p.start)))-R-3)}px;top:${top}px"></div>`:"";
return `${ghost}<div class="rbar ${met?"met":""} ${clipped?"clipped":""}" data-rt="${i.key}" style="left:${L}px;width:${W}px;top:${top}px" title="${tip}"><span class="fill" style="width:${frac*100}%"></span>${narrow?"":`<span class="lab">${txt}</span>`}<span class="rs">${ICON.reset}</span><span class="cap"></span>${narrow?`<span class="out">${txt}</span>`:""}</div>`}
function oldTimeline(){
const small=window.innerWidth<700;const DW=small?40:54,NAME=small?92:160,NDAYS=28;
const w0=Date.UTC(2026,9,13),w1=w0+NDAYS*DAY,W=NDAYS*DW;
const x=t=>(Math.max(w0,Math.min(w1,t))-w0)/DAY*DW;
const days=Array.from({length:NDAYS},(_,i)=>w0+i*DAY);
const grid=days.map((d,i)=>`<i class="gridcol ${new Date(d).getUTCDay()===1?"wk":""}" style="left:${i*DW}px"></i>`).join("");
const axis=`<div class="axis"><div class="corner" style="width:${NAME}px"></div>${days.map(d=>{const dt=new Date(d);return `<div class="day ${dt.getUTCDay()===1?"wk":""}" style="width:${DW}px"><b>${dt.getUTCDate()}</b>${dt.toLocaleString("en-GB",{weekday:"short",timeZone:"UTC"})}</div>`}).join("")}</div>`;
const gs=S.games.filter(g=>S.focus==="all"||S.focus===g);
const evs=liveEvents().filter(e=>gs.includes(e.game)&&(e.end??w1)>w0&&e.start<w1);
const evBar=(e,top,ey)=>{const l=x(e.start),r=x(e.end??w1);return `<div class="bar ${e.start>NOW?"ns":""} ${e.end==null?"open":""} ${e.start<w0?"clip":""} ${e.own?"own":""}" data-ev="${e.id}" tabindex="0" role="button" style="left:${l}px;width:${Math.max(24,r-l-2)}px;top:${top}px;${gv(e.game)}" title="${esc(e.title)}">${ey?`<span class="ey">${G[e.game].short}</span>`:""}${esc(e.title)}</div>`};
const pack=items=>{const rows=[];items.forEach(it=>{let k=rows.findIndex(row=>row.every(o=>o.b<=it.a||o.a>=it.b));if(k<0){rows.push([]);k=rows.length-1}rows[k].push(it)});return rows};
const routinesFor=(g,srv)=>pinnedIn(g,srv,"t").sort((a,b)=>reset(a)-reset(b));
const showServer=g=>multi(g);
const nm=(g,srv)=>`<div class="nm" style="width:${NAME}px"><span>${G[g].short}${srv&&showServer(g)?`<small>${SV[srv].name}</small>`:""}</span></div>`;
let body="",strip="";
// routine rows for one game: one row per routine, server named on the label when the game has several
const routineRows=(g,top0)=>{let top=top0,html="";for(const srv of S.servers[g]){for(const i of routinesFor(g,srv)){html+=drawRoutineOnTimeline(i,x,top,w0,w1,showServer(g)?`${i.r.title} (${SV[srv].short})`:undefined);top+=26}}return{html,h:top-top0}};
if(S.tlStrip){const sg=gs.filter(g=>S.servers[g].some(s=>routinesFor(g,s).length));
strip=`<div class="resets"><span class="grpl">Resets<small>${sg.length?"pinned routines, in their own strip":"none pinned to the timeline"}</small></span>${sg.map(g=>{const rr=routineRows(g,8);return `<div class="lane" style="${gv(g)};height:${rr.h+12}px">${nm(g)}<div class="tr" style="width:${W}px">${grid}${rr.html}</div></div>`}).join("")}</div>`}
if(S.tlMode==="bygame"){
body=gs.map(g=>{const es=evs.filter(e=>e.game===g).sort((a,b)=>a.start-b.start);const rows=pack(es.map(e=>({a:e.start,b:e.end??w1,e})));
const evH=rows.length*38;const rr=S.tlStrip?{html:"",h:0}:routineRows(g,8+evH+(rows.length?4:0));
if(!rows.length&&!rr.h)return"";
return `<div class="lane" style="${gv(g)};height:${8+evH+rr.h+(rr.h?8:4)+(rows.length&&rr.h?4:0)}px">${nm(g)}<div class="tr" style="width:${W}px">${grid}${rows.map((row,k)=>row.map(o=>evBar(o.e,8+k*38)).join("")).join("")}${rr.html}</div></div>`}).join("");
}else{
// Ending soonest: one queue; pinned routines take their place by when they reset.
const items=[...evs.map(e=>({t:e.end??9e15,e})),...(S.tlStrip?[]:gs.flatMap(g=>S.servers[g].flatMap(srv=>routinesFor(g,srv).map(i=>({t:reset(i),i})))))].sort((a,b)=>a.t-b.t);
body=items.map(it=>it.e?`<div class="lane" style="${gv(it.e.game)};height:40px">${nm(it.e.game)}<div class="tr" style="width:${W}px">${grid}${evBar(it.e,5,false)}</div></div>`
:`<div class="lane" style="${gv(it.i.r.game)};height:30px">${nm(it.i.r.game,it.i.srv)}<div class="tr" style="width:${W}px">${grid}${drawRoutineOnTimeline(it.i,x,5,w0,w1)}</div></div>`).join("");
}
const nowX=NAME+x(NOW);const c=counts();
const cnt=g=>liveEvents().filter(e=>g==="all"||e.game===g).length;
return `${whatsNew()}<div class="rt-top"><h1>Timeline</h1><div class="sum">Events as bars. <b>${c.t}</b> pinned routines as slim pills spanning the period they're in, filled as you go, ending at their reset. <button class="btn link" data-tab="routines" style="padding:0">Change pins</button></div></div>
<div class="tl-ctl"><div class="seg"><button data-tlmode="soonest" aria-pressed="${S.tlMode==="soonest"}">Ending soonest</button><button data-tlmode="bygame" aria-pressed="${S.tlMode==="bygame"}">By game</button></div><label class="chk"><input type="checkbox" data-pref="tlStrip" ${S.tlStrip?"checked":""}>Resets in their own strip</label></div>
${focusRow(cnt)}<div class="tlwrap"><div class="board" style="width:${NAME+W}px">${axis}${strip}${body}<div class="now" style="left:${nowX}px"></div><div class="nowchip" style="left:${nowX}px">NOW</div></div></div>
<div class="tl-legend"><span><i style="width:26px;height:12px;border-radius:2px;border-left:3px solid var(--muted);background:color-mix(in srgb,var(--muted) 25%,transparent)"></i>Event</span><span><i style="width:30px;height:12px;border-radius:6px;border:1px solid var(--muted);background:linear-gradient(90deg,color-mix(in srgb,var(--muted) 35%,transparent) 55%,transparent 55%)"></i>Routine: its current period, filled to your progress</span><span><i style="width:2px;height:14px;background:var(--muted)"></i>Its reset</span><span><i style="width:24px;height:12px;border-radius:6px;border:1px dashed var(--muted)"></i>The next period</span></div>`;
}
// ============ onboarding ============
function round4Onboarding(){const o=S.ob;
if(o.step===1)return `<div class="ob"><div class="step">Step 1 of 3</div><h1>Which games do you play?</h1><p class="lede">You'll only see events for these. You can change it, and set each game's server, in Settings.</p>
<div class="gpick">${GAMES.map(g=>`<button data-obg="${g.id}" aria-pressed="${o.games.includes(g.id)}" style="${gv(g.id)}"><span class="sw2"></span>${g.name}</button>`).join("")}</div>
<div class="ob-foot"><span class="cnt">${o.games.length?`${o.games.length} chosen`:"Pick at least one"}</span><button class="btn link" data-act="obskip">Choose all games instead</button><button class="btn solid" data-act="obnext" ${o.games.length?"":"disabled"}>Continue</button></div></div>`;
if(o.step===2){const has=(g,k)=>k==="events"||BUILTINS.some(r=>r.game===g&&kindOf(r)===k&&r.pulls!=="standard");
const allState=k=>{const gs=o.games;const n=gs.filter(g=>o.comp[g][k]).length;return n===0?"off":n===gs.length?"on":"mixed"};
const total=o.games.reduce((s,g)=>s+["dailies","weekly","cycles"].filter(k=>o.comp[g][k]).reduce((t,k)=>t+BUILTINS.filter(r=>r.game===g&&kindOf(r)===k&&r.pulls!=="standard").length,0),0);
return `<div class="ob"><div class="step">Step 2 of 3</div><h1>What do you want to track?</h1><p class="lede">Pick any mix, per game or for all of them. Routines are the things that reset; they get their own tab, where you can follow or drop single ones later.</p>
${typeGuide()}${availabilityNotes(o.games)}<div class="matrix-scroll"><table class="mtx"><thead><tr><th>Game</th>${COMP.map(([k,l])=>`<th>${l}</th>`).join("")}</tr></thead><tbody>
<tr class="all"><td>Same for all<small>Sets every game below</small></td>${COMP.map(([k])=>{const st=allState(k);return `<td><button class="tg" data-oball="${k}" aria-pressed="${st==="on"}" data-mixed="${st==="mixed"}" aria-label="All games: ${k}">${ICON.check}</button></td>`}).join("")}</tr>
${o.games.map(g=>`<tr style="${gv(g)}"><td>${G[g].short}</td>${COMP.map(([k])=>`<td><button class="tg" data-obc="${g}|${k}" aria-pressed="${!!o.comp[g][k]}" aria-label="${G[g].short}: ${k}">${ICON.check}</button></td>`).join("")}</tr>`).join("")}</tbody></table></div>
<div class="ob-foot"><button class="btn link" data-act="obback">Back</button><span class="cnt">${total?`Follows ${total} built-in routines. You'll choose where they show once you've seen the views.`:o.games.some(g=>["dailies","weekly","cycles"].some(k=>o.comp[g][k]))?"Your type choices are saved; no matching built-ins are available yet.":"Events only, for now."}</span><button class="btn solid" data-act="obnext2">Continue</button></div></div>`}
return `<div class="ob"><div class="step">Step 3 of 3</div><h1>Which view should open first?</h1><p class="lede">Both are always one tab away. This is just what you land on.</p><div class="viewpick">${viewCards(o.view,"obview")}</div>
<div class="ob-foot"><button class="btn link" data-act="obback2">Back</button><span class="cnt"></span><button class="btn solid" data-act="obdone">Start</button></div></div>`;
}
// ============ sheets ============
function baseEventSheet(){const e=[...EVENTS,...S.ownEvents].find(x=>x.id===S.sheet);if(!e)return"";
return `<div class="scrim" data-act="closesheet"></div><div class="sheet" role="dialog" style="${gv(e.game)}"><p class="legend" style="color:var(--g)">${G[e.game].name}</p><h2>${esc(e.title)}</h2>${e.desc?`<p style="color:var(--muted);margin:0 0 10px">${esc(e.desc)}</p>`:""}<div class="exact">${e.end?"Ends "+dateStr(e.end):"End date not announced"}</div>${meter(e)}
<div class="acts"><button class="btn solid" data-evst="${e.id}|done">Mark done</button><button class="btn" data-evst="${e.id}|ignored">Ignore</button><button class="btn link" data-act="closesheet">Close</button></div><p style="font-size:12px;color:var(--faint);margin:12px 0 0">Either one moves it to the Archive, with its description and source saved.</p></div>`}
function changelogSheet(){return `<div class="scrim" data-act="closesheet"></div><div class="sheet clog" role="dialog" aria-label="Changelog"><div class="clog-h"><h2>Changelog</h2><button class="close" data-act="closesheet" aria-label="Close">${ICON.x}</button></div>
${CHANGELOG.map(c=>`<section class="rel"><div class="d">${dayStr(c.date)} 2026 · ${esc(c.kind)}</div><h3>${esc(c.title)}</h3>${c.body?`<p>${esc(c.body)}</p>`:""}${c.items.length?`<ul>${c.items.map(t=>`<li>${esc(t)}</li>`).join("")}</ul>`:""}</section>`).join("")}</div>`}
function archiveWhy(){const common=`<h3>Archive: what every variant keeps</h3><p>Archiving copies a snapshot into the save at that moment, so the feed can drop or rewrite the event and the archive still opens:</p><pre>archive: { [eventId]: {
status: done | ignored | expired,
at, // when it was archived
snapshot: { title, desc, start, end, version,
sourceUrl, capturedAt },
progress: { measure: days|stages|amount,
got, need, unit, days: [dayKey] } } }</pre><p>Nothing is ever deleted: archived ids are filtered out of the Checklist and Timeline, and Restore only removes the archive entry (the event returns to the lists if it is still running). "Ran out" is archived by the clock, not by the reader. The detail always says when its copy was taken and flags when the live page no longer lists the event.</p><p>Derivable later from the same records, with no new storage: completion rate per month, game or version; best and current run of finished events; check-ins per day (the season heatmap); near misses (short by one day or stage); events ignored before starting vs. after trying.</p>`;
const v={1:`<h3>Variant 1 · Ledger with a story</h3><p>The month-grouped ledger stays, but every row says what you got out of it ("6/7 days", "short by 50 points", "missed by 1 day") with its own glyph: solid hue check, struck and receded for ignored, a red edge and tint for ran out. A slim header carries four figures and a per-game stacked bar that doubles as a filter.</p><p><b>Trade-offs:</b> the most familiar and the cheapest to build; stats are a header, so they never become a reason to visit. A month with many events is still a long list.</p>`,
2:`<h3>Variant 2 · By game and version</h3><p>The archive is read the way players think about a game: per game, then per version. Each version block has a completion meter (one tick per event) and compact rows. The detail is a full page with a strip placing the event inside its version and your check-in days as pips.</p><p><b>Trade-offs:</b> needs a version table per game (a new, hand-kept or scraped fact; dates here are mockup). Cross-game questions ("what did I miss this week?") are harder. The full page is the roomiest place for future per-event stats.</p>`,
3:`<h3>Variant 3 · History view</h3><p>A past-only timeline of the season: each game a lane, each archived event a bar coloured by status (filled for completed, hatched with a red edge for ran out, outlined for ignored), plus a check-in density strip along the bottom. The list underneath is synced: hovering a bar lights its row and back. Detail opens in a side panel so the board stays in view.</p><p><b>Trade-offs:</b> the most "seeing your season" and closest to the Timeline the reader already knows; weakest on a phone, where bars lose their labels. Stats are implicit in the picture rather than stated.</p>`}[S.arcV];
return common+v}
function oldWhyPanel(){return `<aside class="why"><button class="x" data-act="why">Close</button><h2>Round 3 · Converged</h2>
<p>Routines are managed in one place, the Routines tab, and pinned per routine to the Checklist and/or the Timeline. Each game has a settings page for <i>what kinds</i> to track and <i>which servers</i> it's played on. A game on two servers is two sets of dailies and routines.</p>
<h3>The foundation</h3>
<pre>prefs = {
games: LaneId[] // followed, in order
servers: { [game]: Region[] } // [0] = main server
region: Region // default for new games
defaultView: "checklist"|"timeline"
hideEvents: LaneId[] // "Events" off on a game
routines: {
[instanceKey]: { pins: { checklist, timeline } }
} // key present = followed
changelogSeen: "2026-10-16"
...theme, lists, tlStrip
}
instanceKey = routineId // on the main server
| routineId + "@" + region // on any other server
archive: { [eventId]: {
status: done|ignored|expired, at,
snapshot: { title, desc, start, end,
sourceUrl, progress } } }</pre>
<div class="flag"><b>Schema-guardian territory.</b> Tick keys must not move. Today's <code>dailies:&lt;game&gt;</code> and every <code>routine:</code>/<code>mytask:</code> tick key keep meaning "the main server", so no existing reader's ticks move. Only an <i>added</i> server gets a suffixed key (<code>dailies:genshin@asia</code>, <code>routine:genshin:weekly-bosses@asia</code>). Day keys for that instance are cut on its own server's clock (pass the region, not just the game, to <code>dayKey</code>). Changing the main server re-labels nothing, it only changes which clock the bare key is read on, and that has to be said in the UI.</div>
<p>Every view is a query over one list of instances: the Routines tab reads all of them, the Checklist rail reads <code>pins.checklist</code>, the board reads <code>pins.timeline</code>, filtered by games and the focus. This retires <code>showChores</code>, <code>choresOn/Off</code>, <code>showRoutines</code>, <code>routineGames</code>, <code>routinesOff</code>, <code>routinesAsked</code>, <code>routinesHidden</code> and <code>showWeekly/Monthly/CycleTasks</code>, and folds <code>hiddenGames</code>/<code>knownGames</code> into <code>games</code>.</p>
<h3>Composition, not levels</h3>
<p>Events · Dailies · Weeklies &amp; monthlies · Cycles on a game's page are four independent switches. The three routine ones are shortcuts that follow or unfollow that kind's built-ins (on every server); nothing about them is stored. A partly-followed kind shows "some" and a dashed edge. "Events" is the one stored switch, because it's the only one that isn't a routine.</p>
${archiveWhy()}
<h3>Onboarding order</h3>
<p>Games → what to track (composition) → default view. Where routines show is asked on the reader's first visit to Routines, after they've seen both views.</p>
<h3>Trade-offs</h3>
<ul><li>Two servers doubles the rows. The rail and board name the server only for games that have more than one.</li>
<li>The archive grows forever. Snapshots are small, but export files grow with them.</li>
<li>Follow vs pin is still two ideas. The first-visit hint on Routines carries it.</li></ul></aside>`}
// ============ render ============
function baseRender(){
document.documentElement.dataset.theme=S.theme;
renderChrome();
const app=document.getElementById("app");
if(S.ob){app.innerHTML=`<div class="shell"><header class="top" style="border:0"><div class="mark">EVENT<span>CLOCK</span></div></header><div class="body">${onboarding()}</div></div>`}
else{const body=S.tab==="checklist"?checklistScreen():S.tab==="routines"?routinesScreen():S.tab==="settings"?settingsScreen():S.tab==="archive"?renderArchive():timelineScreen();app.innerHTML=`<div class="shell">${tabs()}<div class="body">${body}</div>${reviewFooter()}</div>`}
document.getElementById("overlay").innerHTML=(S.drawer==="library"?libraryDrawer():S.drawer==="create"?createDrawer():"")+(S.sheet?eventSheet():"")+(S.arcSheet?renderArchiveDetail(S.arcSheet):"")+(S.clog?changelogSheet():"")+(S.why?whyPanel():"")+(S.toast?`<div class="toast" role="status">${S.toast}</div>`:"");
const a=document.querySelector("[autofocus]");if(a){a.focus();a.select?.()}
}
let toastT;function toast(m){S.toast=m;clearTimeout(toastT);toastT=setTimeout(()=>{S.toast=null;render()},3000)}
// ============ interaction ============
document.addEventListener("click",ev=>{
const t=ev.target.closest("button,[data-ev],[data-act],[data-menu],[data-unfollow],[data-amt],[data-rt],.menu span");if(!t)return;
if(t.tagName==="INPUT")return;
const d=t.dataset;
if(d.jump)jump(d.jump);
else if(d.dv){S.defaultView=d.dv}
else if(d.tab){S.tab=d.tab;S.drawer=null;S.menu=null}
else if(d.focus)setFocus(d.focus);
else if(d.round4Expand){if(S.focus!=="all")return;S.open[d.expand]=!S.open[d.expand]}
else if(d.group)S.group=d.group;
else if(d.tlmode)S.tlMode=d.tlmode;
else if(d.tick){const i=parseKey(d.tick);const v=val(i),need=req(i.r);S.progress[d.tick]=v>=need?0:v+1}
else if(d.unfollow){const i=parseKey(d.unfollow);follow(d.unfollow,false);if(i.r.own&&![...S.followed].some(k=>k.split("@")[0]===i.r.id))S.own=S.own.filter(x=>x.id!==i.r.id);S.menu=null;toast(`${i.r.own?"Deleted":"Unfollowed"} ${esc(i.r.title)}${multi(i.r.game)?` on ${SV[i.srv].name}`:""}. Find it again in the library.`)}
else if(d.amt){S.editing=d.amt;S.menu=null}
else if(d.menu!==undefined&&!ev.target.closest(".menu")){S.menu=S.menu===d.menu?null:d.menu}
else if(d.pin){const[k,p]=d.pin.split("|");const v=!pinned(k,p);setPin(k,p,v);toast(`${esc(parseKey(k).r.title)} ${v?"now shows in":"no longer shows in"} the ${p==="c"?"checklist":"timeline"}`)}
else if(d.bulkpin){const[kind,valx,p,v]=d.bulkpin.split("|").flatMap((s,ix)=>ix===0?s.split(":"):[s]);const rs=insts().filter(i=>kind==="g"?i.r.game===valx:i.r.period.unit===valx);rs.forEach(i=>setPin(i.key,p,v==="1"));toast(`${v==="1"?"Pinned":"Unpinned"} ${rs.length} ${v==="1"?"to":"from"} the ${p==="c"?"checklist":"timeline"}`)}
else if(d.follow){const on=!S.followed.has(d.follow);follow(d.follow,on);if(on&&(S.newPin.c||S.newPin.t))toast(`Following. Also shown in the ${[S.newPin.c&&"checklist",S.newPin.t&&"timeline"].filter(Boolean).join(" and ")}.`)}
else if(d.bulk){const[g,u]=d.bulk.split("|");const srvs=S.games.includes(g)?S.servers[g]:["europe"];const rs=BUILTINS.filter(r=>r.game===g&&(u==="all"||r.period.unit===u)&&(S.standardPulls||r.pulls!=="standard"));const ks=rs.flatMap(r=>srvs.map(s=>keyOf(r.id,s,g)));const allOn=ks.every(k=>S.followed.has(k));ks.forEach(k=>follow(k,!allOn));toast(`${allOn?"Unfollowed":"Followed"} ${rs.length} for ${G[g].short}${srvs.length>1?` on ${srvs.length} servers`:""}`)}
else if(d.ex){const e=EXAMPLES.find(x=>x.id===d.ex);S.form={...BLANK_FORM(),game:S.form.game,srv:S.form.srv,...e.f,example:e.id}}
else if(d.fu){S.form.unit=d.fu}
else if(d.fd){S.form.done=d.fd}
else if(d.ev){S.sheet=d.ev}
else if(d.rt){S.tab="routines";toast(`${esc(parseKey(d.rt).r.title)}: tick or edit it on the Routines tab`)}
else if(d.evst){const[id,st]=d.evst.split("|");const e=[...EVENTS,...S.ownEvents].find(x=>x.id===id);S.archived.add(id);
S.archive.unshift({id:"x"+id,ref:id,game:e.game,title:e.title,status:st,start:new Date(e.start).toISOString().slice(0,10),end:e.end?new Date(e.end).toISOString().slice(0,10):"2026-12-31",at:"2026-10-16",desc:e.desc,ver:(VERSIONS[e.game]||[]).find(v=>v[1]<=new Date(e.start).toISOString().slice(0,10)&&new Date(e.start).toISOString().slice(0,10)<v[2])?.[0]||(VERSIONS[e.game]||[[""]]).at(-1)[0],prog:{t:"days",got:st==="done"?1:0,need:1,unit:"days",days:st==="done"?[Math.max(0,Math.floor((NOW-e.start)/864e5))]:[]},src:e.src||"https://example.invalid/yours"});
S.sheet=null;toast(`${st==="done"?"Done":"Ignored"}. It's in the Archive, with its details saved.`)}
else if(d.arc){S.arcSheet=d.arc}
else if(d.restore){const a=S.archive.find(x=>x.id===d.restore);S.archive=S.archive.filter(x=>x!==a);if(a.ref)S.archived.delete(a.ref);else if(Date.parse(a.end+"T03:00:00Z")>NOW&&!EVENTS.some(e=>e.id===a.id)){EVENTS.push({id:a.id,game:a.game,title:a.title,desc:a.desc,start:Date.parse(a.start+"T03:00:00Z"),end:Date.parse(a.end+"T03:00:00Z"),src:a.src});a.ref=a.id}S.arcSheet=null;toast(`Restored ${esc(a.title)}${a.ref?" to the lists":""}`)}
else if(d.arcst)S.arcStatus=d.arcst;
else if(d.arcv){S.arcV=+d.arcv;S.arcSheet=null;S.tab="archive";lastJump="archive"}
else if(d.arcgame){S.arcGame=d.arcgame;S.arcSheet=null}
else if(d.region)S.region=d.region;
else if(d.theme)S.theme=d.theme;
else if(d.sel){S.sel=d.sel;S.addSrv=false}
else if(d.followgame){const g=d.followgame;if(S.games.includes(g)){S.games=S.games.filter(x=>x!==g)}else{S.games=[...S.games,g];S.servers[g]=[S.region]}if(S.focus===g)setFocus("all")}
else if(d.comp){const[g,k]=d.comp.split("|");const st=compState(g,k);const on=st!=="on";const n=applyComp(g,k,on);const lbl=COMP.find(c=>c[0]===k)[1];toast(k==="events"?`${G[g].short} events ${on?"shown":"hidden"} in Checklist and Timeline`:`${G[g].short}: ${on?"followed":"unfollowed"} ${n} ${lbl.toLowerCase()}. Fine-tune on the Routines tab.`)}
else if(d.managein){S.tab="routines";S.group="game";const g=d.managein;setTimeout(()=>document.getElementById("band-"+g)?.scrollIntoView({block:"start"}),0)}
else if(d.srvadd){const[g,s]=d.srvadd.split("|");const base=[...S.followed].filter(k=>!k.includes("@")&&byId(k)?.game===g);S.servers[g]=[...S.servers[g],s];base.forEach(k=>{const nk=`${k}@${s}`;S.followed.add(nk);S.pins[nk]={...(S.pins[k]||{})}});S.addSrv=false;toast(`${G[g].short} on ${SV[s].name}: ${base.length} routines copied, nothing ticked`)}
else if(d.srvdel){const[g,s]=d.srvdel.split("|");S.servers[g]=S.servers[g].filter(x=>x!==s);toast(`Removed ${SV[s].name}. Its ticks stay in your save if you add it back.`)}
else if(d.obg){const o=S.ob;o.games=o.games.includes(d.obg)?o.games.filter(x=>x!==d.obg):[...o.games,d.obg]}
else if(d.obc){const[g,k]=d.obc.split("|");S.ob.comp[g][k]=!S.ob.comp[g][k]}
else if(d.oball){const k=d.oball;const o=S.ob;const gs=o.games.filter(g=>k==="events"||BUILTINS.some(r=>r.game===g&&kindOf(r)===k&&r.pulls!=="standard"));const allOn=gs.every(g=>o.comp[g][k]);gs.forEach(g=>o.comp[g][k]=!allOn)}
else if(d.obview){S.ob.view=d.obview}
else if(d.act){switch(d.act){
case"why":S.why=!S.why;break;
case"reset":{const v=S.arcV;S=fresh();S.arcV=v;lastJump="archive";break}
case"library":S.drawer="library";S.tab="routines";break;
case"create":S.drawer="create";S.form=BLANK_FORM();if(S.tab!=="routines")S.tab="routines";break;
case"closedrawer":S.drawer=null;break;
case"closesheet":S.sheet=null;S.arcSheet=null;S.clog=false;break;
case"clog":S.clog=false;S.tab="settings";S.sel="changelog";S.sheet=null;S.arcSheet=null;break;
case"wngot":S.wnSeen=true;toast("The changelog is in the footer whenever you want it.");break;
case"rthint":S.rtHint=false;break;
case"allgames":S.allGames=true;break;
case"srvopen":S.addSrv=d.g;break;
case"srvcancel":S.addSrv=false;break;
case"mine":{const i=parseKey(d.id);const id="mytask:"+Math.random().toString(36).slice(2,8);S.own.push({...i.r,id,builtin:false,own:true,src:"Yours, copied from the built-in"});const nk=keyOf(id,i.srv,i.r.game);const pins=S.pins[i.key];follow(i.key,false);S.followed.add(nk);S.pins[nk]=pins||{};S.progress[nk]=S.progress[i.key];S.menu=null;toast(`${esc(i.r.title)} is now yours to edit. Its pins came with it.`);break}
case"target":case"editown":S.menu=null;toast("Mockup: opens the routine editor");break;
case"newevent":toast("Mockup: opens Add an event");break;
case"noop":toast("Mockup: not wired up");break;
case"obnext":if(S.ob.games.length){S.ob.games.forEach(g=>S.ob.comp[g]??={events:true,dailies:false,weekly:false,cycles:false});S.ob.step=2}break;
case"obnext2":S.ob.step=3;break;
case"obback":S.ob.step=1;break;
case"obback2":S.ob.step=2;break;
case"obskip":S.ob.games=GAMES.map(g=>g.id);S.ob.games.forEach(g=>S.ob.comp[g]??={events:true,dailies:false,weekly:false,cycles:false});S.ob.step=2;break;
case"obdone":{const o=S.ob;S.games=[...o.games];S.servers=Object.fromEntries(GAMES.map(g=>[g.id,[S.region]]));S.followed=new Set();S.pins={};S.own=[];S.progress={};S.ownEvents=[];S.hideEvents=new Set();
o.games.forEach(g=>{const c=o.comp[g];if(!c.events)S.hideEvents.add(g);["dailies","weekly","cycles"].forEach(k=>{if(c[k])builtinsOf(g,k).forEach(r=>follow(r.id,true,{c:false,t:false}))})});
S.defaultView=o.view;S.tab=o.view;S.ob=null;setFocus("all");S.rtHint=true;S.wnSeen=true;lastJump=o.view;
toast(S.followed.size?`Following ${S.followed.size} routines. They're on the Routines tab until you pin them.`:"Events only. Add routines from the Routines tab any time.");break}
default:return;
}}
else return;
render();
});
document.addEventListener("submit",ev=>{ev.preventDefault();const f=ev.target;
if(f.dataset.amtform){S.progress[f.dataset.amtform]=Math.max(0,+f.v.value||0);S.editing=null;render()}
if(f.id==="createform"){const F=S.form;if(!F.title.trim()){f.title.focus();return}const r=formRoutine(F);const id="mytask:"+Math.random().toString(36).slice(2,8);
S.own.push({id,game:r.game,title:r.title,period:r.period,measure:r.measure,resetHour:r.resetHour,note:F.note,link:F.link,src:"Yours",own:true});
follow(keyOf(id,r.srv,r.game),true,{c:F.pc,t:F.pt});S.drawer=null;S.tab="routines";S.form=BLANK_FORM();toast(`Created and following ${esc(r.title)}`);render()}
});
// form fields: keep state as typed, refresh only the preview line (no re-render, so focus stays put)
document.addEventListener("input",ev=>{const t=ev.target;
if(t.id==="libq"){S.q=t.value;const pos=t.selectionStart;render();const i=document.getElementById("libq");i.focus();i.setSelectionRange(pos,pos);return}
if(t.closest("#createform")&&t.name){const F=S.form;if(["title","note","link","unit2"].includes(t.name))F[t.name]=t.value;else if(["days","nDays","target"].includes(t.name))F[t.name]=t.value;else if(t.name==="from")F.from=t.value;
const pv=document.getElementById("pvline");if(pv)pv.innerHTML=previewLine(F);const tt=document.querySelector(".preview .t");if(tt&&t.name==="title")tt.firstChild.textContent=(F.title.trim()||"Untitled routine")+" "}
});
document.addEventListener("change",ev=>{const t=ev.target;
if(t.dataset.pref){S[t.dataset.pref]=t.checked;render();return}
if(t.dataset.newpin){S.newPin[t.dataset.newpin]=t.checked;return}
if(t.dataset.mini){S.progress[t.dataset.mini]=Math.max(0,+t.value||0);render();return}
if(t.dataset.srvset){const[g,ix]=t.dataset.srvset.split("|");S.servers[g][+ix]=t.value;toast(+ix===0?`${G[g].short} now reads its main ticks on the ${SV[t.value].name} clock`:`Server changed to ${SV[t.value].name}`);render();return}
if(t.id==="arcgame"){S.arcGame=t.value;render();return}
if(t.closest("#createform")&&t.name){const F=S.form;
if(t.name==="game"){F.game=t.value;F.srv=null}else if(t.name==="srv")F.srv=t.value;else if(t.name==="dow")F.dow=+t.value;else if(t.name==="dom")F.dom=+t.value;else if(t.name==="hour")F.hour=+t.value;else if(t.name==="hourMode")F.hourMode=t.value;else if(t.name==="pc")F.pc=t.checked;else if(t.name==="pt")F.pt=t.checked;else return;
render()}
});
document.addEventListener("keydown",ev=>{if(ev.key==="Escape"){S.drawer=null;S.sheet=null;S.arcSheet=null;S.clog=false;S.menu=null;S.editing=null;render()}
if((ev.key==="Enter"||ev.key===" ")&&ev.target.matches?.(".kebab")){ev.preventDefault();ev.target.click()}});
// History view: hovering a bar lights its row and back
const hl=(ev,on)=>{const t=ev.target.closest?.("[data-hl]");if(!t)return;document.querySelectorAll(`[data-hl="${t.dataset.hl}"]`).forEach(x=>x.classList.toggle("hl",on))};
document.addEventListener("mouseover",ev=>hl(ev,true));document.addEventListener("mouseout",ev=>hl(ev,false));
document.addEventListener("focusin",ev=>hl(ev,true));document.addEventListener("focusout",ev=>hl(ev,false));
const REVIEW_EVENT_SOURCES={"Tales of Itzaland I": "https://infinity-nikki.fandom.com/wiki/Tales_of_Itzaland_I", "Build on the Land I": "https://infinity-nikki.fandom.com/wiki/Build_on_the_Land_I", "Build on the Land II": "https://infinity-nikki.fandom.com/wiki/Build_on_the_Land_II", "Face It! Wild Surge Returns": "https://infinity-nikki.fandom.com/wiki/Face_It!_Wild_Surge_Returns", "Tales of Itzaland II": "https://infinity-nikki.fandom.com/wiki/Tales_of_Itzaland_II", "Tales of Itzaland III": "https://infinity-nikki.fandom.com/wiki/Tales_of_Itzaland_III", "Free Gilgamesh or Archer": "https://game8.co/games/Honkai-Star-Rail/archives/408749", "Beach Cafe Festival": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899", "To Temper Thyself and Journey Far": "https://game8.co/games/Genshin-Impact/archives/301601", "The Godforsaken Frostlands": "https://genshin-impact.fandom.com/wiki/The_Godforsaken_Frostlands", "Full-Scale Offensive": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899", "Hunter's Crucible": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Lucent Tides Mystery Box": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Summer Time": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Surf Breaker": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Volley Star": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Stronghold Protocol: Alliance - Part 2": "https://arknights.wiki.gg/wiki/Stronghold_Protocol_Alliance_Part_2", "7-Day Login: 120 crystal, 1 FoV DLC stigma box and more materials": "https://www.arustats.com/en-us/hi3/timeline", "7-Day Login: 300 crystals (cont from v8.9)": "https://www.arustats.com/en-us/hi3/timeline", "Captain's Wishing Tree Secrets": "https://www.arustats.com/en-us/hi3/timeline", "Fenghuang of Vicissitude DLC": "https://www.arustats.com/en-us/hi3/timeline", "Jovial Deception Equipment": "https://www.arustats.com/en-us/hi3/timeline", "Jovial Deception: Shadowdimmer": "https://www.arustats.com/en-us/hi3/timeline", "Mad Pleasure Equipment": "https://www.arustats.com/en-us/hi3/timeline", "Mad Pleasure": "https://www.arustats.com/en-us/hi3/timeline", "Senadina": "https://www.arustats.com/en-us/hi3/timeline", "Topop Event": "https://www.arustats.com/en-us/hi3/timeline", "Journal of Promise": "https://karendar.com/", "Main Story: Steering By Light": "https://karendar.com/", "Chiral Redundancy - Part 1": "https://iopwiki.com/wiki/GFL2_Events", "8-Day Login: 5 Equip Cards": "https://www.arustats.com/en-us/hi3/timeline", "Crane of Taixuan": "https://www.arustats.com/en-us/hi3/timeline", "Shipwreck Salvage": "https://game8.co/games/Neverness-to-Everness/archives/592073", "A Winter Dream Fogged Deep in the Woods": "https://endfield.wiki.gg/wiki/A_Winter_Dream_Fogged_Deep_in_the_Woods", "Fletched Irontip": "https://endfield.wiki.gg/wiki/Fletched_Irontip", "Snow Over Deep Woods": "https://endfield.wiki.gg/wiki/Snow_Over_Deep_Woods", "25M Downloads Campaign": "https://fategrandorder.fandom.com/wiki/25M_Downloads_Campaign_(US)", "P2 Finale": "https://www.arustats.com/en-us/hi3/timeline", "Olga's Secret Diary": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899", "Overdrive - Simulation": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899", "Rei's Gift": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899", "SE-4 BR33ZE": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899", "Virtual Tactical Simulation - Olga": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899", "Circle Gift": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Hourglass Perks": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Market Opening Rehearsal": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Runaway Echoes": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Sync Perks": "https://game8.co/games/Neverness-to-Everness/archives/592073", "All-New Program": "https://game8.co/games/Zenless-Zone-Zero/archives/457176", "Angels Support Operation": "https://game8.co/games/Zenless-Zone-Zero/archives/457176", "Vyrium Vacation Album": "https://game8.co/games/Zenless-Zone-Zero/archives/457176", "Trial of the Bow": "https://endfield.wiki.gg/wiki/Trial_of_the_Bow", "Clink, Clank, Pinball Knight!": "https://game8.co/games/Zenless-Zone-Zero/archives/457176", "Cabbage Farm": "https://www.arustats.com/en-us/hi3/timeline", "Flying to Oxia invitation Contest": "https://www.arustats.com/en-us/hi3/timeline", "Lone Destruction Equipment": "https://www.arustats.com/en-us/hi3/timeline", "Lone Destruction": "https://www.arustats.com/en-us/hi3/timeline", "Shadow Chase Showdown": "https://game8.co/games/Zenless-Zone-Zero/archives/457176", "AIC Support: Chubby Lung Attacks": "https://endfield.wiki.gg/wiki/AIC_Support:_Chubby_Lung_Attacks", "Faerie Sugoroku Insect Cage Game Pre-Release Campaign": "https://fategrandorder.fandom.com/wiki/Faerie_Sugoroku_Insect_Cage_Game_Pre-Release_Campaign_(US)", "Coin Rush Showdown": "https://nikke-goddess-of-victory-international.fandom.com/api.php?action=parse&page=Event&prop=text&formatversion=2&format=json", "Guilty: Mighty Bunny": "https://nikke-goddess-of-victory-international.fandom.com/wiki/Guilty:_Mighty_Bunny", "Sugar": "https://nikke-goddess-of-victory-international.fandom.com/wiki/Sugar", "Breezy Tour": "https://game8.co/games/Neverness-to-Everness/archives/592073", "70% Rate-Up: Lucid Dreamer, Pianissimo, Daemonissa": "https://karendar.com/", "Chiral Redundancy - Part 2": "https://iopwiki.com/wiki/GFL2_Events", "A Journey Like No Other": "https://infinity-nikki.fandom.com/wiki/A_Journey_Like_No_Other", "Duel Channel: Ivy Vine": "https://arknights.wiki.gg/wiki/Duel_Channel_Ivy_Vine", "A Rekviem for the Underworld": "https://genshin-impact.fandom.com/wiki/A_Rekviem_for_the_Underworld_(Event)", "Across the Frozen Wilds, Honing One's Edge": "https://genshin-impact.fandom.com/wiki/Across_the_Frozen_Wilds,_Honing_One%27s_Edge", "Battle Pass - Moontrace": "https://game8.co/games/Genshin-Impact/archives/301601", "Character Test Runs": "https://game8.co/games/Genshin-Impact/archives/301601", "Free Limited 5-Star Anniversary Selector": "https://game8.co/games/Genshin-Impact/archives/301601", "Free Standard 5-Star Anniversary Selector": "https://game8.co/games/Genshin-Impact/archives/301601", "Moontrace": "https://genshin-impact.fandom.com/wiki/Battle_Pass/Moontrace", "Raiment Collection: Warmth of a Sunny Day": "https://genshin-impact.fandom.com/wiki/Raiment_Collection/2026-09-23", "Silvery Moonlight Chronicle": "https://genshin-impact.fandom.com/wiki/Miliastra_Pass/2026-09-23", "Starlit Gala: Wonderland Selects": "https://genshin-impact.fandom.com/wiki/Starlit_Gala:_Wonderland_Selects", "Tabletop Troupe: A Gathering on Adventure's Eve": "https://genshin-impact.fandom.com/wiki/Tabletop_Troupe:_A_Gathering_on_Adventure%27s_Eve", "The Forge Realm's Temper: Clever Stratagems": "https://genshin-impact.fandom.com/wiki/The_Forge_Realm%27s_Temper/2026-09-23", "Adventure! A Golden City of Dreams": "https://infinity-nikki.fandom.com/wiki/Adventure!_A_Golden_City_of_Dreams", "For the Child You Once Were (Event)": "https://infinity-nikki.fandom.com/wiki/For_the_Child_You_Once_Were_(Event)", "Interstellar Encounters": "https://infinity-nikki.fandom.com/wiki/Interstellar_Encounters", "Starlight Contest": "https://infinity-nikki.fandom.com/wiki/Starlight_Contest/2026-09-23", "Sunset Hour": "https://infinity-nikki.fandom.com/wiki/Sunset_Hour", "The Migration of Wild Birds": "https://infinity-nikki.fandom.com/wiki/The_Migration_of_Wild_Birds", "Trial Across a Millennium": "https://infinity-nikki.fandom.com/wiki/Trial_Across_a_Millennium", "When Petal Rain Meets Ink": "https://infinity-nikki.fandom.com/wiki/When_Petal_Rain_Meets_Ink", "Where the Golden Feather Fell (Event)": "https://infinity-nikki.fandom.com/wiki/Where_the_Golden_Feather_Fell_(Event)", "Eishin Flash Speed SSR and Narita Top Road Power SR": "https://game8.co/games/Umamusume-Pretty-Derby/archives/536311", "Wonder Acute (Butterfly Sting)": "https://game8.co/games/Umamusume-Pretty-Derby/archives/536311", "Surprise Screening Plan": "https://game8.co/games/Zenless-Zone-Zero/archives/457176", "Discord Sign-In Event": "https://karendar.com/", "Sin: Swift Bunny": "https://nikke-goddess-of-victory-international.fandom.com/wiki/Sin:_Swift_Bunny", "Silverwing in Pursuit of the Moon": "https://game8.co/games/Genshin-Impact/archives/301601", "Login Campaigns": "https://game8.co/games/Persona-5-Phantom-X/archives/532244", "P3P Collab: Reach for the Moon": "https://game8.co/games/Persona-5-Phantom-X/archives/532244", "P3P Event: Reach fo the Moon": "https://game8.co/games/Persona-5-Phantom-X/archives/532244", "Echoing Bell of an Old City": "https://endfield.wiki.gg/wiki/Echoing_Bell_of_an_Old_City", "Purry Big Feline! RAWR!": "https://endfield.wiki.gg/wiki/Purry_Big_Feline!_RAWR!", "Season of Illusion": "https://endfield.wiki.gg/wiki/Season_of_Illusion", "7-Day Sign-In Rewards": "https://karendar.com/", "A Kindler's Watch": "https://karendar.com/", "Anchored in Faith \u2013 Adelyde: Anabasis": "https://karendar.com/", "BP \u2013 Adelyde Alternate Coating": "https://karendar.com/", "BP - Farwatch File": "https://karendar.com/", "Calamity's End": "https://karendar.com/", "Circuit Calculus": "https://karendar.com/", "CUB \u2013 Kelpie": "https://karendar.com/", "Fading Reverie": "https://karendar.com/", "Journal of Promise \u2014 Adelyde: Anabasis": "https://karendar.com/", "Main Story: Anchored in Faith": "https://karendar.com/", "Pale Pyre": "https://karendar.com/", "Sacrament of Ruin": "https://karendar.com/", "Shrouded Requiem \u2014 Nirvatia Story": "https://karendar.com/", "Unforsaken Beacon": "https://karendar.com/", "Visitor from Afar \u2014 Sign-in for 42 Pulls": "https://karendar.com/", "A Fledgling's Brave Fall": "https://reverse1999.fandom.com/wiki/A_Fledgling%27s_Brave_Fall", "The Temporal Scale": "https://reverse1999.fandom.com/wiki/The_Temporal_Scale", "Faerie Sugoroku Insect Cage Game": "https://fategrandorder.fandom.com/wiki/Faerie_Sugoroku_Insect_Cage_Game_(US)", "Moonlit Osmanthus Sign-In": "https://karendar.com/", "Wrathful Monsoon": "https://karendar.com/", "Stellar Memories \u2014 Rerun Coating Selection Research": "https://karendar.com/", "Anniversary Memories Album": "https://genshin-impact.fandom.com/wiki/Anniversary_Memories_Album", "Genshin Impact 6th Anniversary Forum Commenting Event!": "https://genshin-impact.fandom.com/wiki/Genshin_Impact_6th_Anniversary_Forum_Commenting_Event", "Rainbow's End: Resplendent Starlight": "https://genshin-impact.fandom.com/wiki/Rainbow%27s_End:_Resplendent_Starlight", "Astral Imagea": "https://game8.co/games/Honkai-Star-Rail/archives/408749", "Astral Slammers Squad!": "https://game8.co/games/Honkai-Star-Rail/archives/408749", "Love, Ghosts & Robots": "https://game8.co/games/Honkai-Star-Rail/archives/408749", "Wishpower UP! Down with Voracity!": "https://game8.co/games/Honkai-Star-Rail/archives/408749", "Christmas Vodka and Daiwa Scarlet": "https://game8.co/games/Umamusume-Pretty-Derby/archives/536311", "Narita Brian Power SSR and Air Groove Wit SSR": "https://game8.co/games/Umamusume-Pretty-Derby/archives/536311", "Bathed in Warm Smiles, We Linger": "https://stellasora.miraheze.org/wiki/Bathed_in_Warm_Smiles,_We_Linger/2026-09-28", "Innocent Dream Untouched": "https://stellasora.miraheze.org/wiki/Innocent_Dream_Untouched/2026-09-28", "A Flower Blooms Among the Hundred: Fair and Square Aquatic Showdown": "https://bluearchive.wiki/wiki/A_Flower_Blooms_Among_The_Hundred_%EF%BD%9E_Honorable_Sea_Showdown_%EF%BD%9E/Rerun", "Against the Flow of Time: Character Trial Stage": "https://karendar.com/", "Dream of Stagnant Cycle": "https://karendar.com/", "Ember Ceremony": "https://karendar.com/", "Hour & Minute": "https://karendar.com/", "Overclock Simulation": "https://karendar.com/", "PGR \u00d7 Date A Live V \u2013 Kurumi Tokisaki": "https://karendar.com/", "Scorching Torrent": "https://karendar.com/", "Zafkiel": "https://karendar.com/", "Stygian Onslaught": "https://game8.co/games/Genshin-Impact/archives/301601", "Pukaland Travelogue": "https://game8.co/games/Neverness-to-Everness/archives/592073", "3.7 Moonlit Path": "https://game8.co/games/Wuthering-Waves/archives/453473", "Ascendant Aces": "https://game8.co/games/Wuthering-Waves/archives/453473", "Back to Solaris": "https://game8.co/games/Wuthering-Waves/archives/453473", "Blooms for the Shadow": "https://game8.co/games/Wuthering-Waves/archives/453473", "Cubie Wars": "https://game8.co/games/Wuthering-Waves/archives/453473", "Dreams in the Capsule": "https://game8.co/games/Wuthering-Waves/archives/453473", "Gifts of Waking Moon": "https://game8.co/games/Wuthering-Waves/archives/453473", "Moonlit Path 3.7 Rerun": "https://game8.co/games/Wuthering-Waves/archives/453473", "Past Dreams, Traced Seals": "https://game8.co/games/Wuthering-Waves/archives/453473", "Diary of an Orbie Parent": "https://game8.co/games/Zenless-Zone-Zero/archives/457176", "En-Nah Into Your Lap": "https://game8.co/games/Zenless-Zone-Zero/archives/457176", "Ridgeline Flows of Autumn Sign-In": "https://game8.co/games/Arknights-Endfield/archives/535443", "OrbiPom! MERGE!": "https://endfield.wiki.gg/wiki/OrbiPom!_MERGE!", "Runners' Steeplechase": "https://endfield.wiki.gg/wiki/Runners%27_Steeplechase", "PAWS Equipment": "https://www.arustats.com/en-us/hi3/timeline", "Planar Armament": "https://www.arustats.com/en-us/hi3/timeline", "Star-Tempering Clash": "https://www.arustats.com/en-us/hi3/timeline", "Chronicles of the Hobbling Crow": "https://game8.co/games/Zenless-Zone-Zero/archives/457176", "Stamina Recharge": "https://game8.co/games/Neverness-to-Everness/archives/592073", "-ive aLIVE!": "https://bluearchive.wiki/wiki/-ive_aLIVE!/Permanent", "Coal Lumps Treasure": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Artisan's Search": "https://game8.co/games/Wuthering-Waves/archives/453473", "Wreck-It Huhu": "https://karendar.com/", "Predictive Victory Dynamics": "https://game8.co/games/Genshin-Impact/archives/301601", "4.6 Planar Fissure": "https://game8.co/games/Honkai-Star-Rail/archives/408749", "Data Bounty - Combat Simulation": "https://game8.co/games/Zenless-Zone-Zero/archives/457176", "[Celebration] Critical Phase Transition": "https://arknights.wiki.gg/wiki/Critical_Phase_Transition", "Bountiful Crescendo": "https://game8.co/games/Wuthering-Waves/archives/453473", "Waking Moon Fishing": "https://game8.co/games/Wuthering-Waves/archives/453473", "See You in the New World": "https://reverse1999.fandom.com/wiki/See_You_in_the_New_World", "10th Anniversary": "https://www.arustats.com/en-us/hi3/timeline", "Thelema / Lantern / JD": "https://www.arustats.com/en-us/hi3/timeline", "Starlight Voyage: Heart Unbound": "https://genshin-impact.fandom.com/wiki/Starlight_Voyage/2026-10-19", "Pixel Surge": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Carefree Snowball Fight": "https://game8.co/games/Genshin-Impact/archives/301601", "Interastral Peace Gala: One Take": "https://game8.co/games/Honkai-Star-Rail/archives/408749", "Snap Golden Crackshot": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Echo Erase": "https://game8.co/games/Wuthering-Waves/archives/453473", "Gifts of Singing Drizzle": "https://game8.co/games/Wuthering-Waves/archives/453473", "Overflowing Favor": "https://game8.co/games/Genshin-Impact/archives/301601", "Gold Clash": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Puka Lucky Flip": "https://game8.co/games/Neverness-to-Everness/archives/592073", "Beyond the Waves Rerun": "https://game8.co/games/Wuthering-Waves/archives/453473", "Beyond the Waves": "https://game8.co/games/Wuthering-Waves/archives/453473", "Unrecorded Legends": "https://reverse1999.fandom.com/wiki/Unrecorded_Legends", "4.6 Garden of Plenty": "https://game8.co/games/Honkai-Star-Rail/archives/408749", "Chord Cleansing": "https://game8.co/games/Wuthering-Waves/archives/453473", "Back to Solaris Rerun": "https://game8.co/games/Wuthering-Waves/archives/453473"};
EVENTS.forEach(e=>{e.src=REVIEW_EVENT_SOURCES[e.title]||null});
// Round 4 is a standalone review artifact. This namespace never touches app progress.
const DEMO_KEY='event-clock:mockup:round6:a:v1';
const TYPE_EXAMPLES={events:'Time-limited activities from source pages. Track a deadline once, then complete or ignore it.',dailies:'Chores that reset each game-day, such as daily commissions.',weekly:'Goals that reset each week or month, such as weekly bosses or shop restocks.',cycles:'Routines that follow a game’s own schedule. For example, an endgame mode might reset every 14 days.'};
const CODE_DEMO=[{game:'genshin',code:'MOCKPRIMOWISH',reward:'Primogems ×60 · Mora ×20,000',added:'14 Oct',isNew:true},{game:'genshin',code:'MOCKHEROWIT5',reward:'Hero’s Wit ×5 · Mystic Enhancement Ore ×5',added:'9 Oct',isNew:false},{game:'hsr',code:'MOCKSTELLAR37',reward:'Stellar Jade ×60 · Credits ×10,000',added:'13 Oct',isNew:true}];
const pendingTimers=new Map();let undoTimer;
function round4Defaults(){return{density:'standard',gpTab:'setup',codeSeen:[],redeemed:[],pending:{},undo:[],serverSetup:null,sort:'soonest',eventProgress:{e6:'doing'},arcV:2}}
S={...S,...round4Defaults()};
try{const saved=JSON.parse(localStorage.getItem(DEMO_KEY)||'null');if(saved){S={...S,...saved};for(const k of ['hideEvents','followed','archived'])S[k]=new Set(saved[k]||[]);S.pending={};S.undo=[];S.ob=null;S.drawer=null;S.sheet=null;S.arcSheet=null;S.toast=null;S.serverSetup=null}}catch{}
function persistDemo(){try{localStorage.setItem(DEMO_KEY,JSON.stringify({...S,hideEvents:[...S.hideEvents],followed:[...S.followed],archived:[...S.archived],pending:{},undo:[],toast:null,ob:null,drawer:null,sheet:null,arcSheet:null}))}catch{}}
function typeGuide(){return `<dl class="type-guide">${COMP.map(([k,l])=>`<div><dt>${l}</dt><dd>${TYPE_EXAMPLES[k]}${k==='cycles'?' You can follow individual modes later in Routines.':''}</dd></div>`).join('')}</dl>`}
function round4Chrome(){document.getElementById('chrome').innerHTML=`<b>Round 4 · Combined review</b>${[['checklist','Checklist'],['timeline','Timeline'],['routines','Routines'],['add','Add & choose'],['create','Create routine'],['gamepage','Game settings'],['archive','Archive'],['onboard','New user'],['returning','Returning user']].map(([k,l])=>`<button data-jump="${k}">${l}</button>`).join('')}<span class="sep"></span><button data-demo="arrival">Simulate new code</button><button data-act="why">Review notes</button><button data-act="reset">Reset demo</button>`}
function codeNew(g){return CODE_DEMO.filter(c=>c.game===g&&c.isNew&&!S.codeSeen.includes(c.code)).length}
function codeNotice(){const gs=S.games.filter(g=>codeNew(g));return gs.length?`<div class="code-notice" aria-label="New redeem codes"><span>New since you last looked</span>${gs.map(g=>`<button data-codes="${g}">${G[g].short}: ${codeNew(g)} new code${codeNew(g)===1?'':'s'}</button>`).join('')}</div>`:''}
function whatsNew(){return `${!S.wnSeen?`<section class="wn" aria-label="What's new"><h3>A little more room. Your routines, in one place.</h3><p>Your games and progress carry over. Follow, hide and pin routines in <b>Routines</b>. Choose <b>Standard or Compact</b> above the event list. Completed and ignored events now wait in <b>Archive</b>, with their details saved.</p><div class="acts"><button class="btn solid" data-act="wngot">Got it</button><button class="btn" data-intro="routines">Show me Routines</button><button class="btn" data-act="clog">Read the changelog</button></div></section>`:''}${codeNotice()}`}
function round4Checklist(){let html=compactChecklist();const tools=`<div class="list-tools"><div class="seg" aria-label="Event order"><button data-sort="soonest" aria-pressed="${S.sort==='soonest'}">Ending soonest</button><button data-sort="doing" aria-pressed="${S.sort==='doing'}">Doing first</button></div><div class="seg" aria-label="Event list density"><button data-density="standard" aria-pressed="${S.density==='standard'}">Standard</button><button data-density="compact" aria-pressed="${S.density==='compact'}">Compact</button></div></div>`;
html=html.replace(/<div class="seg"><button aria-pressed="true">Ending soonest<\/button><button[^]*?<\/div><\/div>/,tools+'</div>');
const list=liveEvents().filter(e=>S.focus==='all'||e.game===S.focus).sort((a,b)=>{const da=S.eventProgress[a.id]==='doing',db=S.eventProgress[b.id]==='doing';return (S.sort==='doing'?(+db-+da):0)||((a.end??9e15)-(b.end??9e15))});
if(S.density==='standard')html=html.replace(/<table class="led">[^]*?<\/table>/,`<div class="standard-events" aria-label="Running events">${list.map(e=>{const ms=e.end?e.end-NOW:null;return `<button class="event-entry ${e.desc?'':'no-description'}" data-ev="${e.id}" style="${gv(e.game)}"><span><span class="identity">${G[e.game].name}${S.eventProgress[e.id]==='doing'?' · Doing it':''}${e.own?' · Yours':''}</span><span class="event-title">${esc(e.title)}</span>${e.desc?`<span class="event-description">${esc(e.desc)}</span>`:''}</span><span class="deadline ${heat(ms)}">${ms===null?'Unknown':dur(ms)}<small>${e.end?'Ends '+dayStr(new Date(e.end).toISOString().slice(0,10)):'End not announced'}</small>${meter(e,18)}</span>${ICON.right}</button>`}).join('')||'<p class="empty-band">Nothing running for this game.</p>'}</div>`);
else{const start=html.indexOf('<tbody>'),end=html.indexOf('</tbody>',start);if(start>=0&&end>=0){const body=html.slice(start+7,end);const rows=list.map(e=>body.match(new RegExp('<tr data-ev="'+e.id+'"[^]*?<\\/tr>'))?.[0]||'').join('');if(rows)html=html.slice(0,start+7)+rows+html.slice(end)}}
return html}
function round4GamePage(g){const original=originalGamePage(g);if(!S.games.includes(g))return original;
const start=original.indexOf('<div class="grp">');const end=original.indexOf('<div class="grp"><h3>About this game');const setup=original.slice(start,end);
return `<div style="${gv(g)}"><div class="ptitle"><h2>${G[g].name}</h2><span class="studio">${G[g].studio}</span><button class="btn sm" data-followgame="${g}">Stop following game</button></div><div class="subtabs" aria-label="Game settings sections">${[['setup','Setup'],['codes','Codes'],['about','About']].map(([k,l])=>`<button data-gptab="${k}" aria-current="${S.gpTab===k?'page':'false'}">${l}${k==='codes'&&codeNew(g)?` <span class="new-code">${codeNew(g)} new</span>`:''}</button>`).join('')}</div>${S.gpTab==='setup'?setup+serverSetupPanel(g):S.gpTab==='codes'?codesPage(g):aboutPage(g)}</div>`}
function codesPage(g){const list=CODE_DEMO.filter(c=>c.game===g);return `<h3>Redeem codes</h3><p class="muted-copy">“New” means added since you last looked. Redemption is recorded separately for each server.</p><p class="source-note">Illustrative codes for this prototype; these cannot be redeemed.${g==='genshin'?' Source shape: <a href="https://game8.co/games/Genshin-Impact/archives/304759" target="_blank" rel="noopener">Game8’s Genshin codes page</a>.':''}</p>${list.map(c=>`<section class="code-item"><div class="code-title"><code>${c.code}</code><span class="tag">${S.codeSeen.includes(c.code)?'Seen':'New since last look'}</span></div><p>${c.reward}</p><p class="source-note">Added ${c.added} · Expiry not stated</p><div class="code-actions"><button class="btn sm" data-copycode="${c.code}">Copy code</button>${S.servers[g].map(s=>`<button class="btn sm" data-redeemcode="${c.code}|${s}" aria-pressed="${S.redeemed.includes(c.code+'@'+s)}">${SV[s].name}: ${S.redeemed.includes(c.code+'@'+s)?'Redeemed':'Mark redeemed'}</button>`).join('')}</div></section>`).join('')||'<p class="empty-band">No sample codes for this game. Codes will appear here when a supported source publishes them.</p>'}`}
function baseAboutPage(g){return `<h3>About ${G[g].name}</h3><div class="grp"><div class="trow"><span>Studio</span><b>${G[g].studio}</b></div><div class="trow"><span>Daily chores</span><span>${esc(G[g].chores)}</span></div>${S.servers[g].map(s=>`<div class="trow"><span>${SV[s].name} reset</span><span>04:00 server time · next in ${dur(nextDaily(s)-NOW)}</span></div>`).join('')}</div><p class="source-note">Reset clocks are simplified in this prototype. Version and maintenance dates will appear only when a source states them.</p>`}
function serverSetupPanel(g){const f=S.serverSetup;if(!f||f.g!==g)return '';return `<section class="server-setup"><h3>Set up ${G[g].short} · ${SV[f.s].name}</h3><p class="muted-copy">Events only to start. Choose routines for this server; follows and progress from your other servers stay separate.</p>${typeGuide()}<div class="server-choices">${COMP.filter(([k])=>k!=='events').map(([k,l])=>`<button data-serverkind="${k}" aria-pressed="${f.kinds.includes(k)}">${l}</button>`).join('')}</div><div class="acts"><button class="btn solid" data-serverdone="1">Add ${SV[f.s].name} server</button><button class="btn" data-servercancel="1">Cancel</button></div></section>`}
function round4Archive(){const list=arcList(),t=tally(list);const story=`<section class="story" aria-label="Your archive story"><div><h2>Your time across these games</h2><p>${t.done?`You completed ${t.done} event${t.done===1?'':'s'}. `:''}${t.ignored} set aside, ${t.expired} ended unfinished. Your progress, descriptions and source links stay here, even when a source moves on.</p></div><dl><div><dd>${t.done}</dd><dt>Completed</dt></div><div><dd>${t.ignored}</dd><dt>Ignored</dt></div><div><dd>${t.expired}</dd><dt>Unfinished</dt></div></dl></section>`;
const controls=`<div class="archive-view"><div class="seg" aria-label="Archive view"><button data-arcv="2" aria-pressed="${S.arcV===2}">By game</button><button data-arcv="3" aria-pressed="${S.arcV===3}">History timeline</button></div><span class="source-note">Sample history · progress is saved in this demo</span></div>`;
return `${controls}${S.arcSheet&&S.arcV===2?'':story}${S.arcV===3?arcV3():arcV2()}`}
function arcFacts(a){const running=a.end===null||Date.parse(a.end+'T03:00:00Z')>NOW;return `<p class="desc">${esc(a.desc)||'No description was supplied.'}</p><dl class="dl2"><dt>Dates</dt><dd>${dayStr(a.start)} – ${a.end?dayStr(a.end):'End not announced'}</dd><dt>Status</dt><dd>${ST_LABEL[a.status]}</dd><dt>Progress</dt><dd>${esc(a.progress||achieved(a).line||'No additional progress recorded')}</dd><dt>Archived</dt><dd>${dayStr(a.at)}</dd></dl><div class="saved">${a.src?`Source: <a href="${esc(a.src)}" target="_blank" rel="noopener">${esc(a.src)}</a>`:'Reader-authored or illustrative event; no verified source link.'}<br>Saved description from ${dayStr(a.at)}. The live source may have changed.</div><div class="acts"><button class="btn solid" data-restore="${a.id}">${running?'Restore to active views':'Restore record'}</button><button class="btn" data-act="closesheet">Close</button></div>${running?'':'<p class="source-note">Its deadline has passed, so restoring the record does not make it a live event.</p>'}`}
function arcV2Page(a){return `<article class="v2-page" style="${gv(a.game)}"><button class="btn link v2-back" data-act="closesheet">${ICON.left} Back to ${G[a.game].short} archive</button><h1 class="v2-title">${esc(a.title)}</h1><p class="v2-big ach ${a.status}">${ST_LABEL[a.status]}</p>${progViz(a,true)}${arcFacts(a)}</article>`}
function renderArchiveDetail(id){const a=S.archive.find(a=>a.id===id);if(!a||S.arcV===2)return '';return `<div class="scrim" data-act="closesheet"></div><section class="sheet" role="dialog" aria-modal="true" aria-label="${esc(a.title)}" style="${gv(a.game)}"><h2>${esc(a.title)}</h2>${progViz(a,true)}${arcFacts(a)}</section>`}
function timelineScreen(){const evs=liveEvents().filter(e=>S.focus==='all'||e.game===S.focus),gs=S.games.filter(g=>S.focus==='all'||g===S.focus);const name=innerWidth<700?170:250,W=1680,start=Date.parse('2026-10-14T00:00:00Z'),end=start+28*DAY,x=t=>Math.max(0,Math.min(W,(t-start)/(end-start)*W));
const routines=insts().filter(i=>gs.includes(i.r.game)&&pinned(i.key,'t'));const allItems=[...evs.map(e=>({e,g:e.game,t:e.end??9e15})),...routines.map(i=>({i,g:i.r.game,t:reset(i)}))];
const row=it=>{const e=it.e,i=it.i,g=it.g,p=i?periodOf(i.r,i.srv):{start:e.start,end:e.end??end},title=e?e.title:i.r.title,when=e?(e.end?'Ends in '+dur(e.end-NOW):'End not announced'):'Resets in '+dur(p.end-NOW),attr=e?`data-ev="${e.id}"`:`data-rt="${i.key}"`,left=x(p.start),width=Math.max(18,x(p.end)-left);return `<div class="timeline-entry" style="${gv(g)}"><button class="timeline-name" ${attr}>${esc(title)}<small>${e?G[g].short:laneName(g,i.srv)+' · '+progText(i)}</small></button><div class="time-track"><button class="period-block ${i?'routine':''}" ${attr} style="left:${left}px;width:${width}px" aria-label="${esc(title)} · ${esc(when)}" title="${esc(when)}">${i?`<i class="fill" style="width:${Math.min(100,val(i)/req(i.r)*100)}%"></i>`:''}<span>${i?'Reset':'Ends'} · ${e&&!e.end?'Unknown':dayStr(new Date(p.end).toISOString().slice(0,10))}</span></button>${i?`<i class="reset-mark" style="left:${x(p.end)}px"></i>`:''}</div></div>`};
const separator=(g,txt)=>`<div class="game-separator" style="${gv(g)}"><strong>${G[g].name}</strong><span>${txt}</span></div>`;
let body='';if(S.tlStrip||S.tlMode==='bygame'){const ordered=[...gs].sort((a,b)=>S.tlMode==='soonest'?Math.min(...allItems.filter(it=>it.g===a).map(it=>it.t))-Math.min(...allItems.filter(it=>it.g===b).map(it=>it.t)):0);body=ordered.map(g=>{const its=allItems.filter(it=>it.g===g).sort((a,b)=>a.t-b.t);if(!its.length)return '';const events=its.filter(it=>it.e),rs=its.filter(it=>it.i);return separator(g,S.tlStrip?'Events and their adjacent reset strip':'Events & routines')+(S.tlStrip?events.map(row).join('')+(rs.length?`<section class="reset-strip" aria-label="${G[g].short} resets">${separator(g,'Resets · '+rs.length+' routines')}${rs.map(row).join('')}</section>`:''):its.map(row).join(''))}).join('')}else body=allItems.sort((a,b)=>a.t-b.t).map(row).join('');
return `${whatsNew()}<div class="rt-top"><h1>Timeline</h1><div class="sum">One row per event or routine. Your pins decide which routines appear.</div></div><div class="tl-ctl"><div class="seg"><button data-tlmode="soonest" aria-pressed="${S.tlMode==='soonest'}">Ending soonest</button><button data-tlmode="bygame" aria-pressed="${S.tlMode==='bygame'}">By game</button></div><label class="chk"><input type="checkbox" data-pref="tlStrip" ${S.tlStrip?'checked':''}>Separate reset strips</label></div>${S.tlStrip&&S.tlMode==='soonest'?'<p class="timeline-help">Games are ordered by their next deadline, with each reset strip kept beside that game’s events.</p>':''}${focusRow(g=>evs.filter(e=>g==='all'||e.game===g).length)}<div class="timeline-scroller"><div class="timeline-board" style="width:${name+W}px"><div class="time-axis"><strong>Game / activity</strong><div class="ticks">${Array.from({length:28},(_,j)=>`<span style="left:${j*60+8}px">${dayStr(new Date(start+j*DAY).toISOString().slice(0,10))}</span>`).join('')}</div></div>${body||'<p class="empty-band">No events or pinned routines for this selection.</p>'}<div class="today-line" style="left:${name+x(NOW)}px"></div></div></div><p class="timeline-help">Solid bars: events. Dashed bars: routine periods, filled by progress. Scroll horizontally for later dates. Clocks are fixed for this review.</p>`}
function baseEventSheet(){const e=[...EVENTS,...S.ownEvents].find(e=>e.id===S.sheet);if(!e)return '';return `<div class="scrim" data-act="closesheet"></div><section class="sheet" role="dialog" aria-modal="true" aria-label="${esc(e.title)}" style="${gv(e.game)}"><p style="color:var(--g)">${G[e.game].name}</p><h2>${esc(e.title)}</h2><p class="muted-copy">${esc(e.desc)||'The source gave no description.'}</p><p>${e.end?'Ends '+dateStr(e.end):'End date not announced'}</p>${e.src?`<p class="source-note"><a href="${esc(e.src)}" target="_blank" rel="noopener">View source page</a></p>`:''}<div class="acts"><button class="btn solid" data-evst="${e.id}|done">Mark done</button><button class="btn" data-evst="${e.id}|ignored">Ignore</button><button class="btn" data-doing="${e.id}">${S.eventProgress[e.id]==='doing'?'Clear in progress':'Doing it'}</button><button class="btn link" data-act="closesheet">Close</button></div><p class="source-note">Done and Ignore save its details to Archive. You’ll see a brief confirmation before the row leaves.</p></section>`}
function whyPanel(){return `<aside class="why"><button class="btn" data-act="why">Close</button><h2>Round 4 review</h2><p>Compare Standard and Compact using the same busy event list. Focus a game to expand its pinned routines.</p><p>Try completing or ignoring an event: its row confirms the move for 1.5 seconds, then fades. Undo stays available for 8 seconds. Archive keeps its description and source.</p><p>Try Settings → Genshin → add a server. Its routine choices start empty. Codes distinguish seen from redeemed per server. “Simulate new code” exercises arrival notices in Checklist and Timeline.</p><p>New user walks through events-only setup with type explanations. Returning user shows the introduction in whichever view is set as default.</p><p>This standalone prototype uses isolated browser storage. Event content comes from the earlier mockups; codes, archive history and some descriptions are illustrative. Reset clocks and “now” are fixed. Backup and some own-event editors remain placeholders.</p></aside>`}
function round4Render(){const pos=document.querySelector('.timeline-scroller')?.scrollLeft||0;baseRender();const scroller=document.querySelector('.timeline-scroller');if(scroller)scroller.scrollLeft=pos;
for(const [id,p] of Object.entries(S.pending)){document.querySelectorAll(`[data-ev="${id}"]`).forEach(el=>{if(el.closest('.sheet'))return;el.classList.add('archiving');if(p.phase==='fading')el.classList.add('fading');const target=el.querySelector('.event-title,.tcell')||el;target.insertAdjacentHTML('beforeend',`<span class="archive-confirm" role="status">${p.status==='done'?'Completed':'Ignored'} · Moved to Archive</span>`)})}
if(S.undo.length)document.getElementById('overlay').insertAdjacentHTML('beforeend',`<div class="toast undo-note" role="status"><span>${S.undo.length===1?(S.archive.find(a=>a.id===S.undo[0].aid)?.status==='done'?'Completed':'Ignored')+' · Moved to Archive':S.undo.length+' events moved to Archive'}</span><button data-undo="1">Undo</button></div>`)
document.querySelectorAll('.srow[data-sel]').forEach(el=>{const n=codeNew(el.dataset.sel);if(n)el.querySelector('.nm')?.insertAdjacentHTML('beforeend',` <span class="new-code">${n} new code${n===1?'':'s'}</span>`)});
persistDemo()}
function openCodes(g){S.tab='settings';S.sel=g;S.gpTab='codes';CODE_DEMO.filter(c=>c.game===g).forEach(c=>{if(!S.codeSeen.includes(c.code))S.codeSeen.push(c.code)});S.sheet=null;S.arcSheet=null}
function archiveEvent(id,status){if(S.archived.has(id))return;const e=[...EVENTS,...S.ownEvents].find(e=>e.id===id);if(!e)return;const aid='saved:'+id;S.archived.add(id);S.archive.unshift({id:aid,ref:id,game:e.game,title:e.title,status,start:new Date(e.start).toISOString().slice(0,10),end:e.end?new Date(e.end).toISOString().slice(0,10):null,at:'2026-10-16',desc:e.desc,ver:'Saved events',progress:status==='done'?'Marked completed on 16 Oct':'Ignored on 16 Oct',src:e.src||null});S.sheet=null;S.pending[id]={status,phase:'confirm'};S.undo.push({id,aid,previous:S.eventProgress[id]});S.toast=null;clearTimeout(toastT);clearTimeout(undoTimer);undoTimer=setTimeout(()=>{S.undo=[];render()},8000);pendingTimers.set(id,setTimeout(()=>{if(!S.pending[id])return;S.pending[id].phase='fading';document.querySelectorAll(`[data-ev="${id}"]`).forEach(el=>el.classList.add('fading'));pendingTimers.set(id,setTimeout(()=>{delete S.pending[id];pendingTimers.delete(id);render()},matchMedia('(prefers-reduced-motion: reduce)').matches?0:350))},1500))}
// Capture only Round 4 interactions; inherited library/forms continue to work.
document.addEventListener('click',ev=>{const el=ev.target.closest('button');if(!el)return;const d=el.dataset;let handled=true;
if(d.density)S.density=d.density;
else if(d.sort)S.sort=d.sort;
else if(d.codes)openCodes(d.codes);
else if(d.gptab){S.gpTab=d.gptab;if(d.gptab==='codes')openCodes(S.sel)}
else if(d.copycode){navigator.clipboard?.writeText(d.copycode).then(()=>toast('Code copied'),()=>toast('Select the code text to copy it.'));if(!navigator.clipboard)toast('Select the code text to copy it.')}
else if(d.redeemcode){const [code,srv]=d.redeemcode.split('|'),k=code+'@'+srv;S.redeemed=S.redeemed.includes(k)?S.redeemed.filter(x=>x!==k):[...S.redeemed,k]}
else if(d.demo==='arrival'){const c=CODE_DEMO.find(c=>c.game==='genshin');c.isNew=true;S.codeSeen=S.codeSeen.filter(x=>x!==c.code);S.tab='checklist';toast('Demo: a new Genshin code arrived. Redemption marks are unchanged.')}
else if(d.intro){S.tab=d.intro;S.wnSeen=true}
else if(d.evst){const [id,st]=d.evst.split('|');archiveEvent(id,st)}
else if(d.doing){S.eventProgress[d.doing]=S.eventProgress[d.doing]==='doing'?'untouched':'doing';S.sheet=null}
else if(d.undo){for(const u of S.undo){clearTimeout(pendingTimers.get(u.id));pendingTimers.delete(u.id);S.archived.delete(u.id);delete S.pending[u.id];S.archive=S.archive.filter(a=>a.id!==u.aid);S.eventProgress[u.id]=u.previous}S.undo=[];toast('Restored to your active events.')}
else if(d.restore){const a=S.archive.find(a=>a.id===d.restore);if(a){if(a.ref){clearTimeout(pendingTimers.get(a.ref));delete S.pending[a.ref];S.archived.delete(a.ref);S.undo=S.undo.filter(u=>u.id!==a.ref)}else if(a.end===null||Date.parse(a.end+'T03:00:00Z')>NOW){if(!S.ownEvents.some(e=>e.id===a.id))S.ownEvents.push({id:a.id,game:a.game,title:a.title,desc:a.desc,start:Date.parse(a.start+'T03:00:00Z'),end:a.end?Date.parse(a.end+'T03:00:00Z'):null,src:a.src})}S.restoredRecords??=[];S.restoredRecords.push({...a,status:'untouched'});S.archive=S.archive.filter(x=>x!==a);S.arcSheet=null;toast('Restored. Its saved details and progress are retained.')}}
else if(d.srvadd){const [g,s]=d.srvadd.split('|');S.serverSetup={g,s,kinds:[]};S.addSrv=false}
else if(d.serverkind){const f=S.serverSetup;f.kinds=f.kinds.includes(d.serverkind)?f.kinds.filter(k=>k!==d.serverkind):[...f.kinds,d.serverkind]}
else if(d.serverdone){const f=S.serverSetup;if(!S.servers[f.g].includes(f.s))S.servers[f.g].push(f.s);for(const kind of f.kinds)for(const r of builtinsOf(f.g,kind))follow(keyOf(r.id,f.s,f.g),true,{c:false,t:false});S.serverSetup=null;toast('Server added. Fine-tune and pin its routines in Routines.')}
else if(d.servercancel)S.serverSetup=null;
else if(d.act==='round4-reset'){for(const t of pendingTimers.values())clearTimeout(t);pendingTimers.clear();clearTimeout(toastT);clearTimeout(undoTimer);S={...fresh(),...round4Defaults(),tab:'checklist'};lastJump='checklist'}
else if(d.jump==='gamepage'){jump('gamepage');S.gpTab='setup'}
else if(d.sel){S.sel=d.sel;S.gpTab='setup';S.addSrv=false;S.serverSetup=null}
else handled=false;
if(handled){ev.preventDefault();ev.stopImmediatePropagation();render()}
},true);
document.addEventListener('keydown',ev=>{if((ev.key==='Enter'||ev.key===' ')&&ev.target.matches('tr[data-ev]')){ev.preventDefault();S.sheet=ev.target.dataset.ev;render()}},true);
function round4Footer(){return `<footer class="foot"><span>Review prototype · fixed clocks, sample codes and archive history.</span><button data-act="clog">Changelog</button><button data-act="why">Review notes</button></footer>`}
const CREDIT_SOURCE_URLS=["https://arknights.wiki.gg", "https://bluearchive.wiki", "https://endfield.wiki.gg", "https://fategrandorder.fandom.com", "https://game8.co", "https://genshin-impact.fandom.com", "https://holodori.wiki", "https://infinity-nikki.fandom.com", "https://iopwiki.com", "https://karendar.com", "https://nikke-goddess-of-victory-international.fandom.com", "https://reverse1999.fandom.com", "https://stellasora.miraheze.org", "https://www.arustats.com"];
// Standalone Round 5 review. All saved choices and backups use the demo namespace.
const DEFAULT_HUES=Object.fromEntries(GAMES.map(g=>[g.id,g.hue]));
S={scheme:'slate',timezone:'automatic',dailyPanel:true,focusStyle:'switcher',gameColors:{},sourceScenario:'mixed',backups:null,beforeRestore:null,backupPreview:null,backupFeedback:null,...S};
const browserZone=()=>Intl.DateTimeFormat().resolvedOptions().timeZone||'UTC';
const displayZone=()=>S.timezone==='automatic'?browserZone():S.timezone;
function dateStr(t){return new Intl.DateTimeFormat('en-GB',{timeZone:displayZone(),weekday:'short',day:'numeric',month:'short',hour:'2-digit',minute:'2-digit',timeZoneName:'short'}).format(t)}
function yourTime(t){return new Intl.DateTimeFormat('en-GB',{timeZone:displayZone(),weekday:'short',hour:'2-digit',minute:'2-digit',timeZoneName:'short'}).format(t)}
function zoneLabel(z){const offset=new Intl.DateTimeFormat('en-GB',{timeZone:z,timeZoneName:'longOffset'}).formatToParts(NOW).find(p=>p.type==='timeZoneName')?.value;return z.replaceAll('_',' ')+' · '+offset}
function zoneOptions(selected){const zones=[...new Set(['UTC','Europe/Copenhagen','America/New_York','America/Los_Angeles','Asia/Tokyo','Asia/Shanghai',...(Intl.supportedValuesOf?.('timeZone')||[])])].sort();return `<option value="automatic" ${selected==='automatic'?'selected':''}>Automatic · ${browserZone()}</option>${zones.map(z=>`<option value="${z}" ${selected===z?'selected':''}>${zoneLabel(z)}</option>`).join('')}`}
function actualTheme(){return S.theme==='system'?(matchMedia('(prefers-color-scheme: light)').matches?'light':'dark'):S.theme}
function contrast(a,b){const lum=h=>{const rgb=h.match(/[a-f\d]{2}/gi).map(x=>parseInt(x,16)/255).map(x=>x<=.04045?x/12.92:((x+.055)/1.055)**2.4);return rgb[0]*.2126+rgb[1]*.7152+rgb[2]*.0722};const x=lum(a),y=lum(b);return(Math.max(x,y)+.05)/(Math.min(x,y)+.05)}
function readableGame(h,bg){if(!/^#[0-9a-f]{6}$/i.test(h))return '#999999';if(contrast(h,bg)>=4.5)return h;const rgb=h.slice(1).match(/../g).map(x=>parseInt(x,16)),toward=actualTheme()==='light'?0:255;for(let f=.02;f<=1;f+=.02){const c='#'+rgb.map(x=>Math.round(x+(toward-x)*f).toString(16).padStart(2,'0')).join('');if(contrast(c,bg)>=4.5)return c}return actualTheme()==='light'?'#000000':'#ffffff'}
function baseApplyAppearance(){document.documentElement.dataset.theme=actualTheme();document.documentElement.dataset.scheme=S.scheme;const v=getComputedStyle(document.documentElement).getPropertyValue('--ground').trim();const bg=/^#[a-f\d]{6}$/i.test(v)?v:'#0e1117';for(const g of GAMES)g.hue=readableGame(S.gameColors[g.id]||DEFAULT_HUES[g.id],bg)}
// A saved type preference remains selectable even when it has no matching routines today.
function availabilityNotes(gs){const empty=gs.flatMap(g=>COMP.filter(([k])=>k!=='events'&&!BUILTINS.some(r=>r.game===g&&kindOf(r)===k)).map(([k,l])=>`${G[g].short}: no built-in ${l.toLowerCase()} available yet.`));return `<div class="availability"><p>Every type is selectable. We remember what you want to track even when there is nothing available today.</p>${empty.length?`<p>${empty.map(esc).join(' ')}</p>`:''}<p>A supported routine between runs is different: its next window will appear when its schedule is available. New built-ins are offered for review in Routines.</p></div>`}
function focusButtons(cnt){return `<div class="focus-buttons">${['all',...S.games].map(g=>`<button data-focus="${g}" aria-pressed="${S.focus===g}">${g==='all'?'All games':G[g].short}<span class="n">${cnt(g)}</span></button>`).join('')}</div>`}
function focusControls(){const cnt=g=>liveEvents().filter(e=>g==='all'||e.game===g).length;if(S.focusStyle==='switcher')return `<div class="focus-tools"><label class="focus-label" for="gamefocus">Show events for</label><select id="gamefocus" data-focus-select>${['all',...S.games].map(g=>`<option value="${g}" ${S.focus===g?'selected':''}>${g==='all'?'All games':G[g].name} (${cnt(g)})</option>`).join('')}</select>${S.focus!=='all'?'<button class="btn sm" data-focus="all">Show all games</button>':''}<span class="focus-current">${cnt(S.focus)} events shown</span></div>`;return `<section class="focus-tools" aria-label="Filter by game"><span class="focus-label">Your games</span>${focusButtons(cnt)}${S.focus!=='all'?'<button class="btn sm" data-focus="all">Clear focus</button>':''}</section>`}
function focusRow(){return S.tab==='timeline'?focusControls():''}
const pinnedIn=(g,srv,p)=>round4PinnedIn(g,srv,p).filter(i=>!(S.dailyPanel&&p==='c'&&i.r.period.unit==='day'));
function baseRailPinned(){const d=insts().filter(i=>i.r.period.unit==='day'&&pinned(i.key,'c')&&(S.focus==='all'||i.r.game===S.focus));const panel=S.dailyPanel?`<section class="daily-panel" aria-label="Today's dailies"><div class="daily-heading"><h3>Today's dailies</h3><button data-tab="routines">Edit pins</button></div>${d.map(i=>`<div class="daily-row" style="${gv(i.r.game)}"><span><strong>${G[i.r.game].short}${multi(i.r.game)?' · '+SV[i.srv].short:''}</strong><small>${esc(G[i.r.game].chores)} · reset ${dur(reset(i)-NOW)}</small></span><button class="tick" data-tick="${i.key}" aria-pressed="${isMet(i)}" aria-label="${esc(laneName(i.r.game,i.srv))} dailies done"><span class="box">${ICON.check}</span></button></div>`).join('')||'<p class="muted-copy">No dailies pinned here. Choose them in Routines.</p>'}</section>`:'';return `<div class="daily-layout"><span>Dailies: ${S.dailyPanel?'own panel':'grouped'}</span><button data-daily-layout="${S.dailyPanel?'grouped':'separate'}">${S.dailyPanel?'Group with routines':'Show separate panel'}</button></div>${panel}${round4Rail()}`}
function checklistScreen(){let html=round4Checklist();if(S.focusStyle==='rail'){const count=g=>liveEvents().filter(e=>g==='all'||e.game===g).length;html=html.replace('<aside class="rail">',`<aside class="rail"><nav class="focus-rail" aria-label="Filter by game"><h3>Your games</h3>${focusButtons(count)}</nav>`)}else html=focusControls()+html;return html}
function baseVariantChrome(){round4Chrome();const el=document.getElementById('chrome');el.querySelector('b').textContent='Round 5 · Review';el.querySelector('b').insertAdjacentHTML('afterend',`<span class="sep"></span><label for="source-demo">Data</label><select id="source-demo" data-source-scenario>${[['current','Current'],['mixed','Some stale'],['stale','All stale'],['never','Never fetched'],['offline','Offline']].map(([k,l])=>`<option value="${k}" ${S.sourceScenario===k?'selected':''}>${l}</option>`).join('')}</select><label for="focus-demo">Focus</label><select id="focus-demo" data-focus-style>${[['toolbar','Game toolbar'],['switcher','Game switcher'],['rail','Left-panel picker']].map(([k,l])=>`<option value="${k}" ${S.focusStyle===k?'selected':''}>${l}</option>`).join('')}</select><button data-setting="backup">Backups</button><button data-setting="about">About</button>`)}
function sampleSourceStates(wholeFeed=false){return (wholeFeed?DEFAULT_GAMES:S.games).map(g=>{let check=NOW-2*H,updated=NOW-6*H,reason='Source check succeeded; the held schedule is recent.',kind='current';if(S.sourceScenario==='never'){check=null;updated=null;reason='No successful source fetch has been recorded. This view is using bundled sample data.';kind='never'}else if(S.sourceScenario==='stale'){check=NOW-8*DAY;updated=NOW-10*DAY;reason='Pull overdue. No recent successful source check; the cause was not reported.';kind='overdue'}else if(['mixed','offline'].includes(S.sourceScenario)&&g==='genshin'){updated=NOW-9*DAY;reason='Site has no new updates. The latest check succeeded, but the page has not changed.';kind='unchanged'}else if(['mixed','offline'].includes(S.sourceScenario)&&g==='endfield'){check=NOW-6*DAY;updated=NOW-9*DAY;reason='Pull overdue. No recent successful source check; the cause was not reported.';kind='overdue'}else if(['mixed','offline'].includes(S.sourceScenario)&&g==='nte'){check=null;updated=null;reason='No successful source fetch has been recorded. A bundled sample schedule is being held.';kind='never'}if(S.sourceScenario==='offline'){reason='Offline: the app cannot check for a newer published feed. '+reason}return {g,check,updated,reason,kind,url:EVENTS.find(e=>e.game===g&&e.src)?.src||null}})}
const sourceAge=t=>t===null?'never':dur(Math.max(0,NOW-t))+' ago';
function sourceRows(rows=sampleSourceStates()){return rows.map(r=>`<div class="source-row" style="${gv(r.g)}"><strong>${G[r.g].name}</strong><div><p>Last successful check: ${r.check===null?'Never':dateStr(r.check)+' · '+sourceAge(r.check)}</p><p>Source content last changed: ${r.updated===null?'Not recorded':dateStr(r.updated)+' · '+sourceAge(r.updated)}</p><p class="reason ${r.kind==='current'?'current':''}">${esc(r.reason)}</p>${r.url?`<a href="${esc(r.url)}" target="_blank" rel="noopener">Check source page</a>`:''}</div></div>`).join('')}
function baseFreshnessNotice(){const rows=sampleSourceStates(),stale=rows.filter(r=>r.kind!=='current');const feedRows=sampleSourceStates(true);const any=feedRows.some(r=>r.updated!==null)?Math.max(...feedRows.filter(r=>r.updated!==null).map(r=>r.updated)):null;return `<section aria-label="Event data freshness"><div class="freshness-bar"><span><b>Event data last refreshed</b> · ${any===null?'No source fetched yet':dateStr(any)+' · '+sourceAge(any)}<br>Illustrative source status · clocks fixed for review</span><button data-check-data ${S.sourceScenario==='offline'?'disabled':''}>${S.sourceScenario==='offline'?'Offline · cannot check':'Check for new data'}</button><button data-setting="sources">Data & sources</button></div>${stale.length||S.sourceScenario==='offline'?`<div class="freshness-warning"><h2>${S.sourceScenario==='offline'?'You’re viewing saved data offline':S.sourceScenario==='never'?'These schedules have not been fetched yet':stale.length===rows.length?'Your selected games have older data':`${stale.length} of your selected games have older source data`}</h2><p>${stale.map(r=>`${G[r.g].short}: ${r.kind==='unchanged'?'source unchanged for '+sourceAge(r.updated).replace(' ago',''):r.kind==='never'?'never fetched':'pull overdue by '+sourceAge(r.check).replace(' ago','')}`).join(' · ')}${stale.length?'. End dates may have changed.':''}</p><details><summary>Why the data is older · source breakdown</summary>${sourceRows(stale.length?stale:rows)}</details></div>`:''}</section>`}
function reviewFooter(){return `<footer class="foot"><span>Event data: ${S.sourceScenario==='current'?'recent source checks':'some schedules may be old'} · <button data-setting="sources">See freshness and reasons</button></span><button data-setting="about">About & credits</button><button data-act="clog">Changelog</button><button data-act="why">Review notes</button></footer>`}
function baseGameSetup(g){let html=round4GamePage(g);if(S.gpTab==='setup'&&S.games.includes(g)){const colour=`<section class="grp"><h3>Game colour<span>Used for ${G[g].short} in every view and on all its servers.</span></h3><div class="game-colour"><input type="color" data-game-colour="${g}" value="${S.gameColors[g]||DEFAULT_HUES[g]}" aria-label="${G[g].name} colour"><input type="text" data-game-hex="${g}" value="${S.gameColors[g]||DEFAULT_HUES[g]}" pattern="#[0-9a-fA-F]{6}" maxlength="7" aria-label="${G[g].name} hex colour"><button class="btn sm" data-colour-reset="${g}">Reset to default</button><span class="colour-preview" style="color:${G[g].hue}">${G[g].short} event</span></div><p class="source-note">The preview adjusts for readable contrast. Your selected base colour stays saved.</p></section>`;html+=colour}return html}
function previousGeneralPane(k){if(k==='sources')return `<div class="ptitle"><h2>Data & sources</h2></div><p class="muted-copy">The latest successful source check and the last change to the source are separate facts. A successful check can confirm an unchanged, old page. Statuses below are illustrative for this prototype.</p>${freshnessNotice()}${sourceRows()}`;
if(k==='about')return aboutClock();if(k==='backup')return backupPane();if(k==='general')return `<div class="ptitle"><h2>Region & time</h2></div><p class="muted-copy">Your default server starts each game’s setup. You can change it per game and track more than one server.</p><div class="grp"><h3>Default game server</h3><div class="seg">${Object.keys(SV).map(s=>`<button data-region="${s}" aria-pressed="${S.region===s}">${SV[s].name}</button>`).join('')}</div></div><div class="grp"><h3>Your timezone</h3><p class="muted-copy">Automatic follows your device. A chosen timezone changes displayed dates and times; game servers still determine resets and deadlines.</p><input class="zone-search" type="search" data-zone-search placeholder="Find a timezone, e.g. Copenhagen" aria-label="Find timezone"><select class="zone-select" data-timezone aria-label="Your timezone">${zoneOptions(S.timezone)}</select><p class="source-note">Currently displaying ${zoneLabel(displayZone())}. Named zones follow daylight saving automatically.</p></div>${`<div class="grp"><h3>Resets for your games</h3>${lanes().map(({g,srv})=>`<div class="trow" style="${gv(g)}"><span class="lbl" style="color:var(--g)">${laneName(g,srv)}<small>${SV[srv].name} server · next reset ${yourTime(nextDaily(srv))}</small></span><span>${dur(nextDaily(srv)-NOW)}</span></div>`).join('')}</div>`}`;
if(k==='appearance')return `<div class="ptitle"><h2>Appearance</h2></div><section class="grp"><h3>Mode</h3><div class="seg">${['dark','light','system'].map(t=>`<button data-theme="${t}" aria-pressed="${S.theme===t}">${t[0].toUpperCase()+t.slice(1)}</button>`).join('')}</div></section><section class="grp"><h3>Colour scheme</h3><p class="muted-copy">Preview the page palette in either mode. Game colours and deadline urgency keep their own meanings.</p><div class="swatches">${[['slate','Slate',['#0e1117','#323b4e','#5b8def']],['warm','Warm',['#181512','#55493b','#9bc4ef']],['plum','Plum',['#17131c','#584768','#bea4f2']]].map(([id,l,cs])=>`<button class="scheme-option" data-scheme="${id}" aria-pressed="${S.scheme===id}"><span class="swatch">${cs.map(c=>`<i style="--swatch:${c}"></i>`).join('')}</span>${l}</button>`).join('')}</div><p class="source-note">Three curated schemes for this review. Individual game colours are edited in that game’s Setup.</p></section><section class="grp"><h3>Dailies on the Checklist</h3><div class="seg"><button data-daily-layout="separate" aria-pressed="${S.dailyPanel}">Separate panel</button><button data-daily-layout="grouped" aria-pressed="${!S.dailyPanel}">Grouped with routines</button></div><p class="muted-copy">The separate panel keeps each daily tick one click away.</p></section>`;
return round4GeneralPane(k)}
function aboutClock(){return `<div class="ptitle"><h2>About Event Clock</h2></div><div class="about-prose"><p>Live and upcoming gacha events, sorted by what expires next. An unofficial, fan-made calendar with progress saved in your browser.</p><h3>Made by</h3><p><a href="https://lucaswinther.info" target="_blank" rel="noopener">Lucas Winther</a> · <a href="https://github.com/StereotypicalCat" target="_blank" rel="noopener">StereotypicalCat</a></p><div class="about-links"><a href="https://github.com/StereotypicalCat/gacha-event-tracker" target="_blank" rel="noopener">Source code</a><a href="https://github.com/StereotypicalCat/gacha-event-tracker/issues/new?template=bug_report.yml" target="_blank" rel="noopener">Report a problem</a><a href="https://ko-fi.com/stereotypicalcat" target="_blank" rel="noopener">Support the project</a></div><h3>With thanks to the source editors</h3><p>The source communities compile and maintain the schedules; Event Clock rearranges their work. ${CREDIT_SOURCE_URLS.map(u=>`<a href="${u}" target="_blank" rel="noopener">${new URL(u).hostname}</a>`).join(' · ')}.</p><p><a href="https://github.com/cicerakes/Game-Time-Master" target="_blank" rel="noopener">Game-Time-Master</a> provides the reference for per-server reset clocks.</p><h3>The studios and games</h3><p>${[...new Set(GAMES.map(g=>g.studio))].map(esc).join(' · ')}. All game names, event names and trademarks belong to their respective owners.</p><h3>Ideas and acknowledgements</h3><p>${[['phuduong85','https://github.com/phuduong85/gacha-event-tracker'],['u/eriatilox','https://www.reddit.com/user/eriatilox/'],['u/lotus_lunaris','https://www.reddit.com/user/lotus_lunaris/'],['u/Neoragex13','https://www.reddit.com/user/Neoragex13/'],['u/ShiroWaffles','https://www.reddit.com/user/ShiroWaffles/']].map(([l,u])=>`<a href="${u}" target="_blank" rel="noopener">${l}</a>`).join(' · ')}.</p><h3>Unofficial and unaffiliated</h3><p>Not endorsed by or connected to any game publisher or source named here. Dates can be wrong or go out of date: check the original source before the final hours.</p><p><button class="btn" data-setting="sources">See data freshness and source status</button></p><p class="source-note">Round 5 review prototype. Codes and archive history are sample content; clocks are fixed. Your production app data is separate.</p></div>`}
function archiveStory(){const list=arcList(),played=list.filter(a=>a.status!=='ignored'),done=played.filter(a=>a.status==='done'),oct=played.filter(a=>a.at.startsWith('2026-10')),rate=as=>as.length?Math.round(as.filter(a=>a.status==='done').length/as.length*100)+'%':'—';let run=0,best=0;const ordered=[...played].sort((a,b)=>a.at.localeCompare(b.at)||a.id.localeCompare(b.id));for(const a of ordered){run=a.status==='done'?run+1:0;best=Math.max(best,run)}const days=list.reduce((n,a)=>n+(a.prog?.t==='days'?a.prog.got:0),0),unfinished=played.filter(a=>a.status==='expired'),near=unfinished.filter(a=>a.prog&&['days','stages'].includes(a.prog.t)&&a.prog.need-a.prog.got===1),games=[...new Set(list.map(a=>a.game))];return `<section class="stats-story" aria-label="Your archive story"><h2>Your season, in a few numbers</h2><p>${done.length} completed, ${list.filter(a=>a.status==='ignored').length} set aside. A record of your saved events, across ${games.length} games.</p><div class="stats-grid"><div><strong>${rate(oct)}</strong><span>finished in October</span><small>${rate(played)} across saved history · ignored events excluded</small></div><div><strong>${run}</strong><span>finished in a row</span><small>Best run ${best} · ordered by archive date</small></div><div><strong>${days}</strong><span>event check-ins logged</span><small>Across events; more than one can fall on the same day</small></div><div><strong>${unfinished.length}</strong><span>ended unfinished</span><small>${near.length} short by one day or stage</small></div></div>${games.map(g=>{const t=tally(list.filter(a=>a.game===g));return `<button class="stats-game" data-arcgame="${S.arcGame===g?'all':g}" style="${gv(g)}"><span class="name">${G[g].short}</span>${stack(t)}<span class="counts">${t.done} done · ${t.expired} unfinished · ${t.ignored} ignored</span></button>`}).join('')}<p class="source-note">Illustrative archive history. Blue: completed · outlined: unfinished · muted: ignored. Filtered statistics reflect the records shown below.</p></section>`}
function renderArchive(){return round4Archive().replace(/<section class="story"[^]*?<\/section>/,archiveStory())}
function baseSetup(){const o=S.ob;if(o.step===0)return `<div class="ob"><div class="step">Step 1 of 4</div><h1>Make it yours</h1><p class="lede">Start with a few preferences. You can change every answer in Settings later.</p><div class="preference-grid"><label>Default game server<select data-onboard-pref="region">${Object.keys(SV).map(k=>`<option value="${k}" ${S.region===k?'selected':''}>${SV[k].name}</option>`).join('')}</select><small>Each game starts here. You can choose a different server for a game or track two accounts separately.</small></label><label>Appearance<select data-onboard-pref="theme">${['dark','light','system'].map(k=>`<option value="${k}" ${S.theme===k?'selected':''}>${k[0].toUpperCase()+k.slice(1)}</option>`).join('')}</select></label><label>Colour scheme<select data-onboard-pref="scheme">${['slate','warm','plum'].map(k=>`<option value="${k}" ${S.scheme===k?'selected':''}>${k[0].toUpperCase()+k.slice(1)}</option>`).join('')}</select></label><label>Your timezone<select data-onboard-pref="timezone">${zoneOptions(S.timezone)}</select><small>Automatic follows your device; server reset clocks stay separate.</small></label><label class="chk"><input type="checkbox" data-onboard-pref="showUpcoming" ${S.showUpcoming?'checked':''}><span>Show events that haven’t started<small>Include upcoming events alongside what is running now.</small></span></label></div><div class="ob-foot"><span class="cnt">Next: choose your games</span><button class="btn solid" data-onboard-start>Continue</button></div></div>`;return round4Onboarding().replace(/Step (\d) of 3/g,(_,n)=>'Step '+(+n+1)+' of 4').replace('<div class="ob-foot"><span class="cnt">','<div class="ob-foot"><button class="btn link" data-onboard-back>Preferences</button><span class="cnt">')}
// Calendar compression is review-only; production will use the documented export contract.
function snapshotState(){const omit=new Set(['backups','beforeRestore','backupPreview','backupFeedback','undo','pending','toast','drawer','sheet','arcSheet','clog','why','serverSetup','ob']);return JSON.parse(JSON.stringify(S,(k,v)=>omit.has(k)?undefined:v instanceof Set?[...v]:v))}
function restoreState(data){const current={backups:S.backups,beforeRestore:S.beforeRestore};S={...S,...clone(data),...current};for(const k of ['hideEvents','followed','archived'])S[k]=new Set(data[k]||[]);S.pending={};S.undo=[];S.ob=null;S.sheet=null;S.arcSheet=null;S.drawer=null;S.backupPreview=null;S.sel='backup';S.tab='settings'}
function compressBackups(items,now=NOW){const day=t=>Math.floor(t/DAY),cut=new Date(now-35*DAY),cutMonth=cut.getUTCFullYear()*12+cut.getUTCMonth(),buckets=new Map();for(const item of [...items].sort((a,b)=>b.at-a.at)){const age=day(now)-day(item.at);if(age<0)continue;let tier,key;if(age<7){tier='daily';key='d'+day(item.at)}else if(age<35){tier='weekly';key='w'+Math.floor((age-7)/7)}else{const d=new Date(item.at),m=cutMonth-(d.getUTCFullYear()*12+d.getUTCMonth());if(m<0||m>=6)continue;tier='monthly';key='m'+m}if(!buckets.has(key))buckets.set(key,{...item,tier})}return [...buckets.values()].sort((a,b)=>b.at-a.at)}
function initialBackups(){const snap=snapshotState();return compressBackups([0,1,2,3,4,5,6,13,20,27,34,42,72,102,132,162,192].map((d,j)=>({id:'sample-backup-'+j,at:NOW-d*DAY,sample:true,data:{...clone(snap),games:j>10?snap.games.slice(0,3):snap.games,progress:j>10?{}:snap.progress}})))}
function keepDailyBackup(){const snap=snapshotState(),today=Math.floor(NOW/DAY);S.backups=compressBackups([...(S.backups||[]).filter(b=>Math.floor(b.at/DAY)!==today),{id:'today',at:NOW,sample:false,data:snap}])}
function backupPane(){const tierLabels={daily:['Daily','Last 7 days'],weekly:['Weekly','Older snapshots, folded into 4 weeks'],monthly:['Monthly','Older snapshots, folded into 6 months']};const selected=S.backups.find(b=>b.id===S.backupPreview);const preview=selected?`<section class="restore-preview" aria-label="Restore preview"><h3>Backup from ${dateStr(selected.at)}</h3><p>Review what this snapshot contains. Restoring replaces the demo’s current progress and preferences. A recovery copy of your current data is saved first.</p><dl><dt>Games</dt><dd>${selected.data.games.length}</dd><dt>Followed routines</dt><dd>${selected.data.followed.length}</dd><dt>Archived events</dt><dd>${selected.data.archive.length}</dd><dt>Recorded progress entries</dt><dd>${Object.keys(selected.data.progress).length}</dd></dl><p class="source-note">${selected.sample?'Sample historical backup for this review.':'Snapshot created from this demo’s saved data.'}</p><div class="acts"><button class="btn solid" data-confirm-restore="${selected.id}">Restore this backup</button><button class="btn" data-cancel-restore>Cancel</button></div></section>`:'';return `<div class="ptitle"><h2>Backup</h2></div><p class="muted-copy">Automatic snapshots keep a recent history of your data in this browser. The latest snapshot in each period is kept as older days fold into weeks, then months.</p><div class="backup-policy"><div><b>7 days</b><span>Daily snapshots</span></div><div><b>4 weeks</b><span>Weekly snapshots</span></div><div><b>6 months</b><span>Monthly snapshots</span></div></div><p class="source-note">Backups are created while you use the app. Days you don’t open it have no new snapshot. These copies live in the same browser; export a file to keep a copy elsewhere. Historical entries below are illustrative.</p><div class="acts"><button class="btn solid" data-backup-now>Back up now</button><button class="btn" data-export-demo>Export demo data</button><label class="btn">Import demo data<input type="file" accept="application/json" data-import-demo hidden></label></div>${S.backupFeedback?`<p role="status" class="source-note">${esc(S.backupFeedback)}</p>`:''}${S.beforeRestore?'<p class="source-note">A copy from before your last restore is available. <button class="btn sm" data-recovery-restore>Undo last restore</button></p>':''}${preview}${Object.entries(tierLabels).map(([tier,[label,period]])=>{const items=S.backups.filter(b=>b.tier===tier);return `<section><div class="backup-heading"><h3>${label}</h3><span>${items.length} snapshots · ${period}</span></div>${items.map(b=>`<button class="backup-row" data-backup-preview="${b.id}"><span>${dateStr(b.at)}<small>${b.data.games.length} games · ${b.data.followed.length} routines · ${b.data.archive.length} archived events${b.sample?' · sample':''}</small></span><span class="btn sm">View backup</span></button>`).join('')||'<p class="source-note">No snapshots in this period yet.</p>'}</section>`}).join('')}`}
function roomyRender(){document.documentElement.dataset.focusLayout=S.focusStyle;applyAppearance();keepDailyBackup();round4Render();const focus=document.querySelector('.focus-tools');document.documentElement.style.setProperty('--focus-offset',(40+(focus?.getBoundingClientRect().height||0))+'px');document.documentElement.dataset.theme=actualTheme();const body=document.querySelector('.shell>.body');if(!S.ob&&['checklist','timeline'].includes(S.tab))body?.insertAdjacentHTML('beforebegin',freshnessNotice());const stale=sampleSourceStates().filter(r=>r.kind!=='current');document.querySelectorAll('.event-entry').forEach(el=>{const e=EVENTS.find(e=>e.id===el.dataset.ev),r=e&&stale.find(r=>r.g===e.game);if(r)el.querySelector('.identity')?.insertAdjacentHTML('beforeend','<span class="source-status-label">Older source data</span>')});document.querySelectorAll('tr[data-ev]').forEach(el=>{const e=EVENTS.find(e=>e.id===el.dataset.ev);if(e&&stale.some(r=>r.g===e.game))el.querySelector('.tcell')?.insertAdjacentHTML('beforeend','<span class="small-source-label">Older source data</span>')});persistDemo()}
if(!Array.isArray(S.backups))S.backups=initialBackups();
function exportDemo(){const blob=new Blob([JSON.stringify({format:'event-clock-round5-demo',data:snapshotState(),backups:S.backups},null,2)],{type:'application/json'}),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='event-clock-round5-demo.json';a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
document.addEventListener('click',ev=>{const el=ev.target.closest('button');if(!el)return;const d=el.dataset;let handled=true;
if(d.setting){S.tab='settings';S.sel=d.setting;S.sheet=null;S.arcSheet=null;S.drawer=null}
else if(d.dailyLayout)S.dailyPanel=d.dailyLayout==='separate';
else if(d.scheme)S.scheme=d.scheme;
else if(d.colourReset)delete S.gameColors[d.colourReset];
else if(d.onboardStart!==undefined)S.ob.step=1;
else if(d.onboardBack!==undefined)S.ob.step=0;
else if(d.oball){const k=d.oball,o=S.ob,on=o.games.every(g=>o.comp[g][k]);o.games.forEach(g=>o.comp[g][k]=!on)}
else if(d.act==='obdone'){S.trackingIntent=clone(S.ob.comp);handled=false}
else if(d.checkData!==undefined){toast('Demo check: no newer published feed. A source check does not force a wiki to update.')}
else if(d.backupPreview){S.backupPreview=d.backupPreview;setTimeout(()=>document.querySelector('.restore-preview')?.scrollIntoView({block:'center',behavior:'instant'}),0)}
else if(d.cancelRestore!==undefined)S.backupPreview=null;
else if(d.confirmRestore){const item=S.backups.find(b=>b.id===d.confirmRestore);if(item){S.beforeRestore=snapshotState();restoreState(item.data);S.backupFeedback='Backup restored. Your previous data is kept as a recovery copy.'}}
else if(d.recoveryRestore!==undefined&&S.beforeRestore){const before=clone(S.beforeRestore);S.beforeRestore=null;restoreState(before);S.backupFeedback='Restored your data from before the backup restore.'}
else if(d.backupNow!==undefined){keepDailyBackup();S.backupFeedback='Today’s backup is up to date. Earlier daily, weekly and monthly snapshots are retained.'}
else if(d.exportDemo!==undefined)exportDemo();
else if(d.act==='reset'){for(const t of pendingTimers.values())clearTimeout(t);pendingTimers.clear();clearTimeout(toastT);clearTimeout(undoTimer);S={...fresh(),...round4Defaults(),scheme:'slate',timezone:'automatic',dailyPanel:true,focusStyle:'switcher',gameColors:{},sourceScenario:'mixed',backups:null,beforeRestore:null,backupPreview:null,backupFeedback:null,tab:'checklist'};S.backups=initialBackups();lastJump='checklist'}
else handled=false;
if(handled){ev.preventDefault();ev.stopImmediatePropagation();render()}
},true);
document.addEventListener('change',async ev=>{const el=ev.target,d=el.dataset;let handled=true;
if(d.sourceScenario!==undefined)S.sourceScenario=el.value;
else if(d.focusStyle!==undefined)S.focusStyle=el.value;
else if(d.focusSelect!==undefined)setFocus(el.value);
else if(d.timezone!==undefined)S.timezone=el.value;
else if(d.onboardPref){S[d.onboardPref]=el.type==='checkbox'?el.checked:el.value}
else if(d.gameColour||d.gameHex){const g=d.gameColour||d.gameHex;if(!/^#[0-9a-f]{6}$/i.test(el.value)){el.setCustomValidity('Use a six-digit hex colour, such as #4EA8DE.');el.reportValidity();return}el.setCustomValidity('');S.gameColors[g]=el.value.toUpperCase()}
else if(d.importDemo!==undefined){try{const text=await el.files[0].text();const parsed=JSON.parse(text);if(parsed.format!=='event-clock-round5-demo'||!Array.isArray(parsed.data?.games)||!Array.isArray(parsed.data?.followed)||!Array.isArray(parsed.data?.archive)||!parsed.data?.progress||!Array.isArray(parsed.backups)||parsed.data.games.some(g=>!G[g]))throw Error();S.beforeRestore=snapshotState();restoreState(parsed.data);S.backups=compressBackups(parsed.backups);S.backupFeedback='Demo data imported. Your previous data is kept as a recovery copy.'}catch{S.backupFeedback='This file is not a valid Round 5 demo backup. Your data has not changed.'}}
else handled=false;if(handled){ev.stopImmediatePropagation();render()}
},true);
document.addEventListener('input',ev=>{if(ev.target.dataset.zoneSearch!==undefined){const q=ev.target.value.toLowerCase(),select=document.querySelector('[data-timezone]');for(const option of select.options)option.hidden=option.value!=='automatic'&&!option.text.toLowerCase().includes(q)}},true);
matchMedia('(prefers-color-scheme: light)').addEventListener('change',()=>{if(S.theme==='system')render()});
function whyPanel(){return `<aside class="why"><button class="btn" data-act="why">Close</button><h2>Round 5 review</h2><p>Use Data and Focus in the review bar to compare freshness states and three game-filter layouts. A separate Dailies panel keeps each tick one click away; switch back to grouped routines to compare.</p><p>Settings includes About and the colophon, source status, manual timezone, colour schemes, game-colour overrides and Backup history. New user begins with preferences, then offers every tracking type even when nothing is available yet.</p><p>Archive restores the richer story statistics above its per-game groups. Completing or ignoring an event still confirms the move, fades the row and offers Undo.</p><p>Backups keep 7 daily, 4 weekly and 6 monthly examples. Review a restore, restore it, then recover your previous data. Historical snapshots are sample content; today’s snapshot reflects this demo.</p><p>This is a standalone prototype with isolated browser storage. Source health, codes and historical progress are illustrative; clocks are fixed and reset rules simplified. Production data, parsers and storage are unchanged. Some own-event editors remain placeholders.</p></aside>`}
function focusControls(){return ''}
function eventFocus(){return `<div class="event-focus"><label for="gamefocus">Game</label><select id="gamefocus" data-focus-select aria-label="Focus on a game">${['all',...S.games].map(g=>`<option value="${g}" ${S.focus===g?'selected':''}>${g==='all'?'All games':G[g].short}</option>`).join('')}</select></div>`}
function baseFreshnessNotice(){const rows=sampleSourceStates(),stale=rows.filter(r=>r.kind!=='current'),feed=sampleSourceStates(true),dates=feed.filter(r=>r.updated!==null).map(r=>r.updated),at=dates.length?Math.max(...dates):null;const status=S.sourceScenario==='offline'?'Offline · saved data':stale.length?`${stale.length} selected game${stale.length===1?'':'s'} with older data`:'';return `<section class="data-status" aria-label="Event data freshness"><div class="summary"><span>Data ${at===null?'never fetched':`refreshed <time datetime="${new Date(at).toISOString()}" title="${dateStr(at)}">${sourceAge(at)}</time>`}</span>${status?`<span class="state">${status}</span>`:''}</div><button data-setting="sources" aria-label="Data freshness details and reasons">Details</button></section>`}
function baseAppearancePane(k){if(k==='changelog')return `<article class="changelog-page"><div class="ptitle"><h2>Changelog</h2></div><p class="muted-copy">Changes and release notes for Event Clock.</p>${CHANGELOG.map(c=>`<section class="rel"><div class="d">${dayStr(c.date)} 2026 · ${esc(c.kind)}</div><h3>${esc(c.title)}</h3>${c.body?`<p>${esc(c.body)}</p>`:''}${c.items.length?`<ul>${c.items.map(t=>`<li>${esc(t)}</li>`).join('')}</ul>`:''}</section>`).join('')}</article>`;if(k==='sources')return `<div class="ptitle"><h2>Data & sources</h2></div><p class="muted-copy">The source check and its last content update are separate. A recently checked page may still hold an older schedule.</p>${freshnessNotice()}<div class="acts" style="margin:18px 0"><button class="btn" data-check-data ${S.sourceScenario==='offline'?'disabled':''}>${S.sourceScenario==='offline'?'Offline · cannot check':'Check for new data'}</button></div><p class="source-note">Illustrative source status · clocks fixed for this review. These rows cover the games you have switched on.</p>${sourceRows()}`;return previousGeneralPane(k)}
function reviewFooter(){const all=sampleSourceStates(true),dates=all.filter(r=>r.updated!==null).map(r=>r.updated),at=dates.length?Math.max(...dates):null;return `<footer class="foot"><span>Event data last refreshed: ${at===null?'no successful source fetch recorded':dateStr(at)+' · '+sourceAge(at)}. <button data-setting="sources">Source status</button></span><button data-setting="about">About & credits</button><button data-act="why">Review notes</button></footer>`}
function renderBaseVariant(){roomyRender();const header=document.querySelector('.ledger-h');if(header&&S.focusStyle!=='rail'){if(S.focusStyle==='toolbar'){header.insertAdjacentHTML('afterend',`<nav class="inline-game-filter" aria-label="Focus on a game">${focusButtons(g=>liveEvents().filter(e=>g==='all'||e.game===g).length)}</nav>`)}else{const title=header.firstElementChild;title.classList.add('list-heading');title.querySelector('h2').insertAdjacentHTML('afterend',eventFocus())}}
const timeline=document.querySelector('.tl-ctl');if(timeline){if(S.focusStyle==='toolbar')timeline.insertAdjacentHTML('afterend',`<nav class="inline-game-filter" aria-label="Focus on a game">${focusButtons(g=>liveEvents().filter(e=>g==='all'||e.game===g).length)}</nav>`);else timeline.insertAdjacentHTML('beforeend',`<div class="timeline-game-filter">${eventFocus()}</div>`)}
persistDemo()}
const VARIANT="a";
const GAME_SOURCE_CREDITS={"nte": [{"url": "https://game8.co/games/Neverness-to-Everness/archives/592073", "id": "nte-game8-events"}], "p5x": [{"url": "https://game8.co/games/Persona-5-Phantom-X/archives/532244", "id": "p5x-game8-events"}], "ba": [{"url": "https://bluearchive.wiki/wiki/Events", "id": "ba-bawiki-events"}], "fgo": [{"url": "https://fategrandorder.fandom.com/api.php?action=parse&page=Event_List_(US)&prop=text&formatversion=2&format=json", "id": "fgo-fandom-events"}], "genshin": [{"url": "https://game8.co/games/Genshin-Impact/archives/301601", "id": "genshin-game8-events"}, {"url": "https://genshin-impact.fandom.com/api.php?action=parse&page=Event&prop=text&formatversion=2&format=json", "id": "genshin-fandom-events"}], "endfield": [{"url": "https://game8.co/games/Arknights-Endfield/archives/535443", "id": "endfield-game8-events"}, {"url": "https://endfield.wiki.gg/wiki/Event", "id": "endfield-wikigg-events"}], "czn": [{"url": "https://game8.co/games/Chaos-Zero-Nightmare/archives/559899", "id": "czn-game8-events"}], "arknights": [{"url": "https://arknights.wiki.gg/wiki/Event", "id": "arknights-akwiki-events"}], "zzz": [{"url": "https://game8.co/games/Zenless-Zone-Zero/archives/457176", "id": "zzz-game8-events"}], "hi3": [{"url": "https://www.arustats.com/en-us/hi3/timeline", "id": "hi3-arustats-events"}], "r1999": [{"url": "https://reverse1999.fandom.com/api.php?action=parse&page=Events&prop=text&formatversion=2&format=json", "id": "r1999-fandom-events"}], "holodori": [{"url": "https://holodori.wiki/wiki/Events", "id": "holodori-holodoriwiki-events"}], "uma": [{"url": "https://game8.co/games/Umamusume-Pretty-Derby/archives/536311", "id": "uma-game8-events"}], "nikki": [{"url": "https://infinity-nikki.fandom.com/api.php?action=parse&page=Event&prop=text&formatversion=2&format=json", "id": "nikki-fandom-events"}], "wuwa": [{"url": "https://game8.co/games/Wuthering-Waves/archives/453473", "id": "wuwa-game8-events"}], "stellasora": [{"url": "https://stellasora.miraheze.org/wiki/Main_Page", "id": "stellasora-stellasorawiki-events"}], "gfl2": [{"url": "https://iopwiki.com/wiki/GFL2_Events", "id": "gfl2-iopwiki-events"}], "hsr": [{"url": "https://game8.co/games/Honkai-Star-Rail/archives/408749", "id": "hsr-game8-events"}], "nikke": [{"url": "https://nikke-goddess-of-victory-international.fandom.com/api.php?action=parse&page=Event&prop=text&formatversion=2&format=json", "id": "nikke-fandom-events"}], "pgr": [{"url": "https://karendar.com/", "id": "pgr-karendar-events"}]};
const VARIANT_INFO={a:{name:'Inline desk',text:'A quiet source-status line; game focus beside the list heading; daily ticks stay in the left rail.'},b:{name:'Game desk',text:'Game focus, daily ticks and source status share the left rail. The event list gets an uninterrupted heading.'},c:{name:'Open ledger',text:'A wide event list with a quick daily strip. Other pinned routines can open above the list.'}};
S.routineDisclosure??='collapsed';S.routinesOpen??=false;
function routineDisplay(){return `<section class="routine-display" aria-label="Checklist display"><label>Dailies on Checklist<select data-daily-display><option value="separate" ${S.dailyPanel?'selected':''}>Separate panel</option><option value="grouped" ${!S.dailyPanel?'selected':''}>Grouped with routines</option></select></label><label>Routine groups<select data-routine-disclosure>${[['collapsed','Collapsed by default'],['expanded','Expanded by default'],['always','Always expanded · no collapse controls']].map(([v,l])=>`<option value="${v}" ${S.routineDisclosure===v?'selected':''}>${l}</option>`).join('')}</select></label><p>Focusing a game opens its routines automatically. Pins below decide which routines appear on your Checklist.</p></section>`}
function routinesScreen(){const html=baseRoutinesScreen(),at=html.indexOf('</div>');const topEnd=html.indexOf('</div>',html.indexOf('Browse the library'))+6;return html.slice(0,topEnd)+routineDisplay()+html.slice(topEnd)}
function railPinned(){let html=baseRailPinned();const rs=insts().filter(i=>pinned(i.key,'c')&&!(S.dailyPanel&&i.r.period.unit==='day')&&(S.focus==='all'||i.r.game===S.focus));const groups=lanes().filter(l=>rs.some(i=>i.r.game===l.g&&i.srv===l.srv));
const doc=document.createElement('div');doc.innerHTML=html;for(const {g,srv} of groups){const k=g+'@'+srv,head=doc.querySelector(`[data-expand="${k}"]`);if(!head)continue;const explicit=Object.hasOwn(S.open,k)?S.open[k]:null,open=S.focus!=='all'||S.routineDisclosure==='always'||(explicit??S.routineDisclosure==='expanded');const block=head.closest('.gblock');block.classList.toggle('open',open);head.setAttribute('aria-expanded',String(open));if(open&&!block.querySelector('.gitems')){const items=rs.filter(i=>i.r.game===g&&i.srv===srv).sort((a,b)=>reset(a)-reset(b));block.insertAdjacentHTML('beforeend',`<div class="gitems">${items.map(miniItem).join('')}</div>`)}if(!open)block.querySelector('.gitems')?.remove();if(S.routineDisclosure==='always'){const non=document.createElement('div');non.className='gblock-h noncollapsible';non.innerHTML=head.innerHTML;non.querySelector('.chev')?.remove();head.replaceWith(non)}}
return doc.innerHTML}
function sourceCompact(){const rows=sampleSourceStates(),stale=rows.filter(r=>r.kind!=='current');const ages=sampleSourceStates(true).filter(r=>r.updated!==null).map(r=>r.updated);return {age:ages.length?sourceAge(Math.max(...ages)):'never fetched',state:S.sourceScenario==='offline'?'Offline · saved data':stale.length?stale.length+' selected games with older data':'',stale}}
function renderChrome(){baseVariantChrome();const el=document.getElementById('chrome');el.querySelector('b').textContent=VARIANT.toUpperCase()+' · '+VARIANT_INFO[VARIANT].name;el.querySelector('b').insertAdjacentHTML('afterend','<a class="prototype-link" href="index.html">Compare all three</a>');el.querySelector('[for="focus-demo"]')?.remove();el.querySelector('[data-focus-style]')?.remove()}
function freshnessNotice(){return VARIANT==='a'?baseFreshnessNotice():''}
function eventSheet(){const html=baseEventSheet();const e=[...EVENTS,...S.ownEvents].find(e=>e.id===S.sheet),status=e&&sampleSourceStates().find(r=>r.g===e.game);if(!status||status.kind==='current'||!e.src)return html;return html.replace('<div class="acts">',`<aside class="event-source-state"><strong>Source content ${status.updated===null?'has never been fetched':'last changed '+sourceAge(status.updated)}</strong><p>${esc(status.reason)}</p><button class="btn sm" data-setting="sources">Source details</button></aside><div class="acts">`)}
function render(){S.routineDisclosure??='collapsed';S.routinesOpen??=false;document.documentElement.dataset.variant=VARIANT;renderBaseVariant();document.documentElement.dataset.variant=VARIANT;
const ev=document.querySelector('.ev'),rail=ev?.querySelector('.rail'),main=ev?.querySelector('.main');
if(VARIANT==='b'&&rail){main.querySelector('.event-focus')?.remove();const count=g=>liveEvents().filter(e=>g==='all'||e.game===g).length;rail.insertAdjacentHTML('afterbegin',`<section class="rail-game-picker"><label for="rail-focus">Show events for</label><select id="rail-focus" data-focus-select aria-label="Focus on a game">${['all',...S.games].map(g=>`<option value="${g}" ${S.focus===g?'selected':''}>${g==='all'?'All games':G[g].name}</option>`).join('')}</select><small>${count(S.focus)} events · ${S.focus==='all'?'across your games':'focused on '+G[S.focus].short}</small></section>`);const status=sourceCompact();rail.insertAdjacentHTML('beforeend',`<section class="rail-status" aria-label="Event data freshness">Data refreshed ${status.age}.${status.state?'<br>'+status.state+'.':''}<br><button data-setting="sources">Source details & reasons</button></section>`)}
if(VARIANT==='c'&&rail){ev.classList.add('open-ledger');const header=main.querySelector('.ledger-h');const status=sourceCompact();header.querySelector('.sub').insertAdjacentHTML('beforeend',`<span class="inline-data" aria-label="Event data freshness">Data refreshed ${status.age}${status.state?' · '+status.state:''}<button data-setting="sources">Details</button></span>`);const next=liveEvents().filter(e=>S.focus==='all'||e.game===S.focus).sort((a,b)=>(a.end??9e15)-(b.end??9e15)).find(e=>e.end);if(next)header.insertAdjacentHTML('afterend',`<button class="next-line" data-next-event="${next.id}" style="${gv(next.game)}"><span class="caption">Next to expire</span><span class="name">${esc(next.title)}<small>${G[next.game].name}</small></span><span class="remaining">${dur(next.end-NOW)}</span></button>`);
let anchor=main.querySelector('.next-line')||header;const layout=rail.querySelector('.daily-layout');if(layout){anchor.after(layout);anchor=layout}const daily=rail.querySelector('.daily-panel');if(daily){daily.classList.add('quick-dailies');const rows=[...daily.querySelectorAll('.daily-row')],wrap=document.createElement('div');wrap.className='daily-items';for(const row of rows){const k=row.querySelector('[data-tick]').dataset.tick,i=parseKey(k);row.querySelector('small').textContent='Resets in '+dur(reset(i)-NOW);wrap.append(row)}daily.append(wrap);anchor.after(daily);anchor=daily}
const pinned=rail.querySelector('.pinned');if(pinned){const container=document.createElement(S.routineDisclosure==='always'?'section':'details');container.className='routine-drawer';if(container.tagName==='DETAILS'){container.open=S.focus!=='all'||S.routinesOpen;container.innerHTML=`<summary>Pinned routines · ${counts().c} on Checklist</summary>`;container.addEventListener('toggle',()=>{S.routinesOpen=container.open;persistDemo()})}else container.innerHTML='<h3>Pinned routines</h3>';container.append(pinned);anchor.after(container)}rail.remove()}
if(S.returningIntro&&!S.ob)document.querySelector('.shell>.body')?.insertAdjacentHTML('beforebegin',returningTour());document.querySelectorAll('input[type="number"]').forEach(el=>{el.step='1'});persistDemo()}
document.addEventListener('change',ev=>{const d=ev.target.dataset;if(d.dailyDisplay!==undefined){S.dailyPanel=ev.target.value==='separate';ev.stopImmediatePropagation();render()}else if(d.routineDisclosure!==undefined){S.routineDisclosure=ev.target.value;S.open={};S.routinesOpen=ev.target.value!=='collapsed';ev.stopImmediatePropagation();render()}},true);
document.addEventListener('click',ev=>{const el=ev.target.closest('button');if(!el)return;const d=el.dataset;if(d.expand){ev.stopImmediatePropagation();if(S.focus!=='all'||S.routineDisclosure==='always')return;const current=Object.hasOwn(S.open,d.expand)?S.open[d.expand]:S.routineDisclosure==='expanded';S.open[d.expand]=!current;render()}else if(d.nextEvent){ev.stopImmediatePropagation();S.sheet=d.nextEvent;render()}},true);
const CUSTOM_DEFAULTS={dark:{ground:'#0E1117',panel:'#141923',text:'#E4E7EF',accent:'#5B8DEF'},light:{ground:'#EEF0F5',panel:'#FFFFFF',text:'#151824',accent:'#2B62C6'}};
function mixColour(a,b,f){const aa=a.slice(1).match(/../g).map(x=>parseInt(x,16)),bb=b.slice(1).match(/../g).map(x=>parseInt(x,16));return '#'+aa.map((n,i)=>Math.round(n+(bb[i]-n)*f).toString(16).padStart(2,'0')).join('')}
function ensureColour(c,bg){if(contrast(c,bg)>=4.5)return c;const target=contrast('#FFFFFF',bg)>contrast('#000000',bg)?'#FFFFFF':'#000000';for(let i=1;i<=50;i++){const next=mixColour(c,target,i/50);if(contrast(next,bg)>=4.5)return next}return target}
function readableGame(h,bg){return /^#[a-f\d]{6}$/i.test(h)?ensureColour(h,bg):ensureColour('#999999',bg)}
function applyAppearance(){const root=document.documentElement;for(const key of ['ground','panel','surface','raised','rule','rule-strong','ink','ink-strong','muted','faint','fill','accent','near','soon','critical','success','calm'])root.style.removeProperty('--'+key);baseApplyAppearance();S.customPalette??=clone(CUSTOM_DEFAULTS);if(S.scheme!=='custom')return;const p=S.customPalette[actualTheme()],text=ensureColour(p.text,p.ground);let surface=p.panel;for(let i=0;i<50&&contrast(text,surface)<4.5;i++)surface=mixColour(surface,p.ground,.1);const tokens={ground:p.ground,panel:surface,surface,raised:mixColour(surface,text,.07),rule:mixColour(p.ground,text,.16),'rule-strong':mixColour(p.ground,text,.3),ink:text,'ink-strong':text,fill:text,muted:ensureColour(mixColour(p.ground,text,.65),p.ground),faint:ensureColour(mixColour(p.ground,text,.6),p.ground),accent:ensureColour(p.accent,p.ground)};const cs=getComputedStyle(root);for(const key of ['near','soon','critical','success','calm']){const c=cs.getPropertyValue('--'+key).trim();if(/^#[a-f\d]{6}$/i.test(c))tokens[key]=ensureColour(c,p.ground)}for(const [k,v] of Object.entries(tokens))root.style.setProperty('--'+k,v);for(const g of GAMES)g.hue=readableGame(S.gameColors[g.id]||DEFAULT_HUES[g.id],p.ground)}
function generalPane(k){const html=baseAppearancePane(k);if(k==='general'){const doc=document.createElement('div');doc.innerHTML=html;const last=[...doc.querySelectorAll('.grp')].find(e=>e.querySelector('h3')?.textContent==='Resets for your games');if(last)last.outerHTML=`<section class="grp"><h3>Reset timing for your games</h3><p class="muted-copy">Each server uses its automatic reset clock. Set an override if the listed time is wrong, and return to Automatic any time.</p>${lanes().map(({g,srv})=>resetEditor(g,srv)).join('')}</section>`;return doc.innerHTML}if(k!=='appearance')return html;const doc=document.createElement('div');doc.innerHTML=html;doc.querySelector('.swatches').insertAdjacentHTML('beforeend',`<button class="scheme-option" data-scheme="custom" aria-pressed="${S.scheme==='custom'}"><span class="swatch"><i style="--swatch:${S.customPalette?.[actualTheme()]?.ground||CUSTOM_DEFAULTS[actualTheme()].ground}"></i><i style="--swatch:${S.customPalette?.[actualTheme()]?.accent||CUSTOM_DEFAULTS[actualTheme()].accent}"></i></span>Custom</button>`);const note=[...doc.querySelectorAll('.source-note')].find(e=>e.textContent.startsWith('Three curated'));if(note)note.textContent='Choose a preset or create a custom palette. Game colours are edited in each game’s Setup.';if(S.scheme==='custom'){S.customPalette??=clone(CUSTOM_DEFAULTS);const mode=actualTheme(),palette=S.customPalette[mode];doc.querySelector('.swatches').insertAdjacentHTML('afterend',`<section class="custom-scheme"><h3>Your ${mode} palette</h3><div class="seg">${['dark','light'].map(m=>`<button data-custom-mode="${m}" aria-pressed="${mode===m}">${m==='dark'?'Dark':'Light'} palette</button>`).join('')}</div><div class="custom-colours">${[['ground','Background'],['panel','Surface'],['text','Text'],['accent','Accent']].map(([key,label])=>`<label>${label}<span><input type="color" value="${palette[key]}" data-custom-colour="${mode}|${key}" aria-label="${label} colour"><input type="text" value="${palette[key]}" data-custom-colour="${mode}|${key}" aria-label="${label} hex colour" pattern="#[a-fA-F0-9]{6}" maxlength="7"></span></label>`).join('')}</div><button class="btn sm" data-custom-reset="${mode}">Reset this custom palette</button><p class="source-note">Light and dark palettes are saved separately. The preview adjusts text and surfaces for readability. Game identities and deadline urgency keep their own colours.</p></section>`)}return doc.innerHTML}
function nextDaily(srv,h=4){const d=new Date(NOW);d.setUTCHours(0,Math.round(resetUTC(srv,h)*60),0,0);if(+d<=NOW)d.setUTCDate(d.getUTCDate()+1);return +d}
const overrideKey=(g,srv)=>g+'@'+srv;
function overrideHour(g,srv){const value=S.resetOverrides?.[overrideKey(g,srv)];if(!value)return 4;const [h,m]=value.split(':').map(Number);return h+m/60}
function resetAt(g,srv){return nextDaily(srv,overrideHour(g,srv))}
function periodOf(r,srv){const value=S.resetOverrides?.[overrideKey(r.game,srv)];if(!value||r.own&&r.resetHour!==undefined)return automaticPeriodOf(r,srv);return automaticPeriodOf({...r,resetHour:overrideHour(r.game,srv)},srv)}
function resetEditor(g,srv){const value=S.resetOverrides?.[overrideKey(g,srv)],when=resetAt(g,srv);return `<div class="reset-editor" style="${gv(g)}"><div><strong>${laneName(g,srv)}</strong><small>${value?'Override: '+value:'Automatic: 04:00'} server time · ${yourTime(when)} · in ${dur(when-NOW)}</small></div><label>Reset timing<select data-reset-mode="${g}|${srv}"><option value="automatic" ${!value?'selected':''}>Automatic</option><option value="custom" ${value?'selected':''}>Custom override</option></select></label>${value?`<label>Server time<input type="time" data-reset-time="${g}|${srv}" value="${value}" required></label><button class="btn sm" data-reset-auto="${g}|${srv}">Revert to Automatic</button>`:''}</div>`}
function renderGamePage(g){const html=baseGameSetup(g);if(S.gpTab!=='setup'||!S.games.includes(g))return html;const doc=document.createElement('div');doc.innerHTML=html;for(const row of doc.querySelectorAll('.srvrow')){const select=row.querySelector('[data-srvset]');if(!select)continue;const srv=select.value,clock=row.querySelector('.clock'),custom=S.resetOverrides?.[overrideKey(g,srv)];if(clock)clock.innerHTML=`<b>${SV[srv].name}</b> · ${custom?'custom '+custom:'automatic 04:00'} reset · ${yourTime(resetAt(g,srv))} · in ${dur(resetAt(g,srv)-NOW)}<small>Separate dailies and routines for this server.</small>`}doc.insertAdjacentHTML('beforeend',`<section class="grp"><h3>Reset timing<span>Correct the reset time if needed; Automatic restores the listed clock.</span></h3>${S.servers[g].map(s=>resetEditor(g,s)).join('')}</section>`);return doc.innerHTML}
function aboutPage(g){const source=GAME_SOURCE_CREDITS[g]||[];return `<h3>About ${G[g].name}</h3><div class="grp"><div class="trow"><span>Studio</span><b>${G[g].studio}</b></div>${S.servers[g].map(s=>`<div class="trow"><span>${SV[s].name} reset</span><span>${yourTime(resetAt(g,s))}${S.resetOverrides?.[overrideKey(g,s)]?' · custom override':' · automatic'}</span></div>`).join('')}</div><section class="grp"><h3>Data sources</h3>${source.map(s=>`<p class="game-source"><a href="${esc(s.url)}" target="_blank" rel="noopener">${esc(new URL(s.url).hostname)}</a><small>${/banner/i.test(s.id)?'Banner schedule':/pass|version/i.test(s.id)?'Battle pass schedule':'Event schedule'}</small></p>`).join('')||'<p class="muted-copy">No built-in source for this game.</p>'}${source.length?`<p class="muted-copy">Thanks to the editors at ${[...new Set(source.map(s=>new URL(s.url).hostname))].map(esc).join(' and ')} for compiling and maintaining these schedules. Event Clock reads their work; their source pages are the authority.</p>`:''}<button class="btn sm" data-setting="sources">Source status and freshness</button></section>`}
document.addEventListener('change',ev=>{const d=ev.target.dataset;let handled=true;if(d.customColour){const [mode,key]=d.customColour.split('|');if(!/^#[a-f\d]{6}$/i.test(ev.target.value)){ev.target.setCustomValidity('Use a six-digit hex colour, such as #4EA8DE.');ev.target.reportValidity();return}ev.target.setCustomValidity('');S.customPalette[mode][key]=ev.target.value.toUpperCase()}else if(d.resetMode){const [g,srv]=d.resetMode.split('|');S.resetOverrides??={};if(ev.target.value==='automatic')delete S.resetOverrides[overrideKey(g,srv)];else S.resetOverrides[overrideKey(g,srv)]='04:00'}else if(d.resetTime){if(!/^\d\d:\d\d$/.test(ev.target.value))return;const [g,srv]=d.resetTime.split('|');S.resetOverrides??={};S.resetOverrides[overrideKey(g,srv)]=ev.target.value}else handled=false;if(handled){ev.stopImmediatePropagation();render()}},true);
document.addEventListener('click',ev=>{const d=ev.target.closest('button')?.dataset;if(!d)return;let handled=true;if(d.customMode)S.theme=d.customMode;else if(d.customReset)S.customPalette[d.customReset]=clone(CUSTOM_DEFAULTS[d.customReset]);else if(d.resetAuto){const [g,srv]=d.resetAuto.split('|');delete S.resetOverrides[overrideKey(g,srv)]}else handled=false;if(handled){ev.stopImmediatePropagation();render()}},true);
function onboarding(){let html=baseSetup();if(S.ob.step===0)html=html.replace(/<label class="chk"><input type="checkbox" data-onboard-pref="showUpcoming"[^]*?<\/label>/,'');if(S.ob.step===2)html=html.replace('<div class="ob-foot">',`<div class="upcoming-choice"><label class="chk"><input type="checkbox" data-onboard-pref="showUpcoming" ${S.showUpcoming?'checked':''}><span>Show events that haven’t started<small>Include upcoming events in Checklist and Timeline.</small></span></label></div><div class="ob-foot">`);return html}
const TOUR_STEPS=[['Your routines have a home','Follow, hide and pin routines in the Routines tab. Checklist display controls whether dailies have a separate panel and whether routine groups start open or stay always expanded.','routines'],['Make the Checklist work for you','Choose Standard or Compact and focus a game beside the list. Daily ticks stay directly available in the separate panel. Source details explain when schedules are older.','checklist'],['Finished events stay available','Done and Ignore move events into Archive with a brief confirmation and Undo. Open any saved event for its description, source and progress.','archive'],['Your settings, together','Choose appearance, timezone and per-game servers, colours or reset overrides. Backup keeps daily, weekly and monthly history. Changelog now has its own page here.','settings']];
function returningTour(){const step=S.returningIntro.step;if(step===0)return `<section class="returning-intro" aria-label="Welcome back"><h2>Welcome back. A little has moved.</h2><p>Your saved games, routines and progress are still here. Take a short tour of the new layout, or keep browsing and use Settings → Changelog for the details.</p><div class="acts"><button class="btn solid" data-tour-next>Take the quick tour</button><button class="btn" data-tour-skip>Keep browsing</button></div></section>`;const item=TOUR_STEPS[step-1];return `<section class="returning-intro" aria-label="Returning user tour"><div class="tour-step">${step} of ${TOUR_STEPS.length}</div><h2>${item[0]}</h2><p>${item[1]}</p><div class="acts">${step>1?'<button class="btn" data-tour-back>Back</button>':''}<button class="btn solid" data-tour-next>${step===TOUR_STEPS.length?'Finish tour':'Continue'}</button><button class="btn link" data-tour-skip>Skip tour</button></div></section>`}
function moveTour(step){if(step>TOUR_STEPS.length){S.returningIntro=null;S.returningSeen=true;S.tab=S.defaultView;return}S.returningIntro={step};S.tab=TOUR_STEPS[step-1][2];S.sel='general';S.sheet=null;S.arcSheet=null;S.drawer=null}
document.addEventListener('click',ev=>{const d=ev.target.closest('button')?.dataset;if(!d)return;let handled=true;if(d.jump==='returning'){jump('returning');S.wnSeen=true;S.returningIntro={step:0}}else if(d.tourNext!==undefined)moveTour(S.returningIntro.step+1);else if(d.tourBack!==undefined)moveTour(S.returningIntro.step-1);else if(d.tourSkip!==undefined){S.returningIntro=null;S.returningSeen=true;S.tab=S.defaultView}else handled=false;if(handled){ev.stopImmediatePropagation();render()}},true);
render();
</script>
</body>
</html>