123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826
<!doctype html>
<!-- Adom Screensaver Settings. Rendered windowed in a WebView2 by adom-screensaver.scr /c
     (and, later, by the Hydrogen Desktop dialog). The C# host injects LIVE Windows settings via
     window.adomConfig(data, applied) every time it opens, and applies the recommended profile when
     the user clicks Apply (the host re-reads + re-injects so the table always shows real state). -->
<html lang="en">
<head>
<meta charset="utf-8">
<style>
  :root { --teal:#00b8b0; --tealbri:#00e6dc; --hi:#e6edf3; --lo:#8b949e; --dim:#6b7280;
          --bg:#0d1117; --panel:#161b22; --line:#21262d; --warn:#e3a008; --ok:#2ea043; }
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body { height:100%; background:var(--bg); color:var(--hi);
    font-family:"Segoe UI",system-ui,sans-serif; user-select:none; }
  body { display:flex; flex-direction:column; height:100vh; overflow:hidden; }
  /* App shell: sticky header + tab bar on top, scrolling panel in the middle, sticky footer at the bottom. */
  .topbar { flex:none; padding:22px 30px 0; }
  .tabs { display:flex; gap:2px; margin-top:18px; padding:0 30px; border-bottom:1px solid var(--line); }
  .tab { font-family:inherit; font-size:13px; font-weight:600; color:var(--lo); background:none;
    border:none; border-radius:0; padding:11px 17px; cursor:pointer; border-bottom:2px solid transparent;
    margin-bottom:-1px; letter-spacing:.01em; }
  .tab:hover { color:var(--hi); border-bottom-color:transparent; }
  .tab.active { color:var(--tealbri); border-bottom-color:var(--teal); }
  .tab .tdot { display:none; width:7px; height:7px; border-radius:50%; background:var(--warn);
    margin-left:7px; vertical-align:1px; box-shadow:0 0 6px rgba(227,160,8,.7); }
  .content { flex:1; overflow-y:auto; padding:6px 30px 26px; }
  .panel { display:none; }
  .panel.active { display:block; }
  .panel .tune:first-child, .panel .why:first-child, .panel table:first-child { margin-top:14px; }
  .footbar { flex:none; display:flex; align-items:center; gap:12px; padding:13px 30px;
    border-top:1px solid var(--line); background:var(--panel); }
  .head { display:flex; align-items:center; gap:14px; }
  .head img { height:30px; }
  .head .t { font-size:17px; font-weight:600; letter-spacing:.02em; }
  .head .t small { display:block; color:var(--teal); font-size:11px; letter-spacing:.42em;
    text-transform:uppercase; font-weight:600; margin-top:2px; }

  .why { margin-top:18px; background:var(--panel); border:1px solid var(--line);
    border-left:3px solid var(--teal); border-radius:8px; padding:15px 18px; }
  .why h2 { font-size:13.5px; color:var(--tealbri); font-weight:600; margin-bottom:8px; }
  .why p { font-size:12.5px; line-height:1.55; color:var(--lo); margin-bottom:8px; }
  .why p:last-child { margin-bottom:0; }
  .why b { color:var(--hi); font-weight:600; }
  .why .code { color:var(--tealbri); font-weight:600; }

  .status { margin-top:16px; display:flex; flex-wrap:wrap; gap:10px 22px; font-size:12.5px;
    padding:0 2px; }
  .status .k { color:var(--dim); }
  .status .v { color:var(--hi); font-weight:600; }
  .status .note { color:var(--lo); font-size:11.5px; }

  table { margin-top:14px; width:100%; border-collapse:collapse; }
  th { text-align:left; font-size:10px; letter-spacing:.16em; text-transform:uppercase;
    color:var(--dim); font-weight:600; padding:0 8px 7px; }
  th.r, td.r { text-align:right; }
  td { padding:9px 8px; border-top:1px solid var(--line); vertical-align:top; font-size:12.5px; }
  td .lab { color:var(--hi); font-weight:600; }
  td .why2 { color:var(--lo); font-size:11px; line-height:1.35; margin-top:2px; max-width:330px; }
  td.cur { color:var(--hi); white-space:nowrap; }
  td.rec { color:var(--lo); white-space:nowrap; }
  tr.bad td.cur { color:var(--warn); font-weight:600; }
  .badge { display:inline-block; min-width:20px; text-align:center; font-size:11px; font-weight:700; }
  .badge.ok { color:var(--ok); }
  .badge.fix { color:var(--warn); }

  .hkrow { margin-top:16px; display:flex; align-items:center; justify-content:space-between; gap:18px;
    padding:13px 15px; border:1px solid var(--line); border-radius:8px; background:var(--panel); }
  .hklab { font-size:12.5px; color:var(--hi); font-weight:600; }
  .hklab b { color:var(--tealbri); }
  .hkwhy { font-size:11px; color:var(--lo); line-height:1.35; margin-top:3px; max-width:440px; }
  .switch { position:relative; display:inline-block; width:42px; height:24px; flex:none; cursor:pointer; }
  .switch input { opacity:0; width:0; height:0; }
  .track { position:absolute; inset:0; background:#2a3340; border-radius:999px; transition:.2s; }
  .track:before { content:""; position:absolute; height:18px; width:18px; left:3px; top:3px; background:var(--lo); border-radius:50%; transition:.2s; }
  .switch input:checked + .track { background:var(--teal); }
  .switch input:checked + .track:before { transform:translateX(18px); background:#04211f; }

  .banner { margin-top:16px; border-radius:8px; padding:11px 15px; font-size:12.5px; font-weight:600; }
  .banner.ok  { background:rgba(46,160,67,.12);  border:1px solid rgba(46,160,67,.4);  color:#5dd879; }
  .banner.bad { background:rgba(227,160,8,.12);  border:1px solid rgba(227,160,8,.4);  color:#f0c548; }

  .actions { margin-top:18px; display:flex; align-items:center; gap:12px; }
  button { font-family:inherit; font-size:13px; font-weight:600; border-radius:8px; cursor:pointer;
    padding:10px 18px; border:1px solid var(--line); background:var(--panel); color:var(--hi); }
  button:hover { border-color:var(--teal); }
  button.primary { background:var(--teal); border-color:var(--teal); color:#04211f; }
  button.primary:hover { background:var(--tealbri); }
  button.primary[disabled] { background:#16332f; border-color:#16332f; color:#4b7a76; cursor:default; }
  .spacer { flex:1; }
  .foot { margin-top:14px; color:var(--dim); font-size:11px; line-height:1.5; }
  .toast { color:var(--ok); font-size:12.5px; font-weight:600; opacity:0; transition:opacity .3s; }
  .toast.show { opacity:1; }
  .spin { display:inline-block; width:11px; height:11px; margin-right:7px; vertical-align:-1px;
    border:2px solid rgba(255,255,255,.22); border-top-color:var(--teal); border-radius:50%;
    animation:spin .6s linear infinite; }
  @keyframes spin { to { transform:rotate(360deg); } }
  button:disabled { opacity:.8; cursor:default; }

  /* Fine-tune section (#2 rest-after, #3 idle timeout, #4 never-off) */
  select { font-family:inherit; font-size:12.5px; font-weight:600; color:var(--hi); background:#1b2330;
    border:1px solid #2c3645; border-radius:7px; padding:7px 10px; cursor:pointer; flex:none; }
  select:hover { border-color:var(--teal); }
  .tune { margin-top:16px; border:1px solid var(--line); border-radius:8px; background:var(--panel); padding:0 15px; }
  .tune .trow { display:flex; align-items:center; justify-content:space-between; gap:18px;
    padding:13px 0; border-top:1px solid var(--line); }
  .tune .trow:first-child { border-top:none; }
  .tune .tlab { font-size:12.5px; color:var(--hi); font-weight:600; }
  .tune .tlab b { color:var(--tealbri); }
  .tune .twhy { font-size:11px; color:var(--lo); line-height:1.35; margin-top:3px; max-width:430px; }
  .tune .twhy.warn { color:var(--warn); }
  .casc-intro { font-size:11.5px; color:var(--lo); line-height:1.5; padding:13px 0 4px; }
  .casc-intro b { color:var(--hi); }
  .tune .tlab .step { display:inline-block; width:17px; height:17px; line-height:17px; text-align:center;
    border-radius:50%; background:var(--teal); color:#04211f; font-size:10.5px; font-weight:700;
    margin-right:7px; vertical-align:1px; }
  .tune .tlab .sub { color:var(--lo); font-weight:400; }
  /* Display-dimming hardware test */
  .tune .hw { font-size:11.5px; color:var(--lo); line-height:1.45; padding:13px 0 10px; }
  .tune .hw b { color:var(--hi); }
  .btestbtn { font-family:inherit; font-size:12px; font-weight:600; color:var(--tealbri); background:#0f141b;
    border:1px solid var(--line); border-radius:7px; padding:7px 13px; cursor:pointer; margin:0 0 12px; }
  .btestbtn:hover { border-color:var(--teal); }
  .prow { display:flex; align-items:center; gap:13px; padding:0 0 12px; }
  .prow .btestbtn { margin:0; flex:none; min-width:215px; }
  .prow .pwhy { font-size:11px; color:var(--lo); line-height:1.4; }
  .prow .pwhy b { color:var(--hi); }
  .btestbox { display:none; align-items:center; gap:14px; padding:0 0 12px; }
  .btestbox .bpct { font-size:30px; font-weight:700; color:var(--tealbri); font-variant-numeric:tabular-nums; min-width:90px; }
  .btestbox .bnote { font-size:11.5px; color:var(--lo); }
  .bresult { font-size:11.5px; line-height:1.4; padding:0 0 13px; }
  .bresult.ok { color:#5dd879; } .bresult.no { color:var(--warn); } .bresult.un { color:var(--lo); }
  /* Modern Standby explainer + live state */
  .msbox { margin-top:16px; border-radius:8px; padding:13px 15px; border:1px solid var(--line); background:var(--panel); }
  .msbox.ok   { border-left:3px solid #2ea043; }
  .msbox.warn { border-left:3px solid var(--warn); }
  .msbox .mshdr { font-size:12.5px; font-weight:700; color:var(--hi); margin-bottom:6px; }
  .msbox .mswhy { font-size:11.5px; color:var(--lo); line-height:1.5; margin-bottom:9px; }
  .msbox .mswhy b { color:var(--hi); }
  .msbox .msstate { font-size:11.5px; line-height:1.5; }
  .msbox .msstate.ok { color:#5dd879; } .msbox .msstate.warn { color:#f0c548; }
  .msbox .msstate b { font-weight:700; }
  /* Cache / debug tab: every page the saver has fetched, the hero it stored, and the score it computes */
  .cachebar { display:flex; align-items:center; gap:14px; margin-top:14px; flex-wrap:wrap; font-size:12px; }
  .cachebar .cnt { color:var(--lo); } .cachebar .cnt b { color:var(--hi); }
  .cachebar select { padding:5px 8px; font-size:11.5px; }
  .cachebar .reload { font-family:inherit; font-size:11.5px; font-weight:600; color:var(--tealbri); background:#0f141b;
    border:1px solid var(--line); border-radius:7px; padding:6px 11px; cursor:pointer; }
  .cachebar .reload:hover { border-color:var(--teal); }
  #cacheList { margin-top:8px; }
  .ccard { display:flex; gap:13px; padding:13px 2px; border-top:1px solid var(--line); }
  .ccard:first-child { border-top:none; }
  .cthumbwrap { flex:none; width:108px; }
  .cthumb { width:108px; height:68px; border-radius:6px; border:1px solid var(--line); object-fit:cover; background:#0a0e14; display:block; }
  .cthumb.miss { display:flex; align-items:center; justify-content:center; color:var(--warn); font-size:10px; font-weight:600; text-align:center; }
  .cmeta { flex:1; min-width:0; }
  .cmeta .cslug { color:var(--tealbri); font-weight:700; font-size:13px; word-break:break-all; }
  .cmeta .ctitle { color:var(--hi); font-weight:600; font-size:12px; margin-top:2px; }
  .cmeta .cbrief { color:var(--lo); font-size:11px; line-height:1.4; margin-top:3px; }
  .cmeta .cfile { color:var(--dim); font-size:10px; margin-top:5px; word-break:break-all; line-height:1.4; }
  .cmeta .cfile .none { color:var(--warn); }
  .ctype { display:inline-block; font-size:9px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
    padding:1px 7px; border-radius:999px; vertical-align:1.5px; margin-left:7px; }
  .ctype.t-app{background:rgba(56,184,255,.16);color:#7cc6ff;} .ctype.t-skill{background:rgba(140,107,247,.2);color:#b9a3ff;}
  .ctype.t-component{background:rgba(244,114,182,.16);color:#f7a8d8;} .ctype.t-bootstrap{background:rgba(46,160,67,.16);color:#5dd879;}
  .cscore { flex:none; width:118px; text-align:right; }
  .cscore .big { font-size:21px; font-weight:700; color:var(--tealbri); font-variant-numeric:tabular-nums; line-height:1; }
  .cscore .brk { font-size:10px; color:var(--lo); margin-top:5px; line-height:1.5; }
  .cscore .brk .f { color:#7cc6ff; font-weight:600; } .cscore .brk .n { color:#5dd879; font-weight:600; }
  .cscore .brk .j { color:var(--dim); }
  .cdlbtn { font-family:inherit; font-size:10.5px; font-weight:600; color:var(--tealbri); background:#0f141b;
    border:1px solid var(--line); border-radius:6px; padding:6px 11px; cursor:pointer; margin-top:8px; white-space:nowrap; }
  .cdlbtn:hover { border-color:var(--teal); } .cdlbtn:disabled { opacity:.45; cursor:default; }
  .ccmp { margin-top:10px; display:none; gap:13px; flex-wrap:wrap; }
  .ccmp.show { display:flex; }
  .ccmp .side { flex:1 1 0; min-width:0; }
  .ccmp .side .lbl { font-size:9.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--dim); margin-bottom:5px; }
  .ccmp .side img { width:100%; border-radius:6px; border:1px solid var(--line); background:#0a0e14; display:block; }
  .ccmp .side .dim { font-size:10px; color:var(--lo); margin-top:4px; font-variant-numeric:tabular-nums; }
  .ccmp .side .err { font-size:10px; color:var(--warn); margin-top:4px; line-height:1.4; }
  .ccmp .verdict { flex:0 0 100%; width:100%; margin-top:3px; font-size:11px; line-height:1.4; }
  .ccmp .verdict.same { color:#5dd879; } .ccmp .verdict.diff { color:var(--warn); }
</style>
</head>
<body>
  <div class="topbar">
    <div class="head">
      <img src="adom-wordmark.png" alt="Adom">
      <div class="t">Screensaver<small>Settings</small></div>
    </div>
    <div class="tabs" id="tabs">
      <button class="tab active" data-tab="cascade">Cascade</button>
      <button class="tab" data-tab="power">Power<span class="tdot" id="powerDot"></span></button>
      <button class="tab" data-tab="preview">Preview</button>
      <button class="tab" data-tab="cache">Cache</button>
      <button class="tab" data-tab="about">About</button>
    </div>
  </div>

  <div class="content">

  <!-- ============ TAB: CASCADE (what plays & when) ============ -->
  <div class="panel active" id="tab-cascade">
  <div class="tune" id="cascade">
    <div class="casc-intro">The screensaver runs as a timed <b>cascade</b>: the Adom logo, then the splash, then the Wiki billboards, then it dims. Set each stage:</div>
    <div class="trow">
      <div>
        <div class="tlab"><span class="step">1</span> Starts after <span class="sub">(idle)</span></div>
        <div class="twhy">How long the machine sits idle before the screensaver appears.</div>
      </div>
      <select id="saverSel">
        <option value="60">1 min</option><option value="120">2 min</option><option value="180">3 min</option>
        <option value="300">5 min</option><option value="600">10 min</option><option value="900">15 min</option>
      </select>
    </div>
    <div class="trow">
      <div>
        <div class="tlab"><span class="step">2</span> Adom logo holds</div>
        <div class="twhy">The Adom mark shows at least this long before the splash (it may run a touch longer while the billboard page finishes loading).</div>
      </div>
      <select id="logoSel">
        <option value="0">Skip</option><option value="1">1 sec</option><option value="2">2 sec</option>
        <option value="3">3 sec</option><option value="5">5 sec</option><option value="8">8 sec</option>
      </select>
    </div>
    <div class="trow">
      <div>
        <div class="tlab"><span class="step">3</span> Splash holds</div>
        <div class="twhy">The Adom splash screen stays up this long before the billboards begin rotating.</div>
      </div>
      <select id="splashSel">
        <option value="2">2 sec</option><option value="3">3 sec</option><option value="5">5 sec</option>
        <option value="8">8 sec</option><option value="10">10 sec</option><option value="15">15 sec</option><option value="30">30 sec</option>
      </select>
    </div>
    <div class="trow">
      <div>
        <div class="tlab"><span class="step">4</span> Each billboard shows for</div>
        <div class="twhy">How long each Wiki billboard stays on screen before the next one fades in.</div>
      </div>
      <select id="slideSel">
        <option value="3">3 sec</option><option value="4">4 sec</option><option value="5">5 sec</option>
        <option value="8">8 sec</option><option value="10">10 sec</option><option value="15">15 sec</option><option value="30">30 sec</option><option value="60">60 sec</option>
      </select>
    </div>
    <div class="trow">
      <div>
        <div class="tlab"><span class="step">5</span> Dim the screen after</div>
        <div class="twhy" id="restWhy">After the screensaver starts, the billboards play for this long, then it rests: plugged in it dims the <b>backlight to near-black</b> (the laptop stays <b>awake</b> so your /remote-control sessions keep running), on battery it powers the panel off to save battery. The dim plus slow drift are what prevent burn-in.</div>
      </div>
      <select id="restSel">
        <option value="60">1 min</option><option value="120">2 min</option><option value="180">3 min</option>
        <option value="300">5 min</option><option value="600">10 min</option><option value="900">15 min</option>
        <option value="0">Never</option>
      </select>
    </div>
  </div>

  <div class="tune" id="content" style="margin-top:16px">
    <div class="trow" style="border-top:none">
      <div>
        <div class="tlab">Include component billboards</div>
        <div class="twhy">By default the billboards show <b>apps</b> and <b>skills</b>. Turn this on to also rotate in <b>components</b> (eval boards, modules, parts) from the Wiki. Every billboard is labeled with its kind. Takes effect the next time the screensaver runs.</div>
      </div>
      <label class="switch"><input type="checkbox" id="compToggle"><span class="track"></span></label>
    </div>
  </div>
  </div><!-- /tab-cascade -->

  <!-- ============ TAB: POWER (keep the machine awake & reachable) ============ -->
  <div class="panel" id="tab-power">
    <div class="msbox warn" id="msbox" style="display:none">
      <div class="mshdr">Modern Standby, and why this screensaver exists</div>
      <div class="mswhy">On a <b>Modern Standby</b> laptop, the moment the display turns <b>off</b>, Windows treats that as a cue to drop into <b>light sleep</b>: it <b>cuts your network and throttles the CPU</b>, so any <b>/remote-control</b> session quietly dies. That's the whole reason this app exists. Instead of turning the display off, it keeps the screen <b>on and dims it to near-black</b>, so your laptop stays fully awake and your sessions survive, while the dim plus slow drift still prevent burn-in.</div>
      <div class="msstate" id="msstate"></div>
    </div>
    <div class="status" id="status"></div>

    <table>
      <thead><tr>
        <th>Setting</th><th class="r">Your setting</th><th class="r">Adom recommends</th><th class="r"></th>
      </tr></thead>
      <tbody id="rows"></tbody>
    </table>

    <div class="banner" id="banner"></div>

    <div class="actions">
      <button class="primary" id="apply">Apply Adom Recommended</button>
      <button id="recheck">Re-check</button>
      <span class="toast" id="toast">Applied ✓</span>
    </div>

    <div class="tune" style="margin-top:16px">
      <div class="trow" style="border-top:none">
        <div>
          <div class="tlab">Never let Windows turn the display off</div>
          <div class="twhy" id="neverWhy">Recommended <b>ON</b> while plugged in. Keeps the display on so the laptop stays <b>awake</b> and your <b>/remote-control</b> sessions survive; the screensaver dims the backlight to near-black and drifts to prevent burn-in. Letting Windows power the display off makes the laptop doze and drops your sessions.</div>
        </div>
        <label class="switch"><input type="checkbox" id="neverToggle"><span class="track"></span></label>
      </div>
    </div>

    <div class="tune" id="bsection" style="margin-top:16px">
      <div class="hw" id="hwinfo">Checking your display hardware…</div>
      <button class="btestbtn" id="btest">Test if the screensaver can dim my screen</button>
      <div class="btestbox" id="btestbox"><span class="bpct" id="bpct">0%</span><span class="bnote" id="bnote"></span></div>
      <div class="bresult un" id="bresult">Not tested yet.</div>
    </div>

    <div class="hkrow">
      <div>
        <div class="hklab">Global hotkey, launch with <b>Win + Shift + L</b></div>
        <div class="hkwhy">Press it anytime to show the screensaver on demand. Turn off if it collides with another shortcut you use.</div>
      </div>
      <label class="switch"><input type="checkbox" id="hkToggle"><span class="track"></span></label>
    </div>
  </div><!-- /tab-power -->

  <!-- ============ TAB: PREVIEW (see it live) ============ -->
  <div class="panel" id="tab-preview">
    <div class="tune" id="previewsection">
      <div class="hw">See it for real on a live Wiki billboard. <b>Move the mouse to exit</b> the preview and come back here.</div>
      <div class="prow">
        <button class="btestbtn" id="previewapp">Test an app billboard</button>
        <span class="pwhy">Shows a real <b>app</b> page with its blue <b>App</b> capsule, at normal brightness.</span>
      </div>
      <div class="prow">
        <button class="btestbtn" id="previewskill">Test a skill billboard</button>
        <span class="pwhy">Shows a real <b>skill</b> page with its purple <b>Skill</b> capsule.</span>
      </div>
      <div class="prow">
        <button class="btestbtn" id="previewcomp">Test a component billboard</button>
        <span class="pwhy">Shows a real <b>component</b> page (pink <b>Component</b> capsule), eval boards, modules and parts, fetched on demand even if component billboards are turned off.</span>
      </div>
      <div class="prow">
        <button class="btestbtn" id="previewpriv">Test a private billboard</button>
        <span class="pwhy">Shows a real <b>private / org-only</b> billboard so you can see the <b>teal background + frame</b> that marks internal pages (vs the plain public look).</span>
      </div>
      <div class="prow">
        <button class="btestbtn" id="previewdim">See the dimmed screen</button>
        <span class="pwhy" id="previewdimwhy">Jumps straight to the dimmed screensaver: the Adom logo and <b>AI Electronics Prototyping</b> drifting slowly, near-black (billboards don't rotate when dimmed). See how dark it really gets.</span>
      </div>
    </div>
  </div><!-- /tab-preview -->

  <!-- ============ TAB: CACHE (what's stored on disk + the score I compute) ============ -->
  <div class="panel" id="tab-cache">
    <div class="hw" style="font-size:11.5px;color:var(--lo);line-height:1.5;padding:13px 0 2px">
      A live look at the saver's on-disk billboard cache: every Wiki page it has fetched, the <b>hero image it
      stored</b>, the page's title &amp; description, and the <b>priority score</b> it computes to order the
      rotation (higher&nbsp;= shown sooner). Hit <b>Download current from Wiki</b> on any row to pull that page's
      hero <b>right now</b> and compare it side-by-side with what's cached, so you can spot a stale hero.
    </div>
    <div class="cachebar">
      <span class="cnt" id="cacheCnt">Reading cache…</span>
      <span style="color:var(--dim)">sort</span>
      <select id="cacheSort"><option value="score">Score</option><option value="slug">Slug</option><option value="fresh">Freshness</option><option value="type">Type</option></select>
      <button class="reload" id="cacheReload">↻ Reload from disk</button>
    </div>
    <div id="cacheList"></div>
  </div><!-- /tab-cache -->

  <!-- ============ TAB: ABOUT (why this exists) ============ -->
  <div class="panel" id="tab-about">
    <div class="why">
      <h2 id="whyHead">Why your machine needs different rules in the AI age</h2>
      <div id="whyBody"></div>
    </div>

    <div class="foot">Reads your <b>live</b> Windows settings every time it opens, never a saved copy.
      Change anything in Control Panel and re-open (or hit Re-check) to see it here. Most settings apply
      without admin; the display still turns off on schedule to protect against burn-in.</div>
  </div><!-- /tab-about -->

  </div><!-- /content -->

  <div class="footbar">
    <span class="spacer"></span>
    <button id="close">Close</button>
  </div>

<script>
  // Host-agnostic: in the .scr it's WebView2 (chrome.webview); embedded in the Hydrogen Desktop
  // dialog it's an iframe, so fall back to posting up to the parent window.
  function send(m){
    try{ if(window.chrome && window.chrome.webview){ window.chrome.webview.postMessage(m); return; } }catch(e){}
    try{ window.parent && window.parent.postMessage({ adomScreensaver: m }, "*"); }catch(e){}
  }
  var recheckAt = 0;  // timestamp of an in-flight Re-check, so adomConfig can finish the spinner
  // HD pushes config data via postMessage instead of ExecuteScriptAsync. Accept either shape.
  window.addEventListener("message", function(ev){
    var d = ev && ev.data; if(!d) return;
    if (d.adomConfigData) { try{ window.adomConfig(d.adomConfigData, !!d.applied); }catch(e){} }
  });
  function esc(s){ return (s==null?"":String(s)).replace(/&/g,"&amp;").replace(/</g,"&lt;"); }

  var CAP = {};   // last hardware capabilities (device/dozeRisk/brightCtl), stashed by adomConfig

  // The intro paragraph is hardware-shaped: a laptop gets the lid/battery/bag story; a desktop gets the
  // simpler "keep the box awake + show the billboard" story; and whether display-off is dangerous depends
  // on Modern Standby, not the chassis.
  function renderIntro(cap){
    cap = cap || {};
    var head = document.getElementById("whyHead"), body = document.getElementById("whyBody");
    if (!head || !body) return;
    var desktop = cap.device === "desktop";
    var doze = !!cap.dozeRisk;
    head.textContent = desktop ? "Why your PC needs different rules in the AI age"
                               : "Why your laptop needs different rules in the AI age";
    var dev = desktop ? "PC" : "laptop";
    var p = [];
    p.push("Windows ships set to <b>aggressively sleep and power down</b> by default, choices Microsoft made long "
      + "before AI agents ran for hours on your behalf. The moment the machine idles "
      + (desktop ? "" : "or the lid shuts ") + "it sleeps, and anything still working dies with it.");
    p.push("But when you're working with <b>Claude</b> and <b>Adom Hydrogen Desktop</b>, you want the AI to "
      + "<b>keep chugging</b>: long sessions that finish, <span class=\"code\">/remote-control</span> threads that "
      + "don't drop while you step away, and your " + dev + " quietly becoming a <b>vibe-coding server</b> that "
      + "drives your frontend, takes screenshots, and builds <b>shotlogs you can watch from your phone</b>. None of "
      + "that happens on a sleeping machine.");
    if (doze){
      p.push("This " + dev + " uses <b>Modern Standby</b>, so the moment the display turns <b>off</b>, Windows drops "
        + "into <b>light sleep</b> that cuts your network and throttles the CPU, killing those sessions. So this app, "
        + "from your <b>system tray</b> (the Adom mark by your clock), keeps the machine <b>awake</b>, shows this "
        + "<b>Wiki billboard</b>, then <b>dims the screen to near-black</b> instead of turning the display off, "
        + "the dim plus slow drift prevent burn-in while the machine stays awake and your sessions survive.");
    } else {
      p.push("This " + dev + " does <b>not</b> use Modern Standby, so turning the display off won't drop your network. "
        + "What still matters is not letting the machine <b>sleep</b>, that's what kills /remote-control here. So this "
        + "app, from your <b>system tray</b> (the Adom mark by your clock), keeps the machine <b>awake</b> and shows "
        + "this <b>Wiki billboard</b>"
        + (cap.brightCtl === 1
            ? ", dimming the backlight to near-black when you step away so the billboard never burns in."
            : ". Since it can't dim this display's backlight, it lets the monitor power off on a timer (safe here, and easier on the panel).") );
    }
    p.push("And if another app (a <b>screen-share, recording, or media tab</b>) is holding your display on so the "
      + "screensaver can't start, the tray icon turns into a <b style=\"color:var(--warn)\">⚠</b> and names what's "
      + "blocking it. Right-click that icon any time for these settings. Here's what we recommend for <b>your</b> "
      + "hardware, and what you've got right now:");
    body.innerHTML = "<p>" + p.join("</p><p>") + "</p>";
  }

  // Modern Standby state + guidance. On hardware that doesn't support MS (most desktops) the whole
  // doze danger is moot, so we say so plainly instead of nagging about a feature that can't apply.
  window.adomMS = function(ms, cap){
    ms = ms || {}; cap = cap || CAP || {};
    var box = document.getElementById("msbox"), st = document.getElementById("msstate");
    var hdr = box ? box.querySelector(".mshdr") : null, why = box ? box.querySelector(".mswhy") : null;
    if (!box || !st) return;
    box.style.display = "block";
    var cls, m;
    if (ms.supported === false){
      // No Modern Standby on this machine: display-off is safe, only sleep is the enemy.
      cls = "ok";
      if (hdr) hdr.textContent = "Modern Standby: not on this machine (that's fine)";
      if (why) why.innerHTML = "<b>Modern Standby</b> is the laptop low-power mode that, on machines that have it, "
        + "treats a display-off as a cue to drop into light sleep, cutting the network and throttling the CPU and "
        + "killing <b>/remote-control</b>. <b>This machine doesn't use it</b>, so turning the display off is safe here.";
      m = "✓ No doze risk on this hardware. The screensaver just keeps the machine from <b>sleeping</b> (so your "
        + "sessions survive) and shows the Wiki billboard; the display can power off normally without dropping anything.";
    } else {
      if (hdr) hdr.textContent = "Modern Standby, and why this screensaver exists";
      if (why) why.innerHTML = "On a <b>Modern Standby</b> machine, the moment the display turns <b>off</b>, Windows "
        + "treats that as a cue to drop into <b>light sleep</b>: it <b>cuts your network and throttles the CPU</b>, so "
        + "any <b>/remote-control</b> session quietly dies. That's the whole reason this app exists. Instead of turning "
        + "the display off, it keeps the screen <b>on and dims it to near-black</b>, so the machine stays fully awake "
        + "and your sessions survive, while the dim plus slow drift still prevent burn-in.";
      if (ms.live && !ms.pending){ cls="ok"; m="✓ Modern Standby is <b>ON</b>, exactly what we want. The lid resumes instantly and the screensaver dims the screen (no slow hibernate)."; }
      else if (!ms.live && ms.pending && ms.after){ cls="warn"; m="⚠ You're <b>between two states</b>: Modern Standby is <b>OFF right now</b> but set to turn back <b>ON after a restart</b>. <b>Restart</b> to finish, then you get instant wake plus the dim-not-off behavior."; }
      else if (ms.live && ms.pending && !ms.after){ cls="warn"; m="⚠ Modern Standby is <b>ON now</b> but set to turn <b>OFF on the next restart</b>, which brings back the slow hibernate. We recommend keeping it on; ask Claude to clear that override."; }
      else if (!ms.live && !ms.pending){ cls="warn"; m="⚠ Modern Standby is <b>OFF</b>. For the best experience (instant wake + the dim-not-off screensaver), turn it back on. Ask Claude to re-enable it (needs admin + a restart)."; }
      else { cls="ok"; m="Modern Standby is <b>ON</b>."; }
    }
    box.className = "msbox " + cls;
    st.className = "msstate " + cls;
    st.innerHTML = m;
  };

  // Display-dimming hardware probe: the host tells us the panel + whether brightness is controllable, and
  // streams the live % during the test as it fades the backlight down then back up.
  window.adomHardware = function(h){
    h = h || {};
    var doze = !!CAP.dozeRisk;
    var el = document.getElementById("hwinfo");
    if (el) el.innerHTML = "Your display: <b>" + esc(h.monitor || "built-in display") + "</b>. " +
      (h.supported ? "Windows exposes a brightness control for this panel, so the screensaver can dim the real <b>backlight</b> to go nearly black when you step away."
                   : "This hardware does <b>not</b> expose a brightness control to apps, so the screensaver can't dim the real backlight.");
    var r = document.getElementById("bresult");
    if (!r) return;
    // When the backlight isn't controllable, the right fallback depends on doze risk: if Modern Standby
    // would kill sessions we MUST keep the display on (dim the picture, a grey, and you can lower brightness
    // by hand); if there's no doze, the honest answer is to just let the monitor power off.
    if (h.controllable === true){ r.className = "bresult ok"; r.textContent = "✓ Tested: the screensaver CAN control your backlight. It'll go nearly black when you step away."; }
    else if (h.controllable === false){
      r.className = "bresult no";
      r.innerHTML = doze
        ? "✗ Tested: your backlight isn't controllable, so the saver dims the <b>picture</b> instead (a darker grey, not true black). Because this machine uses Modern Standby we still keep the display ON; for a darker screen, lower brightness by hand before you step away."
        : "✗ Tested: your backlight isn't controllable. No Modern Standby here though, so the saver just lets the <b>monitor power off</b> on a timer, that's safe and easier on the panel than a bright billboard all night.";
    }
    else { r.className = "bresult un"; r.textContent = "Not tested yet. Run the test below to confirm and record it for the saver."; }
  };
  window.adomBrightTest = function(phase, pct){
    var box = document.getElementById("btestbox"); if(box) box.style.display = "flex";
    var p = document.getElementById("bpct"); if(p) p.textContent = pct + "%";
    var n = document.getElementById("bnote"); if(n) n.textContent =
      phase==="down" ? "fading your backlight down…" :
      phase==="up"   ? "fading back up…" :
      phase==="nosupport" ? "your hardware didn't allow it" : "done";
    if (phase==="done" || phase==="nosupport"){ setTimeout(function(){ if(box) box.style.display="none"; }, 1500); }
  };
  (function(){ var b=document.getElementById("btest"); if(b) b.addEventListener("click", function(){ send("btest"); }); })();
  (function(){
    var ids = ["previewapp","previewskill","previewcomp","previewpriv","previewdim"];
    ids.forEach(function(id){ var b=document.getElementById(id); if(b) b.addEventListener("click", function(){ send(id); }); });
  })();

  function kaNote(ka, onAc){
    if (ka === "ac") return "holding your machine awake, so sessions run as long as you like";
    if (ka === "dc") return "Windows' sleep timer is the give-up clock; plug in to run indefinitely";
    return "the helper isn't running, click Apply to start it";
  }

  // The host calls this on open (applied=false) and after Apply (applied=true).
  window.adomConfig = function(data, applied){
    if (!data) return;
    CAP = data.cap || {};
    try { renderIntro(CAP); } catch(e){}
    try { if (window.adomMS) window.adomMS(data.ms, CAP); } catch(e){}
    try { rewordTune(CAP); } catch(e){}
    var ka = data.keepAwake || "off";
    document.getElementById("status").innerHTML =
      '<span><span class="k">Power right now:</span> <span class="v">' + esc(data.power) + '</span></span>' +
      '<span><span class="k">Keep-awake:</span> <span class="v">' +
        (ka==="ac"?"on (plugged)":ka==="dc"?"passive (battery)":"off") +
        '</span> <span class="note">- ' + kaNote(ka, data.onAc) + '</span></span>';

    var rows = data.rows || [], html = "", bad = 0;
    for (var i=0;i<rows.length;i++){
      var r = rows[i]; if(!r.ok) bad++;
      html += '<tr class="'+(r.ok?"":"bad")+'">' +
        '<td><div class="lab">'+esc(r.label)+'</div><div class="why2">'+esc(r.why)+'</div></td>' +
        '<td class="cur r">'+esc(r.cur)+'</td>' +
        '<td class="rec r">'+esc(r.rec)+'</td>' +
        '<td class="r"><span class="badge '+(r.ok?"ok":"fix")+'">'+(r.ok?"✓":"⚠")+'</span></td>' +
      '</tr>';
    }
    document.getElementById("rows").innerHTML = html;

    var banner = document.getElementById("banner");
    if (bad === 0){
      banner.className = "banner ok";
      banner.textContent = "✓ Your " + (CAP.device === "desktop" ? "PC" : "laptop") + " matches the Adom vibe-coding profile.";
    } else {
      banner.className = "banner bad";
      banner.textContent = "⚠ " + bad + (bad===1?" setting differs":" settings differ") +
        " from the recommended profile, click Apply to bring them in line.";
    }

    var apply = document.getElementById("apply");
    apply.disabled = (bad === 0);
    apply.textContent = (bad === 0) ? "Already applied" : "Apply Adom Recommended";
    // Surface a warning dot on the Power tab when settings differ, so it's discoverable from any tab.
    var pd = document.getElementById("powerDot"); if (pd) pd.style.display = (bad > 0) ? "inline-block" : "none";

    var hk = document.getElementById("hkToggle");
    if (hk) hk.checked = (data.hotkeyEnabled !== false);

    // Fine-tune controls (#2 rest-after, #3 idle timeout, #4 never-off) reflect LIVE state each open.
    if (data.saverSec != null) setSel(document.getElementById("saverSel"), data.saverSec);
    if (data.restAfterSec != null) setSel(document.getElementById("restSel"), data.restAfterSec);
    if (data.logoSec != null) setSelSec(document.getElementById("logoSel"), data.logoSec);
    if (data.splashSec != null) setSelSec(document.getElementById("splashSel"), data.splashSec);
    if (data.slideSec != null) setSelSec(document.getElementById("slideSel"), data.slideSec);
    // The "never turn the display off" toggle is about the PLUGGED-IN rail only (monNeverAc), not both rails;
    // on battery we DO let the display sleep, so the old monNever (AC && DC) read OFF even when AC was correct.
    var nv = document.getElementById("neverToggle"); if (nv) nv.checked = !!data.monNeverAc;
    var ct = document.getElementById("compToggle"); if (ct) ct.checked = !!data.showComponents;
    updateNeverWarn();

    if (applied){
      var t = document.getElementById("toast"); t.textContent = "Applied ✓"; t.classList.add("show");
      setTimeout(function(){ t.classList.remove("show"); }, 2600);
    }
    // finish a Re-check: drop the spinner and flash a confirmation (keep it visible >=550ms)
    if (recheckAt){
      var rb = document.getElementById("recheck");
      var done = function(){
        rb.disabled = false; rb.textContent = "Re-check";
        var tt = document.getElementById("toast"); tt.textContent = "✓ Re-checked just now"; tt.classList.add("show");
        setTimeout(function(){ tt.classList.remove("show"); }, 1700);
        recheckAt = 0;
      };
      var el = Date.now() - recheckAt;
      if (el < 550) setTimeout(done, 550 - el); else done();
    }
  };

  document.getElementById("apply").addEventListener("click", function(){ if(!this.disabled) send("apply"); });
  document.getElementById("recheck").addEventListener("click", function(){
    var rb = this; if (rb.disabled) return;
    rb.disabled = true; rb.innerHTML = '<span class="spin"></span>Re-checking…';
    recheckAt = Date.now();
    send("refresh");
  });
  document.getElementById("close").addEventListener("click", function(){ send("close"); });
  document.getElementById("hkToggle").addEventListener("change", function(){ send(this.checked ? "hotkey:on" : "hotkey:off"); });
  (function(){ var ct=document.getElementById("compToggle"); if(ct) ct.addEventListener("change", function(){ send(this.checked ? "components:on" : "components:off"); }); })();

  // Pick the matching <option>; if a live value isn't in the list, add it so the control shows reality.
  function setSel(sel, val){
    if (!sel) return; val = String(val);
    sel.value = val;
    if (sel.value !== val){
      var o = document.createElement("option");
      o.value = val; o.textContent = (val === "0") ? "Never" : Math.round(Number(val)/60) + " min";
      sel.appendChild(o); sel.value = val;
    }
  }
  // Reword the two fine-tune explainers for the hardware. "Dim the screen after" and "Never turn the
  // display off" mean different things on a desktop, and on a machine where we can't dim the backlight.
  function rewordTune(cap){
    cap = cap || {};
    var desktop = cap.device === "desktop", doze = !!cap.dozeRisk, canDim = cap.brightCtl === 1;
    var rest = document.getElementById("restWhy");
    if (rest){
      rest.innerHTML = "The billboards play for this long, then the screensaver rests: "
        + (canDim
            ? "it dims the <b>backlight to near-black</b>" + (doze ? " (the machine stays <b>awake</b> so your /remote-control sessions keep running)" : "")
            : "it dims the picture to near-black" )
        + (desktop ? "" : ", and on battery it powers the panel off to save battery")
        + ". The dim plus slow drift are what prevent burn-in. A few minutes is good.";
    }
    var pd = document.getElementById("previewdimwhy");
    if (pd){
      pd.innerHTML = "Jumps straight to the dimmed screensaver: the Adom logo and <b>AI Electronics Prototyping</b> "
        + "drifting slowly, near-black (billboards don't rotate when dimmed), "
        + (canDim
            ? "with the <b>hardware backlight</b> dropping to near-black on this panel."
            : "using the <b>software</b> picture dim (this panel's backlight isn't controllable, so it'll be a dark grey, not true black).");
    }
    var nw = document.getElementById("neverWhy");
    if (nw){
      nw.innerHTML = doze
        ? "Recommended <b>ON</b>" + (desktop ? "" : " while plugged in") + ". This machine uses <b>Modern Standby</b>, so "
          + "powering the display off makes it doze and drops your <b>/remote-control</b> sessions. Keeping it on (the "
          + "saver dims the backlight and drifts) is what keeps you awake and burn-in-free."
        : (canDim
            ? "Optional here. No Modern Standby, so display-off won't drop sessions, but leaving it <b>ON</b> lets the "
              + "saver dim the backlight and run the billboard with no burn-in. Either is safe."
            : "Recommended <b>OFF</b> here. No Modern Standby, so letting the monitor power off is safe and won't drop "
              + "sessions; since the saver can't dim this backlight, a powered-off monitor beats a bright billboard all night.");
    }
  }
  function updateNeverWarn(){
    // Warn (orange) only when the toggle is set AGAINST our recommendation: OFF on a doze machine, where
    // turning the display off would kill sessions. Everywhere else, neutral.
    var nv = document.getElementById("neverToggle"), why = document.getElementById("neverWhy");
    var against = CAP.dozeRisk && nv && !nv.checked;
    if (why) why.className = "twhy" + (against ? " warn" : "");
  }
  // Like setSel but labels a missing live value in SECONDS (the cascade stages are second-based).
  function setSelSec(sel, val){
    if (!sel) return; val = String(val);
    sel.value = val;
    if (sel.value !== val){
      var o = document.createElement("option");
      o.value = val; o.textContent = (val === "0") ? "Skip" : val + " sec";
      sel.appendChild(o); sel.value = val;
    }
  }
  document.getElementById("saverSel").addEventListener("change", function(){ send("saver:" + this.value); });
  document.getElementById("restSel").addEventListener("change", function(){ send("restafter:" + this.value); });
  (function(){
    var lg=document.getElementById("logoSel"); if(lg) lg.addEventListener("change", function(){ send("logo:" + this.value); });
    var sp=document.getElementById("splashSel"); if(sp) sp.addEventListener("change", function(){ send("splash:" + this.value); });
    var sl=document.getElementById("slideSel"); if(sl) sl.addEventListener("change", function(){ send("slide:" + this.value); });
  })();
  document.getElementById("neverToggle").addEventListener("change", function(){
    send(this.checked ? "displayoff:never" : "displayoff:rec"); updateNeverWarn();
  });

  // ================= CACHE / DEBUG tab =================
  // The config window's web root IS the saver's cache dir, so manifest.json, showcounts.json and every
  // cached hero file are served right here. We read them directly and recompute the SAME priority score
  // the billboard uses (kept in sync with billboard.html slideScore), so this is a true window into the cache.
  function escA(s){ return esc(s).replace(/"/g,"&quot;").replace(/'/g,"&#39;"); }
  var cacheLoaded = false, cacheData = [], cacheCounts = {};
  function ageDaysC(u){ if(!u) return 1e9; var t=Date.parse(u); if(isNaN(t)) return 1e9; return (Date.now()-t)/86400000; }
  // freshness: <7d 100, <30d 60, <90d 35, <365d 15, older/unknown 5  (mirror of billboard freshScore)
  function freshScoreC(u){ var d=ageDaysC(u); if(d<7)return 100; if(d<30)return 60; if(d<90)return 35; if(d<365)return 15; return 5; }
  // novelty: max(0, 60 - 20*timesShown)  (never-seen +60; gone after ~3 views)
  function noveltyC(slug){ return Math.max(0, 60 - 20*(cacheCounts[slug]||0)); }
  function freshLabelC(u){ var d=ageDaysC(u);
    if(d>=1e9) return "undated"; if(d<1) return "today"; if(d<2) return "yesterday"; if(d<7) return Math.round(d)+"d ago";
    if(d<60) return Math.max(1,Math.round(d/7))+"w ago"; if(d<730) return Math.max(1,Math.round(d/30))+"mo ago"; return Math.round(d/365)+"y ago"; }

  async function loadCache(){
    var cnt = document.getElementById("cacheCnt"), list = document.getElementById("cacheList");
    if (cnt) cnt.textContent = "Reading cache…";
    try { var rc = await fetch("showcounts.json?_=" + Date.now(), {cache:"no-store"}); cacheCounts = (await rc.json()) || {}; } catch(e){ cacheCounts = {}; }
    try {
      var r = await fetch("manifest.json?_=" + Date.now(), {cache:"no-store"});
      var m = await r.json(); cacheData = (m && m.slides) || [];
    } catch(e){
      cacheData = [];
      if (cnt) cnt.innerHTML = '<span style="color:var(--warn)">No cache yet — run the screensaver once to populate it.</span>';
      if (list) list.innerHTML = "";
      return;
    }
    renderCache();
  }
  function renderCache(){
    var cnt = document.getElementById("cacheCnt"), list = document.getElementById("cacheList");
    var sortBy = (document.getElementById("cacheSort") || {}).value || "score";
    var rows = cacheData.map(function(s){ var f=freshScoreC(s.updated), n=noveltyC(s.slug); return {s:s, f:f, n:n, score:f+n}; });
    if (sortBy==="score") rows.sort(function(a,b){ return b.score-a.score; });
    else if (sortBy==="slug") rows.sort(function(a,b){ return (a.s.slug||"").localeCompare(b.s.slug||""); });
    else if (sortBy==="fresh") rows.sort(function(a,b){ return b.f-a.f; });
    else if (sortBy==="type") rows.sort(function(a,b){ return (a.s.type||"").localeCompare(b.s.type||"") || (b.score-a.score); });
    if (cnt) cnt.innerHTML = "<b>" + rows.length + "</b> page" + (rows.length===1?"":"s") + " cached" + (rows.length?" · sorted by " + sortBy:"");
    var html = "";
    for (var i=0;i<rows.length;i++){
      var R = rows[i], s = R.s, media = s.media||"", src = s.src||"", typ = (s.type||"").toLowerCase();
      var remoteOnly = /^https?:/i.test(media);   // media is a streamed URL (video / un-cached), not a local file
      var thumb = media
        ? '<img class="cthumb" referrerpolicy="no-referrer" src="' + escA(media) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'">'
            + '<div class="cthumb miss" style="display:none">no file</div>'
        : '<div class="cthumb miss">no hero</div>';
      var fileLine = media
        ? (remoteOnly ? 'streamed (not cached to disk)' : 'cached file: ' + esc(media))
        : '<span class="none">no hero file</span>';
      html += '<div class="ccard" data-slug="' + escA(s.slug) + '">'
        + '<div class="cthumbwrap">' + thumb + '</div>'
        + '<div class="cmeta">'
        +   '<div class="cslug">' + esc(s.slug) + (typ ? '<span class="ctype t-' + esc(typ) + '">' + esc(typ) + '</span>' : '') + '</div>'
        +   '<div class="ctitle">' + esc(s.title || "(no title)") + '</div>'
        +   '<div class="cbrief">' + esc(s.brief || "") + '</div>'
        +   '<div class="cfile">' + fileLine + ' · updated ' + esc(freshLabelC(s.updated)) + (s.vis && s.vis!=="public" ? ' · <span style="color:#f0c548">' + esc(s.vis) + '</span>' : '') + '</div>'
        +   '<button class="cdlbtn"' + (src ? ' data-src="' + escA(src) + '" data-media="' + escA(media) + '"' : ' disabled title="no remote URL stored for this entry"') + '>'
        +     (src ? 'Download current from Wiki ↓' : 'no remote URL') + '</button>'
        +   '<div class="ccmp"></div>'
        + '</div>'
        + '<div class="cscore"><div class="big">' + R.score + '</div>'
        +   '<div class="brk"><span class="f">fresh ' + R.f + '</span> + <span class="n">new ' + R.n + '</span><br><span class="j">+ jitter 0–12</span></div></div>'
        + '</div>';
    }
    if (list) list.innerHTML = html || '<div class="hw" style="padding:18px 0;color:var(--lo)">Cache is empty.</div>';
    var btns = list ? list.querySelectorAll(".cdlbtn") : [];
    for (var b=0;b<btns.length;b++) (function(btn){ if(!btn.disabled) btn.addEventListener("click", function(){ doCompare(btn); }); })(btns[b]);
  }
  // Fetch the page's CURRENT hero from the wiki and show it beside the cached one, so a stale cache is obvious.
  function doCompare(btn){
    var src = btn.getAttribute("data-src"), media = btn.getAttribute("data-media") || "";
    var cmp = btn.parentNode.querySelector(".ccmp"); if(!cmp) return;
    btn.disabled = true; btn.textContent = "Fetching…";
    var bust = (src.indexOf("?")>=0 ? "&" : "?") + "_=" + Date.now();
    cmp.className = "ccmp show";
    // no-referrer on BOTH: the wiki side needs it to clear Cloudflare hotlink protection (curl-like, no Referer);
    // the cached side is local so it's a harmless no-op there.
    cmp.innerHTML =
        '<div class="side"><div class="lbl">Cached (on disk)</div>'
      +   (media ? '<img referrerpolicy="no-referrer" src="' + escA(media) + '" onload="this.parentNode.querySelector(\'.dim\').textContent=this.naturalWidth+\'×\'+this.naturalHeight; cmpReport(this)" onerror="this.style.display=\'none\';this.parentNode.querySelector(\'.err\').textContent=\'cached file missing on disk\'">'
                : '<div class="err">no cached file</div>')
      +   '<div class="dim"></div><div class="err"></div></div>'
      + '<div class="side"><div class="lbl">Current on Wiki</div>'
      +   '<img referrerpolicy="no-referrer" src="' + escA(src) + bust + '" onload="this.parentNode.querySelector(\'.dim\').textContent=this.naturalWidth+\'×\'+this.naturalHeight; cmpReport(this)" onerror="this.style.display=\'none\';this.parentNode.querySelector(\'.err\').textContent=\'couldn\\\'t fetch — private page or offline\'">'
      +   '<div class="dim"></div><div class="err"></div></div>'
      + '<div class="verdict" style="display:none"></div>';
    setTimeout(function(){ btn.disabled = false; btn.textContent = "↻ Re-download"; }, 1400);
  }
  // Once both images in a compare have loaded, note whether the dimensions match (a cheap stale-hint; we
  // can't pixel-diff a cross-origin image without tainting the canvas, so dimensions + the eyeball test it is).
  window.cmpReport = function(img){
    var cmp = img.closest(".ccmp"); if(!cmp) return;
    var imgs = cmp.querySelectorAll(".side img"), loaded = [];
    for (var i=0;i<imgs.length;i++){ if(imgs[i].naturalWidth>0) loaded.push(imgs[i]); }
    if (loaded.length < 2) return;
    var a = loaded[0], b = loaded[1];
    var v = cmp.querySelector(".verdict"); if(!v) return;
    var same = (a.naturalWidth===b.naturalWidth && a.naturalHeight===b.naturalHeight);
    v.style.display = "block";
    v.className = "verdict " + (same ? "same" : "diff");
    v.innerHTML = same
      ? "✓ Same dimensions — cache looks current. (Eyeball them to be sure.)"
      : "⚠ Different dimensions (" + a.naturalWidth + "×" + a.naturalHeight + " cached vs " + b.naturalWidth + "×" + b.naturalHeight + " on Wiki) — the cached hero may be stale; the saver will self-heal it on the next fetch.";
  };
  (function(){
    var rl = document.getElementById("cacheReload"); if(rl) rl.addEventListener("click", function(){ loadCache(); });
    var so = document.getElementById("cacheSort"); if(so) so.addEventListener("change", function(){ renderCache(); });
  })();
  // Headless hook for "/cshot <out> cache <slug>": (re)load the cache, find that row, scroll it to the top and
  // fire its live wiki compare — so a background screenshot can show the real side-by-side with no mouse.
  window.adomCacheCompare = async function(slug){
    cacheLoaded = true;
    await loadCache();
    var cards = document.querySelectorAll("#cacheList .ccard");
    for (var i=0;i<cards.length;i++){
      if (cards[i].getAttribute("data-slug") === slug){
        try { cards[i].scrollIntoView({block:"start"}); } catch(e){}
        var btn = cards[i].querySelector(".cdlbtn");
        if (btn && !btn.disabled) doCompare(btn);
        return true;
      }
    }
    return false;
  };

  // Tabs: show one panel at a time, reset scroll on switch. Header + tabs + footer stay put; only the
  // middle content scrolls, so the long config is now five short, navigable sections.
  (function(){
    var tabs = document.querySelectorAll('#tabs .tab');
    var content = document.querySelector('.content');
    function show(name){
      for (var i=0;i<tabs.length;i++) tabs[i].classList.toggle('active', tabs[i].getAttribute('data-tab')===name);
      var panels = document.querySelectorAll('.panel');
      for (var j=0;j<panels.length;j++) panels[j].classList.toggle('active', panels[j].id==='tab-'+name);
      if (content) content.scrollTop = 0;
      // Lazy-load the cache the first time the tab is opened (and refresh it on every subsequent open).
      if (name==='cache'){ if(!cacheLoaded){ cacheLoaded = true; } loadCache(); }
    }
    for (var k=0;k<tabs.length;k++) (function(t){ t.addEventListener('click', function(){ show(t.getAttribute('data-tab')); }); })(tabs[k]);
    // deep link: "/c <tab>" opens config.html#tab=<name> so the window can start on a specific tab
    var hm = /[#&]tab=([a-z]+)/.exec(location.hash); if (hm) show(hm[1]);
  })();
</script>
</body>
</html>