123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
<!doctype html>
<!-- Adom Screensaver billboard, rendered in a fullscreen WebView2 hosted by AdomSaver.scr.
     Plays webm (muted, looped), renders SVG + raster, cross-fades, shows the power + display-off
     countdown footer and the Wiki v1 "migrate to v2" kicker. Data comes from manifest.json
     (written by the C# host) served from the virtual host https://adom-screensaver.local/. -->
<html lang="en">
<head>
<meta charset="utf-8">
<style>
  :root { --teal:#00b8b0; --tealbri:#00e6dc; --hi:#e6edf3; --lo:#8b949e; --dim:#6b7280; --line:#1e2733; --bg:#0d1117; }
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body { width:100%; height:100%; overflow:hidden; background:var(--bg);
    font-family:"Segoe UI","Segoe UI Semibold",system-ui,sans-serif; cursor:none; user-select:none; }
  /* The whole billboard rests at 50% (configurable). The host fades the WINDOW out on wake.
     Transparent over the dark <body> + a layer ABOVE #bbmark so the giant brand mark reads as a
     full-bleed watermark BEHIND the slide content (magazine-ad style). */
  #stage { position:fixed; inset:0; background:transparent; z-index:1; filter:brightness(var(--sdim,0.7));
    opacity:1; transition:opacity .55s ease; animation:bbdrift 80s ease-in-out infinite; will-change:transform; }
  /* anti burn-in: slowly drift ALL billboard content (incl. the lid note) so it never sits on the same
     pixels. We first scale to 97% (a ~1.5% margin on every side, visually imperceptible), then drift only
     WITHIN that margin (<=1.0vw/vh) so content can never run off the screen. Display also powers off after rest. */
  /* Content is scaled to 88% -> a ~6% margin on every edge; the drift stays within ±1% so the content
     never comes closer than ~5% to any edge. Humans need margins. */
  @keyframes bbdrift {
    0%   { transform: scale(.83) translate(0, 0); }
    20%  { transform: scale(.83) translate(3.3vw, -2.9vh); }
    40%  { transform: scale(.83) translate(-3.1vw, 3.0vh); }
    60%  { transform: scale(.83) translate(2.8vw, 2.9vh); }
    80%  { transform: scale(.83) translate(-3.2vw, -2.7vh); }
    100% { transform: scale(.83) translate(0, 0); }
  }
  /* Slide = a TWO-COLUMN editorial split: text on the left, the hero shot on the right. Fills a 16:9
     widescreen far better than a centered column, and gives the description room to breathe. */
  .slide { position:absolute; inset:0; display:grid; grid-template-columns:43% 57%;
    align-items:center; column-gap:4.5vw; padding:9vh 6vw 11vh; text-align:left; }
  .slide .coltext { display:flex; flex-direction:column; align-items:flex-start; min-width:0; }
  .slide .colmedia { display:flex; align-items:center; justify-content:center; min-width:0; height:100%; }
  .kicker { font-size:1.25vh; font-weight:600; letter-spacing:.66em; margin-left:.66em;
    text-transform:uppercase; color:var(--teal); margin-bottom:3.4vh; }
  /* visibility badge: the billboards double as a privacy QA surface, so PUBLIC vs ADOM-ONLY must be loud */
  .vbadge { display:inline-flex; align-items:center; gap:.65vw; font-size:1.2vh; font-weight:700;
    letter-spacing:.16em; text-transform:uppercase; padding:.6vh 1.2vw; border-radius:999px; }
  .vbadge::before { content:""; width:.85vh; height:.85vh; border-radius:50%; flex:none; }
  .vbadge .sub { font-weight:600; letter-spacing:.03em; text-transform:none; opacity:.82; }
  .vbadge.pub { color:#5dd879; background:rgba(46,160,67,.13); border:1px solid rgba(46,160,67,.5); }
  .vbadge.pub::before { background:#2ea043; box-shadow:0 0 .9vh rgba(46,160,67,.75); }
  .vbadge.priv { color:#f0c548; background:rgba(240,197,72,.13); border:1px solid rgba(240,197,72,.55); }
  .vbadge.priv::before { background:#f0c548; box-shadow:0 0 .9vh rgba(240,197,72,.75); }
  /* wiki identity block: owner-qualified URL + org/personal + author (the kicker's replacement) */
  .wikiid { margin:.6vh 0 0; }
  .wurl { font-family:"Cascadia Code","Consolas","Courier New",monospace; font-size:1.65vh; letter-spacing:.005em; color:var(--hi); }
  .wurl .wdim { color:var(--dim); }
  .wurl .worg { color:var(--tealbri); font-weight:700; }
  .wurl .wuser { color:var(--lo); font-weight:600; }
  .wmeta { margin-top:.9vh; font-size:1.3vh; color:var(--lo); letter-spacing:.02em; }
  .wmeta b { font-weight:700; } .wmeta .worg { color:var(--tealbri); }
  /* meta row: visibility badge + freshness chip grouped on one line (design-pass: one compact source
     line instead of stacked blocks). The freshness chip leads the eye to NEW content. */
  .metarow { display:flex; align-items:center; gap:1vw; flex-wrap:wrap; margin-bottom:.5vh; }
  .freshchip { font-size:1.2vh; font-weight:700; letter-spacing:.03em; padding:.42vh 1.15vw; border-radius:999px;
    display:inline-flex; align-items:center; gap:.5vw; line-height:1; }
  .freshchip::before { content:"\2726"; font-size:1.05vh; }
  .freshchip.fresh-new { color:#04211f; background:linear-gradient(100deg,#00e6dc,#39b8ff); border:1px solid rgba(0,230,220,.6); box-shadow:0 0 1.4vh rgba(0,184,176,.32); }
  .freshchip.fresh-recent { color:#aeb8c4; background:rgba(255,255,255,.05); border:1px solid var(--line); }
  .freshchip.fresh-old { color:var(--dim); background:transparent; border:1px solid var(--line); }
  .freshchip.fresh-old::before { opacity:.5; }
  /* TYPE capsule (App / Skill / Component) so you can tell at a glance WHAT KIND of wiki repo a billboard is.
     The wiki's `type` field is first-class (the Apps/Skills/Components/Bootstraps header tabs filter on it);
     each kind gets a distinct color + leading dot. */
  .typecap { font-size:1.2vh; font-weight:800; letter-spacing:.08em; text-transform:uppercase;
    padding:.42vh 1.1vw; border-radius:999px; display:inline-flex; align-items:center; gap:.55vw; line-height:1; }
  .typecap::before { content:""; width:.85vh; height:.85vh; flex:none; }
  .typecap.t-app { color:#8cc0ff; background:rgba(0,97,239,.16); border:1px solid rgba(100,171,255,.55); }
  .typecap.t-app::before { background:#64ABFF; border-radius:2px; box-shadow:0 0 .9vh rgba(100,171,255,.8); }
  .typecap.t-skill { color:#c2acff; background:rgba(140,107,247,.16); border:1px solid rgba(140,107,247,.55); }
  .typecap.t-skill::before { background:#8C6BF7; transform:rotate(45deg); box-shadow:0 0 .9vh rgba(140,107,247,.8); }
  .typecap.t-component { color:#ff9ccb; background:rgba(255,126,182,.14); border:1px solid rgba(255,126,182,.5); }
  .typecap.t-component::before { background:#ff7eb6; border-radius:50%; box-shadow:0 0 .9vh rgba(255,126,182,.8); }
  /* Preview wait screen: never leave a preview on a blank screen while a billboard (esp. a component, whose
     hero must download) is still loading. Tell the user exactly what's happening so nothing feels stuck. */
  #previewwait { position:fixed; inset:0; z-index:6; display:none; flex-direction:column;
    align-items:center; justify-content:center; gap:2.4vh; text-align:center; background:#0d1117; }
  #previewwait .pw-logo { width:9vw; max-width:180px; opacity:.92;
    filter:drop-shadow(0 0 28px rgba(0,184,176,.16)); }
  #previewwait .pw-spin { width:3vh; height:3vh; border:.34vh solid rgba(0,184,176,.22);
    border-top-color:var(--tealbri); border-radius:50%; animation:pwspin .8s linear infinite; }
  #previewwait .pw-text { color:var(--hi); font-size:2.1vh; font-weight:500; max-width:62ch;
    line-height:1.55; padding:0 6vw; }
  #previewwait .pw-text b { color:var(--tealbri); font-weight:600; }
  #previewwait .pw-sub { color:var(--lo); font-size:1.5vh; max-width:62ch; padding:0 6vw; min-height:1.5vh; }
  @keyframes pwspin { to { transform:rotate(360deg); } }
  .kicker.v1 { color:var(--lo); letter-spacing:.5em; margin-left:.5em; }
  .herobox { width:100%; max-height:66vh; display:flex; align-items:center; justify-content:center; }
  .herobox > * { max-width:100%; max-height:66vh; object-fit:contain;
    border:1px solid rgba(255,255,255,.07); border-radius:14px; background:#000;
    box-shadow:0 3.5vh 8vh rgba(0,0,0,.62), 0 0 0 1px rgba(0,184,176,.12), 0 0 14vh rgba(0,184,176,.06); }
  .title { margin-top:1.8vh; font-size:4.6vh; font-weight:600; letter-spacing:-.022em;
    line-height:1.06; color:var(--hi); }
  /* description: left-aligned, given room (up to 6 lines) so it isn't cut off at line 3 anymore */
  .brief { margin-top:1.6vh; max-width:52ch; font-size:1.85vh; line-height:1.55; color:var(--lo);
    font-weight:300; display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:6; overflow:hidden; }
  .label { margin-top:3vh; font-size:1.05vh; font-weight:600; letter-spacing:.5em;
    text-transform:uppercase; color:var(--lo); }
  /* CTA pill, wraps to at most 2 lines (no mid-word cut); left-aligned in the text column. */
  .promptbox { margin-top:1.1vh; padding:1.1vh 1.7vw; border:1px solid rgba(0,184,176,.42);
    border-radius:16px; background:rgba(0,184,176,.1); color:var(--tealbri); font-size:1.75vh;
    font-weight:500; letter-spacing:.005em; max-width:46ch; line-height:1.4; text-align:left;
    display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden; }
  .foot { position:absolute; left:0; right:0; bottom:3.4vh; font-size:1.35vh; letter-spacing:.04em;
    color:#aeb8c2; text-align:center; }
  /* Intro splash, a LEFT-ALIGNED editorial layout (logo → eyebrow → headline → sub → live status,
     on a teal accent rail) over a soft off-center glow, with a big faded brand mark anchoring the
     right and a bottom control bar of real keycaps. Held until data is ready, then cross-faded. */
  #intro { position:fixed; inset:0; z-index:5; opacity:1; overflow:hidden;
    background:radial-gradient(70% 90% at 26% 42%, #17212f 0%, #0d1117 58%); }
  /* A solid full-screen cover sits OVER the splash while its text + logo render underneath, then
     fades off (800ms) to reveal them already-drawn, a simple rectangle composites perfectly smooth. */
  /* Plain dark cover, the branded grey logo lives ONLY on the GDI+ fade-to-black phase (showing it
     here too made it visibly jump, since GDI+ and the browser viewport scale it differently). */
  #blackcover { position:fixed; inset:0; z-index:30; background:#0d1117; opacity:1;
    transition:opacity .8s ease; pointer-events:none; will-change:opacity; }
  #blackcover.gone { opacity:0; }
  /* Rest layer: pure black covering everything once the display-off time arrives, so nothing burns in. */
  #restlayer { position:fixed; inset:0; z-index:50; background:#000; display:none; }
  /* FULL-DIM screensaver: the Adom logo + "AI Electronics Prototyping", barely visible, drifting slowly
     (DVD-style bounce). During the full dim the billboards don't rotate, just this logo moves so it can't
     burn in. Backlight is already at ~0 (host), so this faint white logo reads as a barely-there ghost. */
  #dimsaver { position:fixed; left:0; top:0; z-index:55; display:none; text-align:center;
    opacity:.14; will-change:transform; pointer-events:none; }
  #dimsaver #dimlogo { width:15vw; max-width:280px; display:block; margin:0 auto;
    filter:drop-shadow(0 0 1.4vh rgba(0,184,176,.12)); }
  #dimsaver #dimtag { margin-top:1.6vh; color:#e6edf3; font-size:1.9vh; font-weight:500;
    letter-spacing:.34em; text-transform:uppercase; white-space:nowrap; }
  /* Fast dismiss fade, when the user moves the mouse on a billboard, the PAGE fades to black in 200ms
     (GPU-composited CSS; fading the host window over a live WebView2 is slow + renders black), then the
     host hides. z-index above everything so it covers content + the rest layer. */
  #fadeout { position:fixed; inset:0; z-index:60; background:#000; opacity:0; pointer-events:none;
    transition:opacity .5s ease; display:none; }
  /* Billboard brand mark, ONE faint Adom "wing", crisp inline SVG (no blurry PNG scaling), bleeding
     off the top-right corner like the wash of brand on a high-end glossy magazine ad (Drew/John's ask).
     The 118vh square is anchored partly off-screen (top:-9vh, right:-22vh) so the single wing stays
     full-bleed on the right/top/bottom while its left curve fades toward center; the gentle float keeps
     well inside that overscan so it's ALWAYS full-bleed. opacity .035 = ~half as visible (closer to the
     near-black bg). z-index:0 = behind #stage (content). Shown only during the rotation. */
  /* Visibility-tinted FULL background: PUBLIC billboards stay the clean dark screensaver (no wash/frame);
     PRIVATE / org-only billboards get an Adom TEAL wash + a teal screen frame (brand color), so a private
     (-adom / -dev) page is unmistakable next to its public twin. Set per slide. */
  #bgwash { position:fixed; inset:0; z-index:0; pointer-events:none; opacity:0; transition:opacity .55s ease; }
  body.viz-pub #bgwash { opacity:0; }
  body.viz-priv #bgwash { opacity:1;
    background:radial-gradient(95% 115% at 50% 22%, rgba(0,184,176,.17), transparent 60%), linear-gradient(160deg, rgba(0,97,239,.10), transparent 52%);
    box-shadow:inset 0 0 0 .5vh rgba(0,184,176,.5), inset 0 0 16vh rgba(0,184,176,.16); }
  #bbmark { position:fixed; inset:0; margin:auto; width:220vh; height:220vh; opacity:.01; z-index:0;
    pointer-events:none; display:none; will-change:transform; animation:bbmarkfloat 270s ease-in-out infinite; }
  #bbmark svg { width:100%; height:100%; display:block; }
  #bbmark path { fill:#ffffff; }
  /* Very slow random-ish drift (270s loop): the big mark wanders smoothly so a different part bleeds
     off each edge over time, barely-visible (97% transparent) brand wash that exercises the pixels. */
  @keyframes bbmarkfloat {
    0%   { transform: translate(30vh, -10vh); }
    20%  { transform: translate(8vh, 30vh); }
    40%  { transform: translate(-30vh, 12vh); }
    60%  { transform: translate(-14vh, -30vh); }
    80%  { transform: translate(28vh, 22vh); }
    100% { transform: translate(30vh, -10vh); }
  }
  .intro-mark { position:absolute; right:-7vw; top:50%; transform:translateY(-50%);
    width:74vh; height:74vh; opacity:.05; z-index:0; pointer-events:none; }
  .intro-grid { position:absolute; left:0; right:0; top:0; bottom:8.8vh; display:flex; align-items:center; z-index:1; }
  .intro-content { margin-left:9vw; max-width:60vw; padding-left:2.7vw;
    border-left:2px solid rgba(0,184,176,.5); }
  #intrologo { width:10.5vw; max-width:220px; display:block; filter:drop-shadow(0 0 28px rgba(0,184,176,.15)); }
  .intro-tagline { margin-top:1.7vh; color:var(--tealbri); font-size:2.15vh; font-weight:500;
    font-style:italic; letter-spacing:.01em; }
  .intro-eyebrow { margin-top:2.4vh; color:var(--teal); letter-spacing:.44em; font-size:1.2vh;
    font-weight:600; text-transform:uppercase; }
  .intro-head { margin-top:1.4vh; max-width:17ch; color:var(--hi); font-size:4.3vh; font-weight:600;
    letter-spacing:-.015em; line-height:1.12; }
  .intro-head #introdev { color:var(--tealbri); }
  .intro-sub { margin-top:1.8vh; max-width:46ch; color:var(--lo); font-size:1.65vh; line-height:1.6; }
  .intro-sub b { color:var(--hi); font-weight:600; }
  /* Live "nerd stats", power, screen-off + sleep countdowns, keep-awake internals, cache growth. */
  /* A real 2-column grid so every label and every value lines up in rigid columns (a table). */
  .intro-stats { margin-top:2.7vh; display:grid; grid-template-columns:max-content 1fr;
    column-gap:1.9vw; row-gap:1.15vh; align-items:baseline; max-width:68ch; font-size:1.6vh; line-height:1.34; }
  .stat { display:contents; }
  .stat-k { white-space:nowrap; color:var(--lo); letter-spacing:.06em; text-transform:uppercase;
    font-size:1.18vh; font-weight:600; }
  .stat-k::before { content:""; display:inline-block; width:.62vh; height:.62vh; border-radius:50%;
    background:var(--teal); margin-right:.8vw; vertical-align:middle; box-shadow:0 0 .7vh rgba(0,184,176,.55); }
  .stat-v { color:var(--hi); font-weight:500; font-variant-numeric:tabular-nums; }
  .stat-v b { color:var(--tealbri); font-weight:700; }
  .stat-v .mono { font-family:"Cascadia Code","Consolas","Courier New",monospace; font-size:.94em; color:var(--tealbri); letter-spacing:-.01em; }
  .nowtag { color:var(--tealbri); font-weight:700; font-size:.82em; letter-spacing:.04em; }
  /* dual-mode (plugged-in vs battery) value chips: MODE label first, value bold, current mode highlighted.
     Replaces the old "<b>Never</b> plugged in" string that read as "never plugged in". */
  .mcwrap { display:inline-flex; flex-wrap:wrap; gap:.65vw; }
  .mc { display:inline-flex; align-items:baseline; gap:.55vw; padding:.32vh 1.05vw; border-radius:999px;
        border:1px solid var(--line); background:rgba(255,255,255,.018); }
  .mc .mcl { color:var(--lo); font-size:.84em; letter-spacing:.01em; }
  .mc .mcv { color:var(--hi); font-weight:700; font-variant-numeric:tabular-nums; }
  .mc.on { border-color:rgba(0,184,176,.55); background:rgba(0,184,176,.1); }
  .mc.on .mcv { color:var(--tealbri); }
  .mc .mcn { color:var(--tealbri); font-size:.7em; font-weight:700; letter-spacing:.08em; text-transform:uppercase; }
  /* the lid-behaviour rows are reassurances about how the screensaver manages the PC, set apart in amber */
  .lidrow .stat-k { color:#f0c548; }
  .lidrow .stat-k::before { background:#f0c548; box-shadow:0 0 .7vh rgba(240,197,72,.5); }
  .lidrow .stat-v b { color:#f3d27a; }
  /* persistent lid reassurance under each billboard's footer */
  .lidnote { position:absolute; left:0; right:0; bottom:1.2vh; font-size:1.3vh; letter-spacing:.02em;
    color:#c6cfd9; text-align:center; }
  .lidnote b { color:#f3d27a; font-weight:700; }
  .lidnote .rb { color:#f3d27a; font-weight:600; }
  .rebootnudge { margin-top:1.9vh; display:inline-block; padding:.7vh 1.4vw; border-radius:8px;
    background:rgba(240,197,72,.1); border:1px solid rgba(240,197,72,.42); color:#f3d27a;
    font-size:1.42vh; font-weight:600; letter-spacing:.01em; }
  .intro-status { margin-top:3.1vh; display:inline-flex; align-items:center; gap:.7vw;
    padding:.85vh 1.5vw; border:1px solid rgba(0,184,176,.28); border-radius:999px;
    background:rgba(0,184,176,.07); color:var(--tealbri); font-size:1.4vh; }
  .pulse { width:.9vh; height:.9vh; border-radius:50%; background:var(--tealbri); flex:none;
    animation:pulse 1.7s ease-out infinite; }
  @keyframes pulse { 0%{box-shadow:0 0 0 0 rgba(0,230,220,.45)} 70%{box-shadow:0 0 0 1.1vh rgba(0,230,220,0)} 100%{box-shadow:0 0 0 0 rgba(0,230,220,0)} }
  .intro-bar { position:absolute; left:0; right:0; bottom:0; display:flex; align-items:center;
    justify-content:space-between; gap:3vw; padding:2.3vh 4vw; border-top:1px solid var(--line);
    background:linear-gradient(0deg, rgba(8,11,16,.72), transparent); font-size:1.32vh; color:var(--lo); }
  .bar-left, .bar-right { display:flex; align-items:center; gap:1vw; flex-wrap:wrap; }
  .kgrp { display:inline-flex; align-items:center; gap:.35vw; }
  .intro-bar kbd { font-family:inherit; font-size:1.28vh; font-weight:600; color:var(--hi);
    background:#1b2330; border:1px solid #2c3645; border-bottom-width:2px; border-radius:6px;
    padding:.4vh .85vw; box-shadow:0 1px 0 rgba(0,0,0,.45); }
  .intro-bar kbd.soft { background:transparent; border-style:dashed; border-bottom-width:1px;
    color:var(--lo); font-weight:500; }
  .kgrp i { color:var(--dim); font-style:normal; font-size:1.1vh; }
  .cap { color:var(--lo); }
  .sep { color:var(--line); }
  .bar-right b { color:var(--teal); font-weight:600; }
</style>
</head>
<body>
<div id="intro">
  <img class="intro-mark" src="data:image/png;base64,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" alt="">
  <div class="intro-grid">
    <div class="intro-content">
      <img id="intrologo" src="adom-wordmark.png" alt="Adom">
      <div class="intro-tagline">Your life in creative mode</div>
      <div class="intro-eyebrow">Vibe-coding screensaver</div>
      <h1 class="intro-head">Keeps your <span id="introdev">laptop</span> working while you step away.</h1>
      <p class="intro-sub">Your <b>/remote-control</b> sessions keep running. Here's what your machine is doing:</p>
      <div class="intro-stats" id="introstats">
        <div class="stat"><span class="stat-k">Logged in</span><span class="stat-v" id="st-login">checking the Adom Wiki…</span></div>
        <div class="stat"><span class="stat-k">Power</span><span class="stat-v" id="st-power">…</span></div>
        <div class="stat"><span class="stat-k">Display off</span><span class="stat-v" id="st-screen">…</span></div>
        <div class="stat"><span class="stat-k">Sleep</span><span class="stat-v" id="st-sleep">…</span></div>
        <div class="stat lidrow"><span class="stat-k" id="lk-open">Lid open</span><span class="stat-v" id="st-lidopen">…</span></div>
        <div class="stat lidrow" id="row-lidclosed"><span class="stat-k" id="lk-closed">Lid closed</span><span class="stat-v" id="st-lidclosed">…</span></div>
      </div>
      <div id="rebootnudge" class="rebootnudge" style="display:none">⟳ One restart needed to switch the lid &amp; stay-awake settings above on</div>
      <div class="intro-status"><span class="pulse"></span><span id="introstatus">Fetching the latest drops from the Adom Wiki…</span></div>
    </div>
  </div>
  <div class="intro-bar">
    <div class="bar-left">
      <span class="kgrp"><kbd>Win</kbd><i>+</i><kbd>Shift</kbd><i>+</i><kbd>L</kbd></span><span class="cap">launch anytime</span>
      <span class="sep">·</span>
      <kbd class="soft">move&nbsp;mouse&nbsp;/&nbsp;any&nbsp;key</kbd><span class="cap">dismiss</span>
    </div>
    <div class="bar-right">
      <span class="cap">turn it off, ask Claude <b>“turn off the Adom screensaver”</b></span>
    </div>
  </div>
</div>
<div id="bgwash" aria-hidden="true"></div>
<div id="bbmark" aria-hidden="true"><svg viewBox="0 0 1061.33 1061.38" preserveAspectRatio="xMidYMid meet"><path d="M957.65,103.68c-116.2-116.2-282.77-138.02-372.05-48.73-36.23,36.23-54.16,85.18-54.92,138.34h0s0,.07,0,.07c0,.17,0,.35,0,.52v329.6c0,3.99,3.24,7.23,7.23,7.23h326.06c54.75.14,105.25-17.79,142.42-54.97,89.28-89.28,67.46-255.85-48.74-372.05Z"/><path d="M103.68,957.7c116.2,116.2,282.77,138.02,372.05,48.73,36.23-36.23,54.16-85.18,54.92-138.34h0s0-.07,0-.07c0-.17,0-.35,0-.52v-329.6c0-3.99-3.24-7.23-7.23-7.23H197.37c-54.75-.14-105.25,17.79-142.42,54.97-89.28,89.28-67.46,255.85,48.74,372.05Z"/></svg></div>
<div id="stage"></div>
<div id="previewwait">
  <img class="pw-logo" src="adom-wordmark.png" alt="Adom">
  <div class="pw-spin"></div>
  <div class="pw-text" id="pwtext">Loading a billboard from the Adom Wiki&hellip;</div>
  <div class="pw-sub" id="pwsub"></div>
</div>
<div id="blackcover"></div>
<div id="restlayer"></div>
<div id="dimsaver" aria-hidden="true"><img id="dimlogo" src="adom-wordmark.png" alt="Adom"><div id="dimtag">AI Electronics Prototyping</div></div>
<div id="fadeout"></div>
<script>
const HOST = "https://adom-screensaver.local/";
let HOLD = 4200; const FADE = 650; let PER = HOLD + FADE; // ms per slide (HOLD is host-configurable)
let INTRO_MS = 5000; // hold the WebView2 splash this long after it appears, THEN billboards (host-configurable)
// The .scr pushes the user's cascade timing (config dialog) right before revealing the splash.
window.adomTiming = function(t){
  if (!t) return;
  if (typeof t.splashMs === "number" && t.splashMs > 0) INTRO_MS = t.splashMs;
  if (typeof t.slideMs === "number" && t.slideMs > 0) { HOLD = t.slideMs; PER = HOLD + FADE; }
};
let slides = [], idx = -1, started = 0, status = { power:"", off:-1 }, dimSet = false;
var me = null, authed = false;   // logged-in identity (from the host's manifest) so we can show whose filtered view this is
// Live machine state (from adomStats) that the lid/idle copy depends on.
var onAc = true, sleepLeft = -1, deviceWord = "laptop";
function mmssShort(s){ if(s==null||s<0) return ""; var m=Math.floor(s/60), x=s%60; return m+":"+(x<10?"0":"")+x; }
// Single source of truth for the "what happens to /remote-control" story. Adapts to device (laptop vs
// PC), power (AC vs battery), and the live battery sleep countdown. The goal is to keep remote-control
// alive, while honestly conveying when the machine WILL sleep anyway.
function lidInfo(){
  var dev = deviceWord || "laptop";
  if (dev !== "laptop"){           // desktop / no battery / no lid
    return { showClosed:false, openLabel:"When idle",
      openText:"<b>"+dev+" stays awake</b>, screen just dims, so <b>/remote-control</b> stays up." };
  }
  var closed = { closedLabel:"Lid closed",
    closedText:"<b>Sleeps</b> instantly (instant resume); hibernates after 30 min if left." };
  if (onAc){
    return Object.assign({ showClosed:true, openLabel:"Lid open",
      openText:"<b>Stays awake</b>, screen dims to ~10% (not off), so <b>/remote-control</b> stays up." }, closed);
  }
  var cd = mmssShort(sleepLeft);
  return Object.assign({ showClosed:true, openLabel:"Lid open",
    openText:"Screen off; <b>/remote-control</b> up "+(cd?("~<b>"+cd+"</b> more"):"a while")+", then sleeps to save battery." }, closed);
}
function applyLid(){
  var info = lidInfo();
  var kO=document.getElementById("lk-open"), vO=document.getElementById("st-lidopen");
  var rowC=document.getElementById("row-lidclosed"), kC=document.getElementById("lk-closed"), vC=document.getElementById("st-lidclosed");
  if(kO) kO.textContent = info.openLabel;
  if(vO) vO.innerHTML = info.openText;
  if(rowC) rowC.style.display = info.showClosed ? "" : "none";
  if(info.showClosed){ if(kC) kC.textContent = info.closedLabel; if(vC) vC.innerHTML = info.closedText; }
}
function esc(s){ return (s==null?"":String(s)).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); }
function orgList(){ try { return (me && me.orgs && me.orgs.length) ? me.orgs.join(", ") : ""; } catch(e){ return ""; } }
function applyMe(){
  var el = document.getElementById("st-login"); if(!el) return;
  if (!authed){ el.innerHTML = "not logged in, <b>public</b> entries only"; return; }
  var who = me && me.name ? esc(me.name) : "you";
  var orgs = orgList();
  el.innerHTML = "<b>" + who + "</b>" + (orgs ? (" · <b>" + esc(orgs) + "</b> org") : "") + " · your filtered view";
}
let revealedAt = 0;
// Fade the splash up from black. WebView2 defers rendering AND image decode while it's hidden, so if
// we fade the instant it appears, the logo/text aren't painted yet and pop in late (the "blip"). So:
// once the page is actually visible, hold the dark (opacity:0) frame ~420ms to let it render + decode
// the logo/text/watermark, THEN flip to .in so everything rises out of black together, already drawn.
var introFadeScheduled = false;
function startIntroFade(){
  if (introFadeScheduled) return; introFadeScheduled = true;
  // Hold the black cover while the now-visible page renders + decodes the logo/text underneath,
  // THEN fade the cover off over 800ms to reveal the already-drawn splash (no content pop).
  setTimeout(function(){
    var cov = document.getElementById("blackcover");
    if (!cov) return;
    requestAnimationFrame(function(){ requestAnimationFrame(function(){
      cov.classList.add("gone");
      setTimeout(function(){ cov.style.display = "none"; }, 850); // remove after the 800ms fade so it can't cover the billboards
    }); });
  }, 420);
}
// visibilitychange is the reliable "rendering has resumed" signal when the host un-hides the WebView2.
document.addEventListener("visibilitychange", function(){ if (document.visibilityState === "visible") startIntroFade(); });

// The host calls this when it reveals the WebView2 (live) or when a capture nav completes, a backup
// trigger in case this WebView2 config doesn't emit visibility events.
window.adomRevealed = function(){
  if (revealedAt) return;
  revealedAt = Date.now();
  startIntroFade();
};

// The host calls this every second with live power + seconds-to-display-off.
window.adomStatus = function(power, off){ status.power = power || ""; status.off = (off==null?-1:off); paintFoot(); };
// The host streams what the loader is doing right now (so the intro always explains itself).
window.adomLoading = function(text, device){
  const dv = document.getElementById("introdev"); if(dv && device) dv.textContent = device;
  // While a preview is still waiting for its first billboard, stream the live load status into the sub-line
  // so the user can see it's actively working (downloading, reusing cache, etc.), not hung.
  try {
    var pw = document.getElementById("previewwait");
    if (previewMode && pw && pw.style.display !== "none"){ var s = document.getElementById("pwsub"); if (s && text) s.textContent = text; }
  } catch(e){}
};
function mmss(s){ if(s<0) return ""; const m=Math.floor(s/60), x=s%60; return m+":"+(x<10?"0":"")+x; }
function fmtBytes(b){ b=b||0; if(b<1024) return b+" B"; if(b<1048576) return (b/1024).toFixed(0)+" KB"; return (b/1048576).toFixed(1)+" MB"; }
// Reboot nudge: the saver reports rebootPending=true when Modern Standby is set to disabled but a restart
// is still needed to make it (and the lid/stay-awake config) actually take effect. Surface it gently on
// the splash and on every billboard so the saver never claims a state the PC isn't actually in yet.
var rebootPending = false;
function lidnoteHTML(){
  var dev = deviceWord || "laptop", base;
  if (dev !== "laptop"){
    base = "<b>When idle</b> → "+dev+" awake, screen dims, <b>/remote-control stays up</b>";
  } else if (onAc){
    base = "<b>Lid open</b> → awake, screen dims to ~10%, <b>/remote-control stays up</b> &nbsp;·&nbsp; <b>Lid closed</b> → sleeps, resumes instantly";
  } else {
    var cd = mmssShort(sleepLeft);
    base = "<b>Lid open · battery</b> → screen off, <b>/remote-control up "+(cd?("~"+cd+" more"):"")+"</b> then sleeps to save battery &nbsp;·&nbsp; <b>Lid closed</b> → sleeps";
  }
  return rebootPending ? ("<span class='rb'>⟳ Restart once to switch these on</span> &nbsp;·&nbsp; " + base) : base;
}
function applyRebootNudge(){
  var n = document.getElementById("rebootnudge"); if(n) n.style.display = rebootPending ? "inline-block" : "none";
  var cur = document.querySelector("#stage .lidnote"); if(cur) cur.innerHTML = lidnoteHTML();
}
// The host pushes a full live snapshot every second; we render the splash's nerd-stat panel from it.
window.adomStats = function(s){
  try {
    rebootPending = !!s.rebootPending; onAc = !!s.ac;
    sleepLeft = (typeof s.sleepLeft === "number") ? s.sleepLeft : -1;
    deviceWord = s.device || "laptop";
    applyLid(); applyRebootNudge();
    var set = function(id,html){ var el=document.getElementById(id); if(el) el.innerHTML=html; };
    // duration formatter: 0 = Never, <0/absent = "?", else minutes (or m+s)
    var dur = function(x){ if(x==null||x<0) return "?"; if(x===0) return "Never"; if(x<60) return x+" sec"; if(x%60===0) return (x/60)+" min"; return Math.floor(x/60)+" min "+(x%60)+"s"; };
    // Two clean chips, MODE label first so the value can't read as the mode ("Plugged in: Never",
    // not "Never plugged in"); the mode you're in right now is highlighted and tagged.
    var chip = function(label,val,active){
      return "<span class='mc"+(active?" on":"")+"'><span class='mcl'>"+label+"</span>"
        + "<span class='mcv'>"+dur(val)+"</span>" + (active?"<span class='mcn'>now</span>":"") + "</span>";
    };
    var pair = function(ac,dc){
      return "<span class='mcwrap'>"+chip("Plugged in",ac,s.ac)+chip("On battery",dc,!s.ac)+"</span>";
    };
    set("st-power", s.ac ? "<b>Plugged in</b>" : ("On battery · <b>"+(s.pct<=100?s.pct+"%":"?")+"</b>"));
    set("st-screen", pair(s.displayOffAc, s.displayOffDc));
    set("st-sleep",  pair(s.sleepAc, s.sleepDc));
  } catch(e){}
};
function footText(s, i, n){
  // Power-aware: keep-awake actively holds the machine only when plugged in; on
  // battery it stays passive and Windows' own idle-sleep timer is the give-up clock.
  const onAc = /plug/i.test(status.power||"");
  const ka = status.power ? (onAc ? "  ·  keep-awake on" : "  ·  sleeps when idle") : "";
  const right = (status.power||"") + (status.off>=0 ? "  ·  display turns off in "+mmss(status.off) : "") + ka;
  const seen = "  ·  shown " + getCount(s.slug) + "×";   // lifetime view count (proves the deprioritizer is working)
  // the priority score that ordered this slide (higher = shown sooner), with its breakdown so it's transparent
  var fS = freshScore(s.updated), nS = noveltyScore(s.slug);
  const score = "  ·  priority " + Math.round(s._score != null ? s._score : (fS + nS)) + " (fresh " + fS + " + new " + nS + ")";
  const as = (me && me.name) ? "  ·  viewing as " + me.name : "";
  return s.type + "      " + (i+1) + " / " + n + seen + score + as + "      " + right;
}
function paintFoot(){
  const cur = document.querySelector("#stage .foot");
  if (cur && cur._slide) cur.textContent = footText(cur._slide, cur._i, slides.length);
}
// Drop a slide whose hero failed to load (e.g. a stale cached pointer that now 404s) so it
// never shows a broken icon.
function dropSlide(slug){ const i = slides.findIndex(s=>s.slug===slug); if(i>=0) slides.splice(i,1); }

function mediaEl(s){
  const src = /^https?:/i.test(s.media) ? s.media : HOST + encodeURIComponent(s.media);
  // SELF-HEAL: the manifest carries `s.src` = the remote wiki hero URL. If the LOCAL cached file is missing or
  // broken (pruned, never finished downloading, disk wipe), don't show a blank card -- swap to the remote URL
  // and stream the hero straight from the wiki. Only if BOTH fail do we drop the slide. (The next fetch also
  // re-downloads the missing file, so this is belt-and-suspenders: stream now, repopulate the cache later.)
  const remote = s.src && /^https?:/i.test(s.src) ? s.src : "";
  function fallback(el){ if (remote && el.src !== remote){ el.src = remote; return true; } return false; }
  // wiki.adom.inc/blob has Cloudflare HOTLINK PROTECTION: a cross-origin <img>/<video> request carrying a
  // Referer of this page's origin gets a 403 challenge (curl/.NET with no Referer get 200). So when we stream
  // a hero straight from the wiki (the self-heal path, or any un-cached remote media) we MUST send no Referer,
  // exactly like curl does -- otherwise the fallback silently fails and the card still goes blank.
  if (s.kind === "video"){
    const v = document.createElement("video"); v.referrerPolicy = "no-referrer";
    v.src = src; v.muted = true; v.defaultMuted = true; v.volume = 0; v.loop = true;
    v.autoplay = true; v.setAttribute("playsinline",""); v.preload = "auto";
    v.addEventListener("canplay", ()=>{ try{ v.play(); }catch(e){} });
    v.onerror = ()=>{ if (!fallback(v)) dropSlide(s.slug); };
    return v;
  }
  const img = document.createElement("img"); img.referrerPolicy = "no-referrer"; img.src = src; // <img> renders svg + raster
  img.onerror = ()=>{ if (!fallback(img)){ img.style.display="none"; dropSlide(s.slug); } };
  return img;
}
function buildSlide(s, i){
  const d = document.createElement("div"); d.className = "slide";
  const v1 = s.source === "v1";
  // Visibility badge (privacy QA + teaches the wiki's 3 audiences, confirmed in git-wiki auth.js):
  //   public                -> anyone
  //   private + an org      -> only that org's members
  //   private + no org      -> the author ALONE (personal-private)
  // With your token the saver sees private pages too, so this is the page's REAL audience.
  const vis = (s.vis || "public").toLowerCase();
  const pub = vis === "public";
  const orgPriv = !pub && !!(s.org && (""+s.org).length);
  const vb = document.createElement("div"); vb.className = "vbadge " + (pub ? "pub" : "priv");
  if (pub)
    vb.innerHTML = "Public<span class='sub'>· anyone can see &amp; install it</span>";
  else if (orgPriv)
    vb.innerHTML = "Private · " + esc(s.org) + " org<span class='sub'>· only members of the " + esc(s.org) + " org can see it</span>";
  else
    vb.innerHTML = "Private · personal<span class='sub'>· only " + esc(s.author || s.owner || "the owner") + " can see it (not shared with any org)</span>";
  // Freshness chip (from the page's last-updated date), grouped with the badge in one meta row so the
  // eye catches NEW content. Teal for recent, dim for old.
  var flabel = freshLabel(s.updated);
  const mr = document.createElement("div"); mr.className = "metarow";
  // Type capsule FIRST (App / Skill / Component) so the KIND of repo reads instantly, before visibility.
  var tp = (s.type || "").toLowerCase();
  if (tp === "app" || tp === "skill" || tp === "component"){
    const tc = document.createElement("div"); tc.className = "typecap t-" + tp;
    tc.textContent = tp.charAt(0).toUpperCase() + tp.slice(1);
    mr.appendChild(tc);
  }
  mr.appendChild(vb);
  if (flabel){ const fr = document.createElement("div"); fr.className = "freshchip " + freshClass(s.updated); fr.textContent = flabel; mr.appendChild(fr); }
  // Wiki identity block (replaces the old kicker): the owner-qualified URL + org/personal + author, so you
  // can verify the entry is posted right. kyle/x = personal; adom/x = upgraded to Adom-org-owned.
  var owner = s.owner || s.org || "";
  var orgOwned = !!(s.org && (""+s.org).length);
  const idb = document.createElement("div"); idb.className = "wikiid";
  idb.innerHTML =
    '<div class="wurl"><span class="wdim">wiki.adom.inc/</span>' +
      '<span class="' + (orgOwned ? "worg" : "wuser") + '">' + esc(owner) + '</span>' +
      '<span class="wdim">/</span>' + esc(s.slug) + '</div>' +
    '<div class="wmeta">' +
      (orgOwned ? ('<b class="worg">' + esc(s.org) + '</b> org-owned') : 'personal page') +
      (s.author ? (' &nbsp;·&nbsp; by ' + esc(s.author)) : '') + '</div>';
  const hb = document.createElement("div"); hb.className = "herobox"; hb.appendChild(mediaEl(s));
  const t = document.createElement("div"); t.className = "title"; t.textContent = s.title || s.slug;
  const b = document.createElement("div"); b.className = "brief"; b.textContent = s.brief || "";
  const lab = document.createElement("div"); lab.className = "label"; lab.textContent = "Just ask Claude";
  // Install method is a Claude prompt, never a command line. Use the author's prompt only if it's already
  // natural language; otherwise (empty, or a CLI line like "install x"/"adompkg ...") synthesize one.
  var raw = (s.prompt || "").trim();
  var natural = raw && !/^(install\s|adompkg\b|npm\b|sudo\b|cargo\b|pip\b|curl\b)/i.test(raw);
  // trim a trailing tagline (a "Name, then tagline" or "Name: tagline" title) so the prompt uses the clean name
  var name = (s.title || s.slug).split(/\s+[\u2014\u2013\u00b7:|]\s+/)[0].trim();
  var ip = natural ? raw : ("Install " + name);
  const pb = document.createElement("div"); pb.className = "promptbox"; pb.textContent = "“" + ip + "”";
  const f = document.createElement("div"); f.className = "foot"; f._slide = s; f._i = i; f.textContent = footText(s, i, slides.length);
  const ln = document.createElement("div"); ln.className = "lidnote";
  ln.innerHTML = lidnoteHTML();
  // Two columns: text on the left, the hero shot on the right (foot + lidnote stay absolute at the bottom).
  const colText = document.createElement("div"); colText.className = "coltext";
  colText.append(mr, idb, t, b, lab, pb);
  const colMedia = document.createElement("div"); colMedia.className = "colmedia";
  colMedia.appendChild(hb);
  d.append(colText, colMedia, f, ln);
  return d;
}
function showSlide(i){
  const stage = document.getElementById("stage");
  const s = slides[i]; if(!s) return;
  try { showPreviewWait(false); } catch(e){}   // a billboard is rendering now -> drop any preview wait screen
  // visibility-tinted background: PUBLIC -> teal wash, PRIVATE/org -> amber wash + frame
  var pub = ((s.vis || "public").toLowerCase() === "public");
  document.body.classList.toggle("viz-priv", !pub);
  document.body.classList.toggle("viz-pub", pub);
  bumpCount(s.slug);   // count this view so the entry is deprioritized next time

  // fade the stage OUT, swap content, fade IN, so text never overlaps mid-transition.
  stage.style.opacity = "0";
  setTimeout(()=>{
    stage.innerHTML = "";
    const el = buildSlide(s, i);
    stage.appendChild(el);
    const vid = el.querySelector("video"); if(vid){ try{ vid.currentTime=0; vid.play(); }catch(e){} }
    stage.style.opacity = "1";
  }, 460);
}
let introHidden = false;
function hideIntro(){
  if (location.hash.indexOf("holdintro") >= 0) return; // capture mode: keep the explanation splash up
  if (introHidden) return; introHidden = true;
  const el = document.getElementById("intro");
  // The 3s transition is the intro fade-IN; for the hand-off to billboards use a quick fade-OUT.
  el.style.transition = "opacity .7s ease";
  el.style.opacity = "0"; setTimeout(()=>{ el.style.display = "none"; }, 760);
}
// Reveal the faint full-bleed Adom wing behind the billboards (inline SVG, crisp at any scale).
// Start it at a RANDOM point along its existing 270s drift loop (and a random direction) so the wing
// sits in a different spot every launch and never feels repetitive. A negative animation-delay just
// fast-forwards into the SAME keyframe path, so it stays inside the full-bleed constraints already
// defined by bbmarkfloat, we only change WHERE on that path it begins, never the path itself.
function showMark(){
  var bbm = document.getElementById("bbmark");
  if (!bbm) return;
  bbm.style.animationDelay = (-Math.random() * 270).toFixed(2) + "s";   // random phase of the loop
  bbm.style.animationDirection = Math.random() < 0.5 ? "reverse" : "normal"; // same path, either way
  bbm.style.display = "block";
}
// --- per-entry show-count tracking so the rotation DEPRIORITIZES entries you've already seen a lot. A
// short saver session only ever shows the first chunk of the order, so a pure random shuffle kept
// surfacing the same handful; ordering by lifetime show-count instead leads with the least-seen each run.
// Counts come from the C# HOST (it writes showcounts.json synchronously on each show) rather than
// localStorage, which a short-lived, sometimes hard-killed screensaver can't flush reliably. We seed the
// in-memory map from that file at load, order by it, and report each show back so the host persists it.
var counts = {};
function getCount(slug){ return (slug && counts[slug]) ? counts[slug] : 0; }
function bumpCount(slug){ if(!slug) return; counts[slug] = (counts[slug]||0) + 1; try { window.chrome.webview.postMessage("shown:" + slug); } catch(e){} }
async function loadCounts(){ try { const r = await fetch(HOST + "showcounts.json?_=" + Date.now(), {cache:"no-store"}); counts = (await r.json()) || {}; } catch(e){} }
// --- Freshness (from the page's last-updated timestamp, carried in the manifest) ---
function ageDays(updated){
  if(!updated) return 1e9;
  var t = Date.parse(updated); if(isNaN(t)) return 1e9;
  return (Date.now() - t) / 86400000;
}
function freshLabel(updated){
  var d = ageDays(updated);
  if(d >= 1e9) return "";
  if(d < 1)   return "New today";
  if(d < 2)   return "Updated yesterday";
  if(d < 7)   return "Updated " + Math.round(d) + " days ago";
  if(d < 14)  return "Updated last week";
  if(d < 60)  return "Updated " + Math.max(2, Math.round(d/7)) + " weeks ago";
  if(d < 730) return "Updated " + Math.max(2, Math.round(d/30)) + " months ago";
  return "Updated " + Math.round(d/365) + (Math.round(d/365)===1?" year ago":" years ago");
}
function freshClass(updated){ var d = ageDays(updated); return d < 7 ? "fresh-new" : (d < 30 ? "fresh-recent" : "fresh-old"); }

// --- Scoring: lead with NEW + UNSEEN, sink OLD + over-seen. Documented in the wiki README. ---
//   freshness: <7d +100, <30d +60, <90d +35, <365d +15, older/unknown +5
//   novelty:   max(0, 60 - 20*timesShown)  (never-seen +60; gone after ~3 views)
//   jitter:    +0..12  so order varies run-to-run and ties never lock
function freshScore(updated){
  var d = ageDays(updated);
  if(d < 7) return 100; if(d < 30) return 60; if(d < 90) return 35; if(d < 365) return 15; return 5;
}
function noveltyScore(slug){ return Math.max(0, 60 - 20*(counts[slug]||0)); }
function slideScore(s){ return freshScore(s.updated) + noveltyScore(s.slug) + Math.random()*12; }
// Order the rotation by score, highest first, so new + never-seen drops lead and stale repeats fall back.
function shuffleSlides(){
  slides.forEach(function(s){ s._score = slideScore(s); });
  slides.sort(function(a,b){ return b._score - a._score; });
}
// The host calls this when the display-off time arrives: pause ALL video (releases Chromium's
// display wake-lock so the panel can finally power off) and blank to pure black so nothing burns in.
// The rotation stops (tick() bails on `resting`), so no new slide ever restarts a video.
var resting = false;
// Host calls this on a billboard dismiss (mouse move / key): fade the page to black in 200ms, then it
// hides the window. CSS opacity transition = smooth + fast (no host-window-over-WebView2 slowness).
window.adomFadeOut = function(){
  var f = document.getElementById("fadeout");
  if (!f) return;
  f.style.display = "block";
  void f.offsetWidth;            // force reflow so the .2s transition runs from opacity 0
  f.style.opacity = "1";
};
// The full-dim screensaver: the faint Adom logo + tagline drifting slowly around the screen, bouncing off
// the edges (so it covers the whole panel over time and can't burn in). Deliberately slow (~9 px/sec).
var dimDriftOn = false, dimRAF = 0, dimX = 0, dimY = 0, dimVX = 0, dimVY = 0, dimLast = 0;
function startDimDrift(){
  if (dimDriftOn) return; dimDriftOn = true;
  var el = document.getElementById("dimsaver"); if (!el) return;
  el.style.display = "block";
  var W = window.innerWidth, H = window.innerHeight, w = el.offsetWidth, h = el.offsetHeight;
  dimX = (W - w) / 2; dimY = (H - h) / 2;
  var SPEED = 9; // px/sec, deliberately slow ("really slowly")
  var ang = 0.55 + 0.6 * ((Date.now() % 997) / 997); // vary the heading run-to-run so it isn't identical
  dimVX = SPEED * Math.cos(ang); dimVY = SPEED * Math.sin(ang);
  if (Math.abs(dimVY) < 2) dimVY = (dimVY < 0 ? -2 : 2);   // guarantee real 2D travel, never near-horizontal
  dimLast = 0;
  var step = function(ts){
    if (!dimLast) dimLast = ts;
    var dt = Math.min(0.1, (ts - dimLast) / 1000); dimLast = ts;   // time-based so speed is fps-independent
    var W = window.innerWidth, H = window.innerHeight, w = el.offsetWidth, h = el.offsetHeight;
    dimX += dimVX * dt; dimY += dimVY * dt;
    if (dimX <= 0) { dimX = 0; dimVX = Math.abs(dimVX); } else if (dimX + w >= W) { dimX = W - w; dimVX = -Math.abs(dimVX); }
    if (dimY <= 0) { dimY = 0; dimVY = Math.abs(dimVY); } else if (dimY + h >= H) { dimY = H - h; dimVY = -Math.abs(dimVY); }
    el.style.transform = "translate(" + Math.round(dimX) + "px," + Math.round(dimY) + "px)";
    dimRAF = requestAnimationFrame(step);
  };
  dimRAF = requestAnimationFrame(step);
}
window.adomRest = function(dim){
  if (resting) return; resting = true;
  // Drop any PRIVATE-repo tint left over from the last billboard (the teal #bgwash wash + inset screen frame).
  // The rest/dim state is always the clean dark screensaver, so the frame must never linger behind the dimmed
  // Adom logo. Removing viz-priv fades #bgwash out over its .55s transition.
  try { document.body.classList.remove("viz-priv"); document.body.classList.add("viz-pub"); } catch(e){}
  try { var vs = document.querySelectorAll("video"); for (var i=0;i<vs.length;i++){ try{ vs[i].pause(); }catch(e){} } } catch(e){}  // pause video to save power
  if (dim){
    // PLUGGED IN: stop the billboards (they don't make sense dimmed) and show the barely-visible Adom logo
    // + tagline drifting slowly. The .scr also drops the hardware BACKLIGHT to ~0 (the real "nearly black"
    // on an LCD). Screen stays ON so the machine never dozes; the slow drift prevents burn-in.
    try { hideIntro(); } catch(e){}
    var st = document.getElementById("stage"); if (st) st.style.display = "none";
    var bm = document.getElementById("bbmark"); if (bm) bm.style.display = "none";
    document.documentElement.style.setProperty("--sdim", "0.04");
    startDimDrift();
  } else {
    // ON BATTERY: blank to black; the host then powers the panel off to save battery.
    var bb = document.getElementById("bbmark"); if (bb) bb.style.display = "none";
    var r = document.getElementById("restlayer"); if (r) r.style.display = "block";
  }
};
// Drew's ask #1, arrow-key navigation. The C# host's low-level keyboard hook swallows Left/Right
// and calls this (a normal keypress would instead dismiss the saver). dir = +1 next / -1 prev. We
// re-anchor `started` so the auto-rotation simply continues from the slide you landed on, and the
// chosen slide still gets a full dwell before it advances on its own.
window.adomNav = function(dir){
  if (resting || slides.length === 0) return;
  if (started === 0){ shuffleSlides(); started = Date.now(); idx = 0; hideIntro(); showMark(); } // first arrow also enters the rotation
  idx = ((idx + dir) % slides.length + slides.length) % slides.length;
  started = Date.now() - idx * PER;           // align the clock to the manual pick (full dwell, then auto-advance resumes)
  showSlide(idx);
};
// PREVIEW (config dialog "test a billboard" buttons): skip the logo + splash entirely and jump straight
// to a billboard. Drop the splash hold to 0 and hide the intro now; the first tick shows slide 0 the
// instant slides are loaded (the page bg stays plain-dark in the meantime, never the splash).
let previewMode = false, previewFilter = "";
window.adomPreview = function(filter){
  previewMode = true; INTRO_MS = 0; previewFilter = filter || "";
  // The page may have already loaded cached app/skill slides BEFORE this filter arrived. Strip them NOW so
  // a "test a component billboard" can never flash an app/skill while the real component is still loading.
  try { if (previewFilter) slides = slides.filter(previewKeep); } catch(e){}
  try { hideIntro(); } catch(e){}
  try { showPreviewWait(true); } catch(e){}
};
// A preview must never sit on a blank screen while it loads (a component's hero may take a few seconds to
// download). Tell the user what's happening; the host's live load status streams into the sub-line so it's
// visibly working, not stuck. Hidden the instant the first billboard renders (showSlide).
function previewBaseMsg(){
  if (previewFilter === "component") return "Fetching a <b>component</b> billboard from the Adom Wiki. The first one has to download its hero image, so this can take a few seconds&hellip;";
  if (previewFilter === "app")    return "Fetching an <b>app</b> billboard from the Adom Wiki&hellip;";
  if (previewFilter === "skill")  return "Fetching a <b>skill</b> billboard from the Adom Wiki&hellip;";
  if (previewFilter === "private") return "Fetching a <b>private</b> billboard from the Adom Wiki&hellip;";
  return "Loading a billboard from the Adom Wiki&hellip;";
}
function showPreviewWait(on){
  var el = document.getElementById("previewwait"); if (!el) return;
  if (on){ var t = document.getElementById("pwtext"); if (t) t.innerHTML = previewBaseMsg(); el.style.display = "flex"; }
  else el.style.display = "none";
}
// previewFilter narrows the rotation for the config "Test a ___ billboard" buttons:
//   "private" -> private/org pages; "app"/"skill"/"component" -> that wiki type; "" -> no filter.
function previewKeep(s){
  if (!previewFilter) return true;
  if (previewFilter === "private") return (s.vis || "public").toLowerCase() !== "public";
  return (s.type || "").toLowerCase() === previewFilter;
}
function tick(){
  if (resting) return;                        // display is resting, do nothing (no video, no repaint)
  if (!revealedAt) return;                    // wait until the WebView2 is actually on screen
  const now = Date.now();
  if (now - revealedAt < INTRO_MS) return;    // hold the WebView2 splash (0 in preview)
  if (slides.length === 0) return;            // not ready yet (preview: plain-dark page bg until slides land)
  if (started === 0){
    // preview mode: only EVER show the requested kind. If none of that kind has loaded yet, keep the wait
    // screen up and return -- NEVER fall back to showing a different kind (that was the "component -> app" bug).
    if (previewFilter){
      slides = slides.filter(previewKeep);
      if (!slides.length){ showPreviewWait(true); return; }
    }
    shuffleSlides(); started = now; idx = 0; showSlide(0); hideIntro(); showMark(); return;
  }
  const want = Math.floor((now - started)/PER) % slides.length;
  if (want !== idx){ idx = want; showSlide(idx); hideIntro(); }
}

async function loadManifest(){
  try{
    const r = await fetch(HOST + "manifest.json?_=" + Date.now(), {cache:"no-store"});
    const j = await r.json();
    if (j && typeof j.dimPct === "number" && !dimSet){ document.documentElement.style.setProperty("--sdim", (j.dimPct/100).toFixed(2)); dimSet = true; }
    if (j){ if (j.me) me = j.me; authed = !!j.authed; applyMe(); }
    let incoming = j && j.slides ? j.slides : [];
    if (location.hash.indexOf("onlyvideo") >= 0) incoming = incoming.filter(s=>s.kind==="video"); // verification mode
    // preview: only let the requested kind into the rotation (private, or a wiki type)
    if (previewFilter) incoming = incoming.filter(previewKeep);
    // Append NEW slugs, and REFRESH IN PLACE any slug whose hero/content changed since we loaded it.
    // Why in-place refresh matters: at launch the page instantly shows last run's CACHED slides, then the
    // fresh wiki fetch downloads updated heroes into the manifest. Without this, a slide already on screen
    // kept its OLD hero until the NEXT launch (the one-launch lag that made a hero updated "this morning"
    // not appear). Now an updated hero shows within ~3s, same session, WITHOUT reordering the rotation.
    const idxBySlug = {}; slides.forEach((s,i)=>{ idxBySlug[s.slug]=i; });
    incoming.forEach(s=>{
      const at = idxBySlug[s.slug];
      if (at === undefined){ idxBySlug[s.slug]=slides.length; slides.push(s); }       // new -> append
      else {
        const cur = slides[at];
        if (cur && (cur.media !== s.media || cur.updated !== s.updated)){             // changed -> refresh
          slides[at] = s;
          if (at === idx) showSlide(at);   // it's the card on screen right now -> repaint with the new hero
        }
      }
    });
    // and harden: strip anything that slipped in earlier, so the rotation can NEVER drift off the filter
    if (previewFilter) slides = slides.filter(previewKeep);
  }catch(e){}
}

(async function(){
  await loadCounts();               // seed show-counts from the host's file BEFORE the first ordering
  await loadManifest();
  setInterval(loadManifest, 3000); // pick up slides as the host downloads more
  setInterval(tick, 200);
  setTimeout(function(){ window.adomRevealed(); }, 9000); // safety: never hang on the fallback splash
  // Tell the host about input so it can run the adaptive wake (it fades the window out + exits).
  const wake = ()=>{ try{ window.chrome.webview.postMessage("input"); }catch(e){} };
  let armed = false; setTimeout(()=>{ armed = true; }, 1200); // grace so launch doesn't self-dismiss
  // Arrow keys NAVIGATE the billboards (and jump straight in from the splash) and NEVER dismiss.
  // Normally the C# low-level hook swallows arrows before they reach the page; this is the backup
  // path (and what makes arrows work even if the hook isn't active).
  window.addEventListener("keydown", function(e){
    if (e.key === "ArrowRight" || e.key === "ArrowLeft"){
      if (window.adomNav) window.adomNav(e.key === "ArrowRight" ? 1 : -1);
      return; // never wake/dismiss on arrows
    }
    if (armed) wake();
  });
  ["mousemove","mousedown","wheel","touchstart"].forEach(ev=>
    window.addEventListener(ev, ()=>{ if(armed) wake(); }, {passive:true}));
})();
</script>
</body>
</html>