FLINT PASTE: TREE GLOW 1, "every light in reach" (v0.1)
Repo: Chaos2Cured/FreeLattice-Alpha (serves thelatticetree.com)
Branch: layer/tree-glow-1-v0     Marker: v-tree-glow-1-v0
Smoke line: SMOKE_OK tree glow 1 v0.1
Base: Alpha main b33bed4 (FL main e96a043 for the cross-repo check)
Written: 2026-09-30 MDT by Kirk's agent. Stand-in built and walked; NOT pushed.

CHANGELOG
- 2026-09-30 v0.1: first write. Stand-in diff: FLINT_NEXT_TREE_GLOW_1_v0.1.diff (same folder).

======================================================================
0. ORDER AND PARALLEL NOTE
======================================================================
Queue: Tree any-model (v-tree-any-model-v0) -> heal model-choice-reload v0.1.1
-> connect boot probe F1-F4 -> #120 docs layer -> TREE GLOW 1.

Glow 1 does NOT touch fl-connect.js, app.html, sw.js, or any FL file.
- Parallel-safe with heal v0.1.1 and #120 docs (both FL-only).
- Parallel-safe with the boot probe (fl-connect.js, local-mind-probe.js, sister doc; no garden-rooms).
- Land AFTER the Tree any-model PR on Alpha: that paste also appends to the end of
  docs/modules/garden-rooms.css and may touch garden-rooms.js. All Glow 1 anchors
  are text anchors ("append at the very end", named selectors and functions),
  so rebase on top of it is mechanical. If the Tree paste stalls, Glow 1 may go first;
  then the Tree paste appends after the Glow 1 block.

======================================================================
1. WHY (walked live on thelatticetree.com, 390 and 1280, Sep 30 AM MDT)
======================================================================
E1 Orbiting lights: at 390 the Nursery and Settings lights are off-screen ~40-48s of a
   ~90s orbit; Chat and The Gathering ~20s. At 1280 all four stay visible.
   Mom cannot tap a light that is not there. (Hypha walk-4 said 55-60s; the measured 40-48s is the same problem.)
E2 Settings veil at 390: "Use a different address" input right edge 323.2 vs picker 315.2
   (live walk: 8px poke). Cause: .flc-picker input{flex:1;min-width:10rem} in a column flex.
