← Commit history
SKILL.mdadded+53
@@ -0,0 +1,53 @@+---+name: hero-studio+description: Open and drive Hero Studio, the live playground for the hero image v2 definition (lower third = true-text overlay). Use when the user wants to design or preview a wiki hero image, try hero layouts, screenshot bleeds, brand tile themes, or gradient angles, compare title placement (overlay vs image), or see a hero inside the wiki homepage marquee, the index card grid, or an adom-desktop style right bar. Trigger phrases - open hero studio, design a hero, hero playground, hero billboard, preview hero in marquee, lower third overlay, hero layouts, hero themes, rotate the hero gradient.+user-invocable: true+---++# Hero Studio++A local web app (plain Node, no dependencies) that renders a full 16:10 hero live and+shows it inside pixel-faithful wiki contexts. State lives on the server, so you drive+everything with plain HTTP.++## Start and show it++```bash+hero-studio &                      # port 8846 (or: node server.js in the package dir)+```++Show it in a Hydrogen webview using the container proxy URL, for example:++```bash+adom-cli hydrogen webview open-or-refresh --name "Hero Studio" \+  --url "$(echo $VSCODE_PROXY_URI | sed 's/{{port}}/8846/')" --panel-id <webview-pane-id>+```++Pick a webview pane, never the VS Code pane. Screenshot-verify with+`adom-cli hydrogen screenshot panel --name "Hero Studio"`.++## Drive it++```bash+curl -s localhost:8846/state                       # read state+curl -s -X POST localhost:8846/state -H 'Content-Type: application/json' \+  -d '{"layout":"mirror","theme":"onion","gradAngle":245}'+curl -s -X POST localhost:8846/ui/toast -H 'Content-Type: application/json' \+  -d '{"text":"narration for the user","kind":"info"}'+curl -s localhost:8846/console                     # page console, no DevTools+```++State fields: `page` (owner/slug from the bundled trending data), `layout` (bleed-bottom,+bleed-right, bleed-top, bleed-corner-br, bleed-corner-tr, contained, mirror), `theme`+(midnight, pcb, onion, pill, grid, gradient), `gradAngle` (0 to 360, brand default 155),+`titleMode` (overlay-only, image-only, both), `show.logo/title/subtitle/description`,+`overlay.pill/scores/author/slug`, `guides`.++The UI follows within 1 second of any POST (live channel plus poll fallback). Deep links:+append `#contexts` or `#wikigrid` to the URL to land on the in-context previews.++## Refresh the dataset++```bash+./refresh-data.sh    # pulls live adom-wiki page trending, then restart the server+```
bin/hero-studioadded+5
@@ -0,0 +1,5 @@+#!/bin/sh+# Launcher: resolve the real package dir (through the ~/.local/bin symlink) and run the server.+SELF="$(readlink -f "$0")"+DIR="$(cd "$(dirname "$SELF")/.." && pwd)"+exec node "$DIR/server.js" "$@"
data/pages.jsonadded+352
@@ -0,0 +1,352 @@+[+ {+  "owner": "adom",+  "slug": "adom-desktop",+  "type": "app",+  "title": "Adom Desktop",+  "author": "Kyle Bergstedt",+  "score": 1409,+  "stars": 1,+  "installs30d": 180,+  "brief": "Two parts, two machines. DOWNLOAD (below) = the Windows desktop APP , install it on the PC you want Claude to control (the bridge + GUI, signed, auto-updating). PKG INSTALL (this command) = for a clou",+  "tags": [+   "adom-desktop",+   "tauri",+   "claude-code",+   "bridge"+  ],+  "updated_at": "2026-07-16T02:42:33.038Z"+ },+ {+  "owner": "adom",+  "slug": "adom-desktop-fusion-bridge",+  "type": "app",+  "title": "Adom Desktop - Fusion 360 Bridge",+  "author": "Kyle Bergstedt",+  "score": 687,+  "stars": 0,+  "installs30d": 55,+  "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Desktop: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed",+  "tags": [+   "fusion360",+   "autodesk",+   "cad",+   "pcb"+  ],+  "updated_at": "2026-07-16T12:20:55.308Z"+ },+ {+  "owner": "adom",+  "slug": "adom-tts",+  "type": "app",+  "title": "Adom TTS - Text to Speech",+  "author": "John Lauer",+  "score": 607,+  "stars": 0,+  "installs30d": 99,+  "brief": "Adom's shared voice service. Reads your AI's answers back to you while you drive, and renders perfectly-timed voiceover tracks for your demo videos , so you never record your own voice.",+  "tags": [+   "tts",+   "voice",+   "audio",+   "demo"+  ],+  "updated_at": "2026-07-17T01:56:01.851Z"+ },+ {+  "owner": "adom",+  "slug": "adom-browser-extension",+  "type": "app",+  "title": "Adom Native Browser Extension (Chrome + Edge)",+  "author": "John Lauer",+  "score": 601,+  "stars": 0,+  "installs30d": 58,+  "brief": "Your AI's hands in your real, signed-in Chrome & Edge: build, drive, and test web apps, and log past vendor walls, in the browser you already use. The nbrowser_* native-browser bridge (pup's counterpart) - runs in your a",+  "tags": [+   "browser",+   "extension",+   "chrome",+   "edge"+  ],+  "updated_at": "2026-07-15T15:55:57.933Z"+ },+ {+  "owner": "adom",+  "slug": "adom-desktop-puppeteer-bridge",+  "type": "app",+  "title": "Adom Desktop - Puppeteer Bridge",+  "author": "Kyle Bergstedt",+  "score": 586,+  "stars": 1,+  "installs30d": 59,+  "brief": "The Adom Desktop Puppeteer (pup) bridge , drive a real headful browser on the user's desktop from the cloud: open/screenshot/eval/record. Cold-start drives the browser ALREADY on the machine (installed Chrome, else Micro",+  "tags": [+   "skills",+   "pup",+   "puppeteer",+   "browser"+  ],+  "updated_at": "2026-07-15T15:46:26.239Z"+ },+ {+  "owner": "adom",+  "slug": "core",+  "type": "bootstrap",+  "title": "Adom Core",+  "author": "Kyle Bergstedt",+  "score": 576,+  "stars": 0,+  "installs30d": 111,+  "brief": "Bootstrap a standard Adom workstation in one command.",+  "tags": [],+  "updated_at": "2026-07-14T22:03:51.153Z"+ },+ {+  "owner": "adom",+  "slug": "adom-desktop-kicad-bridge",+  "type": "app",+  "title": "Adom Desktop - KiCad Bridge",+  "author": "Kyle Bergstedt",+  "score": 557,+  "stars": 0,+  "installs30d": 57,+  "brief": "Reference implementation of the KiCad bridge , multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.",+  "tags": [+   "kicad",+   "pcb",+   "schematic",+   "eda"+  ],+  "updated_at": "2026-07-15 13:12:33"+ },+ {+  "owner": "adom",+  "slug": "adom-lbr",+  "type": "app",+  "title": "Adom Library",+  "author": "John Lauer",+  "score": 539,+  "stars": 0,+  "installs30d": 82,+  "brief": "adom-lbr , the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON , symbol + footp",+  "tags": [+   "eda",+   "altium",+   "schlib",+   "pcblib"+  ],+  "updated_at": "2026-07-15T22:00:46.442Z"+ },+ {+  "owner": "adom",+  "slug": "adom-wiki-cli",+  "type": "app",+  "title": "Adom Wiki CLI",+  "author": "Colby Knox",+  "score": 510,+  "stars": 1,+  "installs30d": 77,+  "brief": "The Adom Wiki CLI: publish and install packages with verified signatures, cut releases, open PRs and discussions, and discover and administer the wiki, all from your terminal. Replaces adompkg.",+  "tags": [+   "cli",+   "wiki",+   "packages",+   "developer-tools"+  ],+  "updated_at": "2026-07-17T15:21:36.819Z"+ },+ {+  "owner": "adom",+  "slug": "adom-screensaver",+  "type": "app",+  "title": "Adom Screensaver",+  "author": "John Lauer",+  "score": 439,+  "stars": 0,+  "installs30d": 74,+  "brief": "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",+  "tags": [+   "screensaver",+   "keep-awake",+   "vibe-coding",+   "windows"+  ],+  "updated_at": "2026-07-10T20:18:03.283Z"+ },+ {+  "owner": "adom",+  "slug": "wiki",+  "type": "skill",+  "title": "Adom Wiki",+  "author": "Kyle Bergstedt",+  "score": 418,+  "stars": 1,+  "installs30d": 60,+  "brief": "How the Adom Wiki (wiki.adom.inc) works and how to drive it with the adom-wiki CLI: the two storage layers (git page repo vs package registry), hosting source by pushing files (not a tarball), making",+  "tags": [+   "wiki",+   "cli",+   "packages",+   "registry"+  ],+  "updated_at": "2026-07-10T19:58:57.304Z"+ },+ {+  "owner": "adom",+  "slug": "adom-footprint",+  "type": "app",+  "title": "Adom Footprint",+  "author": "John Lauer",+  "score": 375,+  "stars": 0,+  "installs30d": 61,+  "brief": "KiCad footprint creator with layer HUD viewer and delivery to KiCad/Fusion 360",+  "tags": [+   "kicad",+   "footprint",+   "pcb",+   "eda"+  ],+  "updated_at": "2026-07-16T21:48:42.442Z"+ },+ {+  "owner": "adom",+  "slug": "adom-vscode",+  "type": "app",+  "title": "adom-vscode",+  "author": "John Lauer",+  "score": 347,+  "stars": 0,+  "installs30d": 68,+  "brief": "Drive VS Code from your AI , a CLI + extension that lets Claude Code, Codex, and any other process open files, switch modes, paste screenshots, view live AI session stats, and run any VS Code command via a local HTTP API",+  "tags": [+   "cli",+   "vscode",+   "code-server",+   "ide"+  ],+  "updated_at": "2026-06-30T17:31:46.050Z"+ },+ {+  "owner": "adom",+  "slug": "sm02b-srss-tb",+  "type": "component",+  "title": "JST SM02B-SRSS-TB (SH 1.0mm SMT header)",+  "author": "John Lauer",+  "score": 317,+  "stars": 0,+  "installs30d": 59,+  "brief": "JST SM02B-SRSS-TB - 2-pin, 1.0mm pitch, SMT side-entry SH-series board header (mates SHR-02V-S-B wire housing). Interactive JST-vs-KiCad 3D comparison; sym/fp from JST datasheet (ds2sf) + KiCad. Run c",+  "tags": [+   "hardware",+   "connector",+   "jst",+   "sh"+  ],+  "updated_at": "2026-06-29T17:05:39.946Z"+ },+ {+  "owner": "adom",+  "slug": "adompkg",+  "type": "skill",+  "title": "adompkg",+  "author": "Colby Knox",+  "score": 305,+  "stars": 0,+  "installs30d": 59,+  "brief": "The Adom wiki package manager: publish, install, and manage app/skill/component/bootstrap packages.",+  "tags": [+   "adompkg",+   "cli",+   "publishing",+   "wiki"+  ],+  "updated_at": "2026-06-29T17:02:04.570Z"+ },+ {+  "owner": "adom",+  "slug": "adom-symbol",+  "type": "app",+  "title": "Adom Symbol",+  "author": "John Lauer",+  "score": 289,+  "stars": 0,+  "installs30d": 40,+  "brief": "KiCad symbol creator with interactive viewer and delivery to KiCad/Fusion 360",+  "tags": [+   "kicad",+   "symbol",+   "eda",+   "footprint"+  ],+  "updated_at": "2026-07-16T21:48:12.309Z"+ },+ {+  "owner": "adom",+  "slug": "adom",+  "type": "skill",+  "title": "adom",+  "author": "Kyle Bergstedt",+  "score": 284,+  "stars": 0,+  "installs30d": 55,+  "brief": "PRIORITY: invoke FIRST for any Adom platform query BEFORE using MCP tools directly. 'containers' / 'list containers' / 'repos' / 'SSH keys' / 'profile' / 'orgs' / 'workspaces' route through adom-cli ,",+  "tags": [],+  "updated_at": "2026-07-17T13:40:11.855Z"+ },+ {+  "owner": "adom",+  "slug": "adom-cli",+  "type": "skill",+  "title": "adom-cli",+  "author": "Kyle Bergstedt",+  "score": 283,+  "stars": 0,+  "installs30d": 55,+  "brief": "Adom CLI , platform binary for containers, repos, workspaces, orgs, SSH keys, Hydrogen settings, and more",+  "tags": [+   "cli",+   "platform",+   "containers",+   "repos"+  ],+  "updated_at": "2026-07-16T18:39:33.128Z"+ },+ {+  "owner": "adom",+  "slug": "step2glb",+  "type": "app",+  "title": "step2glb",+  "author": "Kyle Bergstedt",+  "score": 275,+  "stars": 0,+  "installs30d": 52,+  "brief": "Convert STEP CAD files to color-preserving GLB models from the command line.",+  "tags": [+   "cad",+   "step",+   "glb",+   "3d"+  ],+  "updated_at": "2026-06-29T17:01:54.873Z"+ },+ {+  "owner": "john",+  "slug": "adom-wiki-skillpack",+  "type": "skill",+  "title": "Adom Wiki Skill Pack",+  "author": "John Lauer",+  "score": 266,+  "stars": 0,+  "installs30d": 45,+  "brief": "How to use the Adom Wiki without breaking pages , the wiki is six tools in one (git, adompkg, releases, discussions, PRs, discovery), and three repo archetypes have emerged from real use: Page, Skillpack, and Family. Plu",+  "tags": [+   "wiki",+   "adompkg",+   "publishing",+   "skillpack"+  ],+  "updated_at": "2026-07-16T01:45:22.990Z"+ }+]\ No newline at end of file
docs/contexts.pngadded
⋯ 1 unchanged line ⋯
docs/hero.pngadded
⋯ 1 unchanged line ⋯
docs/index-cards.pngadded
⋯ 1 unchanged line ⋯
docs/themes.pngadded
⋯ 1 unchanged line ⋯
install.shadded+8
@@ -0,0 +1,8 @@+#!/bin/sh+set -e+mkdir -p "$HOME/.local/bin"+chmod +x "$PWD/bin/hero-studio"+ln -sfn "$PWD/bin/hero-studio" "$HOME/.local/bin/hero-studio"+echo "installed: hero-studio"+echo "run 'hero-studio' (serves on port 8846), then open /proxy/8846/ in a webview"+echo "refresh the sample data with ./refresh-data.sh (needs the adom-wiki CLI)"
package.jsonadded+70
@@ -0,0 +1,70 @@+{+  "slug": "hero-studio",+  "title": "Hero Studio",+  "type": "app",+  "version": "0.1.0",+  "description": "A live playground for the hero image v2 definition: the lower third of every hero is a true-text overlay (type pill, trending score, stars, title, author, wiki slug) rendered by the surface, never baked into the image. Flip screenshot bleeds, brand tile themes, gradient angles, and title placement on real wiki trending data, and see the result in pixel-faithful wiki contexts: the homepage marquee, the index page card grid, and an adom-desktop style right bar.",+  "license": "MIT",+  "tags": [+    "hero",+    "billboard",+    "wiki",+    "design",+    "branding",+    "marquee",+    "studio",+    "brand"+  ],+  "dependencies": {},+  "discovery_triggers": [+    "open hero studio",+    "design a hero image",+    "hero image playground",+    "preview my hero in the marquee",+    "lower third hero overlay",+    "hero billboard lab",+    "try hero layouts and themes",+    "what would my hero look like on the wiki"+  ],+  "discovery_pitch": "Reach for hero-studio when someone wants to design, preview, or debate a wiki hero image: it renders every layout, brand tile theme, and gradient angle live on real trending data, inside faithful copies of the wiki marquee and index card chrome.",+  "sample_prompts": [+    {+      "label": "Explore layouts",+      "prompt": "open hero studio and show me my app's hero with a bleed-right screenshot on the PCB theme"+    },+    {+      "label": "Judge in context",+      "prompt": "how would this hero look in the wiki homepage marquee and the index card grid?"+    },+    {+      "label": "Title placement",+      "prompt": "show me the hero with the title in the overlay only versus baked into the image"+    }+  ],+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  },+  "needs_sudo": false,+  "hero": {+    "headline": "Hero Studio",+    "subhead": "Design the lower-third hero, live.",+    "screenshot": "docs/hero.png"+  },+  "files": [+    "package.json",+    "README.md",+    "SKILL.md",+    "install.sh",+    "uninstall.sh",+    "refresh-data.sh",+    "server.js",+    "bin/hero-studio",+    "public/**",+    "data/**",+    "docs/hero.png",+    "docs/contexts.png",+    "docs/themes.png",+    "docs/index-cards.png"+  ]+}
public/fonts/familjen-grotesk-500.woff2added
⋯ 1 unchanged line ⋯
public/fonts/familjen-grotesk-700.woff2added
⋯ 1 unchanged line ⋯
public/fonts/jetbrains-mono-400.woff2added
⋯ 1 unchanged line ⋯
public/fonts/jetbrains-mono-500.woff2added
⋯ 1 unchanged line ⋯
public/fonts/satoshi-400.woff2added
⋯ 1 unchanged line ⋯
public/fonts/satoshi-500.woff2added
⋯ 1 unchanged line ⋯
public/index.htmladded+783
@@ -0,0 +1,783 @@+<!doctype html>+<html lang="en">+<head>+<meta charset="utf-8">+<meta name="viewport" content="width=device-width, initial-scale=1">+<title>Hero Studio</title>+<link rel="icon" href='data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23e6edf3" stroke-width="1.8"><rect x="3" y="4" width="18" height="16" rx="2"/><line x1="3" y1="14.5" x2="21" y2="14.5"/><line x1="6" y1="17.5" x2="13" y2="17.5"/></svg>'>+<style>+@font-face { font-family: 'Familjen Grotesk'; src: url('fonts/familjen-grotesk-700.woff2') format('woff2'); font-weight: 700; font-display: swap; }+@font-face { font-family: 'Familjen Grotesk'; src: url('fonts/familjen-grotesk-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }+@font-face { font-family: 'Satoshi'; src: url('fonts/satoshi-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }+@font-face { font-family: 'Satoshi'; src: url('fonts/satoshi-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }+@font-face { font-family: 'JetBrains Mono'; src: url('fonts/jetbrains-mono-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }++:root {+  --bg:#0d1117; --surface:#161b22; --elevated:#1c2128; --overlay:#21262d; --border:#30363d;+  --text:#e6edf3; --text-2:#8b949e; --text-3:#484f58;+  --accent:#00b8b0; --accent-bright:#00e6dc; --purple:#8c6bf7; --blue:#58a6ff;+  --green:#3fb950; --red:#f85149; --yellow:#d29922;+  --grad:linear-gradient(100deg,#00e6dc,#39b8ff 60%,#8c6bf7);+}+* { box-sizing:border-box; margin:0; padding:0; }+html,body { height:100%; }+body {+  background:var(--bg); color:var(--text);+  font-family:'Satoshi',sans-serif; font-size:14px;+  display:flex; flex-direction:column; overflow:hidden;+}++/* ---------- 44px header, three zones ---------- */+header {+  height:44px; flex:0 0 44px; display:flex; align-items:center; gap:12px;+  padding:0 12px; background:var(--surface); border-bottom:1px solid var(--border);+}+.brand { display:flex; align-items:center; gap:8px; white-space:nowrap; }+.brand svg { width:16px; height:16px; stroke:var(--text); }+.brand .name { font-family:'Familjen Grotesk'; font-weight:500; font-size:14px; color:var(--accent); }+.brand .sub { color:var(--text-2); font-size:13px; }+.vsep { width:1px; height:22px; background:var(--border); flex:0 0 1px; }+.subject { display:flex; align-items:baseline; gap:10px; min-width:0; }+.subject b { font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }+.subject .meta { font-family:'JetBrains Mono',monospace; font-size:11.5px; color:var(--text-2); white-space:nowrap; }+.actions { margin-left:auto; display:flex; gap:6px; }+.iconbtn {+  width:30px; height:30px; border:1px solid var(--border); border-radius:8px; background:transparent;+  display:flex; align-items:center; justify-content:center; cursor:pointer; transition:all .15s ease;+}+.iconbtn svg { width:16px; height:16px; stroke:var(--text); fill:none; stroke-width:1.8; }+.iconbtn:hover { background:var(--elevated); border-color:var(--text-3); }+.iconbtn.on { border-color:var(--accent); background:rgba(0,184,176,.08); }+@media (max-width:720px){ .subject .meta { display:none; } }++/* ---------- body ---------- */+main { flex:1; display:flex; min-height:0; }+#panel {+  flex:0 0 264px; overflow-y:auto; background:var(--surface); border-right:1px solid var(--border);+  padding:14px 14px 40px;+}+#panel h3 {+  font-family:'Familjen Grotesk'; font-weight:500; font-size:11px; letter-spacing:.08em;+  text-transform:uppercase; color:var(--text-2); margin:18px 0 8px;+}+#panel h3:first-child { margin-top:2px; }+select {+  width:100%; background:var(--elevated); color:var(--text); border:1px solid var(--border);+  border-radius:8px; padding:7px 8px; font-family:'Satoshi'; font-size:13px; cursor:pointer;+}+.optgrid { display:flex; flex-wrap:wrap; gap:6px; }+.opt {+  border:1px solid var(--border); background:var(--elevated); color:var(--text-2); border-radius:8px;+  padding:6px 9px; font-size:12.5px; cursor:pointer; transition:all .15s ease; font-family:'Satoshi';+}+.opt:hover { color:var(--text); border-color:var(--text-3); }+.opt.on { color:var(--text); border-color:var(--accent); background:rgba(0,184,176,.10); }+.swatches { display:flex; gap:6px; }+.swatch {+  width:38px; height:26px; border-radius:6px; border:1px solid var(--border); cursor:pointer;+  transition:all .15s ease; position:relative;+}+.swatch.on { outline:2px solid var(--accent); outline-offset:1px; }+.sw-midnight { background:linear-gradient(155deg,#0a0e14,#0d1117 45%,#071318); }+.sw-pcb { background:linear-gradient(155deg,#03211f,#04302c); }+.sw-onion { background:repeating-radial-gradient(circle at 90% 10%, #101a2c 0 4px, #16223a 4px 6px); }+.sw-pill { background:#0c3a3c; }+.sw-grid { background:#0c3a3c url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12"><rect x="2" y="2" width="8" height="8" fill="none" stroke="%23ffffff" stroke-opacity=".35" stroke-width="1"/></svg>'); }+.sw-grad { background:linear-gradient(100deg,#00e6dc,#39b8ff 60%,#8c6bf7); }+input[type=range] { width:100%; accent-color:var(--accent); cursor:pointer; }+label.chk { display:flex; align-items:center; gap:8px; padding:5px 0; cursor:pointer; color:var(--text); font-size:13px; }+label.chk input { accent-color:var(--accent); width:14px; height:14px; cursor:pointer; }+.hint { font-size:11.5px; color:var(--text-2); line-height:1.45; margin-top:6px; }+.btn {+  width:100%; margin-top:8px; padding:8px 10px; border-radius:8px; border:1px solid var(--border);+  background:var(--elevated); color:var(--text); font-family:'Satoshi'; font-size:13px; cursor:pointer;+  transition:all .15s ease;+}+.btn:hover { border-color:var(--accent); background:rgba(0,184,176,.08); }++/* ---------- stage + hero canvas ---------- */+#stage { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:flex-start; gap:18px; padding:22px; min-width:0; overflow:auto; }+.hero {+  aspect-ratio:16/10; position:relative; overflow:hidden;+  border-radius:12px; border:1px solid var(--border); container-type:inline-size;+  --ga:155deg;+}+#hero { width:100%; max-width:1000px; flex:0 0 auto; box-shadow:0 24px 70px rgba(0,0,0,.5); }++/* backgrounds: continue to the very bottom, per the new rule */+.bg { position:absolute; inset:0; }+/* mirror mode flips the whole background composition (glows, tiles, corner motifs) */+.hero[data-layout=mirror] .bg { transform:scaleX(-1); }+.hero[data-theme=midnight] .bg {+  background:+    radial-gradient(1200px 800px at 88% -10%, rgba(0,184,177,.22), transparent 60%),+    radial-gradient(900px 700px at -5% 110%, rgba(140,107,247,.18), transparent 55%),+    linear-gradient(var(--ga),#0a0e14 0%,#0d1117 45%,#071318 100%);+}+.hero[data-theme=midnight] .bg::after {+  content:''; position:absolute; inset:0;+  background-image:radial-gradient(rgba(255,255,255,.05) 1px, transparent 1.4px);+  background-size:2.2cqw 2.2cqw;+  -webkit-mask-image:radial-gradient(700px 560px at 30% 30%, #000, transparent 75%);+          mask-image:radial-gradient(700px 560px at 30% 30%, #000, transparent 75%);+}+.hero[data-theme=pcb] .bg {+  background:+    radial-gradient(1100px 760px at 85% -8%, rgba(0,230,220,.14), transparent 60%),+    linear-gradient(var(--ga),#031b19 0%,#04302c 55%,#02211e 100%);+}+.hero[data-theme=pcb] .bg::after {+  content:''; position:absolute; inset:0; opacity:.5;+  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160"><g stroke="%2300e6dc" stroke-opacity=".09" fill="none" stroke-width="1.4"><path d="M10 20 h56 l18 18 v50"/><path d="M84 150 v-36 l22 -22 h44"/><path d="M20 140 v-48 l14 -14"/><circle cx="10" cy="20" r="3.2"/><circle cx="84" cy="88" r="3.2"/><circle cx="150" cy="92" r="3.2"/><circle cx="20" cy="140" r="3.2"/><circle cx="34" cy="78" r="3.2"/></g></svg>');+  background-size:10cqw 10cqw;+}+.hero[data-theme=onion] .bg {+  background:+    radial-gradient(900px 700px at -5% 112%, rgba(140,107,247,.16), transparent 55%),+    linear-gradient(var(--ga),#0b1322 0%,#101a2c 55%,#0a111e 100%);+}+/* authentic Kickstand corner dome (brand deck onion motif), teal-to-purple stroke */+.hero[data-theme=onion] .bg::after {+  content:''; position:absolute; inset:0;+  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400"><defs><linearGradient id="g" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stop-color="%238c6bf7"/><stop offset=".55" stop-color="%2339b8ff"/><stop offset="1" stop-color="%2300e6dc"/></linearGradient></defs><g fill="none" stroke="url(%23g)" stroke-width="4" opacity=".5"><path d="M356 400 V234 Q356 60 200 60 Q44 60 44 234 V400"/><path d="M312 400 V250 Q312 110 200 110 Q88 110 88 250 V400"/><path d="M268 400 V266 Q268 160 200 160 Q132 160 132 266 V400"/><path d="M224 400 V282 Q224 210 200 210 Q176 210 176 282 V400"/></g></svg>');+  background-repeat:no-repeat;+  background-position:right -7cqw bottom -9cqw;+  background-size:50cqw auto;+}+.hero[data-theme=pill] .bg {+  background:+    radial-gradient(1000px 720px at 90% -8%, rgba(0,184,177,.12), transparent 60%),+    linear-gradient(var(--ga),#082a29 0%,#0c3a3c 55%,#062220 100%);+}+/* authentic pattern-pill: scattered capsule + circle outlines, mixed brand strokes */+.hero[data-theme=pill] .bg::after {+  content:''; position:absolute; inset:0;+  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="260" height="200"><g fill="none" stroke-width="2"><rect x="16" y="22" width="26" height="62" rx="13" stroke="%2364ABFF" stroke-opacity=".3"/><circle cx="112" cy="38" r="12" stroke="%2300e6dc" stroke-opacity=".28"/><rect x="196" y="14" width="28" height="72" rx="14" stroke="%2339b8ff" stroke-opacity=".3"/><rect x="84" y="128" width="62" height="26" rx="13" stroke="%2300b8b1" stroke-opacity=".28"/><circle cx="226" cy="150" r="10" stroke="%238c6bf7" stroke-opacity=".26"/><rect x="20" y="150" width="26" height="40" rx="13" stroke="%2300e6dc" stroke-opacity=".2"/></g></svg>');+  background-size:16cqw auto;+}+.hero[data-theme=grid] .bg {+  background:+    radial-gradient(1000px 700px at 85% -10%, rgba(0,230,220,.10), transparent 60%),+    linear-gradient(var(--ga),#07302e 0%,#0c3a3c 55%,#052220 100%);+}+/* authentic pattern-grid: fine outlined-square field */+.hero[data-theme=grid] .bg::after {+  content:''; position:absolute; inset:0; opacity:.55;+  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28"><rect x="4" y="4" width="18" height="18" fill="none" stroke="%23ffffff" stroke-opacity=".13" stroke-width="1.3"/></svg>');+  background-size:2.4cqw 2.4cqw;+}+.hero[data-theme=gradient] .bg {+  background:+    linear-gradient(155deg, rgba(5,10,18,.42), rgba(5,10,18,.14) 55%, rgba(5,10,18,.5)),+    linear-gradient(var(--ga),#00c8bf 0%,#2f9dfa 55%,#8c6bf7 100%);+}++/* ---------- image-zone text column ---------- */+.textzone {+  position:absolute; left:5.75cqw; top:8.5cqw; width:33cqw; z-index:3;+}+.hero[data-layout=mirror] .textzone { left:auto; right:5.75cqw; }+.wordmark { display:flex; align-items:center; gap:.7cqw; margin-bottom:2.2cqw; }+.wordmark svg { width:1.15cqw; height:1.15cqw; stroke:#e6edf3; stroke-width:2; fill:none; }+.wordmark span { font-family:'Familjen Grotesk'; font-weight:700; font-size:1.05cqw; letter-spacing:.34em; color:#e6edf3; }+.h-title {+  font-family:'Familjen Grotesk'; font-weight:700; font-size:4.1cqw; line-height:1.02;+  letter-spacing:-.09cqw; color:#e6edf3; margin-bottom:1.4cqw;+}+.h-sub { font-size:1.42cqw; line-height:1.48; color:#aeb8c4; margin-bottom:1.1cqw; }+.h-sub .accent { background:var(--grad); -webkit-background-clip:text; background-clip:text; color:transparent; font-weight:500; }+.h-sub b { color:#e6edf3; font-weight:500; }+.h-desc { font-size:1.05cqw; line-height:1.5; color:#8b949e; max-width:30cqw; }++/* ---------- the app-window mock (the "screenshot") ---------- */+.window {+  position:absolute; z-index:2; border-radius:.9cqw .9cqw 0 0; overflow:hidden;+  background:#10151c; border:1px solid rgba(255,255,255,.09);+  box-shadow:0 1.7cqw 5cqw rgba(0,0,0,.62), 0 0 8cqw rgba(0,184,177,.07);+  display:flex; flex-direction:column;+}+.hero[data-layout=bleed-bottom] .window { right:4.5cqw; top:10cqw; width:52cqw; height:56cqw; }+.hero[data-layout=bleed-right] .window { right:-8cqw; top:9cqw; width:56cqw; height:44cqw; border-radius:.9cqw 0 0 .9cqw; }+.hero[data-layout=bleed-top] .window { right:4.5cqw; top:-6cqw; width:52cqw; height:42cqw; border-radius:0 0 .9cqw .9cqw; }+.hero[data-layout=bleed-corner-br] .window { right:-7cqw; top:14cqw; width:56cqw; height:52cqw; border-radius:.9cqw 0 0 0; }+.hero[data-layout=bleed-corner-tr] .window { right:-7cqw; top:-6cqw; width:56cqw; height:44cqw; border-radius:0 0 0 .9cqw; }+.hero[data-layout=contained] .window { right:4.5cqw; top:8cqw; width:50cqw; height:33cqw; border-radius:.9cqw; }+.hero[data-layout=mirror] .window { left:4.5cqw; top:10cqw; width:52cqw; height:56cqw; }+.titlebar {+  flex:0 0 2.1cqw; background:#181f28; border-bottom:1px solid rgba(255,255,255,.06);+  display:flex; align-items:center; padding:0 .9cqw; gap:.6cqw;+}+.titlebar .t { font-family:'Satoshi'; font-size:.82cqw; color:#8b949e; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }+.winbtns { margin-left:auto; display:flex; gap:.9cqw; align-items:center; }+.winbtns svg { width:.72cqw; height:.72cqw; stroke:#8b949e; stroke-width:2; fill:none; }+.appbody { flex:1; display:flex; min-height:0; }+.sidebar { flex:0 0 11cqw; background:#131920; border-right:1px solid rgba(255,255,255,.05); padding:.8cqw .7cqw; }+.srow { height:1.5cqw; border-radius:.35cqw; background:rgba(255,255,255,.05); margin-bottom:.55cqw; }+.srow.on { background:rgba(0,184,176,.18); border:1px solid rgba(0,184,176,.35); }+.content { flex:1; padding:1cqw 1.1cqw; display:flex; flex-direction:column; gap:.8cqw; }+.crow { display:flex; gap:.6cqw; align-items:center; }+.chipk { height:1.3cqw; border-radius:999px; background:rgba(255,255,255,.07); }+.bars { flex:1; display:flex; align-items:flex-end; gap:.5cqw; min-height:0; }+.bar { flex:1; border-radius:.3cqw .3cqw 0 0; background:linear-gradient(180deg, rgba(0,230,220,.55), rgba(0,184,176,.12)); }+.tbl { display:flex; flex-direction:column; gap:.45cqw; }+.trow { display:flex; gap:.5cqw; }+.tcell { height:1.05cqw; border-radius:.25cqw; background:rgba(255,255,255,.06); }++/* ---------- the lower-third textual overlay ---------- */+.lower {+  position:absolute; left:0; right:0; bottom:0; height:33.4%; z-index:4;+  background:linear-gradient(180deg, rgba(10,14,20,0) 0%, rgba(10,14,20,.62) 22%, rgba(10,14,20,.94) 62%, rgba(10,14,20,.98) 100%);+  display:flex; align-items:flex-end;+}+.lowerin {+  width:100%; padding:0 5.75cqw 3.1cqw; display:flex; align-items:flex-end; gap:2cqw;+}+.lmain { min-width:0; flex:1; }+.badges { display:flex; align-items:center; gap:.9cqw; margin-bottom:1cqw; }+.pill {+  font-family:'Familjen Grotesk'; font-weight:700; font-size:.85cqw; letter-spacing:.14em;+  padding:.3cqw .85cqw; border-radius:999px; border:1px solid; text-transform:uppercase;+}+.pill.app { color:#00e6dc; border-color:rgba(0,230,220,.45); background:rgba(0,184,176,.10); }+.pill.skill { color:#b39dfb; border-color:rgba(140,107,247,.5); background:rgba(140,107,247,.12); }+.pill.bootstrap { color:#79b8ff; border-color:rgba(88,166,255,.5); background:rgba(88,166,255,.12); }+.pill.component { color:#e3b341; border-color:rgba(210,153,34,.5); background:rgba(210,153,34,.12); }+.obadge {+  display:flex; align-items:center; gap:.42cqw; font-family:'JetBrains Mono',monospace;+  font-size:.95cqw; color:#aeb8c4;+}+.obadge svg { width:1cqw; height:1cqw; stroke:#8b949e; stroke-width:1.8; fill:none; }+.obadge b { color:#e6edf3; font-weight:500; }+.l-title { display:block; font-family:'Familjen Grotesk'; font-weight:700; font-size:2.3cqw; line-height:1.1; color:#e6edf3; text-decoration:none; margin-bottom:.55cqw; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }+.l-title:hover { color:#00e6dc; }+.l-author { font-size:1.05cqw; color:#8b949e; }+.l-author a { color:#aeb8c4; text-decoration:none; }+.l-author a:hover { color:#00e6dc; }+.l-slug { flex:0 0 auto; font-family:'JetBrains Mono',monospace; font-size:.95cqw; color:#00b8b1; text-decoration:none; padding-bottom:.15cqw; }+.l-slug:hover { color:#00e6dc; }++/* guides */+.guide { position:absolute; left:0; right:0; top:66.6%; z-index:6; border-top:1.5px dashed rgba(0,230,220,.55); pointer-events:none; display:none; }+.guide span {+  position:absolute; right:1cqw; top:.4cqw; font-family:'JetBrains Mono',monospace; font-size:.8cqw;+  color:#00e6dc; background:rgba(10,14,20,.7); padding:.2cqw .6cqw; border-radius:.35cqw;+}+.hero.guides .guide { display:block; }++/* ---------- in-context previews (chrome copied 1:1 from git-wiki style.css) ---------- */+#contexts { display:flex; flex-wrap:wrap; gap:16px; width:100%; max-width:1000px; align-items:flex-start; }+.ctx { min-width:0; }+.ctxlabel { font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--text-2); margin-bottom:6px; }+.ctxframe { background:#0d1117; border:1px solid var(--border); border-radius:10px; padding:14px; }+.clone-slot { position:absolute; inset:0; }+.clone-slot .hero { width:100%; border-radius:0; border:none; }+.heroslot .hero { width:100%; border-radius:8px; }+.hero.preview .guide { display:none !important; }+.hero.preview a { pointer-events:none; }++/* wiki type badges (style.css .badge) */+#contexts .badge { display:inline-block; padding:2px 10px; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.5px; border-radius:12px; line-height:1.6; text-align:center; }+#contexts .badge-app { background:rgba(56,139,253,.15); color:#58a6ff; border:1px solid rgba(56,139,253,.3); }+#contexts .badge-skill { background:rgba(188,140,255,.15); color:#bc8cff; border:1px solid rgba(188,140,255,.3); }+#contexts .badge-component { background:rgba(210,153,34,.15); color:#d29922; border:1px solid rgba(210,153,34,.3); }+#contexts .badge-bootstrap { background:rgba(0,184,176,.15); color:#00b8b0; border:1px solid rgba(0,184,176,.3); }++/* homepage marquee (style.css .popular-marquee / .mq-* / .ts-slide-*) */+.mqwrap { height:430px; overflow:hidden; position:relative; }+.mqwrap::before, .mqwrap::after { content:""; position:absolute; left:0; right:0; height:64px; z-index:3; pointer-events:none; }+.mqwrap::before { top:0; background:linear-gradient(to bottom,#0d1117,transparent); }+.mqwrap::after { bottom:0; background:linear-gradient(to top,#0d1117,transparent); }+.mqcol { transform:perspective(900px) rotateY(15deg); }+.mq-track { display:flex; flex-direction:column; gap:20px; will-change:transform; animation:mq-roll-up 130s linear infinite; }+.mqwrap:hover .mq-track { animation-play-state:paused; }+@keyframes mq-roll-up { from { transform:translateY(0); } to { transform:translateY(-50%); } }+.ts-slide { position:relative; aspect-ratio:16/10; display:block; overflow:hidden; background:#161b22; color:#fff; border-radius:14px; flex:none; text-decoration:none; }+.ts-hero-fallback { width:100%; height:100%; display:flex; align-items:center; justify-content:center; padding:32px; text-align:center; background:linear-gradient(135deg,#0f766e,#1d4ed8 60%,#6d28d9); font-size:26px; font-weight:700; color:#fff; font-family:'Familjen Grotesk'; }+.ts-slide-shade { position:absolute; inset:0; background:linear-gradient(to top, rgba(5,9,14,.95) 0%, rgba(5,9,14,.82) 28%, rgba(5,9,14,.5) 55%, rgba(5,9,14,0) 82%); }+.ts-slide-info { position:absolute; left:0; right:0; bottom:0; padding:20px 26px; }+.ts-slide-badges { display:flex; align-items:center; gap:10px; margin-bottom:12px; }+#contexts .ts-slide-badges .badge { background:rgba(5,9,14,.72); box-shadow:0 1px 4px rgba(0,0,0,.4); }+.ts-slide-score { display:inline-flex; align-items:center; gap:4px; font-size:12px; font-weight:600; color:#fff; background:rgba(5,9,14,.72); padding:2px 9px; border-radius:10px; box-shadow:0 1px 4px rgba(0,0,0,.4); }+.ts-slide-stars { display:inline-flex; align-items:center; gap:3px; font-size:12px; color:rgba(255,255,255,.85); background:rgba(5,9,14,.72); padding:2px 9px; border-radius:10px; box-shadow:0 1px 4px rgba(0,0,0,.4); }+.ts-slide-title { font-size:23px; font-weight:700; margin:6px 0 3px; color:#fff; font-family:'Familjen Grotesk'; text-shadow:0 1px 2px rgba(0,0,0,.85), 0 2px 14px rgba(0,0,0,.55); }+.ts-slide-brief { font-size:15px; line-height:1.5; color:rgba(255,255,255,.88); display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; margin:0 0 4px; text-shadow:0 1px 2px rgba(0,0,0,.85), 0 2px 10px rgba(0,0,0,.6); }+.ts-slide-owner { font-size:12px; color:rgba(255,255,255,.72); text-shadow:0 1px 2px rgba(0,0,0,.85); }++/* index page card grid (style.css .page-grid / .page-card-*) */+.page-grid { display:grid; grid-template-columns:repeat(2, 1fr); gap:16px; }+.page-card { display:flex; flex-direction:column; text-decoration:none; color:#e6edf3; padding:0; overflow:hidden; background:#161b22; border:1px solid #30363d; border-radius:6px; transition:border-color .2s, box-shadow .2s; }+.page-card:hover { border-color:rgba(0,184,176,.3); box-shadow:0 4px 12px rgba(0,0,0,.2); }+.page-card-hero { aspect-ratio:16/10; background:#1c2128; display:flex; align-items:center; justify-content:center; overflow:hidden; border-bottom:1px solid #30363d; position:relative; }+.page-card-hero-placeholder { display:flex; align-items:center; justify-content:center; width:100%; height:100%; }+.page-card-body { padding:16px; display:flex; flex-direction:column; gap:6px; flex:1; }+.page-card-top { display:flex; align-items:center; gap:8px; }+.page-card-title { font-size:15px; font-weight:600; line-height:1.3; margin:0; }+.page-card-brief { color:#8b949e; font-size:13px; margin:0; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }+.page-card-meta { font-size:12px; color:#484f58; display:flex; align-items:center; gap:4px; margin-top:auto; }+.page-card-stars { display:inline-flex; align-items:center; gap:3px; }+.page-card-tags { display:flex; gap:4px; flex-wrap:wrap; margin-top:4px; }+.chip-sm { display:inline-block; padding:1px 8px; font-size:11px; background:rgba(0,184,176,.15); color:#00b8b0; border-radius:12px; border:1px solid rgba(0,184,176,.3); line-height:1.5; }+.page-card-install { margin-top:8px; padding:6px 8px; background:#0d1117; border:1px solid #30363d; border-radius:6px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#8b949e; display:flex; align-items:center; gap:8px; }+.page-card-install code { background:transparent; color:#8b949e; flex:1; min-width:0; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }+.dot-sep { color:#484f58; }++/* adom-desktop right bar (proposed v2 overlay style) */+.deskmock { background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:10px; display:flex; flex-direction:column; gap:10px; }+.deskhead { font-family:'Familjen Grotesk'; font-weight:500; font-size:11px; letter-spacing:.07em; text-transform:uppercase; color:var(--text-2); }++/* tooltip + toast */+#tip {+  position:fixed; z-index:99999; background:var(--overlay); border:1px solid var(--border);+  color:var(--text); font-size:12px; line-height:1.4; padding:6px 9px; border-radius:8px;+  max-width:240px; pointer-events:none; opacity:0; transform:translateY(3px);+  transition:opacity .18s ease, transform .18s ease; text-transform:none;+}+#tip.show { opacity:1; transform:none; }+#toasts { position:fixed; left:50%; bottom:18px; transform:translateX(-50%); z-index:99998; display:flex; flex-direction:column-reverse; gap:8px; }+.toast {+  display:flex; align-items:center; gap:8px; background:var(--overlay); border:1px solid var(--border);+  border-left:3px solid var(--accent); border-radius:8px; padding:9px 13px; font-size:13px;+  box-shadow:0 8px 30px rgba(0,0,0,.45); animation:tin .2s ease;+}+.toast.success { border-left-color:var(--green); }+.toast.error { border-left-color:var(--red); }+@keyframes tin { from { opacity:0; transform:translateY(6px);} }+</style>+</head>+<body>++<header>+  <div class="brand">+    <svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><line x1="3" y1="14.5" x2="21" y2="14.5"/><line x1="6" y1="17.5" x2="13" y2="17.5"/></svg>+    <span class="name">Hero Studio</span>+    <span class="sub">&middot; lower-third billboard lab</span>+  </div>+  <div class="vsep"></div>+  <div class="subject"><b id="subj-title">...</b><span class="meta" id="subj-meta"></span></div>+  <div class="actions">+    <button class="iconbtn" id="btn-guides" data-tip="Toggle the lower-third guide line on the canvas">+      <svg viewBox="0 0 24 24"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"/><circle cx="12" cy="12" r="2.6"/></svg>+    </button>+    <button class="iconbtn" id="btn-rand" data-tip="Randomize package, layout and theme">+      <svg viewBox="0 0 24 24"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>+    </button>+  </div>+</header>++<main>+  <div id="panel">+    <h3>Package (live wiki data)</h3>+    <select id="sel-page" data-tip="Real pages from adom-wiki page trending: title, type, score, stars, author"></select>++    <h3>Screenshot placement</h3>+    <div class="optgrid" id="grp-layout">+      <button class="opt" data-v="bleed-bottom">Bleed bottom</button>+      <button class="opt" data-v="bleed-right">Bleed right</button>+      <button class="opt" data-v="bleed-top">Bleed top</button>+      <button class="opt" data-v="bleed-corner-br">Corner lower-right</button>+      <button class="opt" data-v="bleed-corner-tr">Corner upper-right</button>+      <button class="opt" data-v="contained">Contained</button>+      <button class="opt" data-v="mirror">Mirror (text right)</button>+    </div>+    <div class="hint">The shot may run under the lower third; the overlay scrim covers it. That is the new rule.</div>++    <h3>Background theme</h3>+    <div class="swatches" id="grp-theme">+      <div class="swatch sw-midnight" data-v="midnight" data-tip="Midnight: near-black gradient + faint dot grid"></div>+      <div class="swatch sw-pcb" data-v="pcb" data-tip="PCB traces: deep teal + circuit field"></div>+      <div class="swatch sw-onion" data-v="onion" data-tip="Onion: navy + concentric arcs"></div>+      <div class="swatch sw-pill" data-v="pill" data-tip="Pill field: scattered capsule outlines from the brand tiling folder"></div>+      <div class="swatch sw-grid" data-v="grid" data-tip="Square grid: fine outlined-square field from the brand tiling folder"></div>+      <div class="swatch sw-grad" data-v="gradient" data-tip="Gradient: bold teal to blue to purple, for flagship launches"></div>+    </div>++    <h3>Gradient angle</h3>+    <input type="range" id="rng-angle" min="0" max="360" step="5" value="155" data-tip="Rotate the base gradient so each hero feels unique while staying on-brand">+    <div class="hint">Angle: <span id="angle-val">155</span> deg. Brand default is 155.</div>++    <h3>Image-zone text</h3>+    <label class="chk"><input type="checkbox" id="chk-logo"> Adom wordmark</label>+    <label class="chk"><input type="checkbox" id="chk-title"> Title (big headline)</label>+    <label class="chk"><input type="checkbox" id="chk-subtitle"> Value-prop subtitle</label>+    <label class="chk"><input type="checkbox" id="chk-description"> Detail line</label>++    <h3>Title placement</h3>+    <div class="optgrid" id="grp-titlemode">+      <button class="opt" data-v="overlay-only">Overlay only</button>+      <button class="opt" data-v="image-only">Image only</button>+      <button class="opt" data-v="both">Both</button>+    </div>+    <div class="hint">John's open question: if the big title lives in the image, the overlay title is redundant. Flip these to compare.</div>++    <h3>Overlay content</h3>+    <label class="chk"><input type="checkbox" id="chk-pill"> Type pill</label>+    <label class="chk"><input type="checkbox" id="chk-scores"> Trending + stars (dynamic)</label>+    <label class="chk"><input type="checkbox" id="chk-author"> Author</label>+    <label class="chk"><input type="checkbox" id="chk-slug"> Wiki slug, lower right</label>+    <button class="btn" id="btn-reset">Reset to proposed default</button>+  </div>++  <div id="stage">+    <div id="hero" class="hero" data-theme="midnight" data-layout="bleed-bottom">+      <div class="bg"></div>++      <div class="textzone">+        <div class="wordmark" id="el-logo">+          <svg viewBox="0 0 24 24"><path d="M12 3 3 21h18z"/></svg><span>ADOM</span>+        </div>+        <div class="h-title" id="el-title"></div>+        <div class="h-sub" id="el-sub"></div>+        <div class="h-desc" id="el-desc"></div>+      </div>++      <div class="window">+        <div class="titlebar">+          <span class="t" id="win-label"></span>+          <span class="winbtns">+            <svg viewBox="0 0 10 10"><line x1="1" y1="5" x2="9" y2="5"/></svg>+            <svg viewBox="0 0 10 10"><rect x="1.5" y="1.5" width="7" height="7"/></svg>+            <svg viewBox="0 0 10 10"><line x1="1.5" y1="1.5" x2="8.5" y2="8.5"/><line x1="8.5" y1="1.5" x2="1.5" y2="8.5"/></svg>+          </span>+        </div>+        <div class="appbody">+          <div class="sidebar">+            <div class="srow on"></div><div class="srow"></div><div class="srow"></div>+            <div class="srow"></div><div class="srow"></div><div class="srow"></div><div class="srow"></div>+          </div>+          <div class="content">+            <div class="crow"><span class="chipk" style="width:22%"></span><span class="chipk" style="width:12%"></span><span class="chipk" style="width:16%"></span></div>+            <div class="bars">+              <div class="bar" style="height:42%"></div><div class="bar" style="height:66%"></div>+              <div class="bar" style="height:35%"></div><div class="bar" style="height:82%"></div>+              <div class="bar" style="height:58%"></div><div class="bar" style="height:91%"></div>+              <div class="bar" style="height:49%"></div><div class="bar" style="height:73%"></div>+            </div>+            <div class="tbl">+              <div class="trow"><span class="tcell" style="flex:2"></span><span class="tcell" style="flex:1"></span><span class="tcell" style="flex:1"></span></div>+              <div class="trow"><span class="tcell" style="flex:2"></span><span class="tcell" style="flex:1"></span><span class="tcell" style="flex:1"></span></div>+              <div class="trow"><span class="tcell" style="flex:2"></span><span class="tcell" style="flex:1"></span><span class="tcell" style="flex:1"></span></div>+            </div>+          </div>+        </div>+      </div>++      <div class="lower">+        <div class="lowerin">+          <div class="lmain">+            <div class="badges">+              <span class="pill app" id="l-pill">APP</span>+              <span class="obadge" id="l-trend" data-tip="Composite trending score from the wiki, recomputed continuously">+                <svg viewBox="0 0 24 24"><polyline points="3 17 9 11 13 15 21 7"/><polyline points="15 7 21 7 21 13"/></svg>+                <b></b>+              </span>+              <span class="obadge" id="l-stars" data-tip="Live star count">+                <svg viewBox="0 0 24 24"><path d="M12 3l2.7 5.6 6.1.8-4.5 4.2 1.1 6-5.4-3-5.4 3 1.1-6L3.2 9.4l6.1-.8z"/></svg>+                <b></b>+              </span>+              <span class="obadge" id="l-installs" data-tip="Installs in the last 30 days">+                <svg viewBox="0 0 24 24"><path d="M12 4v11"/><polyline points="7 10 12 15 17 10"/><path d="M4 19h16"/></svg>+                <b></b>+              </span>+            </div>+            <a class="l-title" id="l-title" target="_blank" href="#"></a>+            <div class="l-author" id="l-author"></div>+          </div>+          <a class="l-slug" id="l-slug" target="_blank" href="#"></a>+        </div>+      </div>++      <div class="guide"><span>overlay zone: lower 1/3, true text only</span></div>+    </div>++    <div id="contexts">+      <div class="ctx" style="flex:1 1 46%">+        <div class="ctxlabel">In context: wiki homepage marquee</div>+        <div class="ctxframe mqwrap"><div class="mqcol"><div class="mq-track" id="mq-track"></div></div></div>+      </div>+      <div class="ctx" style="flex:1 1 46%">+        <div class="ctxlabel">In context: adom-desktop right bar (proposed v2 style)</div>+        <div class="deskmock">+          <div class="deskhead">Trending on the wiki</div>+          <div class="heroslot"></div>+        </div>+      </div>+      <div class="ctx" style="flex:1 1 100%">+        <div class="ctxlabel">In context: wiki index page (/apps grid)</div>+        <div class="ctxframe"><div class="page-grid" id="wikigrid"></div></div>+      </div>+    </div>+  </div>+</main>++<div id="tip"></div>+<div id="toasts"></div>++<script>+/* console relay: the AI reads GET /console instead of DevTools */+(function () {+  const post = (level, args) => fetch('console', { method:'POST', headers:{'Content-Type':'application/json'},+    body: JSON.stringify({ level, text: args.map(a => { try { return typeof a === 'string' ? a : JSON.stringify(a); } catch { return String(a); } }).join(' ') }) }).catch(()=>{});+  ['log','warn','error'].forEach(k => { const o = console[k].bind(console); console[k] = (...a) => { o(...a); post(k, a); }; });+  window.onerror = (m, s, l) => post('error', [m + ' @' + s + ':' + l]);+  window.addEventListener('unhandledrejection', e => post('error', ['unhandledrejection: ' + e.reason]));+})();++let pages = [];+let state = null;++const $ = (id) => document.getElementById(id);+const send = (patch) => fetch('state', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(patch) }).catch(()=>{});++function pageByKey(key) { return pages.find(p => p.owner + '/' + p.slug === key) || pages[0]; }++function firstSentence(s, max) {+  let t = (s || '').split(/(?<=\.)\s/)[0] || s || '';+  if (t.length > max) t = t.slice(0, max).replace(/\s+\S*$/, '') + '...';+  return t;+}++function accentize(text) {+  const words = text.split(' ');+  if (words.length < 3) return '<span class="accent">' + text + '</span>';+  return '<span class="accent">' + words.slice(0, 2).join(' ') + '</span> ' + words.slice(2).join(' ');+}++function render() {+  if (!state || !pages.length) return;+  const p = pageByKey(state.page);+  const hero = $('hero');+  hero.dataset.theme = state.theme;+  hero.dataset.layout = state.layout;+  hero.classList.toggle('guides', !!state.guides);+  hero.style.setProperty('--ga', (state.gradAngle || 155) + 'deg');+  $('rng-angle').value = state.gradAngle || 155;+  $('angle-val').textContent = state.gradAngle || 155;++  // header subject+  $('subj-title').textContent = p.title;+  $('subj-meta').textContent = p.type + ' · score ' + p.score + ' · ' + p.stars + ' stars · ' + p.installs30d + ' installs/30d';+  $('btn-guides').classList.toggle('on', !!state.guides);++  // image zone+  const showTitleInImage = state.show.title && state.titleMode !== 'overlay-only';+  $('el-logo').style.display = state.show.logo ? 'flex' : 'none';+  $('el-title').style.display = showTitleInImage ? 'block' : 'none';+  $('el-title').textContent = p.title;+  $('el-sub').style.display = state.show.subtitle ? 'block' : 'none';+  $('el-sub').innerHTML = accentize(firstSentence(p.brief, 80));+  $('el-desc').style.display = state.show.description ? 'block' : 'none';+  $('el-desc').textContent = firstSentence(p.brief.slice(80), 110);+  $('win-label').textContent = p.title + ' · Adom webview';++  // lower third overlay+  const showTitleInOverlay = state.titleMode !== 'image-only';+  $('l-pill').style.display = state.overlay.pill ? '' : 'none';+  $('l-pill').textContent = p.type.toUpperCase();+  $('l-pill').className = 'pill ' + p.type;+  $('l-trend').style.display = state.overlay.scores ? '' : 'none';+  $('l-trend').querySelector('b').textContent = p.score.toLocaleString('en-US');+  $('l-stars').style.display = state.overlay.scores ? '' : 'none';+  $('l-stars').querySelector('b').textContent = p.stars;+  $('l-installs').style.display = state.overlay.scores ? '' : 'none';+  $('l-installs').querySelector('b').textContent = p.installs30d;+  const url = 'https://wiki.adom.inc/' + p.owner + '/' + p.slug;+  $('l-title').style.display = showTitleInOverlay ? 'block' : 'none';+  $('l-title').textContent = p.title;+  $('l-title').href = url;+  $('l-author').style.display = state.overlay.author ? '' : 'none';+  $('l-author').innerHTML = 'by <a href="' + url + '" target="_blank">' + p.author + '</a>';+  $('l-slug').style.display = state.overlay.slug ? '' : 'none';+  $('l-slug').textContent = 'wiki.adom.inc/' + p.owner + '/' + p.slug;+  $('l-slug').href = url;++  // controls reflect state+  $('sel-page').value = state.page;+  document.querySelectorAll('#grp-layout .opt').forEach(b => b.classList.toggle('on', b.dataset.v === state.layout));+  document.querySelectorAll('#grp-theme .swatch').forEach(b => b.classList.toggle('on', b.dataset.v === state.theme));+  document.querySelectorAll('#grp-titlemode .opt').forEach(b => b.classList.toggle('on', b.dataset.v === state.titleMode));+  $('chk-logo').checked = state.show.logo;+  $('chk-title').checked = state.show.title;+  $('chk-subtitle').checked = state.show.subtitle;+  $('chk-description').checked = state.show.description;+  $('chk-pill').checked = state.overlay.pill;+  $('chk-scores').checked = state.overlay.scores;+  $('chk-author').checked = state.overlay.author;+  $('chk-slug').checked = state.overlay.slug;++  syncPreviews();+}++/* in-context previews: markup mirrors git-wiki lib/templates.js (popularMarquee + categoryListPage) */+const STAR12 = '<svg width="12" height="12" viewBox="0 0 24 24" fill="#d29922" stroke="#d29922" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>';+const TREND13 = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg>';+const IMGPH = '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#484f58" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>';++function relTime(iso) {+  if (!iso) return '';+  const s = (Date.now() - new Date(iso).getTime()) / 1000;+  if (s < 3600) return Math.max(1, Math.floor(s / 60)) + 'm ago';+  if (s < 86400) return Math.floor(s / 3600) + 'h ago';+  return Math.floor(s / 86400) + 'd ago';+}++function marqueeTile(p, isLive) {+  const heroPart = isLive ? '<div class="clone-slot"></div>'+    : '<div class="ts-hero-fallback"><span>' + p.title + '</span></div>';+  return '<a class="ts-slide mq-slide">' + heroPart ++    '<div class="ts-slide-shade"></div><div class="ts-slide-info">' ++    '<div class="ts-slide-badges"><span class="badge badge-' + p.type + '">' + p.type + '</span>' ++    (p.score > 0 ? '<span class="ts-slide-score">' + TREND13 + ' ' + p.score + '</span>' : '') ++    (p.stars > 0 ? '<span class="ts-slide-stars">' + STAR12 + ' ' + p.stars + '</span>' : '') ++    '</div><h3 class="ts-slide-title">' + p.title + '</h3>' ++    (p.brief ? '<p class="ts-slide-brief">' + p.brief + '</p>' : '') ++    '<span class="ts-slide-owner">' + p.owner + '</span></div></a>';+}++function indexCard(p, isLive) {+  const hero = isLive ? '<div class="clone-slot"></div>'+    : '<div class="page-card-hero-placeholder">' + IMGPH + '</div>';+  return '<a class="page-card">' ++    '<div class="page-card-hero">' + hero + '</div>' ++    '<div class="page-card-body">' ++    '<div class="page-card-top"><span class="badge badge-' + p.type + '">' + p.type + '</span></div>' ++    '<h3 class="page-card-title">' + p.title + '</h3>' ++    (p.brief ? '<p class="page-card-brief">' + p.brief + '</p>' : '') ++    '<div class="page-card-meta"><span>' + p.owner + '</span>' ++    (p.updated_at ? '<span class="dot-sep">&middot;</span><span>' + relTime(p.updated_at) + '</span>' : '') ++    (p.stars > 0 ? '<span class="dot-sep">&middot;</span><span class="page-card-stars">' + STAR12 + ' ' + p.stars + '</span>' : '') ++    '</div>' ++    (p.tags && p.tags.length ? '<div class="page-card-tags">' + p.tags.map(t => '<span class="chip-sm">' + t + '</span>').join('') + '</div>' : '') ++    '<div class="page-card-install"><code>adom-wiki pkg install ' + p.owner + '/' + p.slug + '</code></div>' ++    '</div></a>';+}++function cloneHero(imageOnly) {+  const c = $('hero').cloneNode(true);+  c.removeAttribute('id');+  c.querySelectorAll('[id]').forEach(n => n.removeAttribute('id'));+  c.classList.add('preview');+  c.classList.remove('guides');+  if (imageOnly) { const l = c.querySelector('.lower'); if (l) l.remove(); }+  return c;+}++function syncPreviews() {+  const p = pageByKey(state.page);+  const nb = pages.filter(x => x !== p).slice(0, 3);+  // marquee: live tile among real neighbors, content doubled for the -50% loop+  const seq = marqueeTile(p, true) + nb.map(x => marqueeTile(x)).join('');+  $('mq-track').innerHTML = seq + seq;+  // index grid: live card first, then real neighbor cards (no-hero placeholder icon)+  $('wikigrid').innerHTML = indexCard(p, true) + nb.map(x => indexCard(x)).join('');+  // wiki surfaces get the IMAGE PART only (the wiki overlays its own text);+  // the desk bar gets the full v2 hero with its baked-in text overlay+  document.querySelectorAll('#contexts .clone-slot').forEach(s => s.appendChild(cloneHero(true)));+  document.querySelectorAll('#contexts .heroslot').forEach(s => { s.innerHTML = ''; s.appendChild(cloneHero(false)); });+}++let seenToastSeq = 0;+function handleToast(t) {+  if (t && t.seq > seenToastSeq) { seenToastSeq = t.seq; toast(t.text, t.kind); }+}++function toast(text, kind) {+  const el = document.createElement('div');+  el.className = 'toast ' + (kind || 'info');+  el.textContent = text;+  $('toasts').appendChild(el);+  setTimeout(() => el.remove(), 4000);+}++/* wire controls: server owns the truth, SSE echoes back */+$('sel-page').addEventListener('change', e => { send({ page: e.target.value }); });+document.querySelectorAll('#grp-layout .opt').forEach(b => b.addEventListener('click', () => send({ layout: b.dataset.v })));+document.querySelectorAll('#grp-theme .swatch').forEach(b => b.addEventListener('click', () => send({ theme: b.dataset.v })));+document.querySelectorAll('#grp-titlemode .opt').forEach(b => b.addEventListener('click', () => send({ titleMode: b.dataset.v })));+$('chk-logo').addEventListener('change', e => send({ show: { logo: e.target.checked } }));+$('chk-title').addEventListener('change', e => send({ show: { title: e.target.checked } }));+$('chk-subtitle').addEventListener('change', e => send({ show: { subtitle: e.target.checked } }));+$('chk-description').addEventListener('change', e => send({ show: { description: e.target.checked } }));+$('chk-pill').addEventListener('change', e => send({ overlay: { pill: e.target.checked } }));+$('chk-scores').addEventListener('change', e => send({ overlay: { scores: e.target.checked } }));+$('chk-author').addEventListener('change', e => send({ overlay: { author: e.target.checked } }));+$('chk-slug').addEventListener('change', e => send({ overlay: { slug: e.target.checked } }));+$('btn-guides').addEventListener('click', () => send({ guides: !state.guides }));+let angleTimer = null;+$('rng-angle').addEventListener('input', e => {+  $('angle-val').textContent = e.target.value;+  $('hero').style.setProperty('--ga', e.target.value + 'deg'); // instant local feedback+  clearTimeout(angleTimer);+  angleTimer = setTimeout(() => send({ gradAngle: +e.target.value }), 120);+});+$('btn-rand').addEventListener('click', () => {+  const layouts = ['bleed-bottom','bleed-right','bleed-top','bleed-corner-br','bleed-corner-tr','contained','mirror'];+  const themes = ['midnight','pcb','onion','pill','grid','gradient'];+  const angles = [65, 100, 155, 210, 245, 290, 335];+  const p = pages[Math.floor(Math.random() * pages.length)];+  send({ page: p.owner + '/' + p.slug, layout: layouts[Math.floor(Math.random()*layouts.length)],+    theme: themes[Math.floor(Math.random()*themes.length)], gradAngle: angles[Math.floor(Math.random()*angles.length)] });+  toast('Randomized: ' + p.title, 'info');+});+$('btn-reset').addEventListener('click', () => {+  send({ layout:'bleed-bottom', theme:'midnight', titleMode:'overlay-only', gradAngle:155,+    show:{ logo:true, title:true, subtitle:true, description:false },+    overlay:{ pill:true, scores:true, author:true, slug:true }, guides:true });+  toast('Reset to the proposed default: overlay title, image keeps value-prop', 'success');+});++/* tooltips: fixed div, 600ms delay, clamped to viewport on both axes */+(function () {+  const tip = $('tip');+  let timer = null;+  document.addEventListener('mouseover', e => {+    const t = e.target.closest('[data-tip]');+    if (!t) return;+    clearTimeout(timer);+    timer = setTimeout(() => {+      tip.textContent = t.getAttribute('data-tip');+      tip.classList.add('show');+      const r = t.getBoundingClientRect();+      let x = r.left + 14, y = r.bottom + 10;+      const w = tip.offsetWidth, h = tip.offsetHeight, M = 8;+      if (y + h > innerHeight - M) y = r.top - h - 10;+      x = Math.max(M, Math.min(x, innerWidth - w - M));+      y = Math.max(M, Math.min(y, innerHeight - h - M));+      tip.style.left = x + 'px'; tip.style.top = y + 'px';+    }, 600);+  });+  document.addEventListener('mouseout', e => {+    if (e.target.closest && e.target.closest('[data-tip]')) { clearTimeout(timer); tip.classList.remove('show'); }+  });+})();++/* boot: load real pages, subscribe to the live channel */+Promise.all([+  fetch('api/pages').then(r => r.json()),+  fetch('state').then(r => r.json()),+]).then(([pg, st]) => {+  pages = pg;+  state = st;+  seenToastSeq = (st._toast && st._toast.seq) || 0; // don't replay a stale toast on load+  const sel = $('sel-page');+  sel.innerHTML = pages.map(p =>+    '<option value="' + p.owner + '/' + p.slug + '">' + p.title + ' (' + p.type + ')</option>').join('');+  render();+  console.log('hero-studio ready: ' + pages.length + ' live pages');+  if (location.hash) {+    const el = document.querySelector(location.hash);+    if (el) setTimeout(() => el.scrollIntoView({ block: 'start' }), 300);+  }+  const es = new EventSource('events');+  es.addEventListener('state', e => { state = JSON.parse(e.data); render(); });+  es.addEventListener('toast', e => { handleToast(JSON.parse(e.data)); });+  // poll fallback in case the proxy buffers the event stream+  setInterval(() => {+    fetch('state').then(r => r.json()).then(st => {+      handleToast(st._toast);+      if (JSON.stringify(st) !== JSON.stringify(state)) { state = st; render(); }+    }).catch(()=>{});+  }, 1000);+});+</script>+</body>+</html>
refresh-data.shadded+25
@@ -0,0 +1,25 @@+#!/bin/sh+# Rebuild data/pages.json from live wiki trending data (needs the adom-wiki CLI).+set -e+DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"+adom-wiki page trending --json > /tmp/hero-studio-trending.json+node -e '+const fs = require("fs");+const d = JSON.parse(fs.readFileSync("/tmp/hero-studio-trending.json", "utf8"));+const clean = (s) => (s || "").replace(/—/g, ",").replace(/–/g, "-").trim();+const out = d.data.pages.map((p) => ({+  owner: p.owner, slug: p.slug, type: p.type,+  title: clean(p.title || p.slug),+  author: p.author_name || p.owner,+  score: p.composite_score || 0,+  stars: p.star_count || 0,+  installs30d: p.install_count_30d || 0,+  brief: clean(p.brief).slice(0, 220),+  tags: (() => { try { return JSON.parse(p.tags || "[]").slice(0, 4); } catch { return []; } })(),+  updated_at: p.updated_at,+}));+fs.writeFileSync(process.argv[1] + "/data/pages.json", JSON.stringify(out, null, 1));+console.log("wrote " + out.length + " pages");+' "$DIR"+rm -f /tmp/hero-studio-trending.json+echo "OK: data refreshed, restart the server to pick it up"
server.jsadded+129
@@ -0,0 +1,129 @@+// Hero Studio server: state, SSE live channel, console relay, toast hook, static UI.+const http = require('http');+const fs = require('fs');+const path = require('path');++const PORT = 8846;+const ROOT = path.join(__dirname, 'public');+const PAGES = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'pages.json'), 'utf8'));++let state = {+  page: 'adom/adom-tts',+  layout: 'bleed-bottom',   // bleed-bottom | bleed-right | bleed-top | bleed-corner-br | bleed-corner-tr | contained | mirror+  theme: 'midnight',        // midnight | pcb | onion | pill | gradient+  show: { logo: true, title: true, subtitle: true, description: false },+  titleMode: 'overlay-only', // overlay-only | image-only | both+  overlay: { pill: true, scores: true, author: true, slug: true },+  gradAngle: 155,+  guides: true,+};++const consoleBuf = [];+const clients = new Set();+let toastSeq = 0;+let lastToast = null;++function broadcast(event, data) {+  const msg = 'event: ' + event + '\ndata: ' + JSON.stringify(data) + '\n\n';+  for (const res of clients) res.write(msg);+}++function logLine(level, text) {+  consoleBuf.push({ t: new Date().toISOString(), level, text });+  if (consoleBuf.length > 500) consoleBuf.shift();+}++function merge(dst, src) {+  for (const k of Object.keys(src)) {+    if (src[k] && typeof src[k] === 'object' && !Array.isArray(src[k]) && dst[k] && typeof dst[k] === 'object') {+      merge(dst[k], src[k]);+    } else {+      dst[k] = src[k];+    }+  }+}++function readBody(req, cb) {+  let b = '';+  req.on('data', (c) => { b += c; });+  req.on('end', () => {+    try { cb(null, b ? JSON.parse(b) : {}); } catch (e) { cb(e); }+  });+}++const MIME = {+  '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css',+  '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png',+  '.woff2': 'font/woff2',+};++const server = http.createServer((req, res) => {+  const url = new URL(req.url, 'http://x');+  const p = url.pathname;++  if (p === '/state' && req.method === 'GET') {+    res.writeHead(200, { 'Content-Type': 'application/json' });+    return res.end(JSON.stringify(Object.assign({}, state, { _toast: lastToast })));+  }+  if (p === '/state' && req.method === 'POST') {+    return readBody(req, (err, body) => {+      if (err) { res.writeHead(200); return res.end(JSON.stringify({ ok: false, error: 'bad json' })); }+      merge(state, body);+      logLine('info', 'state <- ' + JSON.stringify(body));+      broadcast('state', state);+      res.writeHead(200, { 'Content-Type': 'application/json' });+      res.end(JSON.stringify({ ok: true, state }));+    });+  }+  if (p === '/events') {+    res.writeHead(200, {+      'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive',+      'X-Accel-Buffering': 'no',+    });+    res.write(':' + ' '.repeat(2048) + '\n\n'); // flush proxy buffers+    res.write('event: state\ndata: ' + JSON.stringify(state) + '\n\n');+    clients.add(res);+    const hb = setInterval(() => res.write(': hb\n\n'), 15000);+    req.on('close', () => { clearInterval(hb); clients.delete(res); });+    return;+  }+  if (p === '/ui/toast' && req.method === 'POST') {+    return readBody(req, (err, body) => {+      toastSeq += 1;+      const t = { text: (body && body.text) || '...', kind: (body && body.kind) || 'info', seq: toastSeq };+      lastToast = t;+      logLine('toast', t.text);+      broadcast('toast', t);+      res.writeHead(200, { 'Content-Type': 'application/json' });+      res.end(JSON.stringify({ ok: true }));+    });+  }+  if (p === '/console' && req.method === 'GET') {+    res.writeHead(200, { 'Content-Type': 'application/json' });+    return res.end(JSON.stringify(consoleBuf));+  }+  if (p === '/console' && req.method === 'POST') {+    return readBody(req, (err, body) => {+      if (!err && body) logLine(body.level || 'log', String(body.text || ''));+      res.writeHead(200, { 'Content-Type': 'application/json' });+      res.end(JSON.stringify({ ok: true }));+    });+  }+  if (p === '/api/pages') {+    res.writeHead(200, { 'Content-Type': 'application/json' });+    return res.end(JSON.stringify(PAGES));+  }++  // static+  let file = p === '/' ? '/index.html' : p;+  file = path.normalize(file).replace(/^(\.\.[/\\])+/, '');+  const full = path.join(ROOT, file);+  if (!full.startsWith(ROOT)) { res.writeHead(404); return res.end('not found'); }+  fs.readFile(full, (err, buf) => {+    if (err) { res.writeHead(404); return res.end('not found'); }+    res.writeHead(200, { 'Content-Type': MIME[path.extname(full)] || 'application/octet-stream' });+    res.end(buf);+  });+});++server.listen(PORT, () => console.log('hero-studio on ' + PORT));
uninstall.shadded+3
@@ -0,0 +1,3 @@+#!/bin/sh+rm -f "$HOME/.local/bin/hero-studio"+echo "uninstalled: hero-studio"