@intentius/behold 0.29.6 → 0.29.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/behold",
3
- "version": "0.29.6",
3
+ "version": "0.29.8",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "description": "behold — a live control plane on chant. See your whole estate (every substrate in one graph), coloured by drift; act through delegated, gated Ops.",
package/web/app.js CHANGED
@@ -8,9 +8,12 @@
8
8
  // floating control panel's chrome (panel.js — drag/snap/collapse/tabs, persisted
9
9
  // position), and the theme picker into the panel's View-tab slot (a stable element
10
10
  // renderPanelView never rewrites, so the select mounts once and survives re-renders).
11
+ import "./view-repo.js"; // behold#571: first, so every read carries the page's ?repo=
11
12
  import { createRefreshQueue } from "./refresh-queue.js";
12
13
  import { readViewQuery, writeViewQuery, settleView, settlePlace, refusalLine } from "./view-url.js";
13
14
  import { quietCardLines, readEmbed, selectMessage, viewFromMessage } from "./embed.js";
15
+ import { WORDS as POP_WORDS, cardRoot as popCardRoot, placePopover, popReduce, popState, resourcePopover, rootPopover } from "./kiosk-popover.js";
16
+ import { cssString, hoverCss, hoverFromMessage, hoverLinks, hoverMessage, isLightBg, rootPaint, rootText } from "./kiosk-roots.js";
14
17
  import { gateCards } from "./workspace-gates.js";
15
18
  import { cornerOf, estateLine, hrefOf as tgHref, legend as tgLegend, markLine, rootRows as tgRootRows, waveCards } from "./terragucci.js";
16
19
  import { laneElement as tgLane, rootOfCard as tgRootOfCard, historyElement as tgHistoryLane } from "./terragucci-timeline.js";
@@ -3378,6 +3381,7 @@ let tgTimeline = null; // #506: /api/terragucci/timeline, the audit record's lan
3378
3381
  let tgHistory = null; // #522: /api/lifecycle/history, the gate ledgers' history
3379
3382
  async function initTerragucci() {
3380
3383
  const project = await apiFetch("/api/project").then((r) => r.json()).catch(() => ({}));
3384
+ kioskRoots = project.terragucciRoots || null; // behold#571
3381
3385
  if (!project.terragucciReports) return;
3382
3386
  await loadTerragucci();
3383
3387
  // #505: the chant/lifecycle lane, and what moved while this page is open.
@@ -3485,6 +3489,7 @@ function paintTgProgress() {
3485
3489
  // merged. Not live; redrawn on each render and each read.
3486
3490
  const TG_TONE = { plan: "var(--pending)", wait: "var(--foreign)", busy: "var(--runtime)", ok: "var(--managed)", bad: "var(--degraded)", idle: "var(--neutral)" };
3487
3491
  function drawTgReads() {
3492
+ if (embedOpts.bare) queueMicrotask(kioskDecorate); // behold#571: once this draw is done
3488
3493
  watchTgRuns(); // #571: the wave boxes' jobs, while the components zoom is on screen
3489
3494
  const svg = document.querySelector("#graph svg");
3490
3495
  if (!svg) return;
@@ -4083,6 +4088,271 @@ function markTerragucciCards() {
4083
4088
  }
4084
4089
  drawTgReads(); // #568
4085
4090
  }
4091
+ // --- behold#571: the kiosk frame's popover, root colours and hover links -------
4092
+ // The kiosk frames this page bare, with no Inspect pane. Pointing at a card
4093
+ // shows a popover beside it (web/kiosk-popover.js says what it holds); a
4094
+ // click keeps it open. Each root's name carries a dot in its own colour, and
4095
+ // hovering a card or box here lights its root and wave in the kiosk's other
4096
+ // panes, and theirs light it here (`behold:hover`, web/kiosk-roots.js).
4097
+ // Nothing happens until someone points: a TV with no pointer sees the page
4098
+ // as before.
4099
+ let kioskRoots = null; // /api/project's terragucciRoots: { roots, waves }
4100
+ let kioskPop = popState();
4101
+ let kioskHover = null; // this frame's pointer: { root } | { wave } | null
4102
+ let kioskHostHover = null; // the kiosk's pointer, from its message
4103
+ function kioskRootList() {
4104
+ if (kioskRoots && Array.isArray(kioskRoots.roots) && kioskRoots.roots.length) return kioskRoots.roots;
4105
+ return tgAnswer && Array.isArray(tgAnswer.roots) ? tgAnswer.roots.map((r) => r.path) : [];
4106
+ }
4107
+ function kioskWaves() {
4108
+ return (kioskRoots && kioskRoots.waves) || [];
4109
+ }
4110
+ function kioskLight() {
4111
+ return isLightBg(getComputedStyle(document.documentElement).getPropertyValue("--bg"));
4112
+ }
4113
+ function kioskCompMode() {
4114
+ return !!(view.components && terragucciComponentsRepo && tgComponentFacets);
4115
+ }
4116
+ function kioskRootOfComponent(c) {
4117
+ const m = (tgComponentFacets && tgComponentFacets.roots) || {};
4118
+ return m[c] || c;
4119
+ }
4120
+ /** The terragucci root (its path) of a resources-zoom box name. */
4121
+ function kioskRootOfBox(name) {
4122
+ const r = tgAnswer && Array.isArray(tgAnswer.roots) ? tgAnswer.roots.find((x) => x.name === name) : null;
4123
+ return r ? r.path : name;
4124
+ }
4125
+ /** What the pointer is on: a card's or a box's root, or a wave's box. */
4126
+ function kioskHoverOf(target) {
4127
+ if (!target || !target.closest) return null;
4128
+ const card = target.closest("[data-node-id]");
4129
+ if (card) {
4130
+ const id = card.getAttribute("data-node-id");
4131
+ if (kioskCompMode()) return { root: kioskRootOfComponent(id) };
4132
+ const at = popCardRoot(tgAnswer, id);
4133
+ return at ? { root: at.path } : null;
4134
+ }
4135
+ const rect = target.closest("rect[data-group-id]");
4136
+ if (!rect) return null;
4137
+ const g = rect.getAttribute("data-group-id") || "";
4138
+ const w = /^wave-(\d+)$/.exec(g);
4139
+ if (w) return { wave: Number(w[1]) };
4140
+ const root = kioskRootOfBox(g);
4141
+ return kioskRootList().includes(root) ? { root } : null;
4142
+ }
4143
+ const sameHover = (a, b) => (a && a.root) === (b && b.root) && (a && a.wave) === (b && b.wave);
4144
+ function kioskSetHover(h) {
4145
+ if (sameHover(h, kioskHover)) return;
4146
+ kioskHover = h;
4147
+ kioskGlow();
4148
+ if (!embedOpts.host || window.parent === window) return;
4149
+ try {
4150
+ window.parent.postMessage(hoverMessage(h), embedOpts.host);
4151
+ } catch {
4152
+ /* the parent went away */
4153
+ }
4154
+ }
4155
+ /** Light what the two pointers link: a root's box (or card) and a wave's box, with one style element that survives a redraw. */
4156
+ function kioskGlow() {
4157
+ let style = document.getElementById("k-hover-style");
4158
+ if (!style) {
4159
+ style = document.createElement("style");
4160
+ style.id = "k-hover-style";
4161
+ document.head.appendChild(style);
4162
+ }
4163
+ const a = hoverLinks(kioskHover, kioskWaves());
4164
+ const b = hoverLinks(kioskHostHover, kioskWaves());
4165
+ const links = { roots: [...new Set([...a.roots, ...b.roots])], waves: [...new Set([...a.waves, ...b.waves])] };
4166
+ const boxes = (root) => {
4167
+ const names = new Set([root]);
4168
+ const r = tgAnswer && Array.isArray(tgAnswer.roots) ? tgAnswer.roots.find((x) => x.path === root || x.name === root) : null;
4169
+ if (r) names.add(r.name);
4170
+ const comps = tgComponentFacets && tgComponentFacets.roots ? Object.keys(tgComponentFacets.roots).filter((c) => tgComponentFacets.roots[c] === root) : [];
4171
+ for (const c of comps) names.add(c);
4172
+ return [...names].map((n) => `#graph rect[data-group-id=${cssString(n)}], #graph g[data-node-id=${cssString(n)}]`).join(", ");
4173
+ };
4174
+ style.textContent = hoverCss(links, { root: boxes, wave: (w) => `#graph rect[data-group-id="wave-${w}"]` }, "filter: drop-shadow(0 0 2px var(--focus)) drop-shadow(0 0 7px var(--focus));");
4175
+ }
4176
+ /** The dots by each root's name: on its box's title (Resources) or its card (Roots by wave). */
4177
+ function kioskDots(svg) {
4178
+ for (const d of svg.querySelectorAll("[data-k-dot]")) d.remove();
4179
+ const roots = kioskRootList();
4180
+ if (!roots.length) return;
4181
+ const light = kioskLight();
4182
+ const dotBy = (text, root) => {
4183
+ const colour = rootPaint(root, roots, { light });
4184
+ if (!colour) return;
4185
+ let bb;
4186
+ try {
4187
+ bb = text.getBBox();
4188
+ } catch {
4189
+ return;
4190
+ }
4191
+ if (!bb.width) return;
4192
+ const c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
4193
+ c.setAttribute("data-k-dot", "1");
4194
+ // After the name: before it sits the card's icon.
4195
+ c.setAttribute("cx", String(bb.x + bb.width + 9));
4196
+ c.setAttribute("cy", String(bb.y + bb.height / 2));
4197
+ c.setAttribute("r", "5.5");
4198
+ // The theme's own root colour (--root-<i>): a style, since a fill attribute takes no var().
4199
+ c.style.fill = colour;
4200
+ c.setAttribute("pointer-events", "none");
4201
+ text.parentNode.insertBefore(c, text.nextSibling);
4202
+ };
4203
+ if (kioskCompMode()) {
4204
+ for (const card of svg.querySelectorAll("[data-node-id]")) {
4205
+ const id = card.getAttribute("data-node-id");
4206
+ const t = [...card.querySelectorAll("text")].find((x) => (x.textContent || "").trim() === id);
4207
+ if (t) dotBy(t, kioskRootOfComponent(id));
4208
+ }
4209
+ return;
4210
+ }
4211
+ for (const rect of svg.querySelectorAll("rect[data-group-id]")) {
4212
+ const name = rect.getAttribute("data-group-id");
4213
+ const root = kioskRootOfBox(name);
4214
+ if (!roots.includes(root)) continue;
4215
+ const t = [...(rect.parentNode ? rect.parentNode.querySelectorAll(":scope > text") : [])].find((x) => (x.textContent || "").trim() === name && x.getAttribute("visibility") !== "hidden");
4216
+ if (t) dotBy(t, root);
4217
+ }
4218
+ }
4219
+ function kioskDecorate() {
4220
+ const svg = document.querySelector("#graph svg");
4221
+ if (!svg) return;
4222
+ kioskDots(svg);
4223
+ kioskGlow();
4224
+ kioskPopRender();
4225
+ }
4226
+ function kioskPopEvent(ev) {
4227
+ const next = popReduce(kioskPop, ev, Date.now());
4228
+ if (next === kioskPop) return;
4229
+ kioskPop = next;
4230
+ kioskPopRender();
4231
+ }
4232
+ function kioskPopRender() {
4233
+ let pop = document.getElementById("k-pop");
4234
+ const svg = document.querySelector("#graph svg");
4235
+ const id = kioskPop.shown;
4236
+ const card = id && svg ? svg.querySelector(`[data-node-id="${CSS.escape(id)}"]`) : null;
4237
+ const answer = tgAnswerDrawn();
4238
+ const content = !card || !answer ? null : kioskCompMode() ? rootPopover(answer, kioskRootOfComponent(id), resourcesByComponent[id]) : resourcePopover(answer, id);
4239
+ if (!content) {
4240
+ if (pop) pop.hidden = true;
4241
+ return;
4242
+ }
4243
+ if (!pop) {
4244
+ pop = document.createElement("div");
4245
+ pop.id = "k-pop";
4246
+ pop.setAttribute("role", "tooltip");
4247
+ document.body.appendChild(pop);
4248
+ }
4249
+ pop.replaceChildren();
4250
+ pop.classList.toggle("k-pinned", kioskPop.pinned);
4251
+ const roots = kioskRootList();
4252
+ const light = kioskLight();
4253
+ const add = (tag, cls, text) => {
4254
+ const n = document.createElement(tag);
4255
+ n.className = cls;
4256
+ if (text !== undefined) n.textContent = text;
4257
+ pop.appendChild(n);
4258
+ return n;
4259
+ };
4260
+ const list = (rows) => {
4261
+ const ul = add("ul", "k-pop-lines");
4262
+ for (const r of rows) {
4263
+ const li = document.createElement("li");
4264
+ li.className = r.tone === "unknown" ? "k-pop-unknown" : "";
4265
+ li.textContent = r.text;
4266
+ ul.appendChild(li);
4267
+ }
4268
+ return ul;
4269
+ };
4270
+ if (content.address !== undefined) {
4271
+ add("div", "k-pop-addr", content.address);
4272
+ add("div", "k-pop-root").appendChild(rootText(document, `It sits in root ${content.root}.`, roots, { light }));
4273
+ add("p", "k-pop-head", content.head);
4274
+ if (content.lines.length) {
4275
+ const ul = list(content.lines);
4276
+ if (content.more) ul.appendChild(Object.assign(document.createElement("li"), { className: "k-pop-more", textContent: `and ${content.more} more` }));
4277
+ }
4278
+ if (content.applied) {
4279
+ add("p", "k-pop-note", content.applied.when);
4280
+ list(content.applied.lines);
4281
+ } else if (content.note) add("p", "k-pop-note", content.note);
4282
+ } else {
4283
+ add("div", "k-pop-addr").appendChild(rootText(document, content.root, roots, { light }));
4284
+ add("p", "k-pop-head", content.head);
4285
+ const ul = add("ul", "k-pop-lines");
4286
+ for (const r of content.rows) {
4287
+ const li = document.createElement("li");
4288
+ li.className = "k-pop-res";
4289
+ const st = document.createElement("span");
4290
+ st.className = `k-pop-state k-pop-${r.state}`;
4291
+ st.textContent = r.words;
4292
+ li.append(document.createTextNode(`${r.address} `), st);
4293
+ ul.appendChild(li);
4294
+ }
4295
+ if (content.more > 0) ul.appendChild(Object.assign(document.createElement("li"), { className: "k-pop-more", textContent: `and ${content.more} more` }));
4296
+ }
4297
+ // behold#571: a finger opens the popover with a tap, so a touch screen reads the tap's words.
4298
+ const touchOnly = typeof matchMedia === "function" && matchMedia("(hover: none)").matches;
4299
+ add("p", "k-pop-hint", kioskPop.pinned ? (touchOnly ? POP_WORDS.tapPinnedHint : POP_WORDS.pinnedHint) : POP_WORDS.pinHint);
4300
+ pop.hidden = false;
4301
+ const cb = card.getBoundingClientRect();
4302
+ const pb = pop.getBoundingClientRect();
4303
+ const at = placePopover({ left: cb.left, top: cb.top, width: cb.width, height: cb.height }, { width: pb.width, height: pb.height }, { width: window.innerWidth, height: window.innerHeight });
4304
+ pop.style.left = `${Math.round(at.left)}px`;
4305
+ pop.style.top = `${Math.round(at.top)}px`;
4306
+ }
4307
+ /** Wire the frame's pointer once: the popover, and the hover links to the kiosk. Bare frames only. */
4308
+ function wireKioskFrame() {
4309
+ if (!embedOpts.bare) return;
4310
+ const graph = document.getElementById("graph");
4311
+ if (!graph) return;
4312
+ graph.addEventListener("pointerover", (e) => {
4313
+ const card = e.target.closest && e.target.closest("[data-node-id]");
4314
+ if (card) {
4315
+ // The card's own tooltips would sit over the popover.
4316
+ for (const t of card.querySelectorAll("title")) t.remove();
4317
+ kioskPopEvent({ type: "enter", id: card.getAttribute("data-node-id") });
4318
+ }
4319
+ kioskSetHover(kioskHoverOf(e.target));
4320
+ });
4321
+ graph.addEventListener("pointerout", (e) => {
4322
+ const card = e.target.closest && e.target.closest("[data-node-id]");
4323
+ const into = e.relatedTarget;
4324
+ if (card && !(into && card.contains(into))) kioskPopEvent({ type: "leave", id: card.getAttribute("data-node-id") });
4325
+ if (!into || !graph.contains(into)) kioskSetHover(null);
4326
+ });
4327
+ graph.addEventListener(
4328
+ "click",
4329
+ (e) => {
4330
+ const card = e.target.closest && e.target.closest("[data-node-id]");
4331
+ if (card) kioskPopEvent({ type: "click", id: card.getAttribute("data-node-id") });
4332
+ else kioskPopEvent({ type: "outside" });
4333
+ },
4334
+ true,
4335
+ );
4336
+ document.addEventListener("click", (e) => {
4337
+ if (!graph.contains(e.target) && !(e.target.closest && e.target.closest("#k-pop"))) kioskPopEvent({ type: "outside" });
4338
+ });
4339
+ document.addEventListener("keydown", (e) => {
4340
+ if (e.key === "Escape") kioskPopEvent({ type: "escape" });
4341
+ });
4342
+ setInterval(() => kioskPopEvent({ type: "tick" }), 1000);
4343
+ window.addEventListener("message", (e) => {
4344
+ if (!embedOpts.host || e.origin !== embedOpts.host) return;
4345
+ if (e.data && e.data.type === "behold:unpin") return kioskPopEvent({ type: "outside" });
4346
+ // behold#571: the kiosk's theme picker moved; follow it here without a reload.
4347
+ if (e.data && e.data.type === "behold:theme" && typeof e.data.name === "string") return void setTheme(e.data.name, { persist: false });
4348
+ const h = hoverFromMessage(e.data);
4349
+ if (h === undefined) return;
4350
+ kioskHostHover = h;
4351
+ kioskGlow();
4352
+ });
4353
+ }
4354
+ wireKioskFrame();
4355
+
4086
4356
  function markAge(marks) {
4087
4357
  const newest = marks.map((m) => m.run.finished).sort().pop();
4088
4358
  const s = Math.max(0, (Date.now() - Date.parse(newest)) / 1000);
@@ -5718,6 +5988,54 @@ function ensureZoomControls(host) {
5718
5988
  },
5719
5989
  { passive: false },
5720
5990
  );
5991
+ // behold#571: on a touch screen (the kiosk on a phone), one finger pans and two pinch to zoom.
5992
+ let touch = null;
5993
+ const touchPoints = (e) => [...e.touches].map((t) => ({ x: t.clientX, y: t.clientY }));
5994
+ host.addEventListener(
5995
+ "touchstart",
5996
+ (e) => {
5997
+ if (!vb) return;
5998
+ touch = { pts: touchPoints(e), moved: false };
5999
+ },
6000
+ { passive: true },
6001
+ );
6002
+ host.addEventListener(
6003
+ "touchmove",
6004
+ (e) => {
6005
+ const s = currentSvg();
6006
+ if (!touch || !vb || !s) return;
6007
+ const pts = touchPoints(e);
6008
+ const r = s.getBoundingClientRect();
6009
+ e.preventDefault();
6010
+ if (pts.length >= 2 && touch.pts.length >= 2) {
6011
+ const dist = (p) => Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y) || 1;
6012
+ const mid = (p) => ({ x: (p[0].x + p[1].x) / 2, y: (p[0].y + p[1].y) / 2 });
6013
+ const f = dist(touch.pts) / dist(pts);
6014
+ const m = mid(pts);
6015
+ const m0 = mid(touch.pts);
6016
+ const cx = vb[0] + ((m.x - r.left) / r.width) * vb[2];
6017
+ const cy = vb[1] + ((m.y - r.top) / r.height) * vb[3];
6018
+ const nw = Math.min(vbInit[2] * 3, Math.max(vbInit[2] / 60, vb[2] * f));
6019
+ const nh = nw * (vb[3] / vb[2]);
6020
+ vb[0] = cx - ((cx - vb[0]) * nw) / vb[2] - ((m.x - m0.x) / r.width) * nw;
6021
+ vb[1] = cy - ((cy - vb[1]) * nh) / vb[3] - ((m.y - m0.y) / r.height) * nh;
6022
+ vb[2] = nw;
6023
+ vb[3] = nh;
6024
+ } else if (pts.length === 1 && touch.pts.length === 1) {
6025
+ vb[0] -= ((pts[0].x - touch.pts[0].x) / r.width) * vb[2];
6026
+ vb[1] -= ((pts[0].y - touch.pts[0].y) / r.height) * vb[3];
6027
+ }
6028
+ if (!touch.moved && touch.pts.length && Math.abs(pts[0].x - touch.pts[0].x) + Math.abs(pts[0].y - touch.pts[0].y) > 3) touch.moved = true;
6029
+ if (touch.moved) panMoved = true;
6030
+ touch.pts = pts;
6031
+ vbAtFit = false;
6032
+ applyVB();
6033
+ },
6034
+ { passive: false },
6035
+ );
6036
+ host.addEventListener("touchend", (e) => {
6037
+ touch = e.touches.length ? { pts: touchPoints(e), moved: !!(touch && touch.moved) } : null;
6038
+ });
5721
6039
  let drag = false;
5722
6040
  let px = 0;
5723
6041
  let py = 0;
@@ -6214,6 +6532,8 @@ function ensureLayoutDrag(host) {
6214
6532
  layoutWired = true;
6215
6533
  host.addEventListener("pointerdown", (e) => {
6216
6534
  if (e.button !== 0 || !layoutIndex) return;
6535
+ // behold#571: in the kiosk's bare frame a finger pans the graph; it never moves a card.
6536
+ if (e.pointerType === "touch" && embedOpts.bare) return;
6217
6537
  const t = layoutTargetOf(e);
6218
6538
  if (!t || !currentSvg()) return;
6219
6539
  layoutDrag = { ...t, x0: e.clientX, y0: e.clientY, base: layoutDeltas[t.id] || {}, moved: false };
package/web/index.html CHANGED
@@ -419,6 +419,30 @@
419
419
  .bare #app { grid-template-columns: 1fr 0 !important; }
420
420
  .bare #graph svg > rect[fill^="url(#pin-"] { display: none; }
421
421
  .bare #graph svg { cursor: default; }
422
+ /* behold#571: the marks drawn over a card let the pointer through to it, so pointing anywhere on the card opens its popover. */
423
+ .bare #graph text[data-tg-root], .bare #graph g[data-tg-root] { pointer-events: none; }
424
+ /* behold#571: the kiosk frame's popover, beside the card it describes. */
425
+ #k-pop { position: fixed; z-index: 50; max-width: min(30em, 46vw); padding: 0.7em 0.9em 0.6em; background: var(--panel); color: var(--fg);
426
+ border: 1px solid var(--rule); border-radius: 8px; box-shadow: 0 6px 22px color-mix(in srgb, var(--shadow) 70%, transparent);
427
+ font: 14px/1.4 var(--font-ui); pointer-events: auto; }
428
+ #k-pop[hidden] { display: none; }
429
+ /* behold#571: on a phone the popover takes the frame's width, less a margin. */
430
+ @media (max-width: 600px) { #k-pop { max-width: calc(100vw - 16px); } }
431
+ #k-pop.k-pinned { border-color: var(--focus); }
432
+ #k-pop p { margin: 0.25em 0; }
433
+ .k-pop-addr { font: 700 1.05em var(--font-mono); overflow-wrap: anywhere; }
434
+ .k-pop-root { color: var(--muted); font-size: 0.92em; }
435
+ .k-pop-head { font-weight: 600; }
436
+ .k-pop-lines { list-style: none; margin: 0.2em 0 0.3em; padding: 0.35em 0.6em; background: var(--well); border-radius: 6px; font: 0.95em/1.45 var(--font-mono); }
437
+ .k-pop-lines li { overflow-wrap: anywhere; }
438
+ .k-pop-unknown, .k-pop-unknown.k-pop-state { color: var(--foreign); }
439
+ .k-pop-more { color: var(--muted); }
440
+ .k-pop-note { color: var(--muted); font-size: 0.92em; }
441
+ .k-pop-hint { color: var(--muted); font-size: 0.85em; border-top: 1px solid var(--rule); padding-top: 0.35em; margin-top: 0.4em !important; }
442
+ .k-pop-state { margin-left: 0.4em; font-family: var(--font-ui); font-weight: 600; white-space: nowrap; }
443
+ .k-pop-change { color: var(--pending); }
444
+ .k-pop-same { color: var(--muted); }
445
+ .k-root::before { content: ""; display: inline-block; width: 0.6em; height: 0.6em; border-radius: 50%; background: var(--k-root); margin: 0 0.3em 0.1em 0.05em; vertical-align: middle; }
422
446
  #panel-tabs #hintk { flex: none; align-self: center; padding: 2px 6px; margin-left: 2px; }
423
447
  #panel-collapse { flex: none; align-self: center; background: none; border: none; color: var(--muted);
424
448
  font-size: var(--t-body); cursor: pointer; line-height: 1; padding: 2px 4px; }