scroll /* runeboard_II.js — Runeboard II * Pillthat morphs through five states. As it grows,its corners * soften less — radius shrinksas footprint expands. * * State cycle (toggleadvances forward, ◌ collapses): * COMPRESSED→ CITRINE → MORGANITE →CITRINE_MORGANITE → EXPANDED → COMPRESSED* * Requires: anime.js (loaded before thisscript in init-shell.html) */ var RuneboardII= (function () { // Width is constant (matchedto the citrine+morganite footprint). // Onlyheight and corner radius change as the boardgrows. 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: Inputexpanded further, with a content box mounted// above the composer. mylar key names thecontent variant — // 'dev' wires the debugpane (formerly ?dev). mylar: { // Two equalpanes (input + output) inside the Runeboard //— see CSS: both #rb2-body and.rb2-pane-mylar are flex:1 // and split theavailable height equally. The actual //content variant (dev/things/chat…) lives in// currentMylarVariant and is cycled by theMylar State // button independent of thisstate's identity. height: 890, radius: 12,showCitrine: true, showMorganite: true,expanded: true, mylar: true } }; // Cycleorder — a clean upward walk through theelemental stack. // Once we land in Mylar,advance cycles through the variants // (Dev→ Thing → Gems → Roll) instead ofthrough ORDER; the // press after the lastvariant 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; cyclesvariants then resets) // // Citrine pane isnow the Spacetime Position Bar; the class name// stays for compatibility but the bar readsas 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) — orthogonalaxis selected by the View pill on // theresult box. The Mylar State button cyclessubjects; switching // subject snaps to thatsubject's default view. // // Things/Gemsshare a focusedThing — the pair toggle hopsbetween // those two subjects without changingwhat's focused. var MYLAR_SUBJECTS = ['terminal', 'coin', 'card', 'dice', 'feed','forge', 'hexcube', 'grimoire', 'subu','things', 'rings', 'gems', 'dev' ]; var VIEWS= ['text', '2d', '3d']; varSUBJECT_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' }; // Glyphlayer that best matches each Mylar subject'sinteraction context. // 'being' is the outerpage context (not a Mylar subject) — used onload. 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); varcurrentView =SUBJECT_DEFAULT_VIEW[currentSubject]; //Cached "focused Thing" used by both thing +gems variants so a // single fetch feeds bothviews. var focusedThing = null; varfocusedThingFetching = false; // HexCubenavigation path — mirrors NavigationStatefrom // ui/mod.rs. State + 4 position pairs(HC / Cube / Cluster / Set) // = 9 glyph cellsexactly. 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'sper-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'semerald // phase and the Spectraglyph Mylarrender this content. // sacred day → 5-digitzero-padded ("00346") // time → 4-digit HHMM// cycle → moon/mercury/venus phase counters(TBD layout) function pad(n, w) { returnString(n).padStart(w, '0'); } functionsubuDigits() { 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-overlaysurfaces: // world chip manifest (fetched),exposed gems (window.__hexGems), // Sacred Day(epoch math), and a recent-rolls counter forcontext. var SACRED_DAY_ZERO_EPOCH =1749400000; // from init-dev-overlay.jsfunction sacredDay() { returnMath.floor((Date.now() / 1000 -SACRED_DAY_ZERO_EPOCH) / 86400); } vardevChips = []; var devChipsFetching = false;function kickDevChipsFetch() { if(devChips.length || devChipsFetching) return;devChipsFetching = true; var world =window.WORLD_NAME || newURLSearchParams(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; }); } // RollMylar consumes the dice world's existing eventstream — // every settled die dispatches'dice-landed' on window. We // keep anewest-first ring buffer feeding ourrendering. var diceLandings = []; varMAX_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 theThing/Gems Mylar // and pop the Runeboard openif it isn't already in mylar state. functionfetchAndFocus(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. varpullBtn =e.target.closest('.rb2-pull-btn[data-action="pull"]');if (pullBtn) { doPull(); return; } // Subjectpill — 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. varvchip = 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 thekeys // beneath, not the Mylar content.Refresh just the pill host // so itsactive-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 toggleinside Things/Gems — flips between those two// subjects without changing focusedThing. varpair = 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; ifuser 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 isshowing. Debounced so a sweep // across manyitems doesn't fire a flood of fetches. varhoverTimer = null;document.addEventListener('mouseover',function (e) { var bp =e.target.closest('.bp-item[data-doc]'); if(!bp) return; if (currentSubject !== 'things'&& currentSubject !== 'gems') return; vardocId = bp.dataset.doc; if (!docId) return; if(hoverTimer) clearTimeout(hoverTimer);hoverTimer = setTimeout(function () {fetchAndFocus(docId, function () {rerenderIfActive(currentSubject); }); }, 150);}); function rerenderIfActive(subject) { varrb = 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 toits default. Re-renders pane if open. functionsetSubject(subject) { if(MYLAR_SUBJECTS.indexOf(subject) < 0) return;currentSubject = subject; subjectIdx =MYLAR_SUBJECTS.indexOf(subject); currentView =SUBJECT_DEFAULT_VIEW[subject]; // Switch glyphlayer to match the new subject's interactioncontext var defaultLayer =SUBJECT_DEFAULT_LAYER[subject] || 'wands'; if(defaultLayer !== currentLayer) { currentLayer= defaultLayer; applyLayer(currentLayer); varpillHost = 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 6px24px 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 itsnormal color; the // body itself becomes theone sage shape that holds both the // typingarea and the keys. Citrine pill floats insideit. '#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.18sease;', '}', '#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: transform0.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'sleft/right edge in non-expanded // states(body padding 6 + morganite 2 + center-offset12 = 20 // → pill width 320 aligns exactlywith the keys above/below). ' margin: 014px;', ' box-shadow: inset 0 2px 6pxrgba(0,0,0,0.45);', ' display: none;', 'align-items: center;', ' justify-content:center;', '}', // Black pill carries a smallEarth (filled-down) triangle in // the center.The triangle's fill swaps citrine → emerald// every 4s; same shape, just the colorcycles. '.rb2-pane-citrine .rb2-pill-glyph {',' width: 16px; height: 16px;', ' display:block;', ' transition: fill 0.6s ease;', '}',// Citrine grid — 9 cells with glyphs, twodividers carving // them into three pathsegments. '.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.6sease;', '}', '.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 blockshows during emerald (Subu) phase — 9 cellsin a // grid that matches the keyboard's9-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 6pxrgba(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) andtime (5-8). '.rb2-led-cell[data-led^="t"] {color: #45c08a; }', '.rb2-led-cell { position:relative; }', // Vertical line after the daysection (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 colonriding 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: 1pxsolid #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-keyi.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 thanthe 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 bumpin size. '.rb2-key .rb2-daw {', ' font-size:18px;', ' line-height: 1;', ' font-family:"Segoe UI Symbol", "Apple Symbols", "Noto SansSymbols 2", "Noto Sans Symbols", sans-serif;','}', // Composer is a region inside the bodyshape — no separate // background box. Thebody itself is the sage "input" surface; //composer just provides the typing affordanceat 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 horizontalmargin matches the keyboard's visible // outeredges: keys are 32px max, 9 of them with 4pxgaps = // 320px row, centered in the morganitepane → the pill takes // the same 320pxfootprint 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 // theRuneboard'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 singlerow inside the // result-pane width. Each pillscrolls horizontally INSIDE // itself when itoverflows; 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: 12px22px 22px;', '}', // Subject pill — widestby chip count; shares grow space with // glyphpill, 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.18sease;', ' 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 intransitionTo). '.rb2-glyph-pill-host {', 'display: none;', ' justify-content: center;',' margin: 0 14px 6px;', '}', // Compact glyphpill — current-layer name + a row of dots,one // per layer in LAYER_ORDER. Active dottinted citrine; others // dim.'.rb2-glyph-pill {', ' background: #0a0a0a;',' border-radius: 999px;', ' padding: 4px12px;', ' 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, transform0.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.18sease, color 0.18s ease;', ' white-space:nowrap;', '}', '.rb2-glyph-chip:hover { color:#fff; }', '.rb2-glyph-chip.active {background: #e5734a; color: #fff; }', // Viewpill — only 3 chips, never overflows; pinnedat natural // width so it doesn\'t consume thesubject/glyph share. '.rb2-view-pill {', 'background: #0a0a0a;', ' border-radius:999px;', ' padding: 3px;', ' display:inline-flex;', ' gap: 2px;', ' flex: 0 0auto;', ' box-shadow: 0 2px 8pxrgba(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.18sease;', '}', '.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; }', // Inlinesubject titles — the sticky head bar'sdropdown shows // subject now, so theredundant 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 panecarries a tiny link // button to flip to itspartner 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.18sease;', '}', '.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'scaption. '.rb2-three-container { width: 100%;height: 100%; min-height: 240px; display:block; position: relative; overflow: hidden;border-radius: 12px; }', '.rb2-three-containercanvas { 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 — flexso the container can fill. '.rb2-mylar-3d {padding: 12px; display: flex; flex-direction:column; }', '.rb2-mylar-3d.rb2-three-container { flex: 1; }', // Dockedworld — strip the fixed-fullscreenpositioning, fill // the Mylar containerinstead. 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 widthinside 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 Subudisplay. '.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 8pxrgba(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); } // Pagescurrently 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 arrowsactually render. functioninjectPhosphorLight() { 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); } functionmount() { 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">' + '<buttonid="rb2-toggle" title="Mode State — cyclemodes"></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>' + '<divclass="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. '<divclass="rb2-pill-citrine">' + citrinePathHtml()+ '</div>' + // Emerald phase (Subu): 9 LEDcells with a divider // line after the daysection and a blinking colon // between hoursand minutes. '<div class="rb2-pill-led">' +'<span class="rb2-led-cell"data-led="d0">0</span>' + '<spanclass="rb2-led-cell" data-led="d1">0</span>' +'<span class="rb2-led-cell"data-led="d2">0</span>' + '<spanclass="rb2-led-cell" data-led="d3">0</span>' +'<span class="rb2-led-cell rb2-led-divend"data-led="d4">0</span>' + '<spanclass="rb2-led-cell" data-led="t0">0</span>' +'<span class="rb2-led-cell"data-led="t1">0</span>' + '<spanclass="rb2-led-cell rb2-led-colon"data-led="t2">0</span>' + '<spanclass="rb2-led-cell" data-led="t3">0</span>' +'</div>' + '</div>' + // Glyph layer selector— sits directly above the keys. '<divclass="rb2-glyph-pill-host"></div>' + '<divclass="rb2-pane-morganite">' + buildKeyboard()+ '</div>' + '</div>' + // Output (Mylar) is asibling 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) advancesforward through // the main cycle — itsglyph already tells you which mode // you'rein, so it doubles as the cycle-modesaffordance.toggleBtn.addEventListener('click', advance);// Circle (hollow outline) is the air toggle:contract back // to compressed, or expand tothe last remembered state.document.getElementById('rb2-collapse').addEventListener('click',toggleStateButton); // Glyph pill renders intoits slot above the keyboard. Subject + // Viewpills 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 ↔ Emeraldcolor swap every 4s. One interval for the //life of the chip; the pill's visibility isindependent. var citrinePane =el.querySelector('.rb2-pane-citrine');setInterval(function () {citrinePane.classList.toggle('is-emerald'); },4000); // Subu tick — refreshes the SPB'sLED digits (and the // Spectraglyph Mylar ifit's active) every second so the // time staysin step with the wall clock. functiontickSubu() { var d = subuDigits(); // Daydigits → 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++) { vartcell =citrinePane.querySelector('[data-led="t' + j +'"]'); if (tcell) tcell.textContent =d.time[j]; } rerenderIfActive('subu'); }tickSubu(); setInterval(tickSubu, 1000);console.log('[RuneboardII] mounted incompressed state'); // Default page-load stateis morganite (keyboard only, 229px). // Glyphlayer seeds from page context: being page →daw, else subject default. // ?dev overridesto the Mylar state so the Dev pane mountsopen. var params = newURLSearchParams(location.search); varinitialState = 'morganite'; varinitialLayerCtx = 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 buttonsinside the header // suppress drag so clickingthem doesn't move the panel. functionattachDrag(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 stilltoggle. 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/toppositioning 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(); } functiononMove(e) { if (!dragging) return; var dx =e.clientX - startX; var dy = e.clientY -startY; // Clamp to viewport so the boardcan't disappear off-edge. var maxX =window.innerWidth - rb.offsetWidth - 8; varmaxY = 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'; } functiononUp(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 statesmap 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 varICON_COLORS = { blue: '#6ec6e0', green:'#6ec06e', orange: '#e5734a' }; functioniconForState(name) { var up = '2.5,15 9,315.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 '<svgviewBox="0 0 18 18" aria-hidden="true">' +'<polygon points="' + down + '" fill="' +ICON_COLORS.green + '"/></svg>'; case'citrine_morganite': return '<svg viewBox="0 018 18" aria-hidden="true">' + '<polygonpoints="' + 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 ''; } } // Keylabels mirror hex.hexcraft.dev's runeboard Ikeymap: numbers // 1-9 on top, thenQWERTY/ASDFGH/ZXCVBNM rows split into three3×3 // sub-grids (nav | wand | rune). Thecenter cell of each 3×3 is // rendered as acircle key; everything else is a roundedsquare. 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 throughfour layers. // Each entry is the per-keycontent for that layer in row-major order. //Phosphor icons render via <i class="ph-lightph-NAME"></i> — the // being page alreadypulls 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-bitcount, top row = 1s binary: (function () { varout = []; 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 arrowsfrom origin (move) // Wand 2: repeat of wand 1// Wand 3: inward arrows toward a centrecrosshair (target / look) arrows: ['','','','','','','','','', // Top wand rowph('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 rowph('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 rowph('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: glyphcoordinates — every wand cell gets a2-triangle // coord. Same pattern repeats inall three wands; meaning of // the wand(move/select/look) is independent of theencoding. coords: (function () { var out = [];for (var i = 0; i < 9; i++) out.push(String(i+ 1)); for (var r = 0; r < 3; r++) { for (varc = 0; c < 9; c++) out.push(coord(c % 3, r));} return out; })(), // Default composite layer— each wand carries a different // glyphtype rather than the same one repeated. //Wand 1 (Move): outward phosphor arrows // Wand2 (Select): elemental triangles + circles(charge = ▲) // Wand 3 (Look): 2-trianglecoordinate glyphs wands: (function () { varout = []; for (var i = 0; i < 9; i++)out.push(String(i + 1)); // Row 1 — topout.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 transporticons mirroring the // being page's.bp-tl-transport palette. Number row = zoom +// marker controls; wand 3×3 = transport rowrepeated across // all three wands with playat 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. Sameshape as DAW but // semantically different:these are cycle counters, not // transportcontrols. 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++) { varpos = (r * 3) + (c % 3);out.push(dawSym(wand[pos])); } } return out;})() }; // Layer cycle order — Glyph pillwalks this forward. Binary kept // in LAYERSbut dropped from the cycle. var LAYER_ORDER =['wands', 'coords', 'daw', 'cycles','keyboard', 'elemental', 'arrows']; varcurrentLayer = '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 1616" 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 a2-triangle glyph // encoding (column, row)within its 3×3 wand. Left triangle = col //(▼ left / △ mid / ▲ right). Righttriangle = 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 — hollowup 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 Unicodeastronomical glyph so it can be //sized/styled independently of the default keyfont. function dawSym(s) { return '<spanclass="rb2-daw">' + s + '</span>'; } functionbuildKeyboard() { var html = ''; for (var r =0; r < KEY_ROWS.length; r++) { html += '<divclass="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 += '<divclass="rb2-key mod" data-key="' + m + '">' + m+ '</div>'; }); html += '</div>'; return html;} // Three.js Mylar — lazy-loaded scene thatmirrors the 2D page in // 3D. Right now: arotating icosahedron coloured fromfocusedThing's // sapphire (Thing type) with alabel below. Hover/click any // .bp-item andthe 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); } functionteardown3D() { 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; } functioninit3D(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 = newTHREE.IcosahedronGeometry(1.1, 0); var mat =new THREE.MeshStandardMaterial({ color:0xcccccc, flatShading: true, metalness: 0.2,roughness: 0.6 }); var mesh = newTHREE.Mesh(geom, mat); scene.add(mesh); varkey = new THREE.DirectionalLight(0xffffff,1.2); key.position.set(2, 3, 2);scene.add(key); scene.add(newTHREE.AmbientLight(0x5b6e6a, 0.7));threedState = { renderer: renderer, scene:scene, camera: camera, mesh: mesh, container:container }; update3DFromFocused(); functionloop() { 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 livepage-background #world-canvas // INTO theMylar pane via a FLIP transform animation.Restore on // leave. This reuses Web Vessel'sactual Three.js world rather than // running asecond renderer. var worldDocked = false; varworldHome = null; function findWorldDiv() { //Only treat the world as "real" if it actuallyhas 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 thatcase. var div =document.getElementById('world-canvas'); if(!div || !div.querySelector('canvas')) returnnull; return div; } functiondockWorldCanvas(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'); varlast = 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: snapto the divs old visible position, then glidehome // 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(newEvent('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'); varlast = 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); elsediv.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 '<buttonclass="rb2-view-chip' + (v === view ? 'active' : '') + '" data-view="' + v + '">' +(v === 'text' ? 'TXT' : v.toUpperCase()) +'</button>'; }).join('') + '</div>'; } //Glyph pill — compact dot row + name of theactive layer. Each // dot is clickable to jumpto 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 '<divclass="rb2-glyph-pill">' + '<spanclass="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 thehead bar is the same idiom. functionsubjectPillHtml(subject) { return '<divclass="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 headbar — 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-canvaswhen present // — reuses Web Vessel's actualThree.js world. Falls back to // our localicosahedron 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-contentrb2-mylar-3d">' + '<divclass="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">' + '<divclass="rb2-three-container"></div>' + '<divclass="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 (ifany) and tear down // the fallback WebGL loop.if (worldDocked) restoreWorldCanvas(); if(threedState) teardown3D(); pane.innerHTML =headBarHtml(subject, view) + '<divclass="rb2-mylar-content">' +buildMylar(subject, view) + '</div>'; } //Mylar State button glyph — a small rightwardtriangle (▶) that // tints by variant so theuser can see which output is active. //Subject color — used by both the Mylar Statebutton glyph and // any subject-tinted UI inthe 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 — feedsurface forge: '#e5734a', // citrine — forgehexcube: '#2bb3a3', // teal — 3D / hexcubegrimoire: '#9c27b0', // amethyst — grimoiresubu: '#2e8b57', // emerald — Subu things:'#e5734a', // citrine — Thing rings:'#b15bd1', // amethyst — Rings gems:'#b15bd1', // amethyst — Gems dev: '#6ec06e'// green — dev }; functionmylarIcon(subject) { var color =MYLAR_COLORS[subject] || '#888'; return '<svgviewBox="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; varnext = (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 agiven Mylar variant. The dev variant is // astructured read-out of Runeboard internals —it's the dev // window from ?dev re-housedinside the Mylar layout, so the // panel actsas a debug HUD while the composer below stayslive // for input. function rowsHtml(rows) {return rows.map(function (r) { return '<divclass="rb2-mylar-row"><spanclass="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); returns.replace(/[&<>"]/g, function (c) { return {'&':'&amp;', '<':'&lt;', '>':'&gt;','"':'&quot;' }[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. vareventLog = []; 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; // Terminalis now the universal Text fallback — refreshthe // pane whenever the current view istext-on-anything. if (currentView === 'text')rerenderIfActive(currentSubject); } // PullMylar — counter + last-pulled timestamp. ThePull button // re-runs /api/things/recent andupdates 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 withtheir own // Text rendering use that;everything else lands here in text view.function terminalText(subject) { if(eventLog.length === 0) { return '<divclass="rb2-mylar-title">' + subject + ' ·terminal</div>' + '<divclass="rb2-mylar-loading">no events yet —interact with the Runeboard…</div>'; } varlines = eventLog.slice(0, 40).map(function (e){ var ts = new Date(e.t).toLocaleTimeString();return '<div class="rb2-term-line"><spanclass="rb2-term-ts">' + ts + '</span><spanclass="rb2-term-text">' + escapeHtml(e.line) +'</span></div>'; }).join(''); return '<divclass="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 byview. The 3D view is // handled inrenderMylarPane (canvas preserved); we neverreach // here with view === '3d' unless thesubject has a non-canvas // 3D fallback, whichcurrently none do — placeholder it. functionbuildMylar(subject, view) { if (view === '3d'&& subject !== 'hexcube') returnplaceholder(subject, '3d'); if (subject ==='coin') { if (!focusedThing) {kickFocusedThingFetch(); return '<divclass="rb2-mylar-title">coin</div>' + '<divclass="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) { returnb.localeCompare(a); }); var fentries =fkeys.map(function (k) { var v = fg[k]; return[k, typeof v === 'object' ? JSON.stringify(v): String(v)]; }); return '<divclass="rb2-mylar-head">' + '<spanclass="rb2-mylar-title">coin · verso</span>'+ '</div>' + rowsHtml(fentries); } if (subject=== 'card') { if (view === 'text') returnterminalText('card'); var when = pullState.at? new Date(pullState.at).toLocaleTimeString(): '—'; return '<divclass="rb2-mylar-title">card · deck</div>' +rowsHtml([ ['count', pullState.count], ['lastpull', when], ['status', pullState.busy ?'pulling…' : 'idle'] ]) + '<buttonclass="rb2-pull-btn" data-action="pull">▼pull recent</button>'; } if (subject ==='dice') { if (view === 'text') returnterminalText('dice'); if (diceLandings.length=== 0) { return '<divclass="rb2-mylar-title">dice</div>' + '<divclass="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>'; } varhistory = diceLandings.slice(0,8).map(function (r, i) { var ts = newDate(r.t).toLocaleTimeString(); var label =(r.title ? r.title + ' · ' : '') + 'f' +r.face; return [i === 0 ? 'last' : '−' + i,label + ' ' + ts]; }); return '<divclass="rb2-mylar-title">dice · ' +diceLandings.length + '</div>' + '<divclass="rb2-roll-faces">' + faces + '</div>' +rowsHtml(history); } if (subject === 'feed') {if (view === 'text') returnterminalText('feed'); var feed =document.querySelector('.bp-feed'); if (!feed){ return '<divclass="rb2-mylar-title">feed</div>' + '<divclass="rb2-mylar-loading">.bp-feed not foundon this page</div>'; } return '<divclass="rb2-mylar-title">feed · ' +feed.children.length + '</div>' + '<divclass="rb2-2d-host">' + feed.innerHTML +'</div>'; } if (subject === 'subu') { if (view=== 'text') return terminalText('subu'); var d= subuDigits(); return '<divclass="rb2-mylar-title">subu ·subuglass</div>' + '<divclass="rb2-sg-stack">' + '<divclass="rb2-sg-row">' + '<spanclass="rb2-sg-label">day</span>' + '<spanclass="rb2-sg-led rb2-sg-day">' + d.day +'</span>' + '</div>' + '<divclass="rb2-sg-row">' + '<spanclass="rb2-sg-label">time</span>' + '<spanclass="rb2-sg-led rb2-sg-time">' + d.time +'<span class="rb2-sg-secs">·' + d.seconds +'</span></span>' + '</div>' + '<divclass="rb2-sg-row rb2-sg-cycle">' + '<spanclass="rb2-sg-label">cycle</span>' + '<spanclass="rb2-sg-placeholder">— TBD —</span>'+ '</div>' + '</div>'; } if (subject ==='dev') { // Dev's text view = the devmanifest. 2D variant is the // same contentfor now until a graphical layout exists. varworld = window.WORLD_NAME || newURLSearchParams(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 '<divclass="rb2-mylar-row"><spanclass="rb2-mylar-key">' + escapeHtml(c.name) +'</span><span class="rb2-mylar-val">' +escapeHtml((c.byte_weight || 0) + 'b') +'</span></div>'; }).join('') : '<divclass="rb2-mylar-loading">loadingchips…</div>'; return '<divclass="rb2-mylar-title">◇ dev · ' +escapeHtml(world) + '</div>' + rowsHtml([['sacred day', sacredDay()], ['world', world],['chips', devChips.length || '—'], ['gems',gemKeys.length], ['being', window.BEING_NAME|| '—'] ]) + '<divclass="rb2-mylar-section">chips</div>' +chipsList; } if (subject === 'things') { if(!focusedThing) { kickFocusedThingFetch();return '<divclass="rb2-mylar-loading">fetching focusedThing…</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>' + '<buttonclass="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 '<divclass="rb2-mylar-loading">fetchinggems…</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">' + '<spanclass="rb2-mylar-title">gem facets</span>' +'<button class="rb2-pair-toggle"data-target="things">◂ things</button>' +'</div>' + rowsHtml(entries); } } // Forge /Grimoire / Hexcube / Rings — subjects withno 2D // renderer yet. Text view → universalterminal fallback; // 2D → placeholder. if(view === 'text') returnterminalText(subject); returnplaceholder(subject, view); } functionkickFocusedThingFetch() { 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 afocused-thing-dependent subject is active. varrb = 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-keycontent swap for morganite-only glyph cycling.Skips the // mod row (last row) — only the4×9 main grid rotates. functionapplyLayer(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 (varc = 0; c < cells.length; c++) {cells[c].innerHTML = layer[r * 9 + c] || ''; }} } // Morganite Switcher icon — acoord-style 2-triangle pair tinted // by layerso the icon reads as "which layer is on thekeys". var LAYER_COLORS = { wands: '#e5734a',// citrine — canonical default (mixed)coords: '#d4af37', // gold — pure coordsdaw: '#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 }; functionmorganiteIcon(layer) { var color =LAYER_COLORS[layer] || '#e5734a'; return '<svgviewBox="0 0 24 16" aria-hidden="true">' +'<polygon points="2,12 11,12 6.5,4" fill="' +color + '"/>' + '<polygon points="13,4 22,417.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); } varglyphCycleTimer = 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); } // Placeholdertypewriter — types each messagechar-by-char, holds, // fades, rotates. Onlyruns while expanded AND the composer is empty.var PLACEHOLDERS = [ 'Write some glyphs...','Try /help for commands', 'Click runes or typedirectly', 'Enter sends, Esc cancels', '/loginto authenticate' ]; var phTimer = null; varphIndex = 0; function typewriter(msg, i) { varc = 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); } } functionstartPlaceholder() { 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'); varmorganite =rb.querySelector('.rb2-pane-morganite'); varcomposer = rb.querySelector('.rb2-composer');var prevShowed = STATES[prev] &&STATES[prev].showCitrine; // Record citrine'spre-toggle position so we can FLIP it into //its new resting place when it's visible onboth sides of the // transition (e.g.citrine_morganite → expanded — the margin// and seam location both move). varcitrineBeforeTop = 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 pillfollows the morganite pane — shown when keysare. var ghostT =rb.querySelector('.rb2-glyph-pill-host'); if(ghostT) ghostT.style.display =s.showMorganite ? 'flex' : 'none'; // Populatethe Mylar pane when this state shows output.The // content variant is held incurrentMylarVariant and is // cycled by theMylar 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 withstate when transitionTo is called // fromoutside advance() (collapse,toggleStateButton, etc.). if (ORDER[orderIdx]!== name) { var foundIdx =ORDER.indexOf(name); orderIdx = foundIdx >= 0? foundIdx : 0; } // Citrine slides into / outof view, never fades. Three cases: // newlyappearing → enter from translateY -40 //leaving → exit to translateY -40 // stayingvisible → 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' }); } elseif (!s.showCitrine && prevShowed) {anime.remove(citrine); anime({ targets:citrine, translateY: [0, -40], duration: 280,easing: 'easeInQuad', complete: function () {citrine.style.display = 'none'; } }); } elseif (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'; } // Statebutton: contract-only when collapsing to air,expand-only // when opening from air, classicsquish-pop between other states. var toggleBtn= rb.querySelector('#rb2-toggle'); if(toggleBtn) { anime.remove(toggleBtn); vargoingToAir = (name === 'compressed'); varleavingAir = (prev === 'compressed' && name!== 'compressed'); var scaleKeys; var swapAt =35; if (goingToAir) { // Just contract; neverre-expand. The Air glyph appears // at thecontracted 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; startfrom a contracted point and bloom // out tothe 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 thelayers. Every other // state honors the user'schosen layer (from Morganite Switcher). if(name === 'morganite') startGlyphCycle(); else{ stopGlyphCycle(); applyLayer(currentLayer);} // Expanded state runs the typewriterplaceholder 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 memorytoggle: // in air → expand to the lastnon-air state visited // anywhere else →collapse back to air, remember current //First press from air (no memory) advances tothe next state in // ORDER as a sensibledefault. var rememberedState = null; functiontoggleStateButton() { if (current ==='compressed') { var target = rememberedState;if (!target || target === 'compressed') { varidx = ORDER.indexOf(current); target =ORDER[(idx + 1) % ORDER.length]; }transitionTo(target); } else { rememberedState= current; transitionTo('compressed'); } } //Advance — walks ORDER until landing inMylar, then cycles the // Mylar variants inplace. After the last variant, the next press// wraps to Air (compressed). orderIdx iscanonical because the // mylar variant cyclehappens *inside* the same orderIdx slot.function advance() { // Mode Switcher now onlywalks main ORDER. Subject cycling lives // onthe dedicated Mylar Switcher button. orderIdx= (orderIdx + 1) % ORDER.length;transitionTo(ORDER[orderIdx]); } functioncollapse() { 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.jsloaded');