−
100%
+
fit
scroll
/* runeboard_II.js — Runeboard II * Pill
that morphs through five states. As it grows,
its corners * soften less — radius shrinks
as footprint expands. * * State cycle (toggle
advances forward, ◌ collapses): * COMPRESSED
→ CITRINE → MORGANITE →
CITRINE_MORGANITE → EXPANDED → COMPRESSED
* * Requires: anime.js (loaded before this
script in init-shell.html) */ var RuneboardII
= (function () { // Width is constant (matched
to the citrine+morganite footprint). // Only
height and corner radius change as the board
grows. var RB2_WIDTH = 360; var STATES = {
compressed: { height: 44, radius: 22,
showCitrine: false, showMorganite: false,
expanded: false }, citrine: { height: 100,
radius: 20, showCitrine: true, showMorganite:
false, expanded: false }, morganite: { height:
229, radius: 18, showCitrine: false,
showMorganite: true, expanded: false },
citrine_morganite: { height: 306, radius: 16,
showCitrine: true, showMorganite: true,
expanded: false }, expanded: { height: 486,
radius: 14, showCitrine: true, showMorganite:
true, expanded: true }, // Mylar: Input
expanded further, with a content box mounted
// above the composer. mylar key names the
content variant — // 'dev' wires the debug
pane (formerly ?dev). mylar: { // Two equal
panes (input + output) inside the Runeboard //
— see CSS: both #rb2-body and
.rb2-pane-mylar are flex:1 // and split the
available height equally. The actual //
content variant (dev/things/chat…) lives in
// currentMylarVariant and is cycled by the
Mylar State // button independent of this
state's identity. height: 890, radius: 12,
showCitrine: true, showMorganite: true,
expanded: true, mylar: true } }; // Cycle
order — a clean upward walk through the
elemental stack. // Once we land in Mylar,
advance cycles through the variants // (Dev
→ Thing → Gems → Roll) instead of
through ORDER; the // press after the last
variant wraps back to Air. // // Air
(compressed) // Earth · SPB (citrine —
Spacetime Position Bar alone) // Earth · SPB
+ Morganite (citrine_morganite) // Fire ·
Input + Morganite + SPB (expanded) // Fire ·
Input + Morganite + SPB + Mylar (mylar; cycles
variants then resets) // // Citrine pane is
now the Spacetime Position Bar; the class name
// stays for compatibility but the bar reads
as SPB everywhere. var ORDER = ['compressed',
'citrine', 'citrine_morganite', 'expanded',
'mylar']; var orderIdx = 0; // Mylar = the
*subject* (what is being looked at). View =
the *lens* // (2D / 3D / Text) — orthogonal
axis selected by the View pill on // the
result box. The Mylar State button cycles
subjects; switching // subject snaps to that
subject's default view. // // Things/Gems
share a focusedThing — the pair toggle hops
between // those two subjects without changing
what's focused. var MYLAR_SUBJECTS = [
'terminal', 'coin', 'card', 'dice', 'feed',
'forge', 'hexcube', 'grimoire', 'subu',
'things', 'rings', 'gems', 'dev' ]; var VIEWS
= ['text', '2d', '3d']; var
SUBJECT_DEFAULT_VIEW = { terminal: 'text',
coin: '2d', card: '2d', dice: '2d', feed:
'2d', forge: 'text', hexcube: '3d', grimoire:
'text', subu: '2d', things: 'text', rings:
'text', gems: 'text', dev: 'text' }; // Glyph
layer that best matches each Mylar subject's
interaction context. // 'being' is the outer
page context (not a Mylar subject) — used on
load. var SUBJECT_DEFAULT_LAYER = { terminal:
'keyboard', coin: 'wands', card: 'wands',
dice: 'coords', feed: 'wands', forge:
'elemental', hexcube: 'coords', grimoire:
'wands', subu: 'cycles', things: 'wands',
rings: 'wands', gems: 'elemental', dev:
'wands', being: 'daw' }; var currentSubject =
'dev'; var subjectIdx =
MYLAR_SUBJECTS.indexOf(currentSubject); var
currentView =
SUBJECT_DEFAULT_VIEW[currentSubject]; //
Cached "focused Thing" used by both thing +
gems variants so a // single fetch feeds both
views. var focusedThing = null; var
focusedThingFetching = false; // HexCube
navigation path — mirrors NavigationState
from // ui/mod.rs. State + 4 position pairs
(HC / Cube / Cluster / Set) // = 9 glyph cells
exactly. Defaults to all-center, active state.
// Future: wire to real nav state via an API /
WS subscription. var navState = { state: 0, //
-1 sealed ▼ / 0 active △ / 1 potential ▲
hcPos: { x: 0, y: 0 }, cubePos: { x: 0, y: 0
}, clusterPos: { x: 0, y: 0 }, setPos: { x: 0,
y: 0 } }; function axisGlyph(v) { return v ===
-1 ? '▼' : v === 1 ? '▲' : '△'; } //
Level colour tint (matches ui/mod.rs's
per-level palette): // state→accent /
HC→purple / Cube→green / Cluster→orange
/ Set→red var CITRINE_LEVEL_COLORS = {
state: '#f1c40f', hc: '#9b59b6', cube:
'#27ae60', cluster: '#e67e22', set: '#e74c3c'
}; function citrinePathHtml() { var ns =
navState; var cells = [ ['state',
axisGlyph(ns.state), true], ['hc',
axisGlyph(ns.hcPos.x), false], ['hc',
axisGlyph(ns.hcPos.y), true], ['cube',
axisGlyph(ns.cubePos.x), false], ['cube',
axisGlyph(ns.cubePos.y), true], ['cluster',
axisGlyph(ns.clusterPos.x), false],
['cluster', axisGlyph(ns.clusterPos.y), true],
['set', axisGlyph(ns.setPos.x), false],
['set', axisGlyph(ns.setPos.y), false] ];
return cells.map(function (c) { var level =
c[0], glyph = c[1], divider = c[2]; return
'<span class="rb2-cit-cell' + (divider ? '
rb2-cit-divend' : '') + '" data-level="' +
level + '"' + ' style="color:' +
CITRINE_LEVEL_COLORS[level] + '">' + glyph +
'</span>'; }).join(''); } // Subu /
Spectraglyph clock helpers — both the SPB's
emerald // phase and the Spectraglyph Mylar
render this content. // sacred day → 5-digit
zero-padded ("00346") // time → 4-digit HHMM
// cycle → moon/mercury/venus phase counters
(TBD layout) function pad(n, w) { return
String(n).padStart(w, '0'); } function
subuDigits() { var now = new Date(); return {
day: pad(sacredDay(), 5), time:
pad(now.getHours(), 2) + pad(now.getMinutes(),
2), seconds: pad(now.getSeconds(), 2) }; } //
Dev Mylar reads the same data dev-overlay
surfaces: // world chip manifest (fetched),
exposed gems (window.__hexGems), // Sacred Day
(epoch math), and a recent-rolls counter for
context. var SACRED_DAY_ZERO_EPOCH =
1749400000; // from init-dev-overlay.js
function sacredDay() { return
Math.floor((Date.now() / 1000 -
SACRED_DAY_ZERO_EPOCH) / 86400); } var
devChips = []; var devChipsFetching = false;
function kickDevChipsFetch() { if
(devChips.length || devChipsFetching) return;
devChipsFetching = true; var world =
window.WORLD_NAME || new
URLSearchParams(location.search).get('world')
|| 'cosmostack'; fetch('/api/worlds/' +
encodeURIComponent(world) + '/chips')
.then(function (r) { return r.ok ? r.json() :
null; }) .then(function (j) { devChipsFetching
= false; devChips = (j && j.chips) || [];
rerenderIfActive('dev'); }) .catch(function ()
{ devChipsFetching = false; }); } // Roll
Mylar consumes the dice world's existing event
stream — // every settled die dispatches
'dice-landed' on window. We // keep a
newest-first ring buffer feeding our
rendering. var diceLandings = []; var
MAX_LANDINGS = 12;
window.addEventListener('dice-landed',
function (e) { var d = (e && e.detail) || {};
diceLandings.unshift({ face: d.face != null ?
d.face : (d.faceIndex != null ? d.faceIndex :
'?'), title: d.title || d.faceTitle || '', t:
Date.now() }); if (diceLandings.length >
MAX_LANDINGS) diceLandings.length =
MAX_LANDINGS; rerenderIfActive('dice'); }); //
Click a .bp-item → focus that Thing into the
Thing/Gems Mylar // and pop the Runeboard open
if it isn't already in mylar state. function
fetchAndFocus(docId, onSet) {
fetch('/api/things/' +
encodeURIComponent(docId)) .then(function (r)
{ return r.ok ? r.json() : null; })
.then(function (j) { if (!j) return;
focusedThing = j.data || j; if (onSet)
onSet(); }) .catch(function () {}); }
document.addEventListener('click', function
(e) { // Pull Mylar's action button. var
pullBtn =
e.target.closest('.rb2-pull-btn[data-action="pull"]');
if (pullBtn) { doPull(); return; } // Subject
pill — switch Mylar subject. var schip =
e.target.closest('.rb2-subject-chip'); if
(schip) { var ns = schip.dataset.subject; if
(ns && ns !== currentSubject) {
logEvent('subject · ' + currentSubject + '
→ ' + ns); setSubject(ns); }
e.preventDefault(); return; } // View pill —
switch lens without changing subject. var
vchip = e.target.closest('.rb2-view-chip'); if
(vchip) { var nv = vchip.dataset.view; if
(VIEWS.indexOf(nv) >= 0 && nv !== currentView)
{ currentView = nv; logEvent('view · ' +
currentSubject + ' · ' + nv); var vpane =
document.querySelector('#rb2
.rb2-pane-mylar'); renderMylarPane(vpane,
currentSubject, currentView); }
e.preventDefault(); return; } // Glyph pill
— switch keyboard glyph layer. Affects the
keys // beneath, not the Mylar content.
Refresh just the pill host // so its
active-dot highlight + name follow. var gchip
= e.target.closest('.rb2-glyph-dot'); if
(gchip) { var nl = gchip.dataset.layer; if
(LAYER_ORDER.indexOf(nl) >= 0 && nl !==
currentLayer) { var prevL = currentLayer;
currentLayer = nl; applyLayer(currentLayer);
logEvent('layer · ' + prevL + ' → ' +
currentLayer); var ghostEl =
document.querySelector('#rb2
.rb2-glyph-pill-host'); if (ghostEl)
ghostEl.innerHTML =
glyphPillHtml(currentLayer); }
e.preventDefault(); return; } // Pair toggle
inside Things/Gems — flips between those two
// subjects without changing focusedThing. var
pair = e.target.closest('.rb2-pair-toggle');
if (pair) { var target = pair.dataset.target;
if (target === 'things' || target === 'gems')
{ setSubject(target); e.preventDefault(); }
return; } var bp =
e.target.closest('.bp-item[data-doc]'); if
(!bp) return; var docId = bp.dataset.doc; if
(!docId) return; fetchAndFocus(docId, function
() { logEvent('bp-item · ' + docId.slice(0,
8)); // First entry: default to Things; if
user is already on Gems // keep Gems active
(same-view inspection). var target =
currentSubject === 'gems' ? 'gems' : 'things';
setSubject(target); if (current !== 'mylar') {
transitionTo('mylar'); } }); }); // Hover a
.bp-item while the Thing/Gems mylar is open
→ refocus // without changing which view is
showing. Debounced so a sweep // across many
items doesn't fire a flood of fetches. var
hoverTimer = null;
document.addEventListener('mouseover',
function (e) { var bp =
e.target.closest('.bp-item[data-doc]'); if
(!bp) return; if (currentSubject !== 'things'
&& currentSubject !== 'gems') return; var
docId = bp.dataset.doc; if (!docId) return; if
(hoverTimer) clearTimeout(hoverTimer);
hoverTimer = setTimeout(function () {
fetchAndFocus(docId, function () {
rerenderIfActive(currentSubject); }); }, 150);
}); function rerenderIfActive(subject) { var
rb = document.getElementById('rb2'); if (!rb
|| !rb.classList.contains('has-mylar'))
return; if (currentSubject !== subject)
return; var pane =
rb.querySelector('.rb2-pane-mylar');
renderMylarPane(pane, currentSubject,
currentView); } // Set subject + reset view to
its default. Re-renders pane if open. function
setSubject(subject) { if
(MYLAR_SUBJECTS.indexOf(subject) < 0) return;
currentSubject = subject; subjectIdx =
MYLAR_SUBJECTS.indexOf(subject); currentView =
SUBJECT_DEFAULT_VIEW[subject]; // Switch glyph
layer to match the new subject's interaction
context var defaultLayer =
SUBJECT_DEFAULT_LAYER[subject] || 'wands'; if
(defaultLayer !== currentLayer) { currentLayer
= defaultLayer; applyLayer(currentLayer); var
pillHost = document.querySelector('#rb2
.rb2-glyph-pill-host'); if (pillHost)
pillHost.innerHTML =
glyphPillHtml(currentLayer); } var btn =
document.getElementById('rb2-mylar-toggle');
if (btn) btn.innerHTML = mylarIcon(subject);
var rb = document.getElementById('rb2'); if
(rb && rb.classList.contains('has-mylar')) {
var pane =
rb.querySelector('.rb2-pane-mylar');
renderMylarPane(pane, currentSubject,
currentView); } } var current = 'compressed';
var RB2_CSS = [ '#rb2 {', ' position: fixed;',
' left: 16px;', ' top: 16px;', ' width:
360px;', ' height: 44px;', ' background:
#ffffff;', ' border: 1.5px solid #111;', '
border-radius: 22px;', ' box-shadow: 0 6px
24px rgba(0,0,0,0.18);', ' z-index: 101;', '
opacity: 0;', ' overflow: hidden;', ' display:
flex;', ' flex-direction: column;', '
box-sizing: border-box;', ' transform:
translateY(-8px);', ' font-family:
"dazzle-unicase", "SF Mono", monospace;', '}',
// Expanded: the Runeboard wrapper stays its
normal color; the // body itself becomes the
one sage shape that holds both the // typing
area and the keys. Citrine pill floats inside
it. '#rb2.expanded #rb2-body {', ' padding:
0;', ' gap: 0;', ' margin: 10px;', '
background: #6e7c74;', ' border-radius:
16px;', ' overflow: visible;', ' flex: 1;', '
display: flex;', ' flex-direction: column;', '
box-sizing: border-box;', '}', '#rb2-header
{', ' display: flex;', ' align-items:
center;', ' justify-content: space-between;',
' padding: 0 16px;', ' height: 44px;', '
flex-shrink: 0;', ' cursor: grab;', '
user-select: none;', ' touch-action: none;', '
background: inherit;', ' position: relative;',
' z-index: 5;', '}', '#rb2.dragging
#rb2-header { cursor: grabbing; }',
'#rb2-title {', ' color: #c62828;', '
font-weight: 700;', ' font-size: 14px;', '
letter-spacing: 2px;', ' text-transform:
uppercase;', ' user-select: none;', '}', //
Title stays its normal red in every state —
no dim on input. '#rb2-toggle {', ' width:
20px; height: 20px;', ' background:
transparent;', ' border: none;', ' cursor:
pointer;', ' padding: 0;', ' display: flex;',
' align-items: center;', ' justify-content:
center;', ' transition: transform 0.18s
ease;', '}', '#rb2-toggle:hover { filter:
brightness(1.18); }', '#rb2-toggle svg {
width: 18px; height: 18px; display: block; }',
'#rb2-morganite-toggle {', ' width: 24px;
height: 16px;', ' background: transparent;', '
border: none;', ' cursor: pointer;', '
padding: 0;', ' margin-left: 8px;', ' display:
flex;', ' align-items: center;', '
justify-content: center;', ' transition:
transform 0.18s ease;', '}',
'#rb2-morganite-toggle:hover { filter:
brightness(1.18); }',
'#rb2-morganite-toggle:active { transform:
scale(0.85); }', '#rb2-morganite-toggle svg {
width: 22px; height: 14px; display: block; }',
'#rb2-mylar-toggle {', ' width: 20px; height:
20px;', ' background: transparent;', ' border:
none;', ' cursor: pointer;', ' padding: 0;', '
margin-left: 8px;', ' display: none;', '
align-items: center;', ' justify-content:
center;', ' transition: transform 0.18s ease,
opacity 0.18s ease;', '}', '#rb2.has-mylar
#rb2-mylar-toggle { display: flex; }',
'#rb2-mylar-toggle:hover { filter:
brightness(1.18); }',
'#rb2-mylar-toggle:active { transform:
scale(0.85); }', '#rb2-mylar-toggle svg {
width: 14px; height: 14px; display: block; }',
'#rb2-collapse {', ' width: 12px; height:
12px;', ' border-radius: 50%;', ' background:
transparent;', ' border: 1.5px solid
#c62828;', ' cursor: pointer;', ' padding:
0;', ' margin-right: 8px;', ' transition:
background 0.18s ease;', '}',
'#rb2-collapse:hover { background: #c62828;
}', '#rb2-collapse { transition: transform
0.18s ease, background 0.18s ease; }',
'#rb2-collapse:active { transform:
scale(0.85); }', '#rb2.expanded #rb2-collapse
{ border-color: #ffb4b4; }', '#rb2-controls {
display: flex; align-items: center; }',
'#rb2-body {', ' padding: 0 6px 6px;', '
display: flex;', ' flex-direction: column;', '
gap: 6px;', ' opacity: 0;', '}',
'.rb2-pane-citrine {', ' background:
#0a0a0a;', ' border-radius: 999px;', ' height:
36px;', // 14px matches the keyboard's
left/right edge in non-expanded // states
(body padding 6 + morganite 2 + center-offset
12 = 20 // → pill width 320 aligns exactly
with the keys above/below). ' margin: 0
14px;', ' box-shadow: inset 0 2px 6px
rgba(0,0,0,0.45);', ' display: none;', '
align-items: center;', ' justify-content:
center;', '}', // Black pill carries a small
Earth (filled-down) triangle in // the center.
The triangle's fill swaps citrine → emerald
// every 4s; same shape, just the color
cycles. '.rb2-pane-citrine .rb2-pill-glyph {',
' width: 16px; height: 16px;', ' display:
block;', ' transition: fill 0.6s ease;', '}',
// Citrine grid — 9 cells with glyphs, two
dividers carving // them into three path
segments. '.rb2-pane-citrine .rb2-pill-citrine
{', ' display: grid;', '
grid-template-columns: repeat(9, 1fr);', '
gap: 4px;', ' width: 100%;', ' height: 100%;',
' align-items: center;', ' box-sizing:
border-box;', ' font-family: SFMono-Regular,
ui-monospace, Menlo, Monaco, monospace;', '
color: #e5734a;', ' transition: color 0.6s
ease;', '}', '.rb2-pane-citrine.is-emerald
.rb2-pill-citrine { display: none; }',
'.rb2-cit-cell {', ' text-align: center;', '
max-width: 32px;', ' justify-self: center;', '
width: 100%;', ' font-size: 14px;', '
line-height: 1;', ' position: relative;', '}',
// Right-edge dividers after cells 2 and 5 →
3 path segments. '.rb2-cit-divend::after {', '
content: "";', ' position: absolute;', '
right: -2px;', ' top: 12%;', ' bottom: 12%;',
' width: 1px;', ' background:
rgba(229,115,74,0.55);', '}', // LED block
shows during emerald (Subu) phase — 9 cells
in a // grid that matches the keyboard's
9-column layout above/below.
'.rb2-pane-citrine .rb2-pill-led {', '
display: none;', ' grid-template-columns:
repeat(9, 1fr);', ' gap: 4px;', ' width:
100%;', ' height: 100%;', ' align-items:
center;', ' box-sizing: border-box;', '
font-family: SFMono-Regular, ui-monospace,
Menlo, Monaco, monospace;', ' font-weight:
700;', ' font-size: 16px;', ' color:
#2e8b57;', ' text-shadow: 0 0 6px
rgba(46,139,87,0.55);', '}',
'.rb2-pane-citrine.is-emerald .rb2-pill-led {
display: grid; }', '.rb2-led-cell {', '
text-align: center;', ' max-width: 32px;', '
justify-self: center;', ' width: 100%;', '}',
// Subtle divider tint between day (0-4) and
time (5-8). '.rb2-led-cell[data-led^="t"] {
color: #45c08a; }', '.rb2-led-cell { position:
relative; }', // Vertical line after the day
section (after d4). '.rb2-led-divend::after
{', ' content: "";', ' position: absolute;', '
right: -2px;', ' top: 12%;', ' bottom: 12%;',
' width: 1px;', ' background:
rgba(46,139,87,0.55);', '}', // Blinking colon
riding the left edge of t2 (the minutes start)
// so it reads as HH:MM.
'.rb2-led-colon::before {', ' content: ":";',
' position: absolute;', ' left: -6px;', ' top:
50%;', ' transform: translateY(-52%);', '
color: #45c08a;', ' font-weight: 700;', '
animation: rb2-blink 1s steps(1) infinite;',
'}', '@keyframes rb2-blink { 50% { opacity: 0;
} }', '.rb2-pane-morganite {', ' display:
none;', ' flex-direction: column;', ' gap:
4px;', ' padding: 4px 2px 6px;', '}',
'.rb2-row { display: flex; gap: 4px;
justify-content: center; }', '.rb2-key {', '
flex: 1;', ' aspect-ratio: 1;', ' max-width:
32px;', ' background: #fff5f5;', ' border: 1px
solid #1a1a1a;', ' border-radius: 8px;', '
display: flex;', ' align-items: center;', '
justify-content: center;', ' font-size:
11px;', ' color: #1a1a1a;', '}',
'.rb2-key.circle { border-radius: 50%; }',
'.rb2-key.mod { flex: 1.2; font-size: 9px;
color: #444; background: #fff; }',
'#rb2.expanded .rb2-key { background: #1f2622;
border-color: #4a5a52; color: #cdd; }',
'#rb2.expanded .rb2-key.mod { background:
#2a322e; color: #aab; }', '.rb2-key
i.ph-light, .rb2-key i.ph-fill { font-size:
16px; line-height: 1; }', '.rb2-key .rb2-glyph
{ width: 14px; height: 14px; display: block;
}', // 2-triangle coord glyph is wider than
the single-tri/circle. '.rb2-key
.rb2-glyph.rb2-coord { width: 22px; height:
14px; }', // DAW Unicode glyphs —
astronomical symbols benefit from a //
symbol-aware font fallback and a slight bump
in size. '.rb2-key .rb2-daw {', ' font-size:
18px;', ' line-height: 1;', ' font-family:
"Segoe UI Symbol", "Apple Symbols", "Noto Sans
Symbols 2", "Noto Sans Symbols", sans-serif;',
'}', // Composer is a region inside the body
shape — no separate // background box. The
body itself is the sage "input" surface; //
composer just provides the typing affordance
at its top. '.rb2-composer {', ' background:
transparent;', ' flex: 1;', ' min-height:
120px;', ' padding: 24px;', ' color:
#f0f4f1;', ' font-family: SFMono-Regular,
ui-monospace, Menlo, Monaco, monospace;', '
font-size: 15px;', ' line-height: 1.4;', '
outline: none;', ' display: none;', '
position: relative;', ' z-index: 1;', '
box-sizing: border-box;', '}',
'.rb2-composer:empty::before {', ' content:
attr(data-placeholder);', ' color: #f0f4f1;',
' pointer-events: none;', ' transition:
opacity 0.4s ease;', '}',
'.rb2-composer.placeholder-fade:empty::before
{ opacity: 0; }', '#rb2.expanded .rb2-composer
{ display: block; }', // Pill horizontal
margin matches the keyboard's visible // outer
edges: keys are 32px max, 9 of them with 4px
gaps = // 320px row, centered in the morganite
pane → the pill takes // the same 320px
footprint by hugging the keyboard's flanks.
'#rb2.expanded .rb2-pane-citrine {', '
display: flex;', ' position: relative;', '
z-index: 3;', ' margin: -18px 10px 8px;', '
box-shadow: 0 4px 14px rgba(0,0,0,0.28);',
'}', '#rb2.expanded .rb2-pane-morganite {', '
padding: 8px 4px 8px;', ' background:
transparent;', ' margin-top: 0;', ' position:
relative;', ' z-index: 1;', '}',
'#rb2.expanded .rb2-key { border-color:
#5a6a62; }', // Mylar (output) pane —
sibling of the input box, lives in // the
Runeboard's outer surface below the keyboard.
Matches // the input box's footprint exactly:
same width, same height. '.rb2-pane-mylar {',
' display: none;', ' margin: 8px 10px 10px;',
' background: #c4ccc8;', ' color: #2a322e;', '
border-radius: 16px;', ' padding: 0;', '
font-family: SFMono-Regular, ui-monospace,
Menlo, Monaco, monospace;', ' font-size:
12px;', ' line-height: 1.6;', ' overflow-y:
auto;', ' box-sizing: border-box;', '
flex-shrink: 0;', ' position: relative;', '}',
// Sticky header — three pills on a single
row inside the // result-pane width. Each pill
scrolls horizontally INSIDE // itself when it
overflows; the row never wraps and never //
extends past the Runeboard.
'.rb2-mylar-head-bar {', ' position: sticky;',
' top: 0;', ' background: #c4ccc8;', '
padding: 8px 14px;', ' display: flex;', '
align-items: center;', ' flex-wrap: nowrap;',
' gap: 6px;', ' z-index: 10;', '
border-radius: 16px 16px 0 0;', '
border-bottom: 1px solid rgba(0,0,0,0.08);',
'}', '.rb2-mylar-content {', ' padding: 12px
22px 22px;', '}', // Subject pill — widest
by chip count; shares grow space with // glyph
pill, scrolls inside if too narrow.
'.rb2-subject-pill {', ' background:
#0a0a0a;', ' border-radius: 999px;', '
padding: 3px;', ' display: flex;', ' gap:
2px;', ' flex: 1 1 0;', ' min-width: 0;', '
overflow-x: auto;', ' scrollbar-width: none;',
' box-shadow: 0 2px 8px rgba(0,0,0,0.35);',
'}', '.rb2-subject-pill::-webkit-scrollbar {
display: none; }', '.rb2-subject-chip {', '
background: transparent;', ' color: #aab5af;',
' border: none;', ' border-radius: 999px;', '
padding: 3px 9px;', ' font-family:
SFMono-Regular, ui-monospace, Menlo, Monaco,
monospace;', ' font-size: 9px;', '
letter-spacing: 0.5px;', ' text-transform:
lowercase;', ' cursor: pointer;', '
transition: background 0.18s ease, color 0.18s
ease;', ' white-space: nowrap;', '}',
'.rb2-subject-chip:hover { color: #fff; }',
'.rb2-subject-chip.active { background:
#e5734a; color: #fff; }', // Glyph pill host
— slot above the keyboard. Hidden until the
// morganite pane is shown (set in
transitionTo). '.rb2-glyph-pill-host {', '
display: none;', ' justify-content: center;',
' margin: 0 14px 6px;', '}', // Compact glyph
pill — current-layer name + a row of dots,
one // per layer in LAYER_ORDER. Active dot
tinted citrine; others // dim.
'.rb2-glyph-pill {', ' background: #0a0a0a;',
' border-radius: 999px;', ' padding: 4px
12px;', ' display: inline-flex;', '
align-items: center;', ' gap: 10px;', '
box-shadow: 0 2px 8px rgba(0,0,0,0.35);', '}',
'.rb2-glyph-name {', ' color: #e5734a;', '
font-family: SFMono-Regular, ui-monospace,
Menlo, Monaco, monospace;', ' font-size:
10px;', ' letter-spacing: 0.5px;', '
text-transform: lowercase;', ' min-width:
56px;', '}', '.rb2-glyph-dots {', ' display:
inline-flex;', ' gap: 6px;', ' align-items:
center;', '}', '.rb2-glyph-dot {', ' width:
6px;', ' height: 6px;', ' border-radius:
50%;', ' background: #555;', ' border: none;',
' padding: 0;', ' cursor: pointer;', '
transition: background 0.18s ease, transform
0.18s ease;', '}', '.rb2-glyph-dot:hover {
background: #aab5af; }',
'.rb2-glyph-dot.active {', ' background:
#e5734a;', ' transform: scale(1.4);', '}',
'.rb2-glyph-chip {', ' background:
transparent;', ' color: #aab5af;', ' border:
none;', ' border-radius: 999px;', ' padding:
3px 9px;', ' font-family: SFMono-Regular,
ui-monospace, Menlo, Monaco, monospace;', '
font-size: 9px;', ' letter-spacing: 0.5px;', '
text-transform: lowercase;', ' cursor:
pointer;', ' transition: background 0.18s
ease, color 0.18s ease;', ' white-space:
nowrap;', '}', '.rb2-glyph-chip:hover { color:
#fff; }', '.rb2-glyph-chip.active {
background: #e5734a; color: #fff; }', // View
pill — only 3 chips, never overflows; pinned
at natural // width so it doesn\'t consume the
subject/glyph share. '.rb2-view-pill {', '
background: #0a0a0a;', ' border-radius:
999px;', ' padding: 3px;', ' display:
inline-flex;', ' gap: 2px;', ' flex: 0 0
auto;', ' box-shadow: 0 2px 8px
rgba(0,0,0,0.35);', '}', '.rb2-view-chip {', '
background: transparent;', ' color: #aab5af;',
' border: none;', ' border-radius: 999px;', '
padding: 3px 10px;', ' font-family:
SFMono-Regular, ui-monospace, Menlo, Monaco,
monospace;', ' font-size: 9px;', '
letter-spacing: 1px;', ' text-transform:
uppercase;', ' cursor: pointer;', '
transition: background 0.18s ease, color 0.18s
ease;', '}', '.rb2-view-chip:hover { color:
#fff; }', '.rb2-view-chip.active { background:
#e5734a; color: #fff; }', '#rb2.has-mylar
.rb2-pane-mylar { display: block; flex: 1; }',
'.rb2-mylar-row { display: flex;
justify-content: space-between; gap: 12px; }',
'.rb2-mylar-key { color: #6e7a72; flex-shrink:
0; }', '.rb2-mylar-val { color: #1a1a1a;
font-weight: 600; text-align: right; overflow:
hidden; text-overflow: ellipsis; white-space:
nowrap; max-width: 220px; }', // Inline
subject titles — the sticky head bar's
dropdown shows // subject now, so the
redundant in-content titles are hidden.
'.rb2-mylar-title { display: none; }',
'.rb2-mylar-loading { color: #6e7a72;
font-style: italic; }', '.rb2-roll-faces {
display: grid; grid-template-columns:
repeat(3, 1fr); gap: 6px; margin-bottom: 12px;
}', '.rb2-roll-face { aspect-ratio: 1;
display: flex; align-items: center;
justify-content: center; background: #fff;
border: 1px solid #1a1a1a; border-radius: 6px;
color: #6e7a72; font-size: 16px; }',
'.rb2-roll-face.lit { background: #e5734a;
color: #fff; border-color: #c95a30;
font-weight: 700; font-size: 20px; }',
'.rb2-roll-result { text-align: center; color:
#1a1a1a; font-size: 13px; }',
'.rb2-mylar-section { color: #6e7a72; margin:
10px 0 4px; font-size: 11px; text-transform:
uppercase; letter-spacing: 1px; }', //
Thing/Gems share one slot — each pane
carries a tiny link // button to flip to its
partner without changing focusedThing.
'.rb2-mylar-head { display: flex; align-items:
center; justify-content: space-between; gap:
8px; margin-bottom: 10px; }', '.rb2-mylar-head
.rb2-mylar-title { margin-bottom: 0; }',
'.rb2-pair-toggle {', ' background:
transparent;', ' color: #6e7a72;', ' border:
1px solid #6e7a72;', ' border-radius: 999px;',
' padding: 2px 10px;', ' font-family:
SFMono-Regular, ui-monospace, Menlo, Monaco,
monospace;', ' font-size: 10px;', '
letter-spacing: 0.5px;', ' text-transform:
lowercase;', ' cursor: pointer;', '
transition: background 0.18s ease, color 0.18s
ease;', '}', '.rb2-pair-toggle:hover {
background: #1a1a1a; color: #fff;
border-color: #1a1a1a; }', // 3D Mylar —
canvas fills, label sits below on the lightest
// grey background so it reads as the window's
caption. '.rb2-three-container { width: 100%;
height: 100%; min-height: 240px; display:
block; position: relative; overflow: hidden;
border-radius: 12px; }', '.rb2-three-container
canvas { display: block; width: 100%
!important; height: 100% !important; }',
'.rb2-three-label { text-align: center; color:
#1a1a1a; font-weight: 600; font-size: 12px;
padding-top: 8px; border-top: 1px solid
#aab5af; }', // Mylar 3D content slot — flex
so the container can fill. '.rb2-mylar-3d {
padding: 12px; display: flex; flex-direction:
column; }', '.rb2-mylar-3d
.rb2-three-container { flex: 1; }', // Docked
world — strip the fixed-fullscreen
positioning, fill // the Mylar container
instead. Important to outrank the world\'s //
own inline + #world-canvas { position: fixed }
selectors. '.rb2-docked-world {', ' position:
relative !important;', ' top: auto !important;
left: auto !important;', ' right: auto
!important; bottom: auto !important;', '
width: 100% !important; height: 100%
!important;', ' z-index: auto !important;', '
transform-origin: top left;', ' opacity: 1
!important;', '}', '.rb2-docked-world canvas {
width: 100% !important; height: 100%
!important; }', // Terminal lines —
timestamp gutter + monospace text.
'.rb2-term-line { display: flex; gap: 8px;
padding: 1px 0; font-size: 11px; }',
'.rb2-term-ts { color: #6e7a72; flex-shrink:
0; min-width: 64px; }', '.rb2-term-text {
color: #1a1a1a; word-break: break-all; }', //
2D Mylar — host container holds the cloned
.bp-feed at scale. '.rb2-2d-host {', '
margin-top: 6px;', ' transform: scale(0.5);',
' transform-origin: top left;', ' width:
200%;', ' pointer-events: none;', ' color:
#1a1a1a;', '}', // Pull button — full width
inside the Pull pane. '.rb2-pull-btn {', '
margin-top: 12px;', ' width: 100%;', '
padding: 10px 12px;', ' background: #c62828;',
' color: #fff;', ' border: none;', '
border-radius: 10px;', ' font-family:
SFMono-Regular, ui-monospace, Menlo, Monaco,
monospace;', ' font-size: 12px;', '
letter-spacing: 0.5px;', ' text-transform:
uppercase;', ' cursor: pointer;', '
transition: background 0.18s ease;', '}',
'.rb2-pull-btn:hover { background: #b51e1e;
}', // Spectraglyph Mylar — full-size Subu
display. '.rb2-sg-stack { display: flex;
flex-direction: column; gap: 18px; margin-top:
18px; }', '.rb2-sg-row { display: flex;
align-items: baseline; justify-content:
space-between; gap: 12px; }', '.rb2-sg-label {
color: #6e7a72; font-size: 11px;
text-transform: uppercase; letter-spacing:
2px; }', '.rb2-sg-led {', ' font-family:
SFMono-Regular, ui-monospace, Menlo, Monaco,
monospace;', ' font-weight: 700;', ' color:
#2e8b57;', ' text-shadow: 0 0 8px
rgba(46,139,87,0.45);', ' letter-spacing:
3px;', '}', '.rb2-sg-day { font-size: 38px;
}', '.rb2-sg-time { font-size: 32px; }',
'.rb2-sg-secs { font-size: 14px; opacity: 0.7;
margin-left: 6px; letter-spacing: 1px; }',
'.rb2-sg-cycle .rb2-sg-placeholder { color:
#aab5af; font-style: italic; font-size: 12px;
}', ].join('\n'); function injectCSS() { if
(document.getElementById('rb2-styles'))
return; var style =
document.createElement('style'); style.id =
'rb2-styles'; style.textContent = RB2_CSS;
document.head.appendChild(style); } // Pages
currently only load Phosphor's 'fill' weight;
the Runeboard // uses the lighter 'light'
weight for the arrow keys. Pull that //
stylesheet in from the chip so the arrows
actually render. function
injectPhosphorLight() { if
(document.getElementById('rb2-ph-light'))
return; var l =
document.createElement('link'); l.id =
'rb2-ph-light'; l.rel = 'stylesheet'; l.href =
'https://unpkg.com/@phosphor-icons/
[email protected]
/src/light/style.css';
document.head.appendChild(l); } function
mount() { injectCSS(); injectPhosphorLight();
// Hide the legacy rb1 once RB2 mounts —
both being scaffolded // together produces the
"two runeboards stacked" glitch. var legacy =
document.getElementById('runeboard'); if
(legacy) legacy.style.display = 'none'; var el
= document.createElement('div'); el.id =
'rb2'; el.innerHTML = '<div id="rb2-header">'
+ '<span id="rb2-title">Runeboard</span>' +
'<div id="rb2-controls">' + '<button
id="rb2-toggle" title="Mode State — cycle
modes"></button>' + '<button id="rb2-collapse"
title="Collapse / expand"></button>' +
'</div>' + '</div>' + '<div id="rb2-body">' +
'<div class="rb2-composer"
contenteditable="true"
data-placeholder="input"></div>' + '<div
class="rb2-pane-citrine">' + // Citrine phase:
HexCube navigation path glyphs. // Cell 0:
state (▼ sealed / △ active / ▲
potential). // Cells 1-2: HexCube position (x,
y). // Cells 3-4: Cube position. // Cells 5-6:
Cluster position. // Cells 7-8: Set position.
// Dividers after state, HC, Cube, Cluster —
each // level boundary marked. '<div
class="rb2-pill-citrine">' + citrinePathHtml()
+ '</div>' + // Emerald phase (Subu): 9 LED
cells with a divider // line after the day
section and a blinking colon // between hours
and minutes. '<div class="rb2-pill-led">' +
'<span class="rb2-led-cell"
data-led="d0">0</span>' + '<span
class="rb2-led-cell" data-led="d1">0</span>' +
'<span class="rb2-led-cell"
data-led="d2">0</span>' + '<span
class="rb2-led-cell" data-led="d3">0</span>' +
'<span class="rb2-led-cell rb2-led-divend"
data-led="d4">0</span>' + '<span
class="rb2-led-cell" data-led="t0">0</span>' +
'<span class="rb2-led-cell"
data-led="t1">0</span>' + '<span
class="rb2-led-cell rb2-led-colon"
data-led="t2">0</span>' + '<span
class="rb2-led-cell" data-led="t3">0</span>' +
'</div>' + '</div>' + // Glyph layer selector
— sits directly above the keys. '<div
class="rb2-glyph-pill-host"></div>' + '<div
class="rb2-pane-morganite">' + buildKeyboard()
+ '</div>' + '</div>' + // Output (Mylar) is a
sibling of the input box, not a // child —
it sits OUTSIDE the sage container, in the //
Runeboard's outer surface, below the keyboard.
'<div class="rb2-pane-mylar"></div>';
document.body.appendChild(el); var toggleBtn =
document.getElementById('rb2-toggle');
toggleBtn.innerHTML =
iconForState('compressed');
toggleBtn.dataset.iconState = 'compressed'; //
Mode State (state-glyph triangle) advances
forward through // the main cycle — its
glyph already tells you which mode // you're
in, so it doubles as the cycle-modes
affordance.
toggleBtn.addEventListener('click', advance);
// Circle (hollow outline) is the air toggle:
contract back // to compressed, or expand to
the last remembered state.
document.getElementById('rb2-collapse').addEventListener('click',
toggleStateButton); // Glyph pill renders into
its slot above the keyboard. Subject + // View
pills render inside the Mylar pane's head bar.
var ghost =
el.querySelector('.rb2-glyph-pill-host'); if
(ghost) ghost.innerHTML =
glyphPillHtml(currentLayer); attachDrag(el);
anime({ targets: '#rb2', opacity: [0, 1],
translateY: [-8, 0], duration: 420, easing:
'easeOutCubic' }); // Citrine ↔ Emerald
color swap every 4s. One interval for the //
life of the chip; the pill's visibility is
independent. var citrinePane =
el.querySelector('.rb2-pane-citrine');
setInterval(function () {
citrinePane.classList.toggle('is-emerald'); },
4000); // Subu tick — refreshes the SPB's
LED digits (and the // Spectraglyph Mylar if
it's active) every second so the // time stays
in step with the wall clock. function
tickSubu() { var d = subuDigits(); // Day
digits → d0..d4 cells, time digits →
t0..t3. for (var i = 0; i < 5; i++) { var cell
= citrinePane.querySelector('[data-led="d' + i
+ '"]'); if (cell) cell.textContent =
d.day[i]; } for (var j = 0; j < 4; j++) { var
tcell =
citrinePane.querySelector('[data-led="t' + j +
'"]'); if (tcell) tcell.textContent =
d.time[j]; } rerenderIfActive('subu'); }
tickSubu(); setInterval(tickSubu, 1000);
console.log('[RuneboardII] mounted in
compressed state'); // Default page-load state
is morganite (keyboard only, 229px). // Glyph
layer seeds from page context: being page →
daw, else subject default. // ?dev overrides
to the Mylar state so the Dev pane mounts
open. var params = new
URLSearchParams(location.search); var
initialState = 'morganite'; var
initialLayerCtx = window.BEING_NAME ? 'being'
: currentSubject; currentLayer =
SUBJECT_DEFAULT_LAYER[initialLayerCtx] ||
'wands'; if (params.has('dev')) {
currentSubject = 'dev'; subjectIdx =
MYLAR_SUBJECTS.indexOf('dev'); currentView =
SUBJECT_DEFAULT_VIEW.dev; currentLayer =
SUBJECT_DEFAULT_LAYER.dev; var mbtn =
document.getElementById('rb2-mylar-toggle');
if (mbtn) mbtn.innerHTML = mylarIcon('dev');
initialState = 'mylar'; } setTimeout(function
() { transitionTo(initialState); }, 480); } //
Pointer-based drag on the header. The buttons
inside the header // suppress drag so clicking
them doesn't move the panel. function
attachDrag(rb) { var header =
rb.querySelector('#rb2-header'); var startX =
0, startY = 0, originLeft = 0, originTop = 0,
dragging = false; function onDown(e) { //
Ignore clicks on the controls so toggles still
toggle. if (e.target.closest('#rb2-controls'))
return; dragging = true;
rb.classList.add('dragging'); var r =
rb.getBoundingClientRect(); originLeft =
r.left; originTop = r.top; startX = e.clientX;
startY = e.clientY; // Switch to left/top
positioning so we can move freely.
rb.style.left = originLeft + 'px';
rb.style.top = originTop + 'px';
rb.style.right = 'auto'; rb.style.bottom =
'auto'; try {
header.setPointerCapture(e.pointerId); } catch
(_) {} e.preventDefault(); } function
onMove(e) { if (!dragging) return; var dx =
e.clientX - startX; var dy = e.clientY -
startY; // Clamp to viewport so the board
can't disappear off-edge. var maxX =
window.innerWidth - rb.offsetWidth - 8; var
maxY = window.innerHeight - rb.offsetHeight -
8; var nx = Math.min(Math.max(8, originLeft +
dx), maxX); var ny = Math.min(Math.max(8,
originTop + dy), maxY); rb.style.left = nx +
'px'; rb.style.top = ny + 'px'; } function
onUp(e) { if (!dragging) return; dragging =
false; rb.classList.remove('dragging'); try {
header.releasePointerCapture(e.pointerId); }
catch (_) {} }
header.addEventListener('pointerdown',
onDown);
header.addEventListener('pointermove',
onMove); header.addEventListener('pointerup',
onUp);
header.addEventListener('pointercancel',
onUp); } // State Button glyph. Five states
map to four elemental shapes: // compressed
→ Air (hollow ▲) light blue // citrine →
Earth (filled ▼) light blue // morganite →
Earth (filled ▼) green // citrine_morganite
→ Earth (filled ▼) orange // expanded
(input) → Fire (filled ▲) orange var
ICON_COLORS = { blue: '#6ec6e0', green:
'#6ec06e', orange: '#e5734a' }; function
iconForState(name) { var up = '2.5,15 9,3
15.5,15'; var down = '2.5,3 15.5,3 9,15';
switch (name) { case 'compressed': return
'<svg viewBox="0 0 18 18" aria-hidden="true">'
+ '<polygon points="' + up + '" fill="none"
stroke="' + ICON_COLORS.blue + '"
stroke-width="1.6"
stroke-linejoin="round"/></svg>'; case
'citrine': return '<svg viewBox="0 0 18 18"
aria-hidden="true">' + '<polygon points="' +
down + '" fill="' + ICON_COLORS.blue +
'"/></svg>'; case 'morganite': return '<svg
viewBox="0 0 18 18" aria-hidden="true">' +
'<polygon points="' + down + '" fill="' +
ICON_COLORS.green + '"/></svg>'; case
'citrine_morganite': return '<svg viewBox="0 0
18 18" aria-hidden="true">' + '<polygon
points="' + down + '" fill="' +
ICON_COLORS.orange + '"/></svg>'; case
'expanded': return '<svg viewBox="0 0 18 18"
aria-hidden="true">' + '<polygon points="' +
up + '" fill="' + ICON_COLORS.orange +
'"/></svg>'; default: return ''; } } // Key
labels mirror hex.hexcraft.dev's runeboard I
keymap: numbers // 1-9 on top, then
QWERTY/ASDFGH/ZXCVBNM rows split into three
3×3 // sub-grids (nav | wand | rune). The
center cell of each 3×3 is // rendered as a
circle key; everything else is a rounded
square. var KEY_ROWS = [
['1','2','3','4','5','6','7','8','9'],
['Q','W','E','R','T','Y','U','I','O'],
['A','S','D','F','G','H','J','K','L'],
['Z','X','C','V','B','N','M',',','.'] ]; //
Morganite-only state cycles the keys through
four layers. // Each entry is the per-key
content for that layer in row-major order. //
Phosphor icons render via <i class="ph-light
ph-NAME"></i> — the // being page already
pulls in the Phosphor stylesheet. var LAYERS =
{ // Layer 0: ordinary keyboard keyboard:
KEY_ROWS.flat(), // Layer 1: elemental glyphs
(triangles + circles per rb1 keymap)
elemental: [
'1','2','3','4','5','6','7','8','9',
ph('arrow-up-left'), ph('arrow-up'),
ph('arrow-up-right'), tri('up'),
tri('up','fill'), tri('up'), tri('up'),
tri('up','fill'), tri('up'), ph('arrow-left'),
ph('crosshair'), ph('arrow-right'), circ(),
tri('up'), circ('fill'), circ(), tri('up'),
circ('fill'), ph('arrow-down-left'),
ph('arrow-down'), ph('arrow-down-right'),
tri('up','fill'), tri('down','fill'),
tri('up','fill'), tri('up','fill'),
tri('down','fill'), tri('up','fill') ], //
Layer 2: binary — read column as a 4-bit
count, top row = 1s binary: (function () { var
out = []; for (var r = 0; r < 4; r++) { for
(var c = 0; c < 9; c++) { out.push(((c + 1) >>
r) & 1 ? '1' : '0'); } } return out; })(), //
Layer 3: arrows. // Wand 1: outward arrows
from origin (move) // Wand 2: repeat of wand 1
// Wand 3: inward arrows toward a centre
crosshair (target / look) arrows: [
'','','','','','','','','', // Top wand row
ph('arrow-up-left'), ph('arrow-up'),
ph('arrow-up-right'), ph('arrow-up-left'),
ph('arrow-up'), ph('arrow-up-right'),
ph('arrow-down-right'),ph('arrow-down'),
ph('arrow-down-left'), // Middle wand row
ph('arrow-left'), ph('dot-outline'),
ph('arrow-right'), ph('arrow-left'),
ph('dot-outline'), ph('arrow-right'),
ph('arrow-right'), ph('crosshair'),
ph('arrow-left'), // Bottom wand row
ph('arrow-down-left'), ph('arrow-down'),
ph('arrow-down-right'), ph('arrow-down-left'),
ph('arrow-down'), ph('arrow-down-right'),
ph('arrow-up-right'), ph('arrow-up'),
ph('arrow-up-left') ], // Layer 4: glyph
coordinates — every wand cell gets a
2-triangle // coord. Same pattern repeats in
all three wands; meaning of // the wand
(move/select/look) is independent of the
encoding. coords: (function () { var out = [];
for (var i = 0; i < 9; i++) out.push(String(i
+ 1)); for (var r = 0; r < 3; r++) { for (var
c = 0; c < 9; c++) out.push(coord(c % 3, r));
} return out; })(), // Default composite layer
— each wand carries a different // glyph
type rather than the same one repeated. //
Wand 1 (Move): outward phosphor arrows // Wand
2 (Select): elemental triangles + circles
(charge = ▲) // Wand 3 (Look): 2-triangle
coordinate glyphs wands: (function () { var
out = []; for (var i = 0; i < 9; i++)
out.push(String(i + 1)); // Row 1 — top
out.push( ph('arrow-up-left'), ph('arrow-up'),
ph('arrow-up-right'), tri('up'),
tri('up','fill'), tri('down','fill'), coord(0,
0), coord(1, 0), coord(2, 0) ); // Row 2 —
middle (charge row: S / G / K) out.push(
ph('arrow-left'), ph('dot-outline'),
ph('arrow-right'), circ(), tri('up','fill'),
circ('fill'), coord(0, 1), coord(1, 1),
coord(2, 1) ); // Row 3 — bottom out.push(
ph('arrow-down-left'), ph('arrow-down'),
ph('arrow-down-right'), tri('down','fill'),
tri('down'), tri('up','fill'), coord(0, 2),
coord(1, 2), coord(2, 2) ); return out; })(),
// Layer 5: DAW — actual phosphor transport
icons mirroring the // being page's
.bp-tl-transport palette. Number row = zoom +
// marker controls; wand 3×3 = transport row
repeated across // all three wands with play
at the center (charge key). daw: (function ()
{ var nums = [ ph('globe-hemisphere-east'),
ph('magnifying-glass-minus'),
ph('magnifying-glass'),
ph('magnifying-glass-plus'), ph('crosshair'),
ph('music-note'), ph('music-notes'),
ph('repeat'), ph('speaker-high') ]; var wand =
[ ph('rewind'), ph('skip-back'),
ph('skip-forward'), ph('stop'), ph('play'),
ph('pause'), ph('record'),
ph('arrow-counter-clockwise'),
ph('fast-forward') ]; var out = []; for (var i
= 0; i < 9; i++) out.push(nums[i]); for (var r
= 0; r < 3; r++) { for (var c = 0; c < 9; c++)
{ var pos = (r * 3) + (c % 3);
out.push(wand[pos]); } } return out; })(), //
Layer 6: Cycles — Subu planet symbols. Same
shape as DAW but // semantically different:
these are cycle counters, not // transport
controls. cycles: (function () { var nums =
['☉','☽','☿','♀','♁','♂','♃','♄','⛢'];
var wand =
['☽','☿','♀','♂','☉','♃','♄','⊕','⛢'];
var out = []; for (var i = 0; i < 9; i++)
out.push(dawSym(nums[i])); for (var r = 0; r <
3; r++) { for (var c = 0; c < 9; c++) { var
pos = (r * 3) + (c % 3);
out.push(dawSym(wand[pos])); } } return out;
})() }; // Layer cycle order — Glyph pill
walks this forward. Binary kept // in LAYERS
but dropped from the cycle. var LAYER_ORDER =
['wands', 'coords', 'daw', 'cycles',
'keyboard', 'elemental', 'arrows']; var
currentLayer = 'wands'; function ph(name) {
return '<i class="ph-light ph-' + name +
'"></i>'; } function tri(dir, fill) { var f =
fill === 'fill'; var pts = dir === 'down' ?
'2,4 14,4 8,14' : '8,2 14,14 2,14'; return
'<svg viewBox="0 0 16 16"
class="rb2-glyph"><polygon points="' + pts +
'" ' + (f ? 'fill="currentColor"' :
'fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linejoin="round"') +
'/></svg>'; } function circ(fill) { var f =
fill === 'fill'; return '<svg viewBox="0 0 16
16" class="rb2-glyph"><circle cx="8" cy="8"
r="5" ' + (f ? 'fill="currentColor"' :
'fill="none" stroke="currentColor"
stroke-width="1.5"') + '/></svg>'; } //
Glyph-coordinate system — each wand key is a
2-triangle glyph // encoding (column, row)
within its 3×3 wand. Left triangle = col //
(▼ left / △ mid / ▲ right). Right
triangle = row (▲ top / △ mid / // ▼
bottom). Center cell of every wand is △△ =
the charge key. function coordTri(x, kind) {
var pts; if (kind === 'fd') { pts = x + ',4 '
+ (x + 9) + ',4 ' + (x + 4.5) + ',12'; return
'<polygon points="' + pts + '"
fill="currentColor"/>'; } pts = x + ',12 ' +
(x + 9) + ',12 ' + (x + 4.5) + ',4'; if (kind
=== 'fu') { return '<polygon points="' + pts +
'" fill="currentColor"/>'; } // hu — hollow
up return '<polygon points="' + pts + '"
fill="none" stroke="currentColor"
stroke-width="1.4" stroke-linejoin="round"/>';
} function coord(c, r) { var leftKind = ['fd',
'hu', 'fu'][c]; var rightKind = ['fu', 'hu',
'fd'][r]; return '<svg viewBox="0 0 24 16"
class="rb2-glyph rb2-coord">' + coordTri(2,
leftKind) + coordTri(13, rightKind) +
'</svg>'; } // DAW symbol — wraps a Unicode
astronomical glyph so it can be //
sized/styled independently of the default key
font. function dawSym(s) { return '<span
class="rb2-daw">' + s + '</span>'; } function
buildKeyboard() { var html = ''; for (var r =
0; r < KEY_ROWS.length; r++) { html += '<div
class="rb2-row">'; for (var c = 0; c < 9; c++)
{ var isCenter = (r === 2) && (c === 1 || c
=== 4 || c === 7); var cls = 'rb2-key' +
(isCenter ? ' circle' : ''); var label =
KEY_ROWS[r][c]; html += '<div class="' + cls +
'" data-key="' + label + '">' + label +
'</div>'; } html += '</div>'; } var mods =
['ctrl','opt','cmd','space','tab','shift','esc','del','back'];
html += '<div class="rb2-row">';
mods.forEach(function (m) { html += '<div
class="rb2-key mod" data-key="' + m + '">' + m
+ '</div>'; }); html += '</div>'; return html;
} // Three.js Mylar — lazy-loaded scene that
mirrors the 2D page in // 3D. Right now: a
rotating icosahedron coloured from
focusedThing's // sapphire (Thing type) with a
label below. Hover/click any // .bp-item and
the 3D updates to follow. No interaction yet.
var threedState = null; var threedLoading =
false; var THREEJS_URL =
'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js';
function loadThreeJs(cb) { if (window.THREE) {
cb(); return; } if (threedLoading) {
setTimeout(function () { loadThreeJs(cb); },
100); return; } threedLoading = true; var s =
document.createElement('script'); s.src =
THREEJS_URL; s.onload = function () {
threedLoading = false; cb(); }; s.onerror =
function () { threedLoading = false; };
document.head.appendChild(s); } function
teardown3D() { if (!threedState) return; if
(threedState.raf)
cancelAnimationFrame(threedState.raf); if
(threedState.renderer)
threedState.renderer.dispose(); threedState =
null; } function sapphireColor(s) { var m = {
being: 0x6ec6e0, video: 0xe5734a, scroll:
0x9c27b0, chip: 0x6ec06e, ring: 0xb15bd1,
temple: 0xffc107, 'mesh-node': 0x2bb3a3 };
return m[s] || 0xcccccc; } function
init3D(container) { if (!window.THREE) return;
teardown3D(); var W = container.clientWidth ||
320; var H = container.clientHeight || 280;
var renderer = new THREE.WebGLRenderer({
antialias: true, alpha: true });
renderer.setPixelRatio(window.devicePixelRatio
|| 1); renderer.setSize(W, H);
renderer.setClearColor(0x000000, 0);
container.appendChild(renderer.domElement);
var scene = new THREE.Scene(); var camera =
new THREE.PerspectiveCamera(45, W / H, 0.1,
100); camera.position.z = 4; var geom = new
THREE.IcosahedronGeometry(1.1, 0); var mat =
new THREE.MeshStandardMaterial({ color:
0xcccccc, flatShading: true, metalness: 0.2,
roughness: 0.6 }); var mesh = new
THREE.Mesh(geom, mat); scene.add(mesh); var
key = new THREE.DirectionalLight(0xffffff,
1.2); key.position.set(2, 3, 2);
scene.add(key); scene.add(new
THREE.AmbientLight(0x5b6e6a, 0.7));
threedState = { renderer: renderer, scene:
scene, camera: camera, mesh: mesh, container:
container }; update3DFromFocused(); function
loop() { if (!threedState) return; if
(!container.isConnected) { teardown3D();
return; } threedState.mesh.rotation.x +=
0.006; threedState.mesh.rotation.y += 0.009;
renderer.render(scene, camera);
threedState.raf = requestAnimationFrame(loop);
} loop(); } function update3DFromFocused() {
if (!threedState || !threedState.mesh) return;
var g = (focusedThing &&
focusedThing.gem_values) || {};
threedState.mesh.material.color.setHex(sapphireColor(g.sapphire));
} // ── World-canvas docking
──────────────────────────────────────────
// When 3D view is active, hoist the live
page-background #world-canvas // INTO the
Mylar pane via a FLIP transform animation.
Restore on // leave. This reuses Web Vessel's
actual Three.js world rather than // running a
second renderer. var worldDocked = false; var
worldHome = null; function findWorldDiv() { //
Only treat the world as "real" if it actually
has a <canvas> // inside — an empty
#world-canvas div means a page reserved a //
slot but no world script ever populated it.
Fall back to the // local icosahedron in that
case. var div =
document.getElementById('world-canvas'); if
(!div || !div.querySelector('canvas')) return
null; return div; } function
dockWorldCanvas(container) { if (worldDocked)
return; var div = findWorldDiv(); if (!div ||
!container) return; worldHome = { parent:
div.parentElement, nextSibling:
div.nextSibling, style:
div.getAttribute('style') || '' }; var first =
div.getBoundingClientRect();
container.appendChild(div);
div.classList.add('rb2-docked-world'); var
last = div.getBoundingClientRect(); var dx =
first.left - last.left; var dy = first.top -
last.top; var sx = last.width ? first.width /
last.width : 1; var sy = last.height ?
first.height / last.height : 1; // FLIP: snap
to the divs old visible position, then glide
home // over 720ms with an expo-out ease.
anime.set(div, { translateX: dx, translateY:
dy, scaleX: sx, scaleY: sy }); anime({
targets: div, translateX: 0, translateY: 0,
scaleX: 1, scaleY: 1, duration: 720, easing:
'easeOutExpo', complete: function () { anime({
targets: div, scaleX: [1, 1.04, 1], scaleY:
[1, 1.04, 1], duration: 320, easing:
'easeOutBack' }); window.dispatchEvent(new
Event('resize')); } }); worldDocked = true;
logEvent('dock · world → mylar'); }
function restoreWorldCanvas() { if
(!worldDocked || !worldHome) return; var div =
findWorldDiv(); if (!div) { worldDocked =
false; worldHome = null; return; } var first =
div.getBoundingClientRect();
worldHome.parent.insertBefore(div,
worldHome.nextSibling || null);
div.classList.remove('rb2-docked-world'); var
last = div.getBoundingClientRect(); var dx =
first.left - last.left; var dy = first.top -
last.top; var sx = last.width ? first.width /
last.width : 1; var sy = last.height ?
first.height / last.height : 1; anime.set(div,
{ translateX: dx, translateY: dy, scaleX: sx,
scaleY: sy }); var savedStyle =
worldHome.style; anime({ targets: div,
translateX: 0, translateY: 0, scaleX: 1,
scaleY: 1, duration: 560, easing:
'easeOutExpo', complete: function () { if
(savedStyle) div.setAttribute('style',
savedStyle); else
div.removeAttribute('style');
window.dispatchEvent(new Event('resize')); }
}); worldDocked = false; worldHome = null;
logEvent('dock · world ← mylar'); }
function viewPillHtml(subject, view) { return
'<div class="rb2-view-pill">' +
VIEWS.map(function (v) { return '<button
class="rb2-view-chip' + (v === view ? '
active' : '') + '" data-view="' + v + '">' +
(v === 'text' ? 'TXT' : v.toUpperCase()) +
'</button>'; }).join('') + '</div>'; } //
Glyph pill — compact dot row + name of the
active layer. Each // dot is clickable to jump
to that layer. function glyphPillHtml(layer) {
var dots = LAYER_ORDER.map(function (l) {
return '<button class="rb2-glyph-dot' + (l ===
layer ? ' active' : '') + '" data-layer="' + l
+ '" title="' + l + '"></button>';
}).join(''); return '<div
class="rb2-glyph-pill">' + '<span
class="rb2-glyph-name">' + escapeHtml(layer) +
'</span>' + '<span class="rb2-glyph-dots">' +
dots + '</span>' + '</div>'; } // Subject pill
— same shape, lists every Mylar subject.
Replaces // the dropdown so everything in the
head bar is the same idiom. function
subjectPillHtml(subject) { return '<div
class="rb2-subject-pill">' +
MYLAR_SUBJECTS.map(function (s) { return
'<button class="rb2-subject-chip' + (s ===
subject ? ' active' : '') + '" data-subject="'
+ s + '">' + s + '</button>'; }).join('') +
'</div>'; } function headBarHtml(subject,
view) { // Glyph pill moved out of the head
bar — it lives above the // keyboard now,
since it controls what's on the keys. return
'<div class="rb2-mylar-head-bar">' +
subjectPillHtml(subject) +
viewPillHtml(subject, view) + '</div>'; }
function renderMylarPane(pane, subject, view)
{ if (!pane) return; if (view === '3d') { //
Prefer docking the page's live #world-canvas
when present // — reuses Web Vessel's actual
Three.js world. Falls back to // our local
icosahedron scene on pages without a world.
var realWorld = findWorldDiv(); if (realWorld)
{ if
(!pane.querySelector('.rb2-three-container'))
{ pane.innerHTML = headBarHtml(subject, view)
+ '<div class="rb2-mylar-content
rb2-mylar-3d">' + '<div
class="rb2-three-container"></div>' +
'</div>'; var c =
pane.querySelector('.rb2-three-container');
dockWorldCanvas(c); } else { var oldHead =
pane.querySelector('.rb2-mylar-head-bar'); if
(oldHead) oldHead.outerHTML =
headBarHtml(subject, view); } return; } //
Fallback: local icosahedron scene. if
(!pane.querySelector('.rb2-three-container'))
{ pane.innerHTML = headBarHtml(subject, view)
+ '<div class="rb2-mylar-content">' + '<div
class="rb2-three-container"></div>' + '<div
class="rb2-three-label">—</div>' + '</div>';
var ic =
pane.querySelector('.rb2-three-container');
loadThreeJs(function () { init3D(ic); }); }
else { var oldHead2 =
pane.querySelector('.rb2-mylar-head-bar'); if
(oldHead2) oldHead2.outerHTML =
headBarHtml(subject, view); }
update3DFromFocused(); var label =
pane.querySelector('.rb2-three-label'); if
(label) { var name = focusedThing &&
(focusedThing.display_title ||
(focusedThing.gem_values || {}).onyx);
label.textContent = name || '—'; } return; }
// Leaving 3D → restore the docked world (if
any) and tear down // the fallback WebGL loop.
if (worldDocked) restoreWorldCanvas(); if
(threedState) teardown3D(); pane.innerHTML =
headBarHtml(subject, view) + '<div
class="rb2-mylar-content">' +
buildMylar(subject, view) + '</div>'; } //
Mylar State button glyph — a small rightward
triangle (▶) that // tints by variant so the
user can see which output is active. //
Subject color — used by both the Mylar State
button glyph and // any subject-tinted UI in
the pane. var MYLAR_COLORS = { terminal:
'#222222', // ink — terminal/event log coin:
'#d4d4dc', // pearl — flip card: '#c62828',
// ruby — pull dice: '#6ec6e0', // sky —
roll feed: '#ffb300', // topaz — feed
surface forge: '#e5734a', // citrine — forge
hexcube: '#2bb3a3', // teal — 3D / hexcube
grimoire: '#9c27b0', // amethyst — grimoire
subu: '#2e8b57', // emerald — Subu things:
'#e5734a', // citrine — Thing rings:
'#b15bd1', // amethyst — Rings gems:
'#b15bd1', // amethyst — Gems dev: '#6ec06e'
// green — dev }; function
mylarIcon(subject) { var color =
MYLAR_COLORS[subject] || '#888'; return '<svg
viewBox="0 0 16 16" aria-hidden="true">' +
'<polygon points="4,2 13,8 4,14" fill="' +
color + '"/></svg>'; } function advanceMylar()
{ if (MYLAR_SUBJECTS.length === 0) return; var
next = (subjectIdx + 1) %
MYLAR_SUBJECTS.length; var prev =
currentSubject; subjectIdx = next;
currentSubject = MYLAR_SUBJECTS[next];
currentView =
SUBJECT_DEFAULT_VIEW[currentSubject];
logEvent('mylar · ' + prev + ' → ' +
currentSubject); var mylarBtn =
document.getElementById('rb2-mylar-toggle');
if (mylarBtn) mylarBtn.innerHTML =
mylarIcon(currentSubject); var rb =
document.getElementById('rb2'); if (rb &&
rb.classList.contains('has-mylar')) { var pane
= rb.querySelector('.rb2-pane-mylar');
renderMylarPane(pane, currentSubject,
currentView); } } // Build the content for a
given Mylar variant. The dev variant is // a
structured read-out of Runeboard internals —
it's the dev // window from ?dev re-housed
inside the Mylar layout, so the // panel acts
as a debug HUD while the composer below stays
live // for input. function rowsHtml(rows) {
return rows.map(function (r) { return '<div
class="rb2-mylar-row"><span
class="rb2-mylar-key">' + escapeHtml(r[0]) +
'</span><span class="rb2-mylar-val">' +
escapeHtml(r[1]) + '</span></div>';
}).join(''); } function escapeHtml(s) { s =
String(s == null ? '' : s); return
s.replace(/[&<>"]/g, function (c) { return {
'&':'&', '<':'<', '>':'>',
'"':'"' }[c]; }); } // Terminal Mylar —
passive event log of Runeboard internals. //
logEvent() is called from transitionTo,
variant switches, click // and hover handlers;
the terminal renders newest-first. var
eventLog = []; var EVENT_LOG_MAX = 60;
function logEvent(line) { eventLog.unshift({
t: Date.now(), line: line }); if
(eventLog.length > EVENT_LOG_MAX)
eventLog.length = EVENT_LOG_MAX; // Terminal
is now the universal Text fallback — refresh
the // pane whenever the current view is
text-on-anything. if (currentView === 'text')
rerenderIfActive(currentSubject); } // Pull
Mylar — counter + last-pulled timestamp. The
Pull button // re-runs /api/things/recent and
updates the values. Hex-style // "pull" =
fetch the freshest Things, refresh the count.
var pullState = { count: '—', at: null,
busy: false }; function doPull() { if
(pullState.busy) return; pullState.busy =
true; rerenderIfActive('card');
fetch('/api/things/recent?limit=20')
.then(function (r) { return r.ok ? r.json() :
null; }) .then(function (j) { pullState.busy =
false; if (!j) return; pullState.count =
j.count != null ? j.count : (j.things ?
j.things.length : '?'); pullState.at =
Date.now(); logEvent('pull · ' +
pullState.count + ' recent');
rerenderIfActive('card'); }) .catch(function
() { pullState.busy = false; }); } // Terminal
= the universal Text fallback. Subjects with
their own // Text rendering use that;
everything else lands here in text view.
function terminalText(subject) { if
(eventLog.length === 0) { return '<div
class="rb2-mylar-title">' + subject + ' ·
terminal</div>' + '<div
class="rb2-mylar-loading">no events yet —
interact with the Runeboard…</div>'; } var
lines = eventLog.slice(0, 40).map(function (e)
{ var ts = new Date(e.t).toLocaleTimeString();
return '<div class="rb2-term-line"><span
class="rb2-term-ts">' + ts + '</span><span
class="rb2-term-text">' + escapeHtml(e.line) +
'</span></div>'; }).join(''); return '<div
class="rb2-mylar-title">' + subject + ' ·
terminal · ' + eventLog.length + '</div>' +
lines; } function placeholder(subject, view) {
return '<div class="rb2-mylar-title">' +
escapeHtml(subject) + ' · ' + view + '</div>'
+ '<div class="rb2-mylar-loading">—
rendering not yet implemented —</div>'; } //
buildMylar dispatches by subject, then by
view. The 3D view is // handled in
renderMylarPane (canvas preserved); we never
reach // here with view === '3d' unless the
subject has a non-canvas // 3D fallback, which
currently none do — placeholder it. function
buildMylar(subject, view) { if (view === '3d'
&& subject !== 'hexcube') return
placeholder(subject, '3d'); if (subject ===
'coin') { if (!focusedThing) {
kickFocusedThingFetch(); return '<div
class="rb2-mylar-title">coin</div>' + '<div
class="rb2-mylar-loading">no focused Thing —
click a .bp-item first</div>'; } if (view ===
'text') return terminalText('coin'); // 2D =
verso/flip card. var fg =
focusedThing.gem_values || {}; var fkeys =
Object.keys(fg).sort(function (a, b) { return
b.localeCompare(a); }); var fentries =
fkeys.map(function (k) { var v = fg[k]; return
[k, typeof v === 'object' ? JSON.stringify(v)
: String(v)]; }); return '<div
class="rb2-mylar-head">' + '<span
class="rb2-mylar-title">coin · verso</span>'
+ '</div>' + rowsHtml(fentries); } if (subject
=== 'card') { if (view === 'text') return
terminalText('card'); var when = pullState.at
? new Date(pullState.at).toLocaleTimeString()
: '—'; return '<div
class="rb2-mylar-title">card · deck</div>' +
rowsHtml([ ['count', pullState.count], ['last
pull', when], ['status', pullState.busy ?
'pulling…' : 'idle'] ]) + '<button
class="rb2-pull-btn" data-action="pull">▼
pull recent</button>'; } if (subject ===
'dice') { if (view === 'text') return
terminalText('dice'); if (diceLandings.length
=== 0) { return '<div
class="rb2-mylar-title">dice</div>' + '<div
class="rb2-mylar-loading">no rolls yet —
waiting for a <code>dice-landed</code>
event</div>'; } var latest = diceLandings[0];
var face = String(latest.face != null ?
latest.face : '?'); var fnum = parseInt(face,
10); var faces = ''; for (var f = 1; f <= 9;
f++) { faces += '<span class="rb2-roll-face' +
(Number.isFinite(fnum) && (fnum + 1) === f ? '
lit' : '') + '">' + f + '</span>'; } var
history = diceLandings.slice(0,
8).map(function (r, i) { var ts = new
Date(r.t).toLocaleTimeString(); var label =
(r.title ? r.title + ' · ' : '') + 'f' +
r.face; return [i === 0 ? 'last' : '−' + i,
label + ' ' + ts]; }); return '<div
class="rb2-mylar-title">dice · ' +
diceLandings.length + '</div>' + '<div
class="rb2-roll-faces">' + faces + '</div>' +
rowsHtml(history); } if (subject === 'feed') {
if (view === 'text') return
terminalText('feed'); var feed =
document.querySelector('.bp-feed'); if (!feed)
{ return '<div
class="rb2-mylar-title">feed</div>' + '<div
class="rb2-mylar-loading">.bp-feed not found
on this page</div>'; } return '<div
class="rb2-mylar-title">feed · ' +
feed.children.length + '</div>' + '<div
class="rb2-2d-host">' + feed.innerHTML +
'</div>'; } if (subject === 'subu') { if (view
=== 'text') return terminalText('subu'); var d
= subuDigits(); return '<div
class="rb2-mylar-title">subu ·
subuglass</div>' + '<div
class="rb2-sg-stack">' + '<div
class="rb2-sg-row">' + '<span
class="rb2-sg-label">day</span>' + '<span
class="rb2-sg-led rb2-sg-day">' + d.day +
'</span>' + '</div>' + '<div
class="rb2-sg-row">' + '<span
class="rb2-sg-label">time</span>' + '<span
class="rb2-sg-led rb2-sg-time">' + d.time +
'<span class="rb2-sg-secs">·' + d.seconds +
'</span></span>' + '</div>' + '<div
class="rb2-sg-row rb2-sg-cycle">' + '<span
class="rb2-sg-label">cycle</span>' + '<span
class="rb2-sg-placeholder">— TBD —</span>'
+ '</div>' + '</div>'; } if (subject ===
'dev') { // Dev's text view = the dev
manifest. 2D variant is the // same content
for now until a graphical layout exists. var
world = window.WORLD_NAME || new
URLSearchParams(location.search).get('world')
|| 'cosmostack'; kickDevChipsFetch(); var gems
= window.__hexGems || {}; var gemKeys =
Object.keys(gems); var chipsList =
devChips.length ? devChips.slice(0,
12).map(function (c) { return '<div
class="rb2-mylar-row"><span
class="rb2-mylar-key">' + escapeHtml(c.name) +
'</span><span class="rb2-mylar-val">' +
escapeHtml((c.byte_weight || 0) + 'b') +
'</span></div>'; }).join('') : '<div
class="rb2-mylar-loading">loading
chips…</div>'; return '<div
class="rb2-mylar-title">◇ dev · ' +
escapeHtml(world) + '</div>' + rowsHtml([
['sacred day', sacredDay()], ['world', world],
['chips', devChips.length || '—'], ['gems',
gemKeys.length], ['being', window.BEING_NAME
|| '—'] ]) + '<div
class="rb2-mylar-section">chips</div>' +
chipsList; } if (subject === 'things') { if
(!focusedThing) { kickFocusedThingFetch();
return '<div
class="rb2-mylar-loading">fetching focused
Thing…</div>'; } if (view === 'text' || view
=== '2d') { var g = focusedThing.gem_values ||
{}; return '<div class="rb2-mylar-head">' +
'<span class="rb2-mylar-title">' +
escapeHtml(focusedThing.display_title ||
g.onyx || 'untitled') + '</span>' + '<button
class="rb2-pair-toggle"
data-target="gems">gems ▸</button>' +
'</div>' + rowsHtml([ ['type', g.sapphire ||
focusedThing.type || '—'], ['id',
(focusedThing.document_id || '').slice(0, 8)],
['created', (focusedThing.created_at ||
'').slice(0, 19).replace('T', ' ')], ['vis',
(g.amethyst && g.amethyst.visibility) ||
'public'], ['onyx', g.onyx || '—'],
['pearl', typeof g.pearl === 'string' ?
g.pearl : (g.pearl && g.pearl.title) || '—']
]); } } if (subject === 'gems') { if
(!focusedThing) { kickFocusedThingFetch();
return '<div
class="rb2-mylar-loading">fetching
gems…</div>'; } if (view === 'text' || view
=== '2d') { var gem = focusedThing.gem_values
|| {}; var order = ['onyx', 'sapphire',
'ruby', 'pearl', 'amethyst', 'diamond',
'topaz', 'citrine']; var entries = order
.filter(function (k) { return gem[k] !==
undefined; }) .map(function (k) { return [k,
typeof gem[k] === 'object' ?
JSON.stringify(gem[k]) : gem[k]]; }); return
'<div class="rb2-mylar-head">' + '<span
class="rb2-mylar-title">gem facets</span>' +
'<button class="rb2-pair-toggle"
data-target="things">◂ things</button>' +
'</div>' + rowsHtml(entries); } } // Forge /
Grimoire / Hexcube / Rings — subjects with
no 2D // renderer yet. Text view → universal
terminal fallback; // 2D → placeholder. if
(view === 'text') return
terminalText(subject); return
placeholder(subject, view); } function
kickFocusedThingFetch() { if (focusedThing ||
focusedThingFetching) return;
focusedThingFetching = true;
fetch('/api/things/list?limit=1')
.then(function (r) { return r.ok ? r.json() :
null; }) .then(function (j) {
focusedThingFetching = false; var t = j &&
(j.things && j.things[0]); if (!t) return;
focusedThing = t; // Re-render if a
focused-thing-dependent subject is active. var
rb = document.getElementById('rb2'); if (rb &&
rb.classList.contains('has-mylar') &&
(currentSubject === 'things' || currentSubject
=== 'gems' || currentSubject === 'coin')) {
var pane =
rb.querySelector('.rb2-pane-mylar');
renderMylarPane(pane, currentSubject,
currentView); } }) .catch(function () {
focusedThingFetching = false; }); } // Per-key
content swap for morganite-only glyph cycling.
Skips the // mod row (last row) — only the
4×9 main grid rotates. function
applyLayer(name) { var rb =
document.getElementById('rb2'); if (!rb)
return; var keys =
rb.querySelectorAll('.rb2-pane-morganite
.rb2-row'); var layer = LAYERS[name] ||
LAYERS.keyboard; for (var r = 0; r < 4 && r <
keys.length; r++) { var cells =
keys[r].querySelectorAll('.rb2-key'); for (var
c = 0; c < cells.length; c++) {
cells[c].innerHTML = layer[r * 9 + c] || ''; }
} } // Morganite Switcher icon — a
coord-style 2-triangle pair tinted // by layer
so the icon reads as "which layer is on the
keys". var LAYER_COLORS = { wands: '#e5734a',
// citrine — canonical default (mixed)
coords: '#d4af37', // gold — pure coords
daw: '#FF9800', // accent orange — transport
(being-page DAW) cycles: '#2e8b57', // emerald
— Subu cycle planets keyboard: '#1a1a1a', //
ink — alpha letters elemental: '#6ec06e', //
green — elemental glyphs arrows: '#6ec6e0',
// sky — nav arrows binary: '#b15bd1' //
amethyst — binary count }; function
morganiteIcon(layer) { var color =
LAYER_COLORS[layer] || '#e5734a'; return '<svg
viewBox="0 0 24 16" aria-hidden="true">' +
'<polygon points="2,12 11,12 6.5,4" fill="' +
color + '"/>' + '<polygon points="13,4 22,4
17.5,12" fill="' + color + '"/>' + '</svg>'; }
function advanceLayer() { var idx =
LAYER_ORDER.indexOf(currentLayer); var next =
LAYER_ORDER[(idx + 1) % LAYER_ORDER.length];
var prev = currentLayer; currentLayer = next;
applyLayer(currentLayer); var btn =
document.getElementById('rb2-morganite-toggle');
if (btn) btn.innerHTML =
morganiteIcon(currentLayer); logEvent('layer
· ' + prev + ' → ' + currentLayer); } var
glyphCycleTimer = null; var glyphCycleIndex =
0; function startGlyphCycle() {
stopGlyphCycle(); glyphCycleIndex = 0;
applyLayer(LAYER_ORDER[0]); glyphCycleTimer =
setInterval(function () { glyphCycleIndex =
(glyphCycleIndex + 1) % LAYER_ORDER.length;
var pane = document.querySelector('#rb2
.rb2-pane-morganite'); if (!pane) {
stopGlyphCycle(); return; } anime({ targets:
pane, opacity: [1, 0], duration: 250, easing:
'easeInQuad', complete: function () {
applyLayer(LAYER_ORDER[glyphCycleIndex]);
anime({ targets: pane, opacity: [0, 1],
duration: 250, easing: 'easeOutQuad' }); } });
}, 2800); } function stopGlyphCycle() { if
(glyphCycleTimer) {
clearInterval(glyphCycleTimer);
glyphCycleTimer = null; }
applyLayer(currentLayer); } // Placeholder
typewriter — types each message
char-by-char, holds, // fades, rotates. Only
runs while expanded AND the composer is empty.
var PLACEHOLDERS = [ 'Write some glyphs...',
'Try /help for commands', 'Click runes or type
directly', 'Enter sends, Esc cancels', '/login
to authenticate' ]; var phTimer = null; var
phIndex = 0; function typewriter(msg, i) { var
c = document.querySelector('#rb2
.rb2-composer'); if (!c || c.textContent ||
current !== 'expanded') return; if (i <=
msg.length) {
c.setAttribute('data-placeholder',
msg.substring(0, i)); phTimer =
setTimeout(function () { typewriter(msg, i +
1); }, 35); } else { phTimer =
setTimeout(function () {
c.classList.add('placeholder-fade'); phTimer =
setTimeout(function () { phIndex = (phIndex +
1) % PLACEHOLDERS.length;
c.setAttribute('data-placeholder', '');
c.classList.remove('placeholder-fade');
phTimer = setTimeout(function () {
typewriter(PLACEHOLDERS[phIndex], 0); }, 120);
}, 400); }, 2800); } } function
startPlaceholder() { stopPlaceholder(); var c
= document.querySelector('#rb2
.rb2-composer'); if (!c || current !==
'expanded') return;
typewriter(PLACEHOLDERS[phIndex], 0); }
function stopPlaceholder() { if (phTimer) {
clearTimeout(phTimer); phTimer = null; } var c
= document.querySelector('#rb2
.rb2-composer'); if (c) {
c.classList.remove('placeholder-fade');
c.setAttribute('data-placeholder', 'input'); }
} function transitionTo(name) { var s =
STATES[name]; if (!s) return; var prev =
current; current = name; var rb =
document.getElementById('rb2'); var body =
document.getElementById('rb2-body'); if (!rb
|| !body) return; var citrine =
rb.querySelector('.rb2-pane-citrine'); var
morganite =
rb.querySelector('.rb2-pane-morganite'); var
composer = rb.querySelector('.rb2-composer');
var prevShowed = STATES[prev] &&
STATES[prev].showCitrine; // Record citrine's
pre-toggle position so we can FLIP it into //
its new resting place when it's visible on
both sides of the // transition (e.g.
citrine_morganite → expanded — the margin
// and seam location both move). var
citrineBeforeTop = null; if (s.showCitrine &&
prevShowed) { citrineBeforeTop =
citrine.getBoundingClientRect().top; }
rb.classList.toggle('expanded', s.expanded);
rb.classList.toggle('has-mylar', !!s.mylar);
morganite.style.display = s.showMorganite ?
'flex' : 'none'; composer.style.display =
s.expanded ? 'block' : 'none'; // Glyph pill
follows the morganite pane — shown when keys
are. var ghostT =
rb.querySelector('.rb2-glyph-pill-host'); if
(ghostT) ghostT.style.display =
s.showMorganite ? 'flex' : 'none'; // Populate
the Mylar pane when this state shows output.
The // content variant is held in
currentMylarVariant and is // cycled by the
Mylar State button independently of the //
main state cycle. if (s.mylar) { var mylarPane
= rb.querySelector('.rb2-pane-mylar');
renderMylarPane(mylarPane, currentSubject,
currentView); } // Keep orderIdx aligned with
state when transitionTo is called // from
outside advance() (collapse,
toggleStateButton, etc.). if (ORDER[orderIdx]
!== name) { var foundIdx =
ORDER.indexOf(name); orderIdx = foundIdx >= 0
? foundIdx : 0; } // Citrine slides into / out
of view, never fades. Three cases: // newly
appearing → enter from translateY -40 //
leaving → exit to translateY -40 // staying
visible → FLIP from old position to new if
(s.showCitrine && !prevShowed) {
citrine.style.display = 'flex';
citrine.style.opacity = '1';
citrine.style.transform = 'translateY(-40px)';
anime.remove(citrine); anime({ targets:
citrine, translateY: [-40, 0], duration: 460,
delay: 180, easing: 'easeOutCubic' }); } else
if (!s.showCitrine && prevShowed) {
anime.remove(citrine); anime({ targets:
citrine, translateY: [0, -40], duration: 280,
easing: 'easeInQuad', complete: function () {
citrine.style.display = 'none'; } }); } else
if (s.showCitrine && prevShowed) {
citrine.style.display = 'flex';
citrine.style.opacity = '1'; // Force layout,
measure new resting position var _force =
rb.offsetHeight; var afterTop =
citrine.getBoundingClientRect().top; var dy =
citrineBeforeTop - afterTop;
anime.remove(citrine); if (Math.abs(dy) > 1) {
citrine.style.transform = 'translateY(' + dy +
'px)'; anime({ targets: citrine, translateY:
[dy, 0], duration: 460, easing: 'easeOutCubic'
}); } else { citrine.style.transform =
'translateY(0)'; } } else {
citrine.style.display = 'none'; } // State
button: contract-only when collapsing to air,
expand-only // when opening from air, classic
squish-pop between other states. var toggleBtn
= rb.querySelector('#rb2-toggle'); if
(toggleBtn) { anime.remove(toggleBtn); var
goingToAir = (name === 'compressed'); var
leavingAir = (prev === 'compressed' && name
!== 'compressed'); var scaleKeys; var swapAt =
35; if (goingToAir) { // Just contract; never
re-expand. The Air glyph appears // at the
contracted apex and rides the shrink in.
scaleKeys = [ { value: 0.2, duration: 220,
easing: 'easeInQuad' }, { value: 1, duration:
180, easing: 'easeOutQuad' } ]; swapAt = 50; }
else if (leavingAir) { // Just expand; start
from a contracted point and bloom // out to
the remembered state's glyph.
toggleBtn.style.transform = 'scale(0.2)';
scaleKeys = [ { value: 0.2, duration: 1,
easing: 'linear' }, { value: 1, duration: 340,
easing: 'easeOutBack' } ]; swapAt = 0; } else
{ scaleKeys = [ { value: 0.2, duration: 140,
easing: 'easeInQuad' }, { value: 1, duration:
240, easing: 'easeOutBack' } ]; } anime({
targets: toggleBtn, scale: scaleKeys, update:
function (a) { if (a.progress >= swapAt &&
toggleBtn.dataset.iconState !== name) {
toggleBtn.dataset.iconState = name;
toggleBtn.innerHTML = iconForState(name); } }
}); } // Morganite-only state auto-cycles the
layers. Every other // state honors the user's
chosen layer (from Morganite Switcher). if
(name === 'morganite') startGlyphCycle(); else
{ stopGlyphCycle(); applyLayer(currentLayer);
} // Expanded state runs the typewriter
placeholder rotation. if (name === 'expanded')
startPlaceholder(); else stopPlaceholder();
anime({ targets: '#rb2', height: s.height,
borderRadius: s.radius, duration: 480, easing:
'easeOutQuart' }); anime({ targets:
'#rb2-body', opacity: name === 'compressed' ?
0 : 1, duration: 320, easing: 'easeOutQuad'
}); if (prev !== name) logEvent('state · ' +
prev + ' → ' + name);
console.log('[RuneboardII] ' + prev + ' → '
+ name); } // The State Button is a memory
toggle: // in air → expand to the last
non-air state visited // anywhere else →
collapse back to air, remember current //
First press from air (no memory) advances to
the next state in // ORDER as a sensible
default. var rememberedState = null; function
toggleStateButton() { if (current ===
'compressed') { var target = rememberedState;
if (!target || target === 'compressed') { var
idx = ORDER.indexOf(current); target =
ORDER[(idx + 1) % ORDER.length]; }
transitionTo(target); } else { rememberedState
= current; transitionTo('compressed'); } } //
Advance — walks ORDER until landing in
Mylar, then cycles the // Mylar variants in
place. After the last variant, the next press
// wraps to Air (compressed). orderIdx is
canonical because the // mylar variant cycle
happens *inside* the same orderIdx slot.
function advance() { // Mode Switcher now only
walks main ORDER. Subject cycling lives // on
the dedicated Mylar Switcher button. orderIdx
= (orderIdx + 1) % ORDER.length;
transitionTo(ORDER[orderIdx]); } function
collapse() { transitionTo('compressed'); }
return { mount: mount, transitionTo:
transitionTo, advance: advance, collapse:
collapse, get state() { return current; } };
})(); if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded',
RuneboardII.mount); } else {
RuneboardII.mount(); }
console.log('[RuneboardII] runeboard_II.js
loaded');
△