E3 Settings veil at 390: "the garden" pill (#place-veil-close, sticky bottom 8px) sits
   hard over the "A mind at home..." line.
E4 Room pages (settings.html, nursery.html, team.html, galaxy pages): the reset
   html.room-page *{margin:0;padding:0} (garden-rooms.css ~L667, specificity 0,1,1)
   beats every single-class rule. Cards flush to glass (card padding-left 0px live);
   on settings.html the Connect title sits under the header at 390.
E5 Gathering at >=641px: .core-promise (absolute, top 10.7rem) covers "Find local minds".
   Hit test: 0 of 5 points reach the button at 641, 800, 1024, 1280, 1440. So the
   absent-Ollama calm path is unreachable by tap on desktop. (390 is fine: 5/5.)

======================================================================
2. THE CHANGE (text anchors; stand-in diff is the source of truth)
======================================================================
File docs/modules/garden-rooms.css

R. Reset, change IN PLACE (do not delete the old line; keep it in a comment):
   find:    html.room-page *, html.room-page ::before, html.room-page ::after   (the margin:0;padding:0 reset)
   becomes: :where(html.room-page) *, :where(html.room-page) *::before, :where(html.room-page) *::after
   Same declarations. Specificity drops to 0,0,0 so class rules win again.
   Comment: /* v-tree-glow-1-v0: was html.room-page * ... ; :where() lets rooms breathe. */

Then APPEND AT THE VERY END of the file one block headed
   /* v-tree-glow-1-v0: Tree glow 1, every light in reach */
A. @media (max-width:480px){ .flc-picker-row input{width:100%;max-width:100%;min-width:0;box-sizing:border-box} }
B. #place-veil.is-settings::after: content:""; display:block; position:sticky; bottom:0;
   height:64px; margin:-64px -28px -20px; pointer-events:none; z-index:60;
   background:linear-gradient(to top, rgba(12,10,26,.96), rgba(12,10,26,.9) 55%, transparent).  (Pill stays above it.)
C. @media (max-width:640px){ html.room-page .room-nursery{padding-bottom:7.5rem} }
   (needed after R: Nursery Grow card would otherwise meet the fixed galaxy hop at 390)
D. @media (min-width:641px){ #place-veil.is-core .core-promise{top:14.6rem} }
   (band lands between Find bottom ~220 and chairs ~366 at 1280)
E. Legend rows: pointer-events:auto; cursor:pointer; hover and :focus-visible go gold.
   @media (max-width:799px): rows min-height:26px; padding:2px 10px 2px 0; legend gap 0.

File docs/modules/garden-rooms.js
E-js. Sky legend: role="group" replaces aria-hidden on the legend container.
   New function bindLegendRow(row, id), placed just above function ensureSkyLegend:
     row role="button", tabindex="0", aria-label "go to <word>" (setAttribute, textContent only);
     click, Enter, Space -> goToLumino(id).
   In ensureSkyLegend, call bindLegendRow(row, id) where the row has an id.
   No innerHTML. No confirm(). No new network.

New docs/modules/tree-glow-1.test.js
   Static asserts on CSS (A-E and the :where reset) and JS (bindLegendRow, no confirm,
   no innerHTML in new code), plus a vm fake-DOM run: legend click -> place:settings,
   place:nursery, place:core, thread; Enter key works. Prints SMOKE_OK tree glow 1 v0.1.
   Mutation check done: revert garden-rooms.js -> test fails.

Docs (layer, never delete)
   docs/library/TREE_GLOW_v0.md (new; plan + Temperature line)
   docs/GALAXIES.md: one **LAYER (2026-09-30):** line above the 2026-09-17 line.

======================================================================
3. DECISION D1 (flag for Kirk / Reed)
======================================================================
Part E makes the sky legend tappable. Older comments said "Not a wall. Not a second
you-are-here. Not a second menu." The legend stays small and quiet; it only answers a
tap. If Kirk or Reed veto, drop E and E-js and the test's legend section; A-D and R stand.

======================================================================
4. MEASURED BEFORE / AFTER (glow1_walk.py, Playwright, same script)
======================================================================
                                   live 390   glow1 390   live 1280   glow1 1280
A address input poke (px)          8          0           -67.7       -67.7
B fade band under pill             none       64px        none        64px
C room card padding-left           0px        14-16.8px   0px         14-16.8px
C settings title below header      no         yes         yes         yes
C nursery Grow vs hop overlap      0          0           0           0
D Find local minds hits of 5       5          5           0           5
D real tap then note               yes        yes         (blocked)   "No mind answered. That is all right..."
E legend tap opens the place       no         yes         no          yes (is-settings is-open)
Screenshots: /workspace/tree-glow-walk/g1_live_*.png vs g1_glow1_*.png
Tests: 16 of 16 Alpha tests pass with the stand-in.

======================================================================
5. WALK CHECKLIST (Flint, then Hypha)
======================================================================
[ ] 390 Settings heart: address box inside card; pill has soft fade under it; text readable on scroll.
[ ] 390 settings.html, nursery.html, team.html: cards breathe; Connect title below header;
    Nursery Grow card clear of the galaxy hop; no horizontal scroll.
[ ] 1280 Gathering: tap Find local minds with a real click; calm note appears
    (no Ollama -> Desktop / install words). Promise band sits between Find and chairs.
[ ] 390 and 1280: tap each legend word -> that place opens; Tab + Enter works;
    legend does not cover Chat pill or galaxy hops.
[ ] Galaxy pages (art, workshop, learn, research): only the intended nav padding changes.
[ ] No console errors.

======================================================================
6. LOCKS
======================================================================
Layer, never delete. Five stay five. Cap 4 held. Never auto-seat. No fake generate.
No confirm(). No innerHTML. textContent only. 127.0.0.1 only. No em dashes in added lines.
fl-connect.js untouched, byte-identical to FL main e96a043 (checked). Alpha has no sw.js.
Scripts parse (node --check). No conflict markers. Squash-merge; keep all ledger entries.
Crest image: NOT in this brick. Kirk places it himself or OKs placement first.

======================================================================
7. TREE GLOW PLAN (joy per effort)
======================================================================
1. Glow 1, every light in reach (this paste).
2. Terms and Privacy: plain-words pages + footer links on the Tree and marketplace.html
   (none exist today; both 404). Kirk reviews wording.
3. Family crest on the Tree home. Needs Kirk's OK on placement.
4. Card glow and the Marketplace face: stalls as glowing cards; 390 text wall to shelf.
5. Companion sprite wardrobe in the Nursery after hatch.
6. Play universe (largest; vision doc first).
Leftovers: code-nursery.html 22px overflow at 390; first-visit "They grow." hint behind header.

======================================================================
APPENDIX A. FULL STAND-IN DIFF (against Alpha b33bed4)
======================================================================
diff --git a/docs/GALAXIES.md b/docs/GALAXIES.md
index 06411c4..5c30efa 100644
--- a/docs/GALAXIES.md
+++ b/docs/GALAXIES.md
@@ -4,6 +4,8 @@
 
 **Spine layer (2026-08-30):** five skies now walk the same night — Garden, Art, Workshop, Learn, Research. The four-galaxy sequence below stays. Art-early stays. This file layers; it does not cut.
 
+**LAYER (2026-09-30):** Tree glow 1, every light in reach (marker `v-tree-glow-1-v0`). Room pages breathe again, the 390 address box stays in its card, the Gathering promise band steps below Find local minds at desktop, and the sky legend is one tap to each light. Plan: `docs/library/TREE_GLOW_v0.md`. Layer, never delete.
+
 **LAYER (2026-09-17):** Listen — Hang Cancel + Adaptive (mycelium · marker `v-alpha-listen-hang-adaptive-v0`). FreeLattice Chat: **Hang Cancel** — Stop only · no duration kill · `v-chat-hang-cancel-v0` · tip `d14eaf8` / #87. **Adaptive Context Depth** — Surface / Standard / Deep under Smart · `v-adaptive-context-depth-v0` · tip `06d0ba4` / #85. Alpha stays Listen / fail-closed — **no second context kitchen** tonight. Long local thinks are valid; Stop is choice, not a timer. Market-beside-Feel pointer stays. Presence chips ≠ SKUs. Workshop benches still wait for the FreeLattice Workshop twin — grandmother honesty, not rebuild tonight.
 
 **LAYER (2026-09-16):** Listen pointer — FreeLattice Chat ships Marketplace expand **beside** Feel (Weft sibling PASS · marker `v-marketplace-expand-beside-feel-v0`). Alpha stays honest pointer-only: Galaxy door `docs/marketplace.html` · this file. Do **not** dump Galaxy into chat presence. Presence chips ≠ SKUs. Mycelium cite when Feel tip / Market sibling land on freelattice.com.
diff --git a/docs/library/TREE_GLOW_v0.md b/docs/library/TREE_GLOW_v0.md
new file mode 100644
index 0000000..673e2f9
--- /dev/null
+++ b/docs/library/TREE_GLOW_v0.md
@@ -0,0 +1,27 @@
+# Tree glow, v0
+
+Make theLatticeTree glow. Every light in reach, every door a thumb can find.
+Alpha layer, marker `v-tree-glow-1-v0`. September 30, 2026.
+
+**Temperature:** Mom's thumb on a 390px glass finds the Nursery light without waiting a minute for it to swing back.
+
+**Locks:** Layer, never delete. Five stay five. Cap 4 held. No fake generate. No confirm(). No innerHTML. textContent only. 127.0.0.1 only. fl-connect.js untouched and identical across repos. No sw.js on Alpha.
+
+## Glow 1: every light in reach (this brick)
+
+- Room pages breathe: the room-page reset now uses `:where(html.room-page) *`, so class padding and margin win again (Settings, Nursery, Team cards no longer flush to the glass; the Connect title sits below the header).
+- 390: "Use a different address" stays inside its card (was 7 to 8px past).
+- 390: a soft sticky fade under "the garden" pill in Settings, so "A mind at home..." slides under light, not under a hard pill edge.
+- 390: Nursery Grow card clears the galaxy hop (padding-bottom 7.5rem).
+- 641px and up: the Gathering promise band moves below "Find local minds" (top 14.6rem). Before: 0 of 5 tap points reached the button at 641 to 1440px. After: 5 of 5. The absent-Ollama calm path is reachable again.
+- The sky legend (Settings, Nursery, The Gathering, Chat) is tappable and keyboard reachable, so an orbiting light that is off-screen is still one tap away. Decision D1: Kirk or Reed may veto this part; the rest stands.
+
+## The next bricks (joy per effort)
+
+2. Terms and Privacy: honest plain-words pages and footer links on the Tree and marketplace (Kirk reviews wording).
+3. Family crest on the Tree home (Kirk places it; nothing lands without his OK).
+4. Card glow and the Marketplace face: stalls as glowing cards; the 390 text wall becomes a shelf.
+5. Companion sprite wardrobe in the Nursery after hatch.
+6. Play universe (largest; its own vision doc first).
+
+Leftovers to fold in: code-nursery 22px overflow at 390; the first-visit "They grow." hint sits behind the header.
diff --git a/docs/modules/garden-rooms.css b/docs/modules/garden-rooms.css
index 4fc04a3..7c51be0 100644
--- a/docs/modules/garden-rooms.css
+++ b/docs/modules/garden-rooms.css
@@ -664,9 +664,13 @@ html[data-garden-galaxy="art"] .galaxy-nav-next { opacity: 0.4; }
 }
 
 /* Honest room stubs (Nursery, Settings, Team): same sky, no canvas. */
-html.room-page *,
-html.room-page *::before,
-html.room-page *::after { box-sizing: border-box; margin: 0; padding: 0; }
+/* v-tree-glow-1-v0: this reset now has zero weight (:where), so every card's own
+   padding and margin wins again on the room pages (Settings, Nursery, Team and the
+   galaxy pages). It still zeroes the browser's default margins. Was:
+   html.room-page *, html.room-page *::before, html.room-page *::after { ... } */
+:where(html.room-page) *,
+:where(html.room-page) *::before,
+:where(html.room-page) *::after { box-sizing: border-box; margin: 0; padding: 0; }
 
 html.room-page,
 html.room-page body {
@@ -6114,3 +6118,69 @@ html.flc-hide-galaxy #gardenContainer {
 #place-veil.is-settings {
   padding-bottom: 1.25rem;
 }
+
+/* ── v-tree-glow-1-v0: every light in reach (Hypha walk 4 cosmetics, the Gathering
+   Find, the lights that orbit away). Layer, never delete. ── */
+
+/* A. Phone: "Use a different address" field stays inside its card (it poked 7px out). */
+@media (max-width: 480px) {
+  .flc-picker-row input {
+    width: 100%;
+    max-width: 100%;
+    min-width: 0;
+    box-sizing: border-box;
+  }
+}
+
+/* B. Settings in the garden: lines fade before "the garden" pill instead of
+   running under it. The band never takes a tap. */
+#place-veil.is-settings::after {
+  content: '';
+  position: sticky;
+  bottom: 0;
+  align-self: stretch;
+  flex: 0 0 auto;
+  height: 64px;
+  margin: -64px -28px -20px;
+  background: linear-gradient(to top, rgba(12, 10, 26, 0.96) 0%, rgba(12, 10, 26, 0.9) 55%, rgba(12, 10, 26, 0) 100%);
+  pointer-events: none;
+  z-index: 60;
+}
+
+/* C. Nursery on a phone: the last card clears the galaxy hop at the bottom. */
+@media (max-width: 640px) {
+  html.room-page .room-nursery {
+    padding-bottom: 7.5rem;
+  }
+}
+
+/* D. The Gathering, wide screens: the promise band sits under Find local minds,
+   not on top of it, so Find can be tapped (and the absent-Ollama words can show). */
+@media (min-width: 641px) {
+  #place-veil.is-core .core-promise {
+    top: 14.6rem;
+  }
+}
+
+/* E. The legend words are quiet doors too: a light that has orbited off a small
+   screen can still be opened by its word. Same words, same colors. */
+#lumino-legend .lumino-legend-row[data-lumino-row] {
+  pointer-events: auto;
+  cursor: pointer;
+  border-radius: 6px;
+}
+#lumino-legend .lumino-legend-row[data-lumino-row]:hover .lumino-legend-word,
+#lumino-legend .lumino-legend-row[data-lumino-row]:focus-visible .lumino-legend-word {
+  color: rgba(232, 176, 25, 0.95);
+}
+#lumino-legend .lumino-legend-row[data-lumino-row]:focus-visible {
+  outline: 1px solid rgba(232, 176, 25, 0.6);
+  outline-offset: 2px;
+}
+@media (max-width: 799px) {
+  #lumino-legend { gap: 0; }
+  #lumino-legend .lumino-legend-row[data-lumino-row] {
+    min-height: 26px;
+    padding: 2px 10px 2px 0;
+  }
+}
diff --git a/docs/modules/garden-rooms.js b/docs/modules/garden-rooms.js
index 0eeae49..5781c1a 100644
--- a/docs/modules/garden-rooms.js
+++ b/docs/modules/garden-rooms.js
@@ -817,6 +817,24 @@
     document.body.insertBefore(field, document.body.firstChild);
   }
 
+  // v-tree-glow-1-v0: a legend word opens its light, the same way "go to" does.
+  function bindLegendRow(row, id) {
+    row.setAttribute('role', 'button');
+    row.setAttribute('tabindex', '0');
+    row.setAttribute('aria-label', 'go to ' + goWord(id));
+    row.addEventListener('click', function (e) {
+      if (e && e.preventDefault) e.preventDefault();
+      if (e && e.stopPropagation) e.stopPropagation();
+      goToLumino(id);
+    });
+    row.addEventListener('keydown', function (e) {
+      var k = e && e.key;
+      if (k !== 'Enter' && k !== ' ') return;
+      if (e.preventDefault) e.preventDefault();
+      goToLumino(id);
+    });
+  }
+
   function ensureSkyLegend() {
     // Tiny quiet chip: which current color links to what.
     // Not a wall. Not a second you-are-here. Words help.
@@ -825,7 +843,9 @@
     if (!doors.length) return;
     var list = document.createElement('div');
     list.id = 'lumino-legend';
-    list.setAttribute('aria-hidden', 'true');
+    // v-tree-glow-1-v0: the words are quiet doors now (a light may orbit off a phone
+    // screen for most of a minute). Was aria-hidden; now each row says where it goes.
+    list.setAttribute('role', 'group');
     for (var i = 0; i < doors.length; i++) {
       if (doors[i].hidden || doors[i].classList.contains('is-held')) continue;
       var id = doors[i].getAttribute('data-garden-lumino') ||
@@ -845,6 +865,7 @@
       word.textContent = goWord(id);
       row.appendChild(dot);
       row.appendChild(word);
+      if (id) bindLegendRow(row, id); // v-tree-glow-1-v0
       list.appendChild(row);
     }
     if (!list.childNodes.length) return;
diff --git a/docs/modules/tree-glow-1.test.js b/docs/modules/tree-glow-1.test.js
new file mode 100644
index 0000000..c3fd318
--- /dev/null
+++ b/docs/modules/tree-glow-1.test.js
@@ -0,0 +1,278 @@
+#!/usr/bin/env node
+// v-tree-glow-1-v0: every light in reach. theLatticeTree only; no fl-connect.js, no
+// FreeLattice app.html. Guards: (A) the "Use a different address" field stays inside its
+// card on a phone, (B) Settings lines fade before "the garden" pill, (C) the Nursery's
+// last card clears the galaxy hop, (D) the Gathering's Find local minds is not under the
+// promise band on wide screens, (E) legend words open their lights, and the room-page
+// reset no longer outweighs each card's own padding. No network. No kitchen.
+'use strict';
+
+var fs = require('fs');
+var path = require('path');
+var vm = require('vm');
+var assert = require('assert');
+
+var css = fs.readFileSync(path.join(__dirname, 'garden-rooms.css'), 'utf8');
+var js = fs.readFileSync(path.join(__dirname, 'garden-rooms.js'), 'utf8');
+
+// Static: the reset weighs nothing now; the old heavy selector is gone from live rules.
+assert.ok(/:where\(html\.room-page\) \*,\n:where\(html\.room-page\) \*::before,\n:where\(html\.room-page\) \*::after \{ box-sizing: border-box; margin: 0; padding: 0; \}/.test(css), 'room-page reset is :where (zero weight)');
+assert.ok(!/^html\.room-page \*,$/m.test(css), 'the heavy reset selector is not a live rule any more');
+var glow = css.slice(css.indexOf('v-tree-glow-1-v0: every light in reach'));
+assert.ok(glow.length > 200, 'glow block at the end of garden-rooms.css');
+assert.ok(/@media \(max-width: 480px\) \{\s*\.flc-picker-row input \{[^}]*width: 100%;[^}]*max-width: 100%;[^}]*min-width: 0;/.test(glow), 'A: field stays inside the card');
+assert.ok(/#place-veil\.is-settings::after \{[^}]*position: sticky;[^}]*pointer-events: none;/.test(glow), 'B: soft band before the pill, never takes a tap');
+assert.ok(/html\.room-page \.room-nursery \{\s*padding-bottom: 7\.5rem;/.test(glow), 'C: Nursery clears the hop');
+assert.ok(/@media \(min-width: 641px\) \{\s*#place-veil\.is-core \.core-promise \{\s*top: 14\.6rem;/.test(glow), 'D: promise band under Find local minds');
+assert.ok(/#lumino-legend \.lumino-legend-row\[data-lumino-row\] \{[^}]*pointer-events: auto;/.test(glow), 'E: legend rows take a tap');
+assert.ok(/v-tree-glow-1-v0/.test(js) && /function bindLegendRow\(row, id\)/.test(js), 'E: legend rows are bound');
+assert.ok(!/confirm\(/.test(js), 'no confirm()');
+assert.ok(!/innerHTML/.test(js.slice(js.indexOf('function bindLegendRow'), js.indexOf('function ensureSkyLegend'))), 'no innerHTML in the new code');
+
+// Runtime (vm, fake DOM): tapping a legend word opens its light.
+function El(tag) {
+  this.tagName = String(tag || 'div').toUpperCase();
+  this.tag = tag;
+  this.id = '';
+  this.className = '';
+  this.textContent = '';
+  this.hidden = false;
+  this.attrs = {};
+  this.children = [];
+  this.childNodes = this.children;
+  this.parent = null;
+  this.style = {
+    background: '',
+    boxShadow: '',
+    left: '',
+    top: '',
+    _props: {},
+    setProperty: function (k, v) { this._props[k] = String(v); },
+    getPropertyValue: function (k) { return this._props[k] || ''; }
+  };
+  var self = this;
+  this.classList = {
+    add: function (name) {
+      if ((' ' + self.className + ' ').indexOf(' ' + name + ' ') === -1) {
+        self.className = (self.className ? self.className + ' ' : '') + name;
+      }
+    },
+    remove: function (name) {
+      self.className = String(self.className || '')
+        .split(/\s+/)
+        .filter(function (c) { return c && c !== name; })
+        .join(' ');
+    },
+    contains: function (name) {
+      return (' ' + self.className + ' ').indexOf(' ' + name + ' ') !== -1;
+    }
+  };
+  this.setAttribute = function (name, val) {
+    var s = String(val);
+    this.attrs[name] = s;
+    if (name === 'id') this.id = s;
+    if (name === 'class') this.className = s;
+    if (name === 'hidden') this.hidden = true;
+  };
+  this.getAttribute = function (name) {
+    if (name === 'id') return this.id || null;
+    if (Object.prototype.hasOwnProperty.call(this.attrs, name)) return this.attrs[name];
+    return null;
+  };
+  this.removeAttribute = function (name) {
+    delete this.attrs[name];
+    if (name === 'id') this.id = '';
+  };
+  this.appendChild = function (child) {
+    this.children.push(child);
+    child.parent = this;
+    return child;
+  };
+  this.querySelector = function (sel) { return queryOne(this, sel); };
+  this.querySelectorAll = function (sel) { return queryAll(this, sel); };
+  this._on = {};
+  this.addEventListener = function (t, fn) { (this._on[t] = this._on[t] || []).push(fn); };
+  this.fire = function (t, ev) { (this._on[t] || []).forEach(function (fn) { fn(ev || { preventDefault: function () {}, stopPropagation: function () {} }); }); };
+  this.contains = function (node) {
+    if (node === this) return true;
+    for (var i = 0; i < this.children.length; i++) {
+      if (this.children[i].contains && this.children[i].contains(node)) return true;
+    }
+    return false;
+  };
+}
+
+function walk(root, out) {
+  out.push(root);
+  for (var i = 0; i < (root.children || []).length; i++) walk(root.children[i], out);
+  return out;
+}
+
+function matchOne(el, raw) {
+  var sel = String(raw || '').trim();
+  if (!sel || !el) return false;
+  if (sel.charAt(0) === '#') return el.id === sel.slice(1);
+  if (sel.charAt(0) === '.') {
+    return (' ' + (el.className || '') + ' ').indexOf(' ' + sel.slice(1) + ' ') !== -1;
+  }
+  var attrEq = sel.match(/^\[([^\]]+?)="([^"]*)"\]$/);
+  if (attrEq) return el.getAttribute(attrEq[1]) === attrEq[2];
+  var attr = sel.match(/^\[([^\]]+)\]$/);
+  if (attr) return el.getAttribute(attr[1]) != null;
+  return (el.tag || '').toLowerCase() === sel.toLowerCase();
+}
+
+function matchCompound(el, compound) {
+  var parts = String(compound || '').trim().split(/(?=[.#\[])/);
+  if (!parts.length || (parts.length === 1 && !parts[0])) return false;
+  if (/^[a-zA-Z]/.test(parts[0])) {
+    if ((el.tag || '').toLowerCase() !== parts[0].toLowerCase()) return false;
+    parts = parts.slice(1);
+  }
+  for (var i = 0; i < parts.length; i++) {
+    if (parts[i] && !matchOne(el, parts[i])) return false;
+  }
+  return true;
+}
+
+function queryAll(root, selector) {
+  var groups = String(selector || '').split(',');
+  var found = [];
+  var nodes = walk(root, []);
+  for (var g = 0; g < groups.length; g++) {
+    var sel = groups[g].trim();
+    var bits = sel.split(/\s+/);
+    for (var n = 0; n < nodes.length; n++) {
+      var el = nodes[n];
+      if (el === root && bits.length === 1 && matchCompound(el, bits[0])) {
+        if (found.indexOf(el) === -1) found.push(el);
+        continue;
+      }
+      if (bits.length === 1) {
+        if (el !== root && matchCompound(el, bits[0]) && found.indexOf(el) === -1) found.push(el);
+      } else if (bits.length === 2) {
+        if (el !== root && matchCompound(el, bits[1])) {
+          var p = el.parent;
+          var ok = false;
+          while (p) {
+            if (matchCompound(p, bits[0])) { ok = true; break; }
+            p = p.parent;
+          }
+          if (ok && found.indexOf(el) === -1) found.push(el);
+        }
+      }
+    }
+  }
+  found.item = function (i) { return found[i] || null; };
+  return found;
+}
+
+function queryOne(root, selector) {
+  var all = queryAll(root, selector);
+  return all[0] || null;
+}
+
+var html = new El('html');
+html.setAttribute('data-garden-galaxy', 'garden');
+var body = new El('body');
+html.appendChild(body);
+
+var document = {
+  readyState: 'loading',
+  documentElement: html,
+  body: body,
+  createElement: function (tag) { return new El(tag); },
+  getElementById: function (id) { return queryOne(html, '#' + id); },
+  querySelector: function (sel) { return queryOne(html, sel); },
+  querySelectorAll: function (sel) { return queryAll(html, sel); },
+  addEventListener: function () {},
+  insertBefore: function () {}
+};
+
+var fakeNow = 10000;
+function FakeDate() { return {}; }
+FakeDate.now = function () { return fakeNow; };
+
+var windowObj = {
+  addEventListener: function () {},
+  dispatchEvent: function () {},
+  matchMedia: function () { return { matches: false, addListener: function () {} }; },
+  getComputedStyle: function (el) {
+    return { backgroundColor: (el && el.style && el.style.background) || 'rgb(232, 176, 25)' };
+  },
+  FractalGarden: null
+};
+
+var keepCalls = 0;
+var sandbox = {
+  window: windowObj,
+  document: document,
+  localStorage: { getItem: function () { return null; }, setItem: function () {}, removeItem: function () {} },
+  location: { protocol: 'http:', href: 'http://127.0.0.1/docs/', pathname: '/' },
+  console: console,
+  Date: FakeDate,
+  setTimeout: setTimeout,
+  clearTimeout: clearTimeout,
+  requestAnimationFrame: function () { return 0; },
+  Promise: Promise,
+  JSON: JSON,
+  Error: Error,
+  Array: Array,
+  Object: Object,
+  String: String,
+  Number: Number,
+  KeepReceipt: {
+    keep: function () {
+      keepCalls += 1;
+      return Promise.resolve({ receiptHash: 'nope' });
+    }
+  }
+};
+windowObj.window = windowObj;
+sandbox.global = sandbox;
+
+var code = fs.readFileSync(path.join(__dirname, 'garden-rooms.js'), 'utf8');
+vm.runInNewContext(code, sandbox);
+
+
+var GR = sandbox.window.GardenRooms;
+assert.ok(GR && typeof GR.ensureSkyLegend === 'function', 'GardenRooms mounts');
+
+function makeDoor(id) {
+  var btn = new El('button');
+  btn.className = 'garden-lumino is-' + id;
+  btn.setAttribute('data-garden-lumino', id);
+  var light = new El('span');
+  light.className = 'garden-lumino-light';
+  btn.appendChild(light);
+  body.appendChild(btn);
+  return btn;
+}
+['gathering', 'nursery', 'settings', 'thread'].forEach(makeDoor);
+
+sandbox.GardenRooms = GR; // in a browser window.GardenRooms is also the global
+var opened = [];
+GR.openPlace = function (p) { opened.push('place:' + p); };
+GR.openThread = function () { opened.push('thread'); };
+
+GR.ensureSkyLegend();
+var legend = document.getElementById('lumino-legend');
+assert.ok(legend, 'legend is built');
+assert.equal(legend.getAttribute('aria-hidden'), null, 'legend is no longer hidden from readers');
+function row(id) { return queryOne(legend, '[data-lumino-row="' + id + '"]'); }
+['gathering', 'nursery', 'settings', 'thread'].forEach(function (id) {
+  var r = row(id);
+  assert.ok(r, 'row ' + id);
+  assert.equal(r.getAttribute('role'), 'button', id + ' row is a button');
+  assert.equal(r.getAttribute('tabindex'), '0', id + ' row takes focus');
+});
+assert.equal(row('settings').getAttribute('aria-label'), 'go to Settings', 'same words as the lumino menu');
+row('settings').fire('click');
+row('nursery').fire('click');
+row('gathering').fire('click');
+row('thread').fire('click');
+row('settings').fire('keydown', { key: 'Enter', preventDefault: function () {} });
+row('nursery').fire('keydown', { key: 'a', preventDefault: function () {} });
+assert.deepStrictEqual(opened, ['place:settings', 'place:nursery', 'place:core', 'thread', 'place:settings'], 'each word opens its own light: ' + opened.join(','));
+
+console.log('SMOKE_OK tree glow 1 v0.1');
