← Commit history

0.7.0: tabbed config (Cascade/Power/Preview/About) + Modern Standby readout, App/Skill/Component capsules + opt-in components with curated 3D-render heroes, offline resilience + burn-in backstops, fast logo dismiss, preview wait screen. README + screenshots refreshed.

John Lauer ·08f2e48581 ·4mo ago ·parent 08a3d9b
9 files changed +1590−331
README.md+164−150
@@ -1,131 +1,28 @@-# Adom Screensaver+# adom-screensaver, the vibe-coding screensaver -**Turn an idle laptop into an always-on vibe-coding machine.** It keeps **Claude** and your **`/remote-control`** sessions alive while you step away, plays a live billboard of the latest Adom Wiki drops as your screensaver, then dims the screen to near-black so the panel never burns in. It manages Windows' power settings honestly and explains every change it makes, and it adapts to *your* hardware: laptop vs desktop, Modern Standby or not, controllable backlight or not.+> πŸͺŸ **Windows only.** This runs on your **Windows laptop**, not a Linux/WSL2 container. -No PowerToys. No always-on "caffeine" hacks. One signed Windows screensaver plus a tiny native keep-awake helper.+Turn your Windows laptop into an always-on home for your Claude Code threads. A branded Adom+screensaver that rests your screen, **keeps your machine awake** (network + Remote Control stay+alive), and, while it rests, runs a **live billboard of the latest Adom Wiki drops**, rendered in a+real Edge/Chromium engine so it plays **video (silently)** and renders **SVG**, not just static images. ----+Launch it any time with **Win + Shift + L**. -## Why a screensaver has to be this smart now+![Adom Screensaver, the branded splash that explains what it's doing while it rests your screen and keeps your machine awake](/blob/app/adom-screensaver/screenshots/hero.png) -Windows' power defaults were designed for a world where an idle laptop should sleep as fast as possible to save battery. That was the right call for a decade. It is exactly wrong for how people work with AI today.+Built for the people who fire up their Claude threads before they head to dinner and need them+still running when they get back. -When you're vibe-coding with Claude and Adom Hydrogen Desktop, you *want* the machine to keep chugging while you walk away: long agent runs that actually finish, `/remote-control` threads that don't drop, screenshots and shotlogs being built for you to watch from your phone, your laptop quietly becoming a **vibe-coding server**. None of that survives on a machine that Microsoft put to sleep 60 seconds after you stood up.+## New in 0.7.0 -So this app deliberately runs your machine **differently than Windows expects, on purpose**, and it tells you exactly how. The whole job is a balance: stay awake and reachable for the AI, but never cook the battery in a bag and never burn a static image into the panel overnight.+- **Tabbed preferences.** The settings window is now organized into **Cascade / Power / Preview / About** tabs, with a live **Modern Standby** readout on the Power tab that explains why your machine light-sleeps when the screen turns off (and why the lid-close recommendation is what it is).+- **More to show, clearly labeled.** Every billboard now wears an **App / Skill / Component** capsule, and you can optionally rotate in **component** pages (eval boards, modules, parts) that have a real 3D render.+- **Tougher when things go sideways.** Survives a **no-wifi** launch (plays your cached drops, never burns in), snaps out instantly when you dismiss the opening logo, and shows an on-screen status whenever something is still loading. ----+![The tabbed preferences window](/blob/app/adom-screensaver/screenshots/config-tabs.png) -## What you see--It plays as a timed cascade. Every stage is configurable (see the cascade settings below).--### 1. The opening-It opens quietly with the ADOM wordmark, dimmed on a near-black screen, holding for a beat while the live billboard loads behind it, then crossfading into the splash.--![Adom Screensaver opening, the dim ADOM wordmark on near-black](/blob/app/adom-screensaver/screenshots/opening.png)--### 2. The splash-The branded splash: who you're signed in as on the Wiki, your power state, and a plain-language summary of how the machine is being kept alive while you're gone.--![Adom Screensaver splash with live machine stats](/blob/app/adom-screensaver/screenshots/splash.png)--### 3. The billboards-Then it becomes a billboard for the latest **Adom Wiki** drops: real apps, skills, and tools, each with a screenshot, who can see it, and the one-line "just ask Claude" prompt to install it. The slides are cached to disk so they play instantly offline, least-recently-seen first so it never loops the same few.--![Adom Wiki billboard, Basic Parts Library](/blob/app/adom-screensaver/screenshots/billboard-1.png)--![Adom Wiki billboard, service-kicad](/blob/app/adom-screensaver/screenshots/billboard-2.png)--![Adom Wiki billboard, adom-ui-linter](/blob/app/adom-screensaver/screenshots/billboard-3.png)--### 4. The dim screensaver-After a few minutes the billboards stop (rotating ads make no sense once the screen is dimmed) and it shows just the Adom logo and "AI Electronics Prototyping" drifting slowly, barely visible, on a near-black panel. The slow drift means it can never burn in, and the machine stays fully awake the whole time.--![Adom Screensaver dim mode, the logo drifting on near-black](/blob/app/adom-screensaver/screenshots/dim.png)--Each billboard always shows its **wiki URL** (`wiki.adom.inc/owner/slug`) and a **freshness badge** ("New today", "Updated 3 days ago", and so on), so you instantly know whether you're looking at something new or something that's been around.--### Public vs private, at a glance--Your view includes both **public** pages and your **private / org-only** pages (the internal `-adom` config pages and the closed-source `-dev` repos behind your published apps). So you never confuse the two, a private page gets an **Adom-teal background and frame**; public pages play on the normal dark backdrop.--![A private, org-only billboard with the Adom-teal background and frame](/blob/app/adom-screensaver/screenshots/private-billboard.png)-------## How it decides what to show you--Old screensavers loop the same handful of images until you stop seeing them. This one **scores every Wiki drop** and leads with what's **new** and what you **haven't seen**, so the rotation stays interesting instead of replaying stale cards.--Each slide gets a score, and the highest scores play first:--- **Freshness** (from the page's last-updated date): updated in the last **week** = +100, last **month** = +60, last **90 days** = +35, last **year** = +15, older = +5.-- **Novelty** (how many times you've already seen it): never seen = +60, dropping by 20 per view, gone after about three views.-- **A little randomness** (+0 to +12) so the order isn't identical every run and nothing gets stuck at the top or bottom.--So a brand-new drop you've never seen floats to the very top (and wears a teal **"New today"** badge), while something old you've watched ten times sinks to the bottom. Every billboard even prints **its own priority score and the breakdown** in the footer (`priority 142 (fresh 100 + new 42)`), so the ranking is fully transparent.--Freshness comes from a **lightweight** check of each page's last-updated timestamp, so new content surfaces within minutes. Hero images are cached to disk **keyed on that timestamp**, so an updated hero is re-fetched the next time the page changes, while unchanged heroes are served straight from cache with no pointless re-downloading.-------## The clever part: honest, hardware-aware power management--A billboard is the easy half. The real work is keeping your machine reachable without wrecking the battery or the panel, and doing it correctly on whatever hardware you have. The app probes your machine and adapts.--### Keeping `/remote-control` alive-While you're plugged in, a tiny signed native helper holds the machine awake (the same `SetThreadExecutionState` keep-awake Windows itself uses, no third-party tool) so long agent runs and `/remote-control` threads keep running and the network stays up. On battery it goes passive so it doesn't drain you flat.--### Modern Standby, and why turning the display *off* is dangerous-Here's the lesson that shaped the whole app. On a **Modern Standby** laptop, the moment the display turns **off**, Windows treats that as a cue to drop into light sleep: it **cuts the network and throttles the CPU**, and your `/remote-control` session quietly dies. So instead of letting the display power off, the screensaver keeps the screen **on and dims it to near-black**. The machine stays awake, your sessions survive, and the dim plus slow drift still prevent burn-in.--### Laptop vs desktop detection-The app detects whether you're on a **laptop** (has a battery and a lid) or a **desktop PC**, and reshapes everything accordingly. A desktop has no lid and no battery, and almost never uses Modern Standby, so display-off is safe there and the advice is completely different. The settings dialog drops the rows that don't apply and rewrites the rest for your machine.--### Backlight-dimming hardware detection-"Dim to near-black" only looks truly black if the app can drive your **hardware backlight** down (via the `WmiMonitorBrightnessMethods` WMI API). Not every panel exposes that. So the app actually **tests your hardware** and branches:--- **Backlight is controllable** (most laptop panels): it drops the real backlight to near-zero, so the dim screen is genuinely black, not a washed-out grey.-- **Backlight is not controllable** (many desktop monitors over HDMI): it falls back. On a Modern Standby machine it keeps the display on and dims the picture in software; on a machine with no doze risk it simply lets the monitor power off on a timer, since that's safe and easier on the panel than a bright billboard all night.--You can run that hardware test yourself, right from the settings dialog.--### Everything is in the settings, tuned to your machine-The settings dialog reads your **live** Windows power settings every time it opens and recommends a profile that's correct for *your* hardware. On a machine with no instant-resume sleep, for example, it recommends never idle-sleeping on battery (so unplugging and walking across the house wakes instantly) instead of the usual short sleep timer.--![Adom Screensaver settings, hardware-aware recommendations](/blob/app/adom-screensaver/screenshots/config-recommendations.png)--The full cascade is yours to tune: idle-before-start, how long the logo holds, how long the splash holds, seconds per billboard, and when it dims. There's also a one-click backlight test and live previews ("test a billboard" and "see the dimmed screen") so you can see exactly how it'll look.--![Adom Screensaver settings, cascade timing and previews](/blob/app/adom-screensaver/screenshots/config-cascade.png)--### Full transparency: it shows you what it changed-Because it adjusts how Windows runs, it never does so silently. A status screen lists **every** system and power setting the screensaver touched, the current state of each, the active power plan, and a one-click way to re-apply (if a vendor profile like Armoury Crate reverted something) or undo everything back to Windows defaults.--![Adom Screensaver status and screen-locks screen](/blob/app/adom-screensaver/screenshots/status.png)-------## When an app is holding your display on--Sometimes the screensaver genuinely *can't* start, because another app is forcing the display to stay on: a screen-share or video call, a recording, a fullscreen video, or an **Adom screen-capture session** (`adom-cli hydrogen sharing` / `getDisplayMedia`, used for screenshots). Windows will not run a screensaver while a display lock is held, so the panel would stay lit all night.--The helper watches for exactly this. It lives in your **system tray** and shows two states:--![Adom Screensaver tray icon, normal and warning states](/blob/app/adom-screensaver/screenshots/tray-icons.png)--When it detects a display lock while you're away, the icon turns amber and a soft, no-sound flyout names the culprit and tells you how to release it:--![Adom Screensaver screen-hold notification](/blob/app/adom-screensaver/screenshots/toast.png)--Right-click the tray icon any time for status, settings, or to launch the screensaver on demand:--![Adom Screensaver tray right-click menu](/blob/app/adom-screensaver/screenshots/tray-menu.png)--> **Adom tip:** if you take a Hydrogen screenshot and the saver later won't start, an `adom-cli hydrogen sharing` session is probably still holding the display. Run `adom-cli hydrogen sharing stop` to release it.-----+![A component billboard with a real 3D render, not a schematic symbol](/blob/app/adom-screensaver/screenshots/billboard-component.png)  ## Install @@ -133,39 +30,156 @@ Right-click the tray icon any time for status, settings, or to launch the screen adompkg install adom-screensaver ``` -This is a **Windows** app. Installing here sets up the skills and stages the deploy; the screensaver itself installs onto your Windows laptop (not the Linux/WSL container). Then just ask Claude to set it up.--## Talk to Claude+**What that does:** on your Adom container (Linux/WSL2) it installs the **skills** and stages the+**prebuilt, signed** Windows binaries, it does **not** run anything there. The screensaver is a+Windows app. To actually put it on your machine, then ask Claude: -Open / launch:-- *"show me the Adom screensaver"*-- *"launch the screensaver"* (or press **Win + Shift + L**)--Set up / configure:-- *"set up the Adom vibe-coding server screensaver on my laptop"*-- *"keep my laptop awake without PowerToys"*-- *"make my screen turn off but keep the machine awake"*-- *"change the screensaver idle timeout to 1 minute"*+```+set up the Adom screensaver on my Windows laptop+``` -Troubleshoot:-- *"why won't my screensaver start"* (it'll check for a display lock)-- *"turn off the Adom screensaver"*+Claude deploys the **prebuilt, code-signed** binaries to your laptop over the `adom-desktop` relay,+wires keep-awake + autostart, registers it as your screensaver, and sets the display-off timing, no+compiler needed, and Windows Defender won't quarantine it (it's signed by "Adom Industries, Inc.").++## What it does++- **Keep-awake, no PowerToys.** A ~20-line helper (`adom-stayawake.exe`) holds+  `SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)` on a permanently-alive thread, so+  the system stays awake (network + Remote Control survive) while the screen is free to turn off.+- **Opens with a gorgeous splash, then the billboards.** A dark cover lifts over ~800ms to reveal+  a power-aware **nerd-stats panel**, live rows for *Power* (plugged / on battery %), *Screen*+  (powers-off countdown), *Sleep* (never while plugged, or the on-battery countdown), *Keep-awake*+  (`ES_SYSTEM_REQUIRED` held β†’ NIC up, `/remote-control` reachable), and *Billboards* (how many are+  loaded + cache size, climbing live), plus the animated "fetching the latest drops" counter.+- **A live Wiki billboard (WebView2).** It then cross-fades through recent apps/skills from the+  Adom Wiki, each with its hero, title, brief, and a **"just ask Claude"** prompt. Because it's a+  real browser tile it **plays webm video muted + looped** and renders **SVG**. Hero images/SVGs are+  **truly cached to disk** so they paint instantly and survive offline (video streams, too large to+  cache). Order is **re-shuffled every run**. Footer shows power + the display-off countdown.+- **Rests the panel, no burn-in.** When your display-off time arrives, the saver **pauses all+  video** (releasing the browser's display wake-lock that would otherwise keep the screen lit),+  **blanks to pure black**, and **powers the panel off**, while keep-awake holds the machine ++  network alive. It does *not* keep cycling bright billboards on a panel that's meant to be off.+  A 60-second heartbeat to `wv.log` records exactly what it did overnight.+- **Both wikis, with a migration nudge.** It pulls **Wiki v2** (canonical) *and* **Wiki v1**+  (legacy). If a page is on both, it shows the v2 version; v1-only cards carry an *"On Wiki v1 Β·+  nudge the team to migrate to v2"* kicker, so the screensaver doubles as a gentle prod to+  finish the v1 β†’ v2 migration.+- **Honest dimming.** On laptops where software can't dim the backlight (e.g. ASUS ROG dGPU/MUX),+  it doesn't pretend to, the on-screen dim is cosmetic (the billboard rests at ~70% brightness); the **real backlight-off is+  Windows' display-off timeout**, which genuinely powers the LEDs down with a clean keypress wake.+- **Instant wake.** Move the mouse and the billboards are gone **immediately**, the window hides+  and the browser tears down in the background (fading a layered window over a live WebView2/video is+  slow and would lag the dismiss, so it doesn't). The lightweight opening phase still fades out+  gracefully. **Never plays audio.**+- **Branded fade-to-black open.** The screensaver fades up from your desktop with a small dark-grey+  Adom mark centered, which then fades itself to black before the splash takes over.+- **Launch on demand, Win + Shift + L.** A global hotkey starts the screensaver instantly (the+  manual counterpart to the idle timer, the way `Win + L` is for lock). Contributed by Drew as+  `adom-screensaver-hotkey` and folded in as a first-class feature, toggleable from the+  preferences screen if it ever collides with another shortcut.+- **Lid-close rides through; no needless lock.** Closing the lid no longer slams the machine to+  sleep, on AC it does nothing, on battery Windows' own idle timer (e.g. 30 min) is the give-up+  clock, so a quick close-and-carry doesn't kill your threads. And because the screensaver only+  *rests the screen* (it doesn't sleep), it **doesn't throw a lock screen**; a real sleep-then-wake+  still asks for sign-in as usual.+- **Public-only, curatable rotation.** Only **public** wiki pages are advertised, explicitly+  org-private/internal pages are filtered out, and you can drop anything else (legacy v1 pages have+  no visibility field) by adding its slug to `exclude.txt`, no rebuild.+- **Signed.** Both binaries are code-signed with Adom's Azure Trusted Signing cert ("Adom+  Industries, Inc.", the same one HD and AD use), so Windows Defender doesn't quarantine the `.scr`.+- **Driveable by other apps.** Hydrogen Desktop / adom-desktop can ask it to get off the screen+  via a named event, a flag file, or a localhost control port (see the `adom-screensaver-wake` skill).++## Screenshots++### The system-tray app+A tiny always-on helper, **adom-stayawake.exe**, lives in your system tray. Right-click it for everything: the status / screen-locks window, the preferences, and a one-click "show the screensaver now" (with its **Win+Shift+L** shortcut spelled out). It also flips to a warning icon if anything is stopping your display from turning off.++![The Adom Screensaver system-tray menu, dark native Win32 menu](/blob/app/adom-screensaver/screenshots/systray-menu.png)++### The status / screen-locks window+The diagnostics screen. It runs with admin rights so it can read `powercfg /requests` and show, **live**, exactly what (if anything) is holding your display on, so the screensaver and display-off can't happen. Healthy state is green; if a screen-share, recording, or capture is pinning the display awake, it names the culprit and tells you how to stop it. Auto-refreshes every 10 seconds.++![The Adom Screensaver status window showing keep-awake state and display-lock holders](/blob/app/adom-screensaver/screenshots/status-window.png)++### The preferences window+Reachable from Windows' Screen Saver **"Settings…"** button (or the tray), it reads your *actual* Windows power settings (lid, sleep, turn-the-display-off, lock) and shows them against the recommended "vibe-coding" profile, with one-click **Apply** for anything that's drifted. Fine-tune the idle timeout, how long the billboards play before the display turns off, and a never-turn-off (kiosk) option.++![Adom Screensaver preferences window, live power-settings drift table](/blob/app/adom-screensaver/screenshots/preferences-window.png)++### The splash, your machine at a glance+When the screensaver starts it briefly shows a branded splash with live nerd-stats: power source, when the display turns off, sleep behavior, the native keep-awake holding `ES_SYSTEM_REQUIRED`, and how many billboards have loaded from the Wiki. Then it hands off to the billboards.++![Adom Screensaver splash with live power and keep-awake nerd stats](/blob/app/adom-screensaver/screenshots/splash.png)++### Live billboard slides, advertising the latest Wiki drops+While the screensaver is up it cycles recent Adom Wiki apps and skills, each with its hero, title, and a **"just ask Claude"** prompt, over a live power + display-off-countdown footer. The order reshuffles every run so it always feels fresh, and the faint Adom mark drifts behind from a random spot each time.++![Billboard slide for Chipfit, a 3D chip to KiCad footprint validator](/blob/app/adom-screensaver/screenshots/billboard-chipfit.png)++![Billboard slide for Container Conduit](/blob/app/adom-screensaver/screenshots/billboard-container-conduit.png)++![Billboard slide for the MOSFET IV Characterization Visualizer](/blob/app/adom-screensaver/screenshots/billboard-mosfet.png)++![Billboard slide for the Adom Safety App](/blob/app/adom-screensaver/screenshots/billboard-safety-app.png)++## Dependencies++| Dependency | Required? | Why |+|---|---|---|+| `adom-desktop` relay | Yes (runtime) | The cloud container drives your Windows laptop through it, pushing files, compiling, shipping the WebView2 SDK, configuring power/registry. Ships with your Adom desktop bridge. |+| WebView2 Runtime | Yes (on the laptop) | Renders the billboard (video + SVG). Ships with Windows 11 / Edge. |+| .NET Framework (`csc.exe`) | Yes (on the laptop) | Ships with every Windows. Compiles the screensaver + keep-awake helper natively. |+| PowerToys | **No** | Explicitly replaced, native keep-awake without it. |++No `adompkg` package dependencies.  ## Features -- **Live Adom Wiki billboard** with disk-cached slides, silent webm playback, and least-seen-first rotation.-- **Native keep-awake**, signed, no PowerToys or third-party caffeine tools.-- **Hardware-aware**: laptop vs desktop, Modern Standby detection, and a real backlight-control test that picks the right dimming strategy.-- **Near-black dim** via the hardware backlight (with a software fallback), plus a slow-drifting logo so it never burns in.-- **`/remote-control`-safe**: never turns the display off on a Modern Standby machine, because that would doze the box and drop your sessions.-- **Fully transparent**: a status screen shows every power setting it changed, with re-apply and one-click undo.-- **Watchdog tray helper** that catches anything holding your display on and names it.-- **A tunable cascade**: idle, logo, splash, per-billboard, and dim timers, all configurable, with live previews.--## What it touches (and how to undo it)--It only adjusts the handful of Windows power settings needed to keep the machine reachable: sleep-after, display-off, hibernate-after, and lid action (tuned per power source and per your hardware), plus registering itself as your screensaver. The status screen's **Undo** button returns all of them to Windows defaults. Everything is reversible.-------*Windows only. Built by Adom. The same Wiki heroes this screensaver shows are the ones [adom-wiki-hero](/adom/adom-wiki-hero) renders, so your app's billboard can run here too.*+- WebView2 billboard of recent Wiki v2 + v1 drops; **webm video (muted/looped)** + **SVG** + raster, all streamed.+- Native keep-awake via `SetThreadExecutionState` (PowerToys-free), autostarting at login.+- Real backlight-off via Windows display-off timeout (true LED power-down) while staying awake.+- Power-state + live display-off countdown footer; v1 "migrate to v2" kicker; randomized order; ~70%-brightness cosmetic dim.+- Adaptive wake (instant on-screen dissolve / panel warm-up when off); no lock unless the machine truly slept.+- **Win + Shift + L** launch hotkey (Drew's contribution), toggleable from preferences; lid-close rides through instead of sleeping.+- Runtime curation via `exclude.txt`; wake IPC (named event + flag + localhost control port) for other Adom apps.+- Honest by design: cosmetic dim, real display-off, never audio.++## Talk to the AI++**Set up / deploy**+- "set up the Adom screensaver on my laptop"+- "install the keep-awake helper and replace PowerToys"+- "turn off the Win+Shift+L screensaver hotkey" (if it collides with another app)++**Configure**+- "change the screensaver idle timeout to 2 minutes"+- "set the display to turn off after 3 minutes"+- "stop showing <slug> in the billboard"++**Run / troubleshoot**+- "trigger the Adom screensaver"+- "is the screensaver running right now?"+- "the billboard is blank" (WebView2 runtime / DLLs)++## Skills++| Skill | Purpose |+|---|---|+| `adom-screensaver` | Deploy, configure, run, troubleshoot via the `adom-desktop` relay. |+| `adom-screensaver-billboard` | Dual-wiki streaming discovery, video/SVG, `exclude.txt` curation, the manifest. |+| `adom-screensaver-wake` | Wake IPC for HD/AD/any app that needs the screen. |+| `adom-screensaver-build` | Compile + ship the WebView2 `.scr` (+ SDK DLLs) and verify renders. |+| `adom-screensaver-rationale` | The *why*, origin story, the staged cascade, and the design reasoning behind every behavior. |++The main skill is [SKILL.md](SKILL.md); the sub-skills live in [skills/](skills/).++## The honest hardware story++Built on a real ASUS ROG Zephyrus, where software can't touch the backlight (`WmiSetBrightness`+ignored on the dGPU/MUX path; NVIDIA blocks `SetDeviceGammaRamp`). So the app doesn't fake it: the+cosmetic dim is a CSS brightness veil (~70%) over the billboard, and the only real software backlight-off is+letting Windows turn the display off, which this app sets up, with keep-awake holding the system+so the network never drops.
dist/win/billboard.html+549−67
@@ -11,52 +11,175 @@   * { 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. */-  #stage { position:fixed; inset:0; background:var(--bg); filter:brightness(var(--dim,0.5));-    opacity:1; transition:opacity .45s ease; }-  .slide { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; }-  .kicker { margin-top:5.2vh; font-size:1.55vh; font-weight:600; letter-spacing:.55em;-    text-transform:uppercase; color:var(--teal); text-align:center; }-  .kicker.v1 { color:var(--lo); letter-spacing:.42em; }-  .herobox { margin-top:1.2vh; width:58vw; height:40vh; display:flex; align-items:center; justify-content:center; }-  .herobox > * { max-width:100%; max-height:100%; object-fit:contain;-    border:1.5px solid rgba(0,184,176,.35); border-radius:6px; background:#000; }-  .title { margin-top:2.4vh; font-size:3.6vh; font-weight:600; color:var(--hi); text-align:center; }-  .brief { margin-top:.6vh; max-width:62vw; font-size:1.7vh; line-height:1.4; color:var(--lo); text-align:center; }-  .label { margin-top:2.4vh; font-size:1.25vh; font-weight:600; letter-spacing:.45em;+  /* 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); }-  .promptbox { margin-top:.9vh; padding:1vh 1.6vw; border:1.4px solid rgba(0,184,176,.5);-    border-radius:10px; background:rgba(0,184,176,.16); color:var(--tealbri); font-size:1.9vh; max-width:72vw;-    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }-  .foot { margin-top:2vh; font-size:1.45vh; color:var(--lo); text-align:center; }-  /* Intro splash β€” a LEFT-ALIGNED editorial layout (logo β†’ eyebrow β†’ headline β†’ sub β†’ live status,+  /* 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+     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-eyebrow { margin-top:3vh; color:var(--teal); letter-spacing:.44em; font-size:1.2vh;+  .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. */+  /* 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; }@@ -68,6 +191,29 @@   .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; }@@ -96,16 +242,19 @@   <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 Claude Code threads under <b>/remote-control</b> keep running β€” here's exactly what your machine is doing right now:</p>+      <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">Power</span><span class="stat-v" id="st-power">β€”</span></div>-        <div class="stat"><span class="stat-k">Screen</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"><span class="stat-k">Keep-awake</span><span class="stat-v" id="st-awake">β€”</span></div>-        <div class="stat"><span class="stat-k">Billboards</span><span class="stat-v" id="st-cache">loading from the Adom Wiki…</span></div>+        <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>@@ -116,18 +265,75 @@       <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>+      <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/";-const HOLD = 4200, FADE = 650, PER = HOLD + FADE; // ms per slide-const INTRO_MS = 15000; // hold the gorgeous WebView2 splash this long after it appears, THEN billboards+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:@@ -150,7 +356,7 @@ function startIntroFade(){ // 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+// 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;@@ -163,26 +369,57 @@ window.adomStatus = function(power, off){ status.power = power || ""; status.off // 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", s.off>0 ? ("powers off in <span class='mono'>"+mmss(s.off)+"</span> to rest the panel")-                  : (s.off===0 ? "resting now β€” panel powered off" : "powers off shortly to rest the panel"));-    if (s.ac) set("st-sleep", "<b>Never</b> while plugged in β€” keep vibe-coding as long as you like");-    else if (s.sleepLeft>0) set("st-sleep", "in <span class='mono'>"+mmss(s.sleepLeft)+"</span> Β· Windows idle-sleep (on battery)");-    else if (s.sleepLeft===0) set("st-sleep", "idle limit reached β€” sleeping");-    else set("st-sleep", "won't idle-sleep on battery");-    if (s.ka==="off") set("st-awake", "helper isn't running");-    else if (s.ac) set("st-awake", "holding <span class='mono'>ES_SYSTEM_REQUIRED</span> β€” the machine never idles, so the NIC stays up and <b>/remote-control</b> stays reachable");-    else set("st-awake", "passive on battery β€” NIC stays up while awake; Windows' idle-sleep timer is the give-up clock");-    // Report what's ACTUALLY in the rotation (cached + freshly fetched), not just the host's-    // fresh-fetch counter β€” otherwise it reads "0 loaded" while cached billboards are already showing.-    set("st-cache", "<b>"+slides.length+"</b> loaded Β· <span class='mono'>"+fmtBytes(s.cacheBytes)+"</span> on disk");+    set("st-screen", pair(s.displayOffAc, s.displayOffDc));+    set("st-sleep",  pair(s.sleepAc, s.sleepDc));   } catch(e){} }; function footText(s, i, n){@@ -190,9 +427,13 @@ function footText(s, i, n){   // 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 ? "  Β·  screen powers off in "+mmss(status.off) : "") + ka;-  const by = s.author ? "  Β·  by "+s.author : "";-  return s.type + by + "      " + (i+1) + " / " + n + "      " + right;+  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");@@ -219,21 +460,80 @@ function mediaEl(s){ function buildSlide(s, i){   const d = document.createElement("div"); d.className = "slide";   const v1 = s.source === "v1";-  const k = document.createElement("div"); k.className = "kicker" + (v1?" v1":"");-  k.textContent = v1 ? "On Wiki v1 Β· nudge the team to migrate to v2" : "New on the Adom Wiki";+  // 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";-  const pb = document.createElement("div"); pb.className = "promptbox"; pb.textContent = "β€Ί  " + (s.prompt || ("install "+s.slug));+  // 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);-  d.append(k, hb, t, b, lab, pb, f);+  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;-  // fade the stage OUT, swap content, fade IN β€” so text never overlaps mid-transition.+  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 = "";@@ -252,27 +552,179 @@ function hideIntro(){   el.style.transition = "opacity .7s ease";   el.style.opacity = "0"; setTimeout(()=>{ el.style.display = "none"; }, 760); }-// Fisher-Yates shuffle so the rotation order is randomized EACH run (the cached manifest otherwise-// preserves last run's order). New slides that stream in afterwards just append to the end.+// 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(){-  for (var i = slides.length - 1; i > 0; i--){ var j = Math.floor(Math.random()*(i+1)); var t = slides[i]; slides[i] = slides[j]; slides[j] = t; }+  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;-window.adomRest = function(){+// 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;-  try { var vs = document.querySelectorAll("video"); for (var i=0;i<vs.length;i++){ try{ vs[i].pause(); }catch(e){} } } catch(e){}-  var r = document.getElementById("restlayer"); if (r) r.style.display = "block";+  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 (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 its full ~5s-  if (slides.length === 0) return;            // not ready yet β€” keep the splash up until slides land-  if (started === 0){ shuffleSlides(); started = now; idx = 0; showSlide(0); hideIntro(); return; }+  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(); } }@@ -281,16 +733,36 @@ 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("--dim", (j.dimPct/100).toFixed(2)); dimSet = true; }+    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-    // append only NEW slugs (don't reorder/restart what's already cycling)-    const have = new Set(slides.map(s=>s.slug));-    incoming.forEach(s=>{ if(!have.has(s.slug)){ slides.push(s); have.add(s.slug); } });+    // 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);@@ -298,7 +770,17 @@ async function loadManifest(){   // 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-  ["mousemove","mousedown","keydown","wheel","touchstart"].forEach(ev=>+  // 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>
dist/win/config.html+457−46
@@ -1,5 +1,5 @@ <!doctype html>-<!-- Adom Screensaver β€” Preferences. Rendered windowed in a WebView2 by adom-screensaver.scr /c+<!-- Adom Screensaver, Preferences. 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). -->@@ -12,7 +12,23 @@   * { 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 { padding:26px 30px 84px; overflow-y:auto; }+  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; }@@ -79,62 +95,232 @@     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; } </style> </head> <body>-  <div class="head">-    <img src="adom-wordmark.png" alt="Adom">-    <div class="t">Screensaver<small>Preferences</small></div>+  <div class="topbar">+    <div class="head">+      <img src="adom-wordmark.png" alt="Adom">+      <div class="t">Screensaver<small>Preferences</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="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="why">-    <h2>Why your laptop needs different rules in the AI age</h2>-    <p>Windows laptops ship set to <b>aggressively sleep and power down</b> to save energy β€” defaults-      Microsoft chose long before AI agents ran for hours on your behalf. The moment the screen idles-      or the lid shuts, the machine sleeps, and anything still working dies with it.</p>-    <p>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 β€” left plugged in β€” your laptop quietly becoming a-      <b>vibe-coding server</b>. The Adom model has the AI driving your frontend too β€” taking screenshots,-      rendering examples, building <b>shotlogs you can watch from your phone</b> β€” and none of that-      happens on a sleeping machine.</p>-    <p>So we tune a <b>fine balance</b>: plugged in, run indefinitely; on battery, ride through brief lid-      closings (walk to the next room) but still <b>sleep on a sensible schedule</b> once you've truly-      stepped away. The screensaver manages these settings so your laptop behaves a little differently-      than Microsoft expects β€” on purpose. Here's what we recommend, and what you've got right now:</p>+  <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> -  <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> -  <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="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 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>-    <label class="switch"><input type="checkbox" id="hkToggle"><span class="track"></span></label>-  </div> -  <div class="banner" id="banner"></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="actions">-    <button class="primary" id="apply">Apply Adom Recommended</button>-    <button id="recheck">Re-check</button>-    <span class="toast" id="toast">Applied βœ“</span>+    <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: 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> -  <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 screen still powers off on schedule to rest the panel.</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.@@ -150,21 +336,144 @@   });   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";+    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>';+        '</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++){@@ -181,20 +490,34 @@     var banner = document.getElementById("banner");     if (bad === 0){       banner.className = "banner ok";-      banner.textContent = "βœ“ Your laptop matches the Adom vibe-coding profile.";+      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.";+        " 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);@@ -222,6 +545,94 @@   });   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();+  });++  // 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 four 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;+    }+    for (var k=0;k<tabs.length;k++) (function(t){ t.addEventListener('click', function(){ show(t.getAttribute('data-tab')); }); })(tabs[k]);+  })(); </script> </body> </html>
dist/win/status.htmladded+229
@@ -0,0 +1,229 @@+<!doctype html>+<!-- Adom Screensaver, Status / diagnostics. Rendered windowed in a WebView2 by adom-screensaver.scr+     /status. The data comes from status.json, written by the ELEVATED adom-stayawake.exe helper (it+     runs `powercfg /requests`, admin-only, to see exactly what's holding the display awake). The .scr+     just injects it via window.adomStatus(data). -->+<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 { background:var(--bg); color:var(--hi); font-family:"Segoe UI",system-ui,sans-serif; user-select:none; }+  body { padding:26px 30px; overflow-y:auto; }+  .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:14px 17px; }+  .why p { font-size:12.5px; line-height:1.55; color:var(--lo); }+  .why b { color:var(--hi); font-weight:600; }++  .card { margin-top:14px; background:var(--panel); border:1px solid var(--line); border-radius:10px;+    padding:15px 18px; border-left:3px solid var(--line); }+  .card.ok   { border-left-color:var(--ok); }+  .card.warn { border-left-color:var(--warn); }+  .card .lbl { font-size:10px; letter-spacing:.16em; text-transform:uppercase; color:var(--dim);+    font-weight:600; }+  .card .big { margin-top:7px; font-size:14.5px; font-weight:600; color:var(--hi); }+  .card .big.ok   { color:#5dd879; }+  .card .big.warn { color:#f0c548; }+  .card .sub { margin-top:7px; font-size:12.5px; line-height:1.55; color:var(--lo); }+  .card .sub b { color:var(--hi); font-weight:600; }+  .card .sub .code { color:var(--tealbri); font-weight:600; font-family:"Cascadia Code","Consolas",monospace; font-size:.94em; }+  .rowhead { display:flex; align-items:center; justify-content:space-between; gap:12px; }+  .live { display:inline-flex; align-items:center; gap:8px; font-size:10px; color:var(--dim);+    letter-spacing:.1em; text-transform:uppercase; font-weight:600; white-space:nowrap; }+  .livedot { width:7px; height:7px; border-radius:50%; background:var(--teal); flex:none;+    box-shadow:0 0 7px rgba(0,184,176,.7); animation:lp 1.8s ease-in-out infinite; }+  @keyframes lp { 0%,100%{opacity:.35} 50%{opacity:1} }+  .live button { background:transparent; border:1px solid var(--line); color:var(--lo); flex:none;+    border-radius:7px; width:26px; height:26px; font-size:15px; line-height:1; cursor:pointer; padding:0; }+  .live button:hover { border-color:var(--teal); color:var(--tealbri); }+  .live button.spin { animation:spin .55s ease; }+  @keyframes spin { to { transform:rotate(360deg); } }+  .holders { margin-top:10px; display:flex; flex-direction:column; gap:7px; }+  .holder { display:flex; align-items:center; gap:10px; padding:9px 12px; border:1px solid var(--line);+    border-radius:8px; background:#0f141b; font-size:12.5px; }+  .holder .dot { width:8px; height:8px; border-radius:50%; flex:none; }+  .holder .dot.warn { background:var(--warn); box-shadow:0 0 7px rgba(227,160,8,.6); }+  .holder .dot.ok   { background:var(--teal); }+  .holder .name { font-weight:600; color:var(--hi); }+  .holder .reason { color:var(--lo); }+  .holder .me { margin-left:auto; font-size:11px; color:var(--dim); }++  .foot { margin-top:16px; color:var(--dim); font-size:11px; line-height:1.5; }+  .actions { margin-top:16px; }+  button { font-family:inherit; font-size:13px; font-weight:600; border-radius:8px; cursor:pointer;+    padding:9px 18px; border:1px solid var(--line); background:var(--panel); color:var(--hi); }+  button:hover { border-color:var(--teal); }+  .scbtn { font-family:inherit; display:inline-block; margin-top:8px; font-size:11.5px; font-weight:600;+    padding:5px 11px; border-radius:7px; border:1px solid var(--line); background:#0f141b; color:var(--tealbri); cursor:pointer; }+  .scbtn:hover { border-color:var(--teal); }+  .scitem { display:flex; flex-direction:column; gap:3px; align-items:stretch; }+  .scitem .toprow { display:flex; align-items:center; gap:10px; width:100%; }+  .scitem .badge { margin-left:auto; font-weight:600; font-size:12px; white-space:nowrap; }+  .scitem .note { margin-left:18px; color:var(--lo); line-height:1.5; }+  .b-ok { color:#5dd879; } .b-warn { color:#f0c548; } .b-dim { color:var(--dim); }+</style>+</head>+<body>+  <div class="head">+    <img src="adom-wordmark.png" alt="Adom">+    <div class="t">Screensaver<small>Status &amp; screen locks</small></div>+  </div>++  <div class="why">+    <p>A small background helper, <b>adom-stayawake.exe</b>, runs all the time. It does three things:+      it <b>keeps your PC awake</b> so long-running AI sessions (Claude / Hydrogen Desktop) don't die when+      you step away, it launches the <b>Adom Wiki billboard</b> as your screensaver, and it <b>watches+      whether anything is stopping your display from turning off</b>. It runs with admin rights so it can+      see exactly what's holding your display on, and that's what this screen shows.</p>+  </div>++  <div id="keepawake" class="card">+    <div class="lbl">Keep-awake</div>+    <div class="big" id="ka-big">…</div>+    <div class="sub" id="ka-sub"></div>+  </div>++  <div id="displaylock" class="card">+    <div class="rowhead">+      <div class="lbl">Can your display turn off right now?</div>+      <div class="live"><span class="livedot"></span><span class="livetxt">live Β· auto every 10s</span><button id="refresh" title="Check again now">↻</button></div>+    </div>+    <div class="big" id="dl-big">…</div>+    <div class="sub" id="dl-sub"></div>+    <div class="holders" id="dl-holders"></div>+  </div>++  <div id="systemlock" class="card">+    <div class="lbl">What's keeping the machine awake</div>+    <div class="sub" id="sys-sub"></div>+    <div class="holders" id="sys-holders"></div>+  </div>++  <div id="syschanges" class="card">+    <div class="rowhead">+      <div class="lbl">System &amp; power changes by Adom Screensaver</div>+      <div class="live" id="sc-scheme"></div>+    </div>+    <div class="sub" style="margin-top:6px">To give you <b>display off but the machine still running</b> on this+      laptop, the screensaver changed a few Windows power settings. Here's exactly what it touched, shown live so+      you always know what's been done to your hardware:</div>+    <div class="holders" id="sc-list" style="margin-top:12px"></div>+    <div class="foot" id="sc-foot" style="margin-top:13px"></div>+    <div id="sc-actions" style="margin-top:10px"></div>+  </div>++  <div class="foot">This re-checks <b>live</b> every 10 seconds while it's open (or hit <b>↻</b>). The+    display lock above is the only thing that can stop the screensaver from starting or the display from+    turning off. The machine keep-awake does not.</div>+  <div class="actions"><button id="close">Close</button></div>+  <div style="height:30px"></div>++<script>+  function send(m){ try{ if(window.chrome && window.chrome.webview){ window.chrome.webview.postMessage(m); } }catch(e){} }+  function esc(s){ return (s==null?"":String(s)).replace(/&/g,"&amp;").replace(/</g,"&lt;"); }+  function holderRow(text, warn){+    var i = text.indexOf(" Β· ");+    var name = i>=0 ? text.slice(0,i) : text;+    var reason = i>=0 ? text.slice(i+3) : "";+    return '<div class="holder"><span class="dot '+(warn?"warn":"ok")+'"></span>' ++      '<span class="name">'+esc(name)+'</span>' + (reason?'<span class="reason">Β· '+esc(reason)+'</span>':'') + '</div>';+  }++  // Called by the .scr host with the helper's status.json.+  window.adomStatus = function(d){+    if(!d) return;+    // keep-awake+    var ac = d.keepAwake === "ac";+    document.getElementById("ka-big").textContent = ac ? "On, your PC won't sleep" : "Passive, on battery";+    document.getElementById("ka-sub").innerHTML = ac+      ? "You're plugged in, so the helper holds the machine awake so your AI sessions keep running and the network stays up while you step away. (Your <b>display</b> still turns off on schedule; keep-awake only stops the machine sleeping.)"+      : "On battery the helper stays passive, so Windows sleeps on its normal schedule (your battery isn't drained flat while you're away). Plug in to keep sessions running indefinitely.";++    // display lock, the one that can block the screensaver+    var dl = (d.display||[]);+    var card = document.getElementById("displaylock");+    var big = document.getElementById("dl-big");+    var sub = document.getElementById("dl-sub");+    var hold = document.getElementById("dl-holders");+    if (dl.length === 0){+      card.className = "card ok"; big.className = "big ok";+      big.textContent = "βœ“ Yes, nothing is holding your display on";+      sub.innerHTML = "Nothing is forcing your display to stay on, so the screensaver will start after about <b>"+(d.saverMin||2)+" min</b> idle, then the display <b>physically turns off</b> so the panel can't burn in." ++        "<br><br>Apps can land here when they're active: a <b>screen-share or video call</b>, a <b>screen recording</b>, an <b>Adom screenshot/sharing session</b>, or a <b>video playing fullscreen</b>. That's normal while they run, but if one is ever left on it'll quietly keep your display from turning off all night. Whenever your display won't turn off, open this screen and it names exactly which app to stop.";+      hold.innerHTML = "";+    } else {+      card.className = "card warn"; big.className = "big warn";+      big.textContent = "⚠ No, an app is holding your display on";+      sub.innerHTML = "While these are running, Windows <b>won't start the screensaver and won't turn the display off</b>. Stop the screen capture to fix it. For an Adom screen-share, run <span class='code'>adom-cli hydrogen sharing stop</span>." ++        (d.elevated===false ? " <i>(The helper isn't elevated, so it can't name the app. It should be running as admin.)</i>" : "");+      hold.innerHTML = dl.map(function(h){ return holderRow(h, true); }).join("");+    }++    // system lock, the machine keep-awake (informational; the helper itself)+    var sys = (d.system||[]);+    document.getElementById("sys-sub").innerHTML = sys.length === 0+      ? "Nothing is holding the machine awake right now."+      : "These hold the <b>machine</b> awake (not the display). This is normal: <b>"+esc(d.helperExe||"adom-stayawake.exe")+"</b> is this helper, keeping your PC awake on purpose. It does <b>not</b> stop the display from turning off.";+    document.getElementById("sys-holders").innerHTML = sys.map(function(h){+      var me = h.indexOf(d.helperExe||"adom-stayawake")>=0;+      return holderRow(h, false).replace('</div>', (me?'<span class="me">that\'s this helper</span>':'')+'</div>');+    }).join("");++    // system & power changes the screensaver made (transparency: show what we did to the hardware, live)+    var sc = (d.systemChanges||[]);+    var scl = document.getElementById("sc-list");+    var anyReverted = false, anyPending = false;+    if (scl){+      scl.innerHTML = sc.map(function(c){+        if(c.state==="reverted") anyReverted = true;+        if(c.state==="pending") anyPending = true;+        var bcls = c.state==="active" ? "b-ok" : (c.state==="reverted"||c.state==="pending" ? "b-warn" : "b-dim");+        var dotcls = c.state==="active" ? "ok" : (c.state==="reverted"||c.state==="pending" ? "warn" : "ok");+        var icon = c.state==="active" ? "βœ“ " : (c.state==="pending" ? "⏳ " : (c.state==="reverted" ? "⚠ " : ""));+        var btn = "";+        if (c.reapply) btn += '<button class="scbtn" data-act="sysreapply">Re-apply</button>';+        if (c.key==="lid") btn += '<button class="scbtn" data-act="openlid">Open Windows lid settings</button>';+        return '<div class="holder scitem">' ++            '<div class="toprow"><span class="dot '+dotcls+'"></span>' ++              '<span class="name">'+esc(c.label)+'</span>' ++              '<span class="badge '+bcls+'">'+icon+esc(c.now)+'</span></div>' ++            '<div class="note">'+esc(c.note)+(btn?'<br>'+btn:'')+'</div>' ++          '</div>';+      }).join("");+    }+    var scheme = document.getElementById("sc-scheme");+    if (scheme) scheme.innerHTML = d.activeScheme ? ('<span class="livedot"></span>power plan: <b style="color:var(--hi)">'+esc(d.activeScheme)+'</b>') : '';+    var scf = document.getElementById("sc-foot");+    if (scf) scf.innerHTML =+      (anyPending  ? "<b class='b-warn'>Restart needed</b> to finish applying the Modern Standby change. " : "") ++      (anyReverted ? "<b class='b-warn'>Something outside Adom changed a setting back</b> (usually an Armoury Crate / vendor power-profile switch). Re-apply above to restore it. " : "") ++      "These are the only system settings the screensaver touches. <b>Undo</b> returns them to Windows defaults (re-enables Modern Standby and a 30-minute sleep on AC); a restart finishes it.";+    var sca = document.getElementById("sc-actions");+    if (sca){+      sca.innerHTML = '<button id="sc-undo">Undo all Adom power changes</button>';+      document.getElementById("sc-undo").onclick = function(){ send("sysundo"); };+      Array.prototype.forEach.call(document.querySelectorAll("#sc-list .scbtn"), function(b){+        b.onclick = function(){ send(b.getAttribute("data-act")); };+      });+    }++    // flash the live indicator so each refresh (manual or the 10s auto) is visibly an update+    var lt = document.querySelector(".livetxt");+    if (lt){ lt.textContent = "updated just now"; clearTimeout(window._ltT); window._ltT = setTimeout(function(){ lt.textContent = "live Β· auto every 10s"; }, 1700); }+  };++  document.getElementById("close").addEventListener("click", function(){ send("close"); });+  var rb = document.getElementById("refresh");+  if (rb) rb.addEventListener("click", function(){ send("refresh"); this.classList.remove("spin"); void this.offsetWidth; this.classList.add("spin"); });+  setInterval(function(){ send("refresh"); }, 10000);   // live: re-check powercfg every 10s while open+</script>+</body>+</html>
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-screensaver",-  "version": "0.6.15",+  "version": "0.7.0",   "description": "Vibe-coding screensaver, fades up a live billboard of the latest Adom Wiki drops (truly cached to disk; plays webm silently, renders SVG) with a power-aware nerd-stats splash, while signed native keep-awake holds the machine so Claude Code + /remote-control threads survive. Win+Shift+L to launch.",   "type": "app",   "keywords": [
page.json+2−2
@@ -4,7 +4,7 @@   "slug": "adom-screensaver",   "title": "Adom Screensaver",   "brief": "Turn your laptop into an always-on vibe coding server: branded screensaver + native keep-awake (no PowerToys) + honest, hardware-aware power management.",-  "version": "0.6.15",+  "version": "0.7.0",   "install": {     "binary_name": "adom-screensaver",     "install_dir": "",@@ -31,7 +31,7 @@     "public": true   },   "hero": {-    "path": "screenshots/hero.png",+    "path": "docs/hero.png",     "type": "image"   },   "sample_prompts": [
screenshots/billboard-component.pngadded
⋯ 1 unchanged line ⋯
screenshots/config-tabs.pngadded
⋯ 1 unchanged line ⋯
skills/adom-screensaver-billboard/SKILL.md+188−65
@@ -1,69 +1,192 @@ --- name: adom-screensaver-billboard-description: How the Adom screensaver's billboard works internally, the WebView2 (Edge/Chromium) viewer, dual-wiki hero discovery (Wiki v2 + v1), media STREAMING (webm video / svg / raster, nothing cached to disk), dedupe-prefer-v2, the "migrate to v2" kicker, randomized order, the exclude.txt curation, and the manifest contract. Use when changing what the billboard advertises, debugging why a page does/doesn't appear, curating the rotation, or touching the renderer. Trigger words, billboard, screensaver billboard, wiki rotation, hero discovery, screensaver exclude, exclude.txt, curate rotation, screensaver manifest, billboard video, billboard svg, why isn't my app in the billboard, screensaver slides, webview2 billboard.+description: >+  How the Adom Screensaver's Wiki billboard pipeline works: where it pulls content, how it caches,+  how often it re-checks, how it minimizes hits to wiki.adom.inc, how it minimizes downloads to the+  user's machine, and how it scores which drop to show. Read this before touching the Wiki loader,+  the cache, the scoring, or the refresh cadence in adom-screensaver.cs / billboard.html.+  Triggers: screensaver caching, billboard refresh, hero cache, manifest, delta skip, scoring,+  freshness, why is the screensaver showing an old hero, minimize wiki hits, screensaver bandwidth. --- -# adom-screensaver-billboard, what the saver shows & how--Parent skill: **adom-screensaver**--While the screensaver is up, the saver is a **WebView2 (Edge/Chromium) billboard** for recent Adom-Wiki drops. It cross-fades app/skill cards (~5s each), each with the page's title, brief, a-**"just ask Claude"** prompt, and a footer with **power state + a "screen powers off in M:SS"-countdown**. Because it's a real browser, it plays **webm video** (muted, looped) and renders-**SVG** + raster, not just bitmaps.--## Architecture (WebView2, not GDI+)-- `adom-screensaver.scr` (C# host) runs a fullscreen `Microsoft.Web.WebView2.WinForms.WebView2`.-- The renderer is **`billboard.html`** (HTML/CSS/JS). The host maps a virtual host-  `https://adom-screensaver.local/` β†’ the cache dir (`SetVirtualHostNameToFolderMapping`) and serves-  `billboard.html` + `manifest.json` from there.-- The host's background loader writes **`manifest.json`**; the page polls it every 3s and-  appends new slides without restarting the current one.-- Live **power + countdown** are pushed each second from C# via-  `ExecuteScriptAsync("window.adomStatus(power, secondsLeft)")`.--## Manifest contract (`cache/manifest.json`)-```json-{ "dimPct": 50,-  "slides": [ {"slug","title","brief","prompt","source":"v2|v1","type":"app|skill",-               "media":"<URL or local file>","kind":"video|image|svg"} ] }-```--## Media is STREAMED, never downloaded-`media` is the **remote wiki URL** for every kind (image/svg/video), the WebView2 streams it-(cross-origin `<img>`/`<video>` is fine for display). Nothing is cached to disk (a hero webm can-be 50MB+; streaming avoids a giant cache and shows instantly). `kind=video` β†’-`<video autoplay muted loop>` (NEVER audio). `kind=svg`/`image` β†’ `<img>` (Chromium renders SVG-+ webp + raster natively).--## Dual-wiki hero discovery-Query **per type** (`?type=app&limit=100` + `?type=skill`) on BOTH wikis, a flat `?limit=N` is-swamped by ~185 component pages. **Wiki v2 first so it WINS dedup**, then Wiki v1 (legacy).-- **v2 hero:** registered `hero_path` β†’ repo root images (`/files` API) β†’ nested-  `screenshots/hero.png`,`docs/hero.png` (tiny ranged-GET existence check). Media = `/blob/...` URL.-- **v1 hero:** `metadata.photo_url` (β†’ `/static/<type>s/<slug>/...`). v1 prompt = `metadata.sample_prompts[0]`.-- **video (preferred over static):** a `.webm`/`.mp4` from the v2 repo files OR a `.webm` URL-  found in the v1 page `content`. Deduped by URL so a shared tour shows once.-- **v1 slides get a grey kicker** `ON WIKI v1 Β· NUDGE THE TEAM TO MIGRATE TO v2` (the saver is a-  nudge to migrate v1 β†’ v2). v2 slides get the teal `NEW ON THE ADOM WIKI`.-- Heroes fetched by a **6-thread pool**; order **shuffled** each run (the first slide varies).--## Exclusions & curation-Structural (code): components & bootstraps (app/skill only), **HD/AD** (`adom-desktop*`,`hd-*`),-**auth** (`auth-*`,`*-auth`), the saver's own page, `test-/smoke-/shared-`.-Per-slug, no rebuild: **`exclude.txt`** in the saver folder (`C:\Users\<user>\adom-screensaver\exclude.txt`,-one slug per line, `#` comments), read at runtime. To drop a page:-`Add-Content exclude.txt "some-slug"`. Currently seeded: `dhl-tracker`, `carbon-preferences`,-`gallia-bundle`, `adom-desktop-blender-bridge`.--## Fast first paint-On a warm launch the previous `manifest.json` + the page render instantly while the loader-re-queries in the background. Cold first run shows the intro card until the first slide resolves.--## Debugging "why isn't page X showing?"-1. type app/skill (not component/bootstrap)? not HD/AD/auth/excluded?-2. Does it have a resolvable hero? v2: `hero_path`/root file/`screenshots|docs/hero.png`; v1:-   `metadata.photo_url`. No hero β†’ correctly skipped (that's what `adom-wiki-publish` PUB-09/14 nudge).-3. In `exclude.txt`?-4. On both wikis β†’ only the **v2** version shows (by design).+# Adom Screensaver - billboard caching, scoring & refresh++The screensaver plays Adom Wiki pages as billboards. This documents the data pipeline so it stays+honest about server load and bandwidth. Source of truth: `Wiki` class in `adom-screensaver.cs` ++`billboard.html`. Everything below is verified behavior, not aspiration.++## One source only++It pulls from exactly **one** wiki: `VW = https://wiki.adom.inc` (the canonical Adom Wiki). The old+`V1` / `V2` constants are **dead** (no code path queries them). All requests are authenticated with+the user's bearer token (`wiki-token.txt`), so the billboards are the user's filtered view (public ++their private/org pages, each badged).++## When it refreshes++`Wiki.Start()` β†’ spawns `Wiki.Run()` **once per screensaver launch** (every idle-fire, or Win+Shift+L).+There is **no** in-session wiki re-poll. `billboard.html` re-reads the **local** `manifest.json` every 3 s+(never touches the wiki). Last run's billboards are shown instantly from the cached manifest while the+fresh `Run()` reconciles in the background.++**In-session in-place refresh (the one-launch-lag fix).** `loadManifest` doesn't just append new slugs -+it also **refreshes a slide already on screen** when its `media`/`updated` changes (repainting the current+card if it's the one showing). Without this, the page kept showing the CACHED hero from launch even after+the fresh `Run()` downloaded an updated one into the manifest, so a hero you updated would only appear on+the NEXT launch. Now it appears within ~3 s of the background fetch, same session, no reordering.++**Where heroes live now (wiki change, 2026-06).** Hero images are **no longer committed repo files** - the+page's `/files` listing returns NO images. The hero is in a hero store, exposed via the page DETAIL's+`hero_path` (e.g. `screenshots/hero.png`) + `hero_type`, and served at the usual+`/blob/<type>/<slug>/<hero_path>`. So discovery MUST read `hero_path` from the **detail** (the LIST omits+it - it comes back empty there) and the `RepoFilesAll` fallback is now mostly dead weight (kept for older+pages). The canonical `/blob/.../screenshots/hero.png` URL still works, so updated heroes are fetched+correctly (verified: the cached `-hid` hero is byte-identical to live).++## Minimizing wiki-server load - the DELTA SKIP (the important part)++Per launch, `Run()` issues just **2 list calls**: `GET /api/v1/pages?type=app&limit=100` and `…type=skill…`.+Those list items already carry each page's `updated_at`.++Then for each page, `ProcessItem` does a **delta skip**: if last run's manifest already has that slug+**at the same `updated_at`** and its cached hero file is still on disk, it **reuses that entry verbatim**+- no per-page detail fetch, no file-list, no hero probe, no download. Only **new or changed** pages do+the full network work (detail + file-list + ranged-GET hero probes + maybe a hero download).++Net: a re-fire where nothing changed costs **2 requests total** (verified: `WIKI: run done, 60 reused+(delta-skip), 0 fetched, 60 total`). Before this, every launch re-fetched all ~60 pages' detail ++files + probes - hundreds of requests per idle-fire.++## The cache (what's on disk)++`%LOCALAPPDATA%\adom-screensaver\cache\`:+- **`manifest.json`** - the slide list (slug, title, brief, prompt, media ref, vis, owner, `updated`, …).+  Rewritten each run from the fresh+reused set; a page removed from the wiki simply drops out.+- **Hero images** (image/SVG) - downloaded and served locally, filename **keyed on the page's update+  stamp**: `SafeName(slug)__<compacted updated_at>.png`. So:+  - page unchanged β†’ same filename β†’ served from disk, **no re-download**;+  - page's hero updated β†’ new `updated_at` β†’ new filename β†’ **re-downloaded once**, and the old+    stamped file (+ the pre-stamp legacy `slug.png`) is **pruned**.+  - The download URL is cache-busted (`?v=<stamp>`) so no proxy serves stale bytes.+- **`showcounts.json`** - per-slug lifetime view counts (host-written synchronously on each show), used+  by the scorer. Survives hard kills (unlike browser localStorage).++## Minimizing downloads / bandwidth++- Heroes are downloaded **at most once per content change** (the stamp key above). Unchanged heroes+  are never re-fetched.+- **Videos are STREAMED**, never downloaded (they can be tens of MB); the manifest carries the remote+  URL and the WebView2 streams it.+- The delta skip means unchanged pages download **nothing**.++## Repo type (App / Skill / Component) - capsule + components opt-in++The wiki's `type` field is first-class (`app` / `skill` / `component` / `bootstrap`) - it's what the header+tabs filter on, NOT tags. The fetch reads it per page and the manifest carries `s.type`. Each billboard+shows a color-coded **type capsule** leading its meta row: App = blue (square dot), Skill = purple (diamond),+Component = pink (round). `IsHeroType` decides what's eligible: **app + skill always; component only when+opted in; bootstrap never.**++**Components are opt-in** (`Prefs.ShowComponents()`, default OFF; config toggle `compToggle` ->+`components:on`/`components:off`). When ON, `AddWork` also fetches `type=component` and `Start()` widens+`Target` 200 -> 360 so components survive the per-launch collection cap (app+skill alone already exceed 200).+Takes effect on the next saver launch.++## Component heroes - raster product visuals only, never a symbol++Most `type=component` pages are jellybean passive parts (0402 caps/resistors) whose only image is a+part-number-named **svg, i.e. a schematic symbol**, and they rarely set `hero_path`. Showing those+full-screen is a joke. So `RepoFilesAll` is component-strict:+- **Skip ALL svg for components** (svg == symbol / schematic / pcb diagram / part-number symbol). Real+  product visuals (3D renders, photos) are raster (png/jpg). Also skip any `schematic`/`symbol`/`footprint`+  file for every type.+- **Prefer a clean 3D hero angle**: `hero` files first, then 3D renders ranked `home`/`iso`/`persp` (clean+  product shots) > plain `front`/`board` (often a viewer screenshot or a dimensioned view) > edge/underside+  `back`/`bottom`/`left`/`right`/`top`/`side`, then screenshots, then `pcb` layout last.+- Components show ONLY with a genuine hero / 3D render / photo: the assembled candidate list for a component+  is `hero + 3D + screenshot` only (NO "rest" random-raster or bare-PCB fallback). A component with no real+  product visual gets **no hero and is dropped** - better no card than a junk card.+- Net (verified on ALL 200 live components): **38 billboard, 162 dropped, 0 junk** - and all 38 have a real+  visual (32 resolve to a clean `3d-home.png` render, the rest to `*-hero.png` / `product-photo.jpg`).+  Reference: `cosmocoil` -> `3d-home.png`.++## No network (offline) - never dead-stop, never burn in++The saver must survive a launch with **no wifi** (e.g. a fresh Airbnb) without stranding a bright+static splash on the panel. Three guarantees, none of which depend on the wiki being reachable:++1. **Don't clobber the cache.** `AddWork` returns whether the list call got a real HTTP response. If the+   wiki was unreachable, `Run()` **skips `WriteManifest()`** so last run's good `manifest.json` stays on+   disk, and re-publishes the cached entries into `Count`. (Previously `Run()` always wrote the manifest+   from an empty `published` list, **wiping** last run's billboards - the hero files stayed on disk but the+   manifest that indexed them was zeroed, so the page had 0 slides and froze on the splash.)+2. **Play cached drops offline.** The page reads the preserved `manifest.json` via the virtual host and+   rotates the cached billboards normally. The splash says `Offline: playing your N saved drops from last+   time.` (or `Offline: no saved drops yet; dimming to protect your screen.` on a true first run).+3. **Burn-in backstops the network can't defeat** (host `OnStatusTick`): dim **early (45s)** when the run+   finished/failed with **nothing to show** (`Wiki.Done && Wiki.Count==0`), and an **absolute 8-min ceiling**+   (`HardRestCapMs`) so even `RestAfter=0` ("never") can't hold a lit screen all night. The normal+   `RestAfterSec` timer (default 300s) still applies on top.++## Scoring - what plays first (`billboard.html shuffleSlides`)++Higher score plays sooner; the goal is to lead with NEW + UNSEEN, sink OLD + over-seen:+- **Freshness** (from `updated`): `<7d` +100, `<30d` +60, `<90d` +35, `<365d` +15, older/unknown +5.+- **Novelty** (from `showcounts.json`): `max(0, 60 - 20 Γ— timesShown)` (never-seen +60; gone after ~3 views).+- **Jitter**: +0..12 so the order varies run-to-run and ties never lock.+Each billboard prints its own `priority N (fresh F + new V)` in the footer for transparency.++## Public vs private background (so the two never get confused)++Each billboard's **full background** is tinted by visibility (set per slide in `showSlide` via a+`viz-pub`/`viz-priv` body class driving `#bgwash`): PUBLIC = the clean dark screensaver (no wash);+PRIVATE / org-only = an Adom TEAL wash + a teal screen frame (brand #00B8B1). This makes a private page (e.g. `adom-gchat-adom`, `adom-google-adom`,+or any `-dev` source page) unmistakable next to its public twin.++**Private pages only appear if the saver is authenticated.** The list/detail/blob calls all send the+`wiki-token.txt` bearer. The hero-probe (`UrlExists`) MUST send it too: a private page's hero blob+401/404s anonymously, so without the token every private page was silently dropped (the bug that made+private `-adom`/`-dev` pages never show). Public heroes probe fine anonymously, which masked it.++## Freshness display++Each slide shows a chip from `updated`: "New today" / "Updated 3 days ago" / "Updated 2 weeks ago" …,+teal for recent, dim for old.++## Exclude list++`exclude.txt` (beside the .scr) holds slugs kept OUT of the rotation, one per line. `Skip()` also+hard-excludes `adom-screensaver`, `test-`/`smoke-`/`shared-` prefixes, `adom-desktop`/`hd-`, and+auth pages. On startup `FilterCachedManifestByExclude()` drops now-excluded slugs from the cached+manifest so a just-excluded card never flashes.++## Correctness invariants (don't regress)++- **One entry per slug.** `AddWork` dedups by slug within the work list; each launch is a fresh process+  so `published` never accumulates across runs. (If you ever see two of the same slug, that's the bug.)+  Note: two *different* slugs about the same topic (e.g. `adom-gchat` the CLI + `adom-gchat-adom` the+  Org Config page) are two real pages, not a duplicate.+- A reused (delta-skip) entry is only trusted if its hero file still exists (`CachedMediaOk`); else it+  falls through to full processing.+- The manifest is rebuilt from the current wiki list each run, so removed pages drop and the set is+  never stale.++## Known gaps β†’ asks filed for Colby (see colby-feedback.md, 2026-06-19)++- `/blob/...` sends `no-cache` with **no ETag/Last-Modified** and ignores conditional GET, so the only+  way to know a hero changed without a page edit is to download it. Want ETag + `304`.+- `last_commit_hash` is only in the page **detail**, not the **list** - with it in the list we'd have a+  perfect per-page/per-hero delta key in one call.+- No bulk popularity/trending endpoint (stars are per-page only) - want `star_count` in the list and/or+  `/api/v1/trending` to factor popularity into the score.++## Quick reference++| Question | Answer |+|---|---|+| Which wiki? | `wiki.adom.inc` only |+| Re-check cadence | once per saver launch (no in-session poll) |+| Requests when nothing changed | 2 (the two list calls); 0 per-page |+| Hero re-download | only when `updated_at` changes |+| Videos | streamed, never downloaded |+| Order | freshness + novelty + jitter (highest first) |