Styling fixes and Olden Era mmodule

This commit is contained in:
Jakub Zych
2026-05-21 15:00:12 +02:00
parent 4ceff4015a
commit dd3493921b
136 changed files with 1028 additions and 111 deletions

View File

@@ -0,0 +1,259 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OPHI-118 / control panel</title>
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}">
<link rel="stylesheet" href="{{ asset('css/hud.css') }}">
<style>
html, body {
color: var(--term-fg);
font-family: var(--mono);
}
body.page-shell {
display: flex;
flex-direction: column;
overflow: hidden;
}
.banner { margin-bottom: 28px; }
/* ---------- Banner / lede ---------- */
.banner h1 {
color: var(--term-fg-bright);
text-shadow: 0 0 12px var(--term-glow);
font-size: 28px;
font-weight: 700;
letter-spacing: 0.22em;
margin-bottom: 6px;
}
.banner .lede {
color: var(--term-fg-dim);
font-size: 13px;
letter-spacing: 0.04em;
}
.banner .lede .caret { color: var(--term-fg-bright); margin-right: 8px; }
/* ---------- Tile grid ---------- */
.tiles {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 22px;
flex: 1 1 auto;
min-height: 0;
}
.tile {
--tile-fg: var(--term-fg);
--tile-fg-hi: var(--term-fg-bright);
--tile-dim: var(--term-fg-dim);
--tile-edge: var(--term-edge);
--tile-glow: var(--term-glow);
position: relative;
background:
linear-gradient(180deg, rgba(80,220,100,0.04) 0%, rgba(0,0,0,0) 60%),
var(--term-bg);
border: 2px solid var(--tile-edge);
border-radius: 6px;
padding: 26px 30px 22px;
color: var(--tile-fg);
text-decoration: none;
display: grid;
grid-template-rows: auto 1fr auto;
gap: 14px;
overflow: hidden;
transition: border-color 140ms ease, transform 140ms ease, box-shadow 140ms ease;
box-shadow:
0 0 30px rgba(80, 220, 100, 0.04),
inset 0 0 60px rgba(0, 30, 0, 0.55);
}
.tile:hover {
border-color: var(--tile-fg-hi);
box-shadow:
0 0 40px var(--tile-glow),
inset 0 0 60px rgba(0, 30, 0, 0.55);
transform: translateY(-2px);
}
.tile:hover .tile-title { text-shadow: 0 0 14px var(--tile-glow); }
.tile::after {
content: '';
position: absolute; inset: 0;
pointer-events: none;
background: repeating-linear-gradient(
to bottom,
rgba(0,0,0,0) 0px,
rgba(0,0,0,0) 2px,
rgba(0,0,0,0.22) 3px,
rgba(0,0,0,0.22) 4px
);
opacity: 0.45;
mix-blend-mode: multiply;
}
/* HUD-cyan variant for the scenes tile */
.tile.cyan {
--tile-fg: var(--hud);
--tile-fg-hi: #c4ecff;
--tile-dim: var(--hud-dim);
--tile-edge: rgba(79, 210, 255, 0.45);
--tile-glow: rgba(79, 210, 255, 0.55);
background:
linear-gradient(180deg, rgba(79,210,255,0.05) 0%, rgba(0,0,0,0) 60%),
var(--term-bg);
color: var(--tile-fg);
}
/* Warn-yellow variant for external links */
.tile.warn {
--tile-fg: var(--warn);
--tile-fg-hi: #ffe580;
--tile-dim: rgba(255, 208, 0, 0.55);
--tile-edge: rgba(255, 208, 0, 0.45);
--tile-glow: rgba(255, 208, 0, 0.55);
background:
linear-gradient(180deg, rgba(255,208,0,0.04) 0%, rgba(0,0,0,0) 60%),
var(--term-bg);
color: var(--tile-fg);
}
.tile-head {
display: flex; align-items: baseline; justify-content: space-between;
gap: 12px;
color: var(--tile-dim);
font-size: 11px; letter-spacing: 0.20em;
}
.tile-slug { color: var(--tile-fg-hi); }
.tile-badge {
border: 1px solid var(--tile-edge);
padding: 2px 8px;
border-radius: 3px;
color: var(--tile-fg);
background: rgba(0, 0, 0, 0.35);
}
.tile-title {
color: var(--tile-fg-hi);
font-size: 38px;
font-weight: 700;
letter-spacing: 0.04em;
line-height: 1.05;
text-shadow: 0 0 8px var(--tile-glow);
transition: text-shadow 140ms ease;
}
.tile-desc {
color: var(--tile-fg);
opacity: 0.85;
font-size: 13px;
line-height: 1.55;
max-width: 90%;
}
.tile-foot {
display: flex; align-items: center; justify-content: space-between;
color: var(--tile-dim);
font-size: 12px; letter-spacing: 0.08em;
}
.tile-target {
color: var(--tile-fg-hi);
font-weight: 700;
}
.tile-target .caret { color: var(--tile-fg); margin-right: 6px; }
.tile-foot .arrow { font-size: 18px; color: var(--tile-fg-hi); }
/* ---------- CRT overlays (full-viewport) ---------- */
.scanlines, .vignette, .flicker { position: fixed; z-index: 999; }
@media (max-width: 1100px) {
body.page-shell { padding: 20px 28px; }
.tiles { grid-template-columns: 1fr; grid-template-rows: auto; }
.tile-title { font-size: 30px; }
}
</style>
</head>
<body class="page-shell">
@include('partials.topbar', ['crumb' => 'CONTROL PANEL'])
<section class="banner">
<h1>STREAMING CONTROL</h1>
<div class="lede"><span class="caret"></span>select an operation</div>
</section>
<main class="tiles">
<a class="tile" href="/onboard">
<div class="tile-head">
<span><span class="tile-slug">[01]</span> &nbsp; PRE-FLIGHT</span>
<span class="tile-badge">INTERNAL</span>
</div>
<div>
<div class="tile-title">ONBOARD</div>
<div class="tile-desc">Compile a Project Loading manifest game, subtitle, countdown, camera/mic state. Pushes the channel title + category to Twitch.</div>
</div>
<div class="tile-foot">
<span class="tile-target"><span class="caret"></span>/onboard</span>
<span class="arrow"></span>
</div>
</a>
<a class="tile cyan" href="/scenes">
<div class="tile-head">
<span><span class="tile-slug">[02]</span> &nbsp; ROUTES</span>
<span class="tile-badge">INTERNAL</span>
</div>
<div>
<div class="tile-title">SCENES</div>
<div class="tile-desc">Every overlay URL OBS browser-sources point at, plus the data endpoints (playlist, cover, audio stream).</div>
</div>
<div class="tile-foot">
<span class="tile-target"><span class="caret"></span>/scenes</span>
<span class="arrow"></span>
</div>
</a>
<a class="tile warn" href="https://ophi118.com" target="_blank" rel="noopener">
<div class="tile-head">
<span><span class="tile-slug">[03]</span> &nbsp; LANDING</span>
<span class="tile-badge">EXTERNAL </span>
</div>
<div>
<div class="tile-title">OPHI118.COM</div>
<div class="tile-desc">Public landing page for the OPHI-118 broadcast. Bio, schedule, social links.</div>
</div>
<div class="tile-foot">
<span class="tile-target"><span class="caret"></span>ophi118.com</span>
<span class="arrow"></span>
</div>
</a>
<a class="tile warn" href="https://gamez.ciemnosc.com" target="_blank" rel="noopener">
<div class="tile-head">
<span><span class="tile-slug">[04]</span> &nbsp; SERVERS</span>
<span class="tile-badge">EXTERNAL </span>
</div>
<div>
<div class="tile-title">GAMEZ.CIEMNOSC.COM</div>
<div class="tile-desc">Roster of the self-hosted game servers currently live connect strings, mod lists, player counts.</div>
</div>
<div class="tile-foot">
<span class="tile-target"><span class="caret"></span>gamez.ciemnosc.com</span>
<span class="arrow"></span>
</div>
</a>
</main>
<div class="scanlines"></div>
<div class="vignette"></div>
<div class="flicker"></div>
<script>
// UTC clock — same look as the in-scene HUD strip.
const clock = document.getElementById('clock');
function tick() {
const d = new Date();
const pad = n => String(n).padStart(2, '0');
clock.textContent = `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
}
tick();
setInterval(tick, 1000);
</script>
</body>
</html>

View File

@@ -1,34 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OPHI-118 / overlays</title>
<style>
body { background:#0a0e0a; color:#5fdc62; font:14px/1.6 monospace; padding:24px; }
a { color:#97f99a; }
h1 { color:#4fd2ff; letter-spacing:.2em; font-size:14px; margin-bottom:12px; }
ul { list-style:none; padding:0; }
li { padding:3px 0; }
small { color:#2c8d2f; }
</style>
</head>
<body>
<h1>OPHI-118 SCENE OVERLAY APP</h1>
<ul>
<li><a href="/landing">/landing</a> <small> stand-by</small></li>
<li><a href="/loading">/loading</a> <small> project load + countdown</small></li>
<li><a href="/game">/game</a> <small> game HUD (camera-tracked)</small></li>
<li><a href="/desktop">/desktop</a> <small> desktop HUD (camera + screen)</small></li>
<li><a href="/goodbye">/goodbye</a> <small> sign-off</small></li>
<li><a href="/music-box">/music-box</a> <small> full music box</small></li>
<li><a href="/music-box/widget">/music-box/widget</a> <small> 549×880 widget</small></li>
<li><a href="/music-box/cover">/music-box/cover</a> <small> cover art</small></li>
<li><a href="/music-box/cover?bars=0">/music-box/cover?bars=0</a> <small> compact cover</small></li>
<li><a href="/music-box/nc">/music-box/nc</a> <small> ncurses-styled music box</small></li>
<li><a href="/music-daemon">/music-daemon</a> <small> audio worker</small></li>
<li>&nbsp;</li>
<li><a href="/data/playlist.js">/data/playlist.js</a> <small> window.__PLAYLIST</small></li>
<li><a href="/cover.jpg">/cover.jpg</a> <small> album art</small></li>
</ul>
</body>
</html>

View File

@@ -4,30 +4,26 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OPHI-118 / onboard</title>
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}">
<link rel="stylesheet" href="{{ asset('css/hud.css') }}">
<style>
/* Onboard wizard is the only page that's not an OBS browser_source it
renders in a real desktop browser at arbitrary viewport sizes. So we
break from the canvas-pixel design language and use a centered
terminal-card layout with breathing room. */
body.overlay-body {
padding: 24px;
body.page-shell {
display: grid;
grid-template-rows: auto 1fr auto;
gap: 24px;
min-height: 100vh;
height: auto;
overflow: auto;
}
.wizard {
width: min(960px, 100%);
margin: 0 auto;
width: 100%;
display: grid;
grid-template-columns: 1fr 360px;
grid-template-columns: 1fr 480px;
gap: 22px;
align-items: start;
}
@media (max-width: 800px) {
@media (max-width: 1100px) {
.wizard { grid-template-columns: 1fr; }
}
@@ -362,12 +358,65 @@
#toast.ok { border-color: var(--term-fg-bright); }
#toast pre { white-space: pre-wrap; margin: 6px 0 0; color: var(--term-fg-dim); }
/* Header strip mirrors the on-stream HUD chrome. */
.hud {
font-size: 16px;
padding: 0 4px;
/* Olden Era extra module — appears only when the picked game is Olden Era. */
#olden-module[hidden] { display: none !important; }
#olden-module {
margin-top: 8px;
padding-top: 16px;
border-top: 1px dashed var(--term-edge);
}
#olden-module::before {
content: '┤ HEROES OF MIGHT & MAGIC: OLDEN ERA ├';
display: block;
color: var(--term-fg-bright);
font-size: 12px;
letter-spacing: 0.28em;
margin-bottom: 14px;
text-shadow: 0 0 6px var(--term-glow);
}
.olden-pick {
display: flex;
align-items: center;
gap: 12px;
}
.olden-pick select {
flex: 1 1 auto;
background: rgba(0, 30, 0, 0.45);
border: 1px solid var(--term-edge);
border-radius: 3px;
color: var(--term-fg-bright);
font: inherit;
font-size: 15px;
padding: 8px 12px;
outline: 0;
text-shadow: 0 0 6px var(--term-glow);
}
.olden-pick select:focus {
border-color: var(--term-fg-bright);
box-shadow: 0 0 0 1px var(--term-fg-bright), 0 0 18px rgba(95, 220, 98, 0.25);
}
.olden-preview {
width: 44px;
height: 44px;
object-fit: contain;
background: rgba(0, 0, 0, 0.4);
border: 1px solid var(--term-edge);
border-radius: 3px;
flex: 0 0 auto;
}
.olden-preview.olden-portrait { object-fit: cover; }
.olden-preview:not([src]),
.olden-preview[src=""] { opacity: 0.25; }
.toggle.olden-color button.olden-red.on {
background: rgba(230, 58, 46, 0.20);
color: #ff6a5e;
text-shadow: 0 0 6px rgba(230, 58, 46, 0.55);
}
.toggle.olden-color button.olden-blue.on {
background: rgba(58, 166, 255, 0.20);
color: #6ec5ff;
text-shadow: 0 0 6px rgba(58, 166, 255, 0.55);
}
.hud .group { gap: 18px; }
/* Footer note. */
.foot-note {
@@ -378,22 +427,13 @@
}
</style>
</head>
<body class="overlay-body">
<body class="page-shell">
<header class="hud hud--onair">
<div class="group">
<span><span class="led"></span>ONBOARD</span>
<span>OPHI-118 // PROJECT MANIFEST</span>
</div>
<div class="group">
<span class="dim">RIG</span>
<span id="rig-name">{{ $telemetry['rig'] ?? '—' }}</span>
</div>
<div class="group">
<span class="dim">UTC</span>
<span id="clock">--:--:--</span>
</div>
</header>
@include('partials.topbar', [
'crumb' => 'ONBOARD',
'context' => '<span class="dim">RIG</span><span id="rig-name">'
. e($telemetry['rig'] ?? '—') . '</span>',
])
<main class="wizard">
<section class="panel" id="form-panel">
@@ -498,6 +538,65 @@
</div>
</div>
<div id="olden-module" hidden>
<div class="form-row">
<label for="ol-my-faction">My Faction</label>
<div class="field olden-pick">
<select id="ol-my-faction" name="olden[my][faction]"></select>
<img class="olden-preview" id="ol-my-faction-img" alt="">
</div>
</div>
<div class="form-row">
<label for="ol-my-hero">My Main Hero</label>
<div class="field olden-pick">
<select id="ol-my-hero" name="olden[my][hero]"></select>
<img class="olden-preview olden-portrait" id="ol-my-hero-img" alt="">
</div>
</div>
<div class="form-row">
<label for="ol-enemy-faction">Enemy Faction</label>
<div class="field olden-pick">
<select id="ol-enemy-faction" name="olden[enemy][faction]"></select>
<img class="olden-preview" id="ol-enemy-faction-img" alt="">
</div>
</div>
<div class="form-row">
<label for="ol-enemy-hero">Enemy Main Hero</label>
<div class="field olden-pick">
<select id="ol-enemy-hero" name="olden[enemy][hero]"></select>
<img class="olden-preview olden-portrait" id="ol-enemy-hero-img" alt="">
</div>
</div>
<div class="form-row">
<label>My Color</label>
<div class="field">
<div class="toggle olden-color" data-toggle="oldenColor" data-value="red">
<button type="button" data-val="red" class="olden-red on">RED · LEFT</button>
<button type="button" data-val="blue" class="olden-blue">BLUE · RIGHT</button>
</div>
<input type="hidden" id="ol-color" name="oldenColor" value="red">
<div class="hint">Red your nick on the matchup's left side, in red. Blue right side, in blue.</div>
</div>
</div>
<div class="form-row">
<label for="ol-map">Map / Template</label>
<div class="field">
<div class="prompt-line">
<span class="caret"></span>
<input
type="text" id="ol-map" name="olden[map]"
autocomplete="off" spellcheck="false"
placeholder="e.g. Crimson Tide">
</div>
</div>
</div>
</div>
<div class="actions">
<button type="submit" class="btn" id="submit-btn">
<span class="spinner" id="spinner"></span>WRITE MANIFEST
@@ -563,7 +662,7 @@
group.querySelectorAll('button').forEach(b => {
b.classList.toggle('on', b.dataset.val === val);
});
hidden.value = val === 'true' ? '1' : '0';
hidden.value = val === 'true' ? '1' : val === 'false' ? '0' : val;
renderPreview();
});
});
@@ -593,6 +692,7 @@
function setGameId(id) {
gameInput.dataset.gameId = id || '';
syncOldenVisibility();
}
function clearGameId() {
if (gameInput.dataset.gameId) setGameId('');
@@ -725,6 +825,84 @@
}).join('');
}
// ─── olden era module ──────────────────────────────────────────────────
const OLDEN_GAME_ID = @json(\App\Support\OldenEra::GAME_ID);
const OLDEN_CATALOG = @json($oldenEra['catalog']);
const OLDEN_SAVED = @json($oldenEra['manifest']);
const oldenModule = $('#olden-module');
const olColorInput = $('#ol-color');
const olColorGroup = document.querySelector('.toggle.olden-color');
const olMap = $('#ol-map');
function syncOldenVisibility() {
if (!oldenModule) return;
oldenModule.hidden = gameInput.dataset.gameId !== OLDEN_GAME_ID;
}
function fillFactionSelect(sel) {
sel.innerHTML = Object.keys(OLDEN_CATALOG)
.map(f => `<option value="${escapeHtml(f)}">${escapeHtml(f)}</option>`).join('');
}
function fillHeroSelect(side, faction) {
const sel = $(`#ol-${side}-hero`);
const heroes = (OLDEN_CATALOG[faction] || {}).heroes || [];
sel.innerHTML = heroes
.map(h => `<option value="${escapeHtml(h.name)}">${escapeHtml(h.name)}</option>`).join('');
}
function updateFactionImg(side) {
const f = $(`#ol-${side}-faction`).value;
$(`#ol-${side}-faction-img`).src = (OLDEN_CATALOG[f] || {}).logo || '';
}
function updateHeroImg(side) {
const f = $(`#ol-${side}-faction`).value;
const h = $(`#ol-${side}-hero`).value;
const hero = ((OLDEN_CATALOG[f] || {}).heroes || []).find(x => x.name === h);
$(`#ol-${side}-hero-img`).src = hero ? hero.portrait : '';
}
function setColor(val) {
if (val !== 'red' && val !== 'blue') val = 'red';
olColorInput.value = val;
olColorGroup.dataset.value = val;
olColorGroup.querySelectorAll('button').forEach(b => {
b.classList.toggle('on', b.dataset.val === val);
});
}
['my', 'enemy'].forEach(side => {
const factionSel = $(`#ol-${side}-faction`);
const heroSel = $(`#ol-${side}-hero`);
fillFactionSelect(factionSel);
factionSel.addEventListener('change', () => {
fillHeroSelect(side, factionSel.value);
updateFactionImg(side);
updateHeroImg(side);
});
heroSel.addEventListener('change', () => updateHeroImg(side));
});
// Pre-populate from saved olden-era-manifest.json (if any).
(function preFillOlden() {
const saved = OLDEN_SAVED || {};
const factions = Object.keys(OLDEN_CATALOG);
if (!factions.length) return;
const pick = (want, side) => {
const f = factions.includes(want?.faction) ? want.faction : factions[0];
$(`#ol-${side}-faction`).value = f;
fillHeroSelect(side, f);
const heroes = (OLDEN_CATALOG[f] || {}).heroes || [];
const wantedHero = want?.hero && heroes.find(h => h.name === want.hero) ? want.hero
: (heroes[0] && heroes[0].name) || '';
$(`#ol-${side}-hero`).value = wantedHero;
updateFactionImg(side);
updateHeroImg(side);
};
pick(saved.my, 'my');
pick(saved.enemy, 'enemy');
setColor(saved.color || 'red');
olMap.value = saved.map || '';
})();
syncOldenVisibility();
// ─── submit ────────────────────────────────────────────────────────────
form.addEventListener('submit', async (e) => {
e.preventDefault();
@@ -740,6 +918,14 @@
microphone: micHidden.value === '1',
pushTwitch: pushInput.checked,
};
if (!oldenModule.hidden) {
payload.olden = {
color: olColorInput.value || 'red',
my: { faction: $('#ol-my-faction').value, hero: $('#ol-my-hero').value },
enemy: { faction: $('#ol-enemy-faction').value, hero: $('#ol-enemy-hero').value },
map: olMap.value.trim() || null,
};
}
try {
const r = await fetch('/onboard', {
method: 'POST',
@@ -756,6 +942,7 @@
return;
}
const lines = [`✓ wrote ${body.path}`];
if (body.oldenPath) lines.push(`✓ wrote ${body.oldenPath}`);
if (body.twitch) lines.push(`twitch: ${body.twitch}`);
lines.push('→ refresh the Project Loading browser source in OBS.');
toast.className = 'on ok';

View File

@@ -56,6 +56,37 @@
<span class="game-name" id="game-name"></span>
<span class="sep" id="game-mode-sep">·</span>
<span class="game-mode" id="game-mode"></span>
@if ($olden)
@php
$me = ['side' => 'my', 'faction' => $olden['my']['faction'], 'hero' => $olden['my']['hero']];
$foe = ['side' => 'enemy', 'faction' => $olden['enemy']['faction'], 'hero' => $olden['enemy']['hero']];
$color = ($olden['color'] ?? 'red') === 'blue' ? 'blue' : 'red';
$myLeft = $color === 'red';
$left = $myLeft ? $me : $foe;
$right = $myLeft ? $foe : $me;
$rig = strtoupper($rigName ?: 'ME');
$nameFor = fn($s) => $s['side'] === 'my' ? $rig : strtoupper($s['hero']);
$url = fn($p) => $oldenAssetBase . '/' . str_replace('%2F', '/', rawurlencode($p));
@endphp
<div class="matchup matchup-{{ $color }}">
<span class="m-side m-{{ $left['side'] }}">
<img class="m-faction" src="{{ $url('factions/' . $left['faction'] . '.png') }}" alt="">
<img class="m-hero" src="{{ $url('heroes/' . $left['faction'] . '/' . $left['hero'] . '.png') }}" alt="">
<span class="m-name">{{ $nameFor($left) }}</span>
</span>
<span class="m-vs">VS</span>
<span class="m-side m-{{ $right['side'] }}">
<span class="m-name">{{ $nameFor($right) }}</span>
<img class="m-hero" src="{{ $url('heroes/' . $right['faction'] . '/' . $right['hero'] . '.png') }}" alt="">
<img class="m-faction" src="{{ $url('factions/' . $right['faction'] . '.png') }}" alt="">
</span>
@if (!empty($olden['map']))
<span class="m-on">ON</span>
<span class="m-map">{{ strtoupper($olden['map']) }}</span>
@endif
</div>
@endif
</div>
<div class="status-line meta-line">
<span class="icon"></span>

View File

@@ -0,0 +1,19 @@
{{--
Shared breadcrumb / nav strip used by every non-overlay webapp page
(/, /scenes, /onboard). Distinct from the in-scene .hud strip so OBS
overlays render identically only browser-facing pages use this.
@param string $crumb uppercase label for the current page (e.g. 'ONBOARD')
@param string $context optional raw HTML for the middle slot;
defaults to the node address.
--}}
<header class="crumb-bar">
<nav class="crumbs">
<span class="led" aria-hidden="true"></span>
<a href="/" class="crumb crumb-root">OPHI-118</a>
<span class="sep"></span>
<span class="crumb crumb-here">{{ $crumb }}</span>
</nav>
<div class="crumb-ctx">{!! $context ?? '<span class="dim">NODE</span>127.0.0.1:1118' !!}</div>
<div class="crumb-clock"><span class="dim">UTC</span><span id="clock">--:--:--</span></div>
</header>

View File

@@ -0,0 +1,107 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OPHI-118 / scenes</title>
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}">
<link rel="stylesheet" href="{{ asset('css/hud.css') }}">
<style>
html, body {
color: var(--term-fg);
font-family: var(--mono);
font-size: 14px;
line-height: 1.55;
user-select: text;
}
h1 {
color: var(--term-fg-bright);
text-shadow: 0 0 8px var(--term-glow);
font-size: 18px; font-weight: 700;
letter-spacing: 0.18em;
margin: 8px 0 4px;
}
.lede {
color: var(--term-fg-dim);
font-size: 13px;
margin-bottom: 26px;
}
.group-title {
color: var(--hud); letter-spacing: 0.14em;
font-size: 12px; font-weight: 700;
margin: 22px 0 8px;
border-bottom: 1px dashed var(--hud-dim);
padding-bottom: 4px;
}
ul { list-style: none; padding: 0; }
li { padding: 4px 0; display: flex; gap: 14px; align-items: baseline; }
li::before {
content: '▸';
color: var(--term-fg-dim);
font-size: 11px;
}
a.route {
color: var(--term-fg-bright);
text-decoration: none;
border-bottom: 1px dotted transparent;
min-width: 280px;
display: inline-block;
}
a.route:hover {
text-shadow: 0 0 8px var(--term-glow);
border-bottom-color: var(--term-fg);
}
.note { color: var(--term-fg-dim); }
.scanlines, .vignette, .flicker { position: fixed; }
</style>
</head>
<body class="page-shell">
@include('partials.topbar', ['crumb' => 'SCENES'])
<h1>SCENE OVERLAY ROUTES</h1>
<div class="lede">Browser sources in <code>basic/scenes/Default_Stream_HUD.json</code> point at these URLs on 127.0.0.1:1118.</div>
<div class="group-title">OVERLAYS</div>
<ul>
<li><a class="route" href="/landing">/landing</a> <span class="note"> stand-by</span></li>
<li><a class="route" href="/loading">/loading</a> <span class="note"> project load + countdown</span></li>
<li><a class="route" href="/game">/game</a> <span class="note"> game HUD (camera-tracked)</span></li>
<li><a class="route" href="/desktop">/desktop</a> <span class="note"> desktop HUD (camera + screen)</span></li>
<li><a class="route" href="/goodbye">/goodbye</a> <span class="note"> sign-off</span></li>
<li><a class="route" href="/music-daemon">/music-daemon</a> <span class="note"> passive WS listener</span></li>
</ul>
<div class="group-title">MUSIC BOX VARIANTS</div>
<ul>
<li><a class="route" href="/music-box">/music-box</a> <span class="note"> full music box</span></li>
<li><a class="route" href="/music-box/widget">/music-box/widget</a> <span class="note"> 549×880 widget</span></li>
<li><a class="route" href="/music-box/cover">/music-box/cover</a> <span class="note"> cover art</span></li>
<li><a class="route" href="/music-box/cover?bars=0">/music-box/cover?bars=0</a> <span class="note"> compact cover</span></li>
<li><a class="route" href="/music-box/nc">/music-box/nc</a> <span class="note"> ncurses-styled music box</span></li>
</ul>
<div class="group-title">DATA ENDPOINTS</div>
<ul>
<li><a class="route" href="/data/playlist.js">/data/playlist.js</a> <span class="note"> window.__PLAYLIST</span></li>
<li><a class="route" href="/cover.jpg">/cover.jpg</a> <span class="note"> current album art</span></li>
<li><a class="route" href="/track">/track?p=&lt;base64&gt;</a> <span class="note"> audio stream</span></li>
</ul>
<div class="scanlines"></div>
<div class="vignette"></div>
<div class="flicker"></div>
<script>
const clock = document.getElementById('clock');
function tick() {
const d = new Date();
const p = n => String(n).padStart(2, '0');
clock.textContent = `${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}`;
}
tick(); setInterval(tick, 1000);
</script>
</body>
</html>