@grafloria/element 0.4.64 → 0.4.66

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": "@grafloria/element",
3
- "version": "0.4.64",
3
+ "version": "0.4.66",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -56,7 +56,7 @@ import { buildCommitCommands, cellFromGridItem, cellToRect, columnUnitFor, gridI
56
56
  import { ensureDashboardKitStyles } from './styles.js';
57
57
  import { captionOfGroup, captionPassThrough, sectionCaptionReserve } from './caption.js';
58
58
  import { TAB_STRIP_HEIGHT } from './tabs.js';
59
- import { BESIDE_BAND, bandRects, containerUnder, resolve as resolveZone, stripUnder } from './zones.js';
59
+ import { BESIDE_BAND, resolve as resolveZone, stripUnder } from './zones.js';
60
60
  import { SequenceCommand, SetGroupCellCommand, tileCommands } from './commit.js';
61
61
  import { EDGE_GRACE } from './board-ctx.js';
62
62
  import { createProjection } from './project.js';
@@ -910,96 +910,6 @@ export function bindDashboardGrid(api, group, options = {}) {
910
910
  * come back by themselves when the widget leaves (S2).
911
911
  */
912
912
  let beside = null;
913
- /**
914
- * A VERTICAL BESIDE, HELD BUT NOT APPLIED (0.4.61). A container's top or
915
- * bottom band pushes it a whole row along its own column — and the zone walk
916
- * reads the container where it RESTS (0.4.60, which is what stopped the
917
- * strip flickering), so the moment the push lands, the painted tabs sit a
918
- * row below the only place that accepts them, and a hand following them down
919
- * pushes them again. The user met it as "it pushes the entire tab group down
920
- * and then I cannot go up to the tab header".
921
- *
922
- * So a vertical band moves NOTHING while the hand is held: an overlay marks
923
- * the cell the widget will take and the container gives way on release — the
924
- * rule a tab's dock and split previews have followed since 0.4.42. The LEFT
925
- * and RIGHT bands keep their live slide (0.4.48): sideways, a full-width
926
- * strip never leaves the hand, and the user missed that slide the one time
927
- * it was frozen.
928
- */
929
- let pendingBeside = null;
930
- let joinEl = null;
931
- const showJoin = (r) => {
932
- const layer = htmlLayer();
933
- if (!layer)
934
- return;
935
- if (!joinEl || joinEl.parentElement !== layer) {
936
- joinEl === null || joinEl === void 0 ? void 0 : joinEl.remove();
937
- joinEl = document.createElement('div');
938
- joinEl.className = 'axdb-join';
939
- layer.prepend(joinEl);
940
- }
941
- joinEl.style.left = `${r.x}px`;
942
- joinEl.style.top = `${r.y}px`;
943
- joinEl.style.width = `${r.width}px`;
944
- joinEl.style.height = `${r.height}px`;
945
- };
946
- /**
947
- * THE LANES a hand is over, painted. 0.4.62 made a tab group's top and
948
- * bottom a fixed 30 px — a good target and an impossible guess, since to put
949
- * a widget ABOVE a panel you point just BELOW its header, inside what reads
950
- * as page content. Nothing moves while the hand is held (0.4.61), so the
951
- * bands can be shown and aimed at. The band you are IN still wears the mark.
952
- */
953
- let lanesEl = null;
954
- const showLanes = (c) => {
955
- var _a;
956
- const layer = htmlLayer();
957
- if (!c || !layer) {
958
- lanesEl === null || lanesEl === void 0 ? void 0 : lanesEl.remove();
959
- lanesEl = null;
960
- return;
961
- }
962
- if (!lanesEl || lanesEl.parentElement !== layer) {
963
- lanesEl === null || lanesEl === void 0 ? void 0 : lanesEl.remove();
964
- lanesEl = document.createElement('div');
965
- lanesEl.className = 'axdb-lanes';
966
- layer.prepend(lanesEl);
967
- }
968
- lanesEl.style.left = `${c.frame.x}px`;
969
- lanesEl.style.top = `${c.frame.y}px`;
970
- lanesEl.style.width = `${c.frame.width}px`;
971
- lanesEl.style.height = `${c.frame.height}px`;
972
- const want = bandRects(c);
973
- while (lanesEl.childElementCount > want.length)
974
- (_a = lanesEl.lastElementChild) === null || _a === void 0 ? void 0 : _a.remove();
975
- while (lanesEl.childElementCount < want.length) {
976
- const el = document.createElement('div');
977
- el.className = 'axdb-lane';
978
- lanesEl.appendChild(el);
979
- }
980
- want.forEach((b, i) => {
981
- const el = lanesEl.children[i];
982
- el.setAttribute('data-lane', b.side);
983
- el.style.left = `${b.rect.x - c.frame.x}px`;
984
- el.style.top = `${b.rect.y - c.frame.y}px`;
985
- el.style.width = `${b.rect.width}px`;
986
- el.style.height = `${b.rect.height}px`;
987
- });
988
- };
989
- const endPendingBeside = () => {
990
- pendingBeside = null;
991
- joinEl === null || joinEl === void 0 ? void 0 : joinEl.remove();
992
- joinEl = null;
993
- };
994
- const endLanes = () => showLanes(null);
995
- /** The cell a vertical beside would take: the container's own, or the row under it. Never the pointer's row — the container is not moving. */
996
- const pendingCellOf = (containerId, side, spans) => {
997
- const it = engine.getItem(containerId);
998
- if (!it)
999
- return null;
1000
- const x = Math.max(0, Math.min(engine.columns - spans.w, it.x));
1001
- return { x, y: side === 'top' ? it.y : it.y + it.h };
1002
- };
1003
913
  const endBeside = (restore) => {
1004
914
  if (!beside)
1005
915
  return;
@@ -1382,8 +1292,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1382
1292
  showRefusal(null, 0, 0);
1383
1293
  }
1384
1294
  }
1385
- endPendingBeside();
1386
- endLanes();
1387
1295
  disarmGlideSoon();
1388
1296
  releasePointer(g.pointerId);
1389
1297
  api.container.style.cursor = '';
@@ -1427,8 +1335,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1427
1335
  };
1428
1336
  const cancelActiveGesture = (notify = true) => {
1429
1337
  var _a, _b, _c;
1430
- endPendingBeside();
1431
- endLanes();
1432
1338
  if (gesture === null || gesture === void 0 ? void 0 : gesture.strip) {
1433
1339
  (_a = options.tabDrop) === null || _a === void 0 ? void 0 : _a.markDrop(null, null);
1434
1340
  gesture.strip = null;
@@ -1573,17 +1479,10 @@ export function bindDashboardGrid(api, group, options = {}) {
1573
1479
  // BEFORE any engine is asked anything. The strip still wins over every
1574
1480
  // board; a band's stickiness and the vacated cell still hold a beside.
1575
1481
  const z = resolveTileZone(g, ev);
1576
- // Show the container's bands while a widget is over it: they are a fixed
1577
- // depth, so they must be visible to be aimed at (0.4.63). A group is never
1578
- // shown them — it is moved by intent and pushes what is in its way.
1579
- showLanes(g.subject === 'node' && !isStatic
1580
- ? containerUnder({ x: ev.world.x, y: ev.world.y, roots: lastRoots, restFrames: lastRests, ghostSubtree: EMPTY_SUBTREE, homeChain: homeChain() })
1581
- : null);
1582
1482
  if (z.kind === 'strip' && options.tabDrop && !isStatic && g.kind !== 'palette' && g.subject === 'node') {
1583
1483
  // -- INTO A STRIP: the widget becomes a new tab there, so it leaves
1584
1484
  // this board (survivors settle home) and the strip marks the slot.
1585
1485
  endBeside(true); // a band's shift gives way to the strip: the container comes back
1586
- endPendingBeside();
1587
1486
  leaveSelf();
1588
1487
  setDim(g, false);
1589
1488
  if (!g.strip || g.strip.containerId !== z.containerId || g.strip.index !== z.index) {
@@ -1602,22 +1501,6 @@ export function bindDashboardGrid(api, group, options = {}) {
1602
1501
  // — at the board's edge the container shifts over to make room, live
1603
1502
  // and gliding, like any widget gives way (0.4.48). On THIS board the
1604
1503
  // engine is driven directly; on another board its leg drives it (4a).
1605
- if (z.kind === 'beside' && !isStatic && z.board.ref === selfPeer && g.subject === 'node' && (z.side === 'top' || z.side === 'bottom')) {
1606
- // -- A VERTICAL BAND: marked, not applied. Nothing moves until release.
1607
- if (beside)
1608
- endBeside(true);
1609
- const cell = pendingCellOf(z.containerId, z.side, g.spans);
1610
- if (cell) {
1611
- leaveSelf();
1612
- setDim(g, false);
1613
- pendingBeside = { id: z.containerId, side: z.side, cell, spans: Object.assign({}, g.spans) };
1614
- showJoin(cellToRect({ x: cell.x, y: cell.y, w: g.spans.w, h: g.spans.h }, frame(), geom(), rows()));
1615
- syncPlaceholder();
1616
- return;
1617
- }
1618
- }
1619
- if (pendingBeside)
1620
- endPendingBeside(); // the hand left the band: the mark goes
1621
1504
  if (z.kind === 'beside' && !isStatic && z.board.ref === selfPeer) {
1622
1505
  const row = rowOfPoint(ev.world.y);
1623
1506
  if (!z.kept || !beside || beside.row !== row)
@@ -2072,13 +1955,6 @@ export function bindDashboardGrid(api, group, options = {}) {
2072
1955
  (_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'commit', kind: g.kind, nodeId: g.id, changed: true });
2073
1956
  return;
2074
1957
  }
2075
- if (pendingBeside) {
2076
- // -- REALIZED ON RELEASE: the container held still while the hand chose;
2077
- // now it gives way, and the commit below reads the settled layout.
2078
- const p = pendingBeside;
2079
- endPendingBeside();
2080
- applyBeside(g, { id: p.id, side: p.side }, p.cell.y);
2081
- }
2082
1958
  if (g.leg) {
2083
1959
  // -- CROSS-CONTAINER COMMIT: one batch across both boards -----------
2084
1960
  const fin = g.leg.adopted.finalize();
@@ -2254,6 +2130,11 @@ export function bindDashboardGrid(api, group, options = {}) {
2254
2130
  // until 216 px of the fluid demo's page meant "above the whole
2255
2131
  // panel" (0.4.62). The sides keep the fifth.
2256
2132
  bandY: layout === 'tabs' ? TAB_STRIP_HEIGHT : 0,
2133
+ // …and the TOP band hangs ABOVE the frame, where a hand looking
2134
+ // for "above this panel" actually goes (0.4.65). A panel holding
2135
+ // the board's first row has no row above it to point at, and the
2136
+ // lane under its header is the last place anyone would try.
2137
+ topOutside: layout === 'tabs' ? TAB_STRIP_HEIGHT : 0,
2257
2138
  inner: innerPeer ? boardRef(innerPeer, depth + 1) : null,
2258
2139
  });
2259
2140
  }
@@ -2298,14 +2179,20 @@ export function bindDashboardGrid(api, group, options = {}) {
2298
2179
  else {
2299
2180
  const spanW = (_b = (_a = engine.getItem(g.id)) === null || _a === void 0 ? void 0 : _a.w) !== null && _b !== void 0 ? _b : g.spans.w;
2300
2181
  const cell = pointToCell(desired.x, desired.y, frame(), geom(), rows(), spanW);
2301
- if (engine.moveCheck(g.id, cell.x, cell.y, { pushSolid }).changed)
2182
+ // A PALETTE CHIP IS A FIRST PLACEMENT THE WHOLE WAY. The anti-jitter
2183
+ // gate measures how deep the mover has penetrated the tile it is
2184
+ // displacing and refuses at 50% or less — so a one-row chip held at the
2185
+ // vertical CENTRE of a two-row widget can never take its cell, and the
2186
+ // chip stays wherever it entered the board. A tile of the board has a
2187
+ // place to jitter back to; a chip being placed has none, and the hand
2188
+ // expects it to follow the pointer. The engine documents `gate: false`
2189
+ // as exactly this mode. Found when the vertical bands stopped lifting
2190
+ // the chip off the board mid-path, which had been hiding it (L99).
2191
+ if (engine.moveCheck(g.id, cell.x, cell.y, Object.assign({ pushSolid }, (g.kind === 'palette' ? { gate: false } : {}))).changed)
2302
2192
  project();
2303
2193
  }
2304
2194
  };
2305
2195
  /** What the pointer means for the dragged tile: the resolve over the live tree, with the beside the hand holds. */
2306
- /** The tree and the rest frames the LAST resolve used — the lanes are painted on exactly those. */
2307
- let lastRoots = [];
2308
- let lastRests = new Map();
2309
2196
  const resolveTileZone = (g, ev) => {
2310
2197
  var _a, _b, _c, _d, _e;
2311
2198
  const roots = zoneRoots();
@@ -2316,8 +2203,6 @@ export function bindDashboardGrid(api, group, options = {}) {
2316
2203
  // pointer, so read live it would answer about a container that is only
2317
2204
  // there because of the answer.
2318
2205
  const rests = restFramesOf(g);
2319
- lastRoots = roots;
2320
- lastRests = rests;
2321
2206
  let strip = null;
2322
2207
  if (((_a = options.tabDrop) === null || _a === void 0 ? void 0 : _a.tabIndexAt) && !isStatic && g.kind !== 'palette' && g.subject === 'node') {
2323
2208
  // A group never becomes a tab: over a container's strip it is over the
@@ -2343,15 +2228,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2343
2228
  strip,
2344
2229
  prev: beside
2345
2230
  ? { containerId: beside.id, side: beside.side, frame0: beside.frame0, vacated: cellToRect({ x: beside.vacated.x, y: beside.vacated.y, w: g.spans.w, h: g.spans.h }, frame(), geom(), rows()) }
2346
- : pendingBeside
2347
- ? // a VERTICAL band the hand holds: the container never moved, so its own frame is the sticky one
2348
- {
2349
- containerId: pendingBeside.id,
2350
- side: pendingBeside.side,
2351
- frame0: (() => { const grp = diagram.getGroup(pendingBeside.id); return grp ? frameOfGroup(grp) : cellToRect({ x: pendingBeside.cell.x, y: pendingBeside.cell.y, w: pendingBeside.spans.w, h: pendingBeside.spans.h }, frame(), geom(), rows()); })(),
2352
- // no vacated cell: nothing has moved, so the BAND alone holds the mark
2353
- }
2354
- : ((_e = (_d = g.leg) === null || _d === void 0 ? void 0 : _d.adopted.besideState()) !== null && _e !== void 0 ? _e : null), // a beside another board holds for the ghost, through its leg
2231
+ : ((_e = (_d = g.leg) === null || _d === void 0 ? void 0 : _d.adopted.besideState()) !== null && _e !== void 0 ? _e : null), // a beside another board holds for the ghost, through its leg
2355
2232
  maxDepth: nesting,
2356
2233
  ghostDepth: g.subject === 'group' ? 1 + levelsInside(g.id) : 0, // a group's widgets sit one board deeper than wherever it lands
2357
2234
  restFrames: rests,
@@ -3348,13 +3225,6 @@ export function bindDashboardGrid(api, group, options = {}) {
3348
3225
  chip === null || chip === void 0 ? void 0 : chip.remove();
3349
3226
  return;
3350
3227
  }
3351
- if (commit && pendingBeside) {
3352
- const p = pendingBeside;
3353
- endPendingBeside();
3354
- applyBeside(g, { id: p.id, side: p.side }, p.cell.y);
3355
- }
3356
- else
3357
- endPendingBeside();
3358
3228
  if (commit && g.leg) {
3359
3229
  // Dropped into another board (a page, a section): its leg closes with
3360
3230
  // that board's displaced tiles; this board displaced nothing.
@@ -22,6 +22,6 @@
22
22
  */
23
23
  export declare const DASHBOARD_KIT_STYLE_ID = "grafloria-dashboard-kit-styles";
24
24
  /** The kit stylesheet as text — for a host that bundles its own CSS, and for the styling spec. */
25
- export declare const DASHBOARD_KIT_CSS = "\n/* ===== reflow glide (armed on the html layer for the whole gesture) ===== */\n.grafloria-html-layer.axdb-glide > .grafloria-node-host {\n transition: left .28s cubic-bezier(.2, 0, .2, 1), top .28s cubic-bezier(.2, 0, .2, 1),\n width .28s cubic-bezier(.2, 0, .2, 1), height .28s cubic-bezier(.2, 0, .2, 1);\n}\n\n/* Motion is a preference (WCAG 2.3.3): no glide for those who asked for none. */\n@media (prefers-reduced-motion: reduce) {\n .grafloria-html-layer.axdb-glide > .grafloria-node-host { transition: none; }\n}\n\n/* ===== keyboard focus: the roving tab stop shows where it is (WCAG 2.4.7) ===== */\n.grafloria-html-layer > .grafloria-node-host:focus-visible {\n outline: 2px solid var(--axdb-accent, #3b52d9);\n outline-offset: 2px;\n border-radius: var(--axdb-rs-radius, 8px);\n}\n\n/* ===== the held tile: transition-exempt ghost, above everything ===== */\n.grafloria-html-layer > .grafloria-node-host.axdb-ghost,\n.grafloria-html-layer.axdb-glide > .grafloria-node-host.axdb-ghost {\n transition: none;\n z-index: 30;\n opacity: .85;\n cursor: grabbing;\n filter: drop-shadow(0 10px 16px rgba(16, 24, 40, .3));\n}\n/* Outside the board: release will REMOVE \u2014 dim the ghost to say so. */\n.grafloria-html-layer > .grafloria-node-host.axdb-ghost.axdb-out { opacity: .35; filter: grayscale(.6); }\n\n/* ===== the kit's chrome glides WITH the tiles it belongs to (0.4.43) =====\n A pushed group's strip, slab and surface used to jump to the new slot while\n its tiles slid there: one group, two motions. */\n.grafloria-html-layer.axdb-glide > .axdb-tabs,\n.grafloria-html-layer.axdb-glide > .axdb-slab,\n.grafloria-html-layer.axdb-glide > .axdb-group-bg {\n transition: left .28s cubic-bezier(.2, 0, .2, 1), top .28s cubic-bezier(.2, 0, .2, 1),\n width .28s cubic-bezier(.2, 0, .2, 1), height .28s cubic-bezier(.2, 0, .2, 1);\n}\n@media (prefers-reduced-motion: reduce) {\n .grafloria-html-layer.axdb-glide > .axdb-tabs,\n .grafloria-html-layer.axdb-glide > .axdb-slab,\n .grafloria-html-layer.axdb-glide > .axdb-group-bg { transition: none; }\n}\n\n/* ===== CARRIED (0.4.43): a group dragged by its strip or band moves as ONE thing =====\n Its whole subtree \u2014 tiles, strips, slabs, surface \u2014 is transition-exempt like\n the ghost and floats with it; the surface casts the shadow. */\n.grafloria-html-layer > .axdb-carried,\n.grafloria-html-layer.axdb-glide > .axdb-carried { transition: none; cursor: grabbing; }\n.grafloria-html-layer > .grafloria-node-host.axdb-carried { z-index: 30; opacity: .92; }\n.grafloria-html-layer > .axdb-group-bg.axdb-carried { z-index: 29; filter: drop-shadow(0 10px 16px rgba(16, 24, 40, .3)); }\n.grafloria-html-layer > .axdb-slab.axdb-carried { z-index: 31; }\n.grafloria-html-layer > .axdb-tabs.axdb-carried { z-index: 32; }\n\n/* ===== the placeholder: dashed slab, truthful, never animated ===== */\n.grafloria-html-layer > .axdb-ph {\n position: absolute;\n border-radius: var(--axdb-rs-radius, 8px);\n background: rgba(30, 34, 45, .14);\n border: 2px dashed rgba(30, 34, 45, .28);\n box-sizing: border-box;\n pointer-events: none;\n z-index: 0;\n transition: none;\n}\n/* A slab move asked for a cell it cannot have (a locked section in the way): the wanted cell in the danger tone. */\n.grafloria-html-layer > .axdb-ph.axdb-ph--no { border-color: var(--axdb-danger, #b3123c); background: var(--axdb-danger-soft, rgba(179, 18, 60, .07)); }\n\n@media (prefers-color-scheme: dark) {\n .grafloria-html-layer > .axdb-ph { background: rgba(220, 225, 240, .12); border-color: rgba(220, 225, 240, .3); }\n}\n\n/* ===== corner resize handle (hover-revealed, radius-matched) ===== */\n:is(.grafloria-node-host, .axdb-slab) > .axdb-rs {\n position: absolute;\n right: 0;\n bottom: 0;\n width: 24px; /* WCAG 2.5.8: a 24-px minimum target (was 18) */\n height: 24px;\n cursor: nwse-resize;\n border-right: 3px solid rgba(120, 130, 148, .55);\n border-bottom: 3px solid rgba(120, 130, 148, .55);\n border-bottom-right-radius: var(--axdb-rs-radius, 8px);\n opacity: 0;\n transition: opacity .12s;\n z-index: 5;\n}\n.grafloria-node-host:hover > .axdb-rs { opacity: 1; }\n/* A finger never hovers: on touch devices the corner handle is always visible\n (review D8 \u2014 hover-only meant no resize at all on a tablet). */\n@media (hover: none) { .grafloria-node-host > .axdb-rs { opacity: .8; } }\n.grafloria-node-host > .axdb-rs:hover { border-color: #3b52d9; }\n/* RTL boards grow leftwards, so the grab corner mirrors with them. */\n:is(.grafloria-node-host, .axdb-slab) > .axdb-rs.axdb-rs--rtl {\n right: auto;\n left: 0;\n cursor: nesw-resize;\n border-right: none;\n border-left: 3px solid rgba(120, 130, 148, .55);\n border-bottom-right-radius: 0;\n border-bottom-left-radius: var(--axdb-rs-radius, 8px);\n}\n\n/* ===== palette drag-in chip (screen-space clone following the cursor) ===== */\n.axdb-drag-chip {\n position: fixed;\n z-index: 9999;\n pointer-events: none;\n opacity: .9;\n filter: drop-shadow(0 8px 14px rgba(16, 24, 40, .3));\n transition: opacity .12s, filter .12s;\n}\n/* A bounded board with no room refuses the entry: the chip dims to say so\n (the same signal a tile ghost gives outside the board). Without this rule\n the class was set and nothing showed \u2014 a refusal a user could not see. */\n.axdb-drag-chip.axdb-out { opacity: .35; filter: grayscale(.7) drop-shadow(0 8px 14px rgba(16, 24, 40, .2)); }\n/* A tab dragged off its strip: VS Code carries the TAB, not the editor, so\n what follows the pointer is a chip wearing the tab's own label. */\n.axdb-tab-chip {\n padding: 5px 12px;\n max-width: 220px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font: 500 12px/1.4 var(--axdb-font, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n color: var(--axdb-tabs-on-fg, #1f2430);\n background: var(--axdb-tabs-on-bg, #fff);\n border-radius: var(--axdb-tab-radius, 6px);\n box-shadow: 0 1px 2px rgba(16, 24, 40, .1), 0 0 0 1px rgba(31, 36, 48, .08);\n}\n@media (prefers-color-scheme: dark) {\n .axdb-tab-chip {\n color: var(--axdb-tabs-on-fg, #eceef4);\n background: var(--axdb-tabs-on-bg, #232836);\n box-shadow: 0 1px 2px rgba(0, 0, 0, .45), 0 0 0 1px rgba(236, 238, 244, .12);\n }\n}\n/* A tab held over ANOTHER group: it will join that group, not the board. The\n group's frame lights up, and its strip marks the slot the tab will take \u2014\n a bar before the tab it lands in front of, or after the last one. */\n.grafloria-html-layer > .axdb-join {\n position: absolute;\n z-index: 32;\n pointer-events: none;\n box-sizing: border-box;\n border: 2px dashed var(--axdb-accent, #3b52d9);\n background: var(--axdb-accent-soft, rgba(59, 82, 217, .08));\n border-radius: var(--axdb-rs-radius, 8px);\n}\n/* THE LANES: while a widget is held over a container, its four bands are\n shown, so \"beside it\" is a place you can see and aim at rather than a\n fixed depth you would have to guess. The band you are IN wears the dashed\n mark (.axdb-join) on top; these only say where the edges are. */\n/* ABOVE the dragged widget (which sits at 30): the whole point of a lane is to\n be seen at the moment the hand is in it, and the widget your hand is carrying\n covers the top one almost completely. Both are pointer-events: none, so\n nothing about the hit testing changes. */\n.grafloria-html-layer > .axdb-lanes { position: absolute; z-index: 31; pointer-events: none; }\n.grafloria-html-layer > .axdb-lanes > .axdb-lane {\n position: absolute;\n box-sizing: border-box;\n /* A SHADED MARGIN, never a line: the dashed outline already means \"the cell\n this will take\", and two dashed treatments at once read as one shape. */\n background: var(--axdb-lane-bg, rgba(59, 82, 217, .13));\n border-radius: 2px;\n}\n@media (prefers-color-scheme: dark) {\n .grafloria-html-layer > .axdb-lanes > .axdb-lane { background: var(--axdb-lane-bg, rgba(134, 155, 255, .17)); }\n}\n.axdb-tabs.axdb-tabs--drop { box-shadow: inset 0 -2px 0 var(--axdb-accent, #3b52d9); }\n.axdb-tab.axdb-tab--drop-before { position: relative; }\n.axdb-tab.axdb-tab--drop-before::before {\n content: \"\"; position: absolute; left: -3px; top: 4px; bottom: 4px; width: 3px;\n border-radius: 2px; background: var(--axdb-accent, #3b52d9);\n}\n.axdb-tabs.axdb-tabs--drop-end::after {\n content: \"\"; display: inline-block; width: 3px; height: 18px; margin: 0 4px;\n border-radius: 2px; background: var(--axdb-accent, #3b52d9); vertical-align: middle; align-self: center;\n}\n\n/* ===========================================================================\n BUILT-IN WIDGET CARDS \u2014 what widgets.ts paints when a page writes no\n renderWidget. Class-prefixed (never element selectors), so this can only\n ever style DOM the kit itself produced. Colours are CSS variables on the\n card, so a page re-skins every built-in widget by setting them once; the\n corner radius follows --axdb-rs-radius, which is also the placeholder's and\n the resize handle's, so the three can never drift apart.\n =========================================================================== */\n.axdb-widget {\n --axdb-ink: #1f2430;\n /* 5.95:1 on the card \u2014 WCAG 1.4.3 for the 9\u201311px captions this paints\n (the #7a8496 it replaced sat at 3.77:1, axe-core's first finding). */\n --axdb-muted: #5a6478;\n --axdb-grid: rgba(120, 130, 148, .22);\n --axdb-card: #fff;\n --axdb-line: #e5e8ef;\n --axdb-soft: rgba(120, 130, 148, .14);\n --axdb-up: #0f7a3d; /* 5.42:1 */\n --axdb-down: #be123c; /* 6.29:1 */\n /* The categorical palette, as tokens so the dark card can carry its own\n steps: every entry clears 3:1 against its card (WCAG 1.4.11). */\n --axdb-c1: #3b52d9; --axdb-c2: #0369a1; --axdb-c3: #0f766e;\n --axdb-c4: #b45309; --axdb-c5: #6d28d9; --axdb-c6: #475569;\n /* The header band: a panel wears its title in a band across its top, the\n way DevExpress, Grafana and Metabase panels do \u2014 tinted ground, hairline\n below, sentence case (0.4.36; the tracked uppercase micro-label before). */\n --axdb-head-bg: #f7f8fb;\n --axdb-head-fg: #2a3140;\n box-sizing: border-box;\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n background: var(--axdb-card);\n border: 1px solid var(--axdb-line);\n border-radius: var(--axdb-rs-radius, 8px);\n box-shadow: 0 1px 2px rgba(16, 24, 40, .04);\n font-family: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n color: var(--axdb-ink);\n}\n/* The host is the card's size container (named, so a widget's inner parts can\n ask the TILE's size as well as their own body's), so the card can shed its\n padding as the tile gets short instead of clipping its own content. */\n.grafloria-html-layer > .grafloria-node-host { container: axdb-tile / size; }\n/* Two class hops on the header: these blocks precede the header's own rule\n below and would lose the cascade at equal specificity \u2014 at 54 px the margin\n stayed 8 px, the body shrank to 14 px and the KPI figure hid. */\n@container axdb-tile (max-height: 90px) {\n .axdb-widget > .axdb-widget-h { padding: 5px 12px; font-size: 11.5px; }\n .axdb-widget > .axdb-widget-b { padding: 6px 12px; }\n}\n/* The row floor: the band gives way to an inline label beside the figure. */\n@container axdb-tile (max-height: 46px) {\n .axdb-widget > .axdb-widget-h { padding: 0 0 0 12px; background: none; border-bottom: 0; }\n .axdb-widget > .axdb-widget-b { padding: 3px 12px; }\n}\n@container axdb-tile (max-height: 26px) {\n .axdb-widget > .axdb-widget-h { display: none; }\n .axdb-widget > .axdb-widget-b { padding: 1px 12px; }\n}\n.axdb-widget-h {\n display: flex;\n align-items: center;\n gap: 8px;\n flex: none;\n min-width: 0;\n padding: 9px 14px;\n background: var(--axdb-head-bg);\n border-bottom: 1px solid var(--axdb-line);\n font: 600 12.5px/1.3 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n letter-spacing: -.005em;\n color: var(--axdb-head-fg);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n/* Drag-handle mode (DevExpress drags an item by its caption): the header is\n the grip and says so with a dot pattern and a grab cursor; the body keeps\n its own cursor because a press there starts nothing. */\n.axdb-drag-handle .axdb-widget > .axdb-widget-h { cursor: grab; }\n.axdb-drag-handle .axdb-widget > .axdb-widget-h::before {\n content: ''; flex: none; width: 8px; height: 12px; opacity: .55;\n background: radial-gradient(circle, currentColor 1.1px, transparent 1.5px) 0 0 / 4px 4px;\n}\n/* A PAINTED GRIP (dragHandle: { grip: true }): the only drag zone, a small\n dotted tab along the card's top edge. Inside sits in the header band and\n the header makes room for it; outside is a tab above the card, the\n DevExpress item bar. The host must not clip it. */\n.grafloria-node-host > .axdb-grip {\n position: absolute; z-index: 4; box-sizing: border-box; width: 24px; height: 12px;\n border: 1px solid var(--axdb-line, #e7eaf1); border-radius: 3px; background: var(--axdb-card, #fff);\n color: var(--axdb-muted, #5a6478); cursor: grab;\n /* Shown on the SELECTED widget only (the DevExpress designer shows an item's\n bar on the selected item): a press or keyboard focus selects, a void\n click clears. Hidden, it takes no pointer either. */\n opacity: 0; pointer-events: none; transition: opacity .12s ease;\n}\n.grafloria-node-host.axdb-selected > .axdb-grip, .grafloria-node-host:focus-within > .axdb-grip { opacity: 1; pointer-events: auto; }\n.grafloria-node-host > .axdb-grip::before {\n content: ''; position: absolute; left: 5px; top: 2px; width: 12px; height: 6px;\n background: radial-gradient(circle, currentColor 1px, transparent 1.4px) 0 0 / 4px 3px;\n}\n.grafloria-node-host > .axdb-grip:hover { border-color: var(--axdb-accent, #3b52d9); color: var(--axdb-accent, #3b52d9); }\n/* The selected card says so, quietly. --axdb-accent themes the grip's hover, the\n ring and the focus outline together; --axdb-accent-ring the ring alone. */\n.grafloria-node-host.axdb-selected > .axdb-widget { box-shadow: 0 0 0 1.5px var(--axdb-accent-ring, rgba(59, 82, 217, .55)), 0 1px 2px rgba(16, 24, 40, .05), 0 1px 3px rgba(16, 24, 40, .05); }\n/* While a resize edge is near, the host and everything in it (a chart canvas\n with its own cursor included) show the edge's cursor. */\n.grafloria-node-host[data-axdb-edge=\"ns-resize\"], .grafloria-node-host[data-axdb-edge=\"ns-resize\"] * { cursor: ns-resize !important; }\n.grafloria-node-host[data-axdb-edge=\"ew-resize\"], .grafloria-node-host[data-axdb-edge=\"ew-resize\"] * { cursor: ew-resize !important; }\n.grafloria-node-host[data-axdb-edge=\"nwse-resize\"], .grafloria-node-host[data-axdb-edge=\"nwse-resize\"] * { cursor: nwse-resize !important; }\n.grafloria-node-host[data-axdb-edge=\"nesw-resize\"], .grafloria-node-host[data-axdb-edge=\"nesw-resize\"] * { cursor: nesw-resize !important; }\n/* INSIDE: centred on the header's text line and flush with the card padding,\n per size tier (padding 13/15, then 8/14, 4/12, 2/12). */\n.grafloria-node-host > .axdb-grip--inside { top: 12px; }\n.grafloria-node-host > .axdb-grip--inside.axdb-grip--left { left: 15px; }\n.grafloria-node-host > .axdb-grip--inside.axdb-grip--right { right: 15px; }\n.grafloria-node-host > .axdb-grip--inside.axdb-grip--center { top: 3px; }\n@container axdb-tile (max-height: 90px) {\n .grafloria-node-host > .axdb-grip--inside { top: 9px; }\n .grafloria-node-host > .axdb-grip--inside.axdb-grip--left { left: 14px; }\n .grafloria-node-host > .axdb-grip--inside.axdb-grip--right { right: 14px; }\n .grafloria-node-host > .axdb-grip--inside.axdb-grip--center { top: 2px; }\n .grafloria-node-host.axdb-gp-inside.axdb-gp-left .axdb-widget > .axdb-widget-h { padding-left: 46px; }\n .grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 46px; }\n .grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 15px; }\n}\n@container axdb-tile (max-height: 46px) {\n .grafloria-node-host > .axdb-grip--inside { top: 5px; }\n .grafloria-node-host > .axdb-grip--inside.axdb-grip--left { left: 12px; }\n .grafloria-node-host > .axdb-grip--inside.axdb-grip--right { right: 12px; }\n .grafloria-node-host > .axdb-grip--inside.axdb-grip--center { top: 1px; }\n .grafloria-node-host.axdb-gp-inside.axdb-gp-left .axdb-widget > .axdb-widget-h { padding-left: 44px; }\n .grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 44px; }\n .grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 13px; }\n}\n@container axdb-tile (max-height: 26px) {\n .grafloria-node-host > .axdb-grip--inside { top: 2px; }\n}\n/* OUTSIDE: a tab on the card's top edge, its corners in line with the card's. */\n/* The OUTSIDE tab lives in the gap between tiles: never taller than the gap\n less a pixel (an 11-px tab in a 10-px gap sat on the tile above), never\n shorter than 6 px so the dots still read. */\n.grafloria-node-host > .axdb-grip--outside { top: calc(-1 * clamp(6px, var(--axdb-gap, 11px) - 1px, 11px)); height: clamp(6px, calc(var(--axdb-gap, 11px) - 1px), 11px); border-bottom-left-radius: 0; border-bottom-right-radius: 0; border-bottom: 0; box-shadow: 0 -1px 2px rgba(16, 24, 40, .08); }\n.grafloria-node-host > .axdb-grip--outside.axdb-grip--left { left: 10px; }\n.grafloria-node-host > .axdb-grip--outside.axdb-grip--right { right: 10px; }\n.grafloria-node-host > .axdb-grip--center { left: 50%; transform: translateX(-50%); }\n/* The header makes room for an inside grip on its side \u2014 inset + the 24-px\n grip + an 8-px gap, per tier (32 px under the band's 14-px inset hid the\n first letter of the title) \u2014 and a centred one sits above the text. */\n.grafloria-node-host.axdb-gp-inside.axdb-gp-left .axdb-widget > .axdb-widget-h { padding-left: 47px; }\n.grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 47px; }\n.grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 17px; }\n.axdb-widget-b { flex: 1; min-height: 0; position: relative; padding: 12px 14px; }\n/* A drag across a STATIC board (nothing prevents the press's default there)\n used to select every label on it; kit cards are not prose. Tables stay\n copyable \u2014 a figure in a grid is the one thing a viewer selects. */\n.axdb-widget { user-select: none; -webkit-user-select: none; }\n.axdb-widget .axdb-table { user-select: text; -webkit-user-select: text; }\n.axdb-widget-b > svg { display: block; width: 100%; height: 100%; }\n.axdb-widget-b.axdb-scroll { overflow: auto; }\n/* A chart WITH a legend under it: the plot yields height, the legend keeps its\n own (without this the 100%-tall svg pushes the legend out of the card). */\n.axdb-widget-b.axdb-has-lg { display: flex; flex-direction: column; }\n.axdb-widget-b.axdb-has-lg > svg { flex: 1; height: auto; min-height: 0; }\n/* The data behind a chart, for readers that cannot see the chart: present in\n the accessibility tree, absent from the picture (WCAG 1.1.1). */\n.axdb-sr {\n position: absolute; top: 0; left: 0; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;\n clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;\n}\n.axdb-widget-empty {\n display: flex; align-items: center; justify-content: center; height: 100%;\n font: 500 11.5px/1.3 system-ui, sans-serif; color: var(--axdb-muted);\n}\n\n/* kpi: value + delta stack, the spark yields its height before they do */\n/* A SIZE CONTAINER, so the card's type scales with the tile instead of\n clipping. Fit-mode shrinks tiles when rows are added (dropping a chart above\n the KPI row took these from 122px to 85px) and fixed 30px/12px lines were\n sliced mid-glyph by the card's overflow \u2014 the audited \"trimmed\" widgets.\n cqh = 1% of the body's own height, clamped so full-size tiles look exactly\n as before and short tiles compress instead of cutting. */\n.axdb-widget-b.axdb-kpi { display: flex; flex-direction: column; container: axdb-kpi / size; }\n/* STEP DOWN, NEVER CLIP. As the body gets shorter the sparkline goes first, then the\n delta, then the value \u2014 the header alone at the row floor. Before this a 99-px row\n drew the sparkline as a 13-px sliver and a 28-px row cut the value mid-glyph (the\n fluid board, after a drag pushed the KPI row down). Thresholds are body heights. */\n/* Two class hops, so these outrank \".axdb-widget-b > svg { display: block }\" \u2014\n a bare .axdb-kpi-s lost that cascade and the sparkline stayed. */\n@container (max-height: 78px) { .axdb-kpi > .axdb-kpi-s { display: none; } }\n@container (max-height: 40px) { .axdb-kpi > .axdb-kpi-d { display: none; } }\n@container (max-height: 16px) { .axdb-kpi > .axdb-kpi-v { display: none; } }\n.axdb-kpi-v { font: 700 clamp(15px, 44cqh, 30px)/1.05 system-ui, sans-serif; letter-spacing: -.02em; color: var(--axdb-ink); white-space: nowrap; }\n.axdb-kpi-d { margin-top: clamp(1px, 5cqh, 6px); font: 600 clamp(9px, 19cqh, 12px)/1.2 system-ui, sans-serif; white-space: nowrap; }\n.axdb-kpi-d span { color: var(--axdb-muted); font-weight: 500; }\n.axdb-kpi-d.up { color: var(--axdb-up); }\n.axdb-kpi-d.down { color: var(--axdb-down); }\n/* Grows into a tall tile (a 3-row KPI is not a number over a void) and never\n takes more than two fifths of the body. */\n.axdb-kpi-s { margin-top: auto; height: auto; min-height: 0; flex: 1 1 34px; max-height: 40%; }\n/* SHORT AND WIDE \u2014 a one-row KPI on a fluid board. Stacked, a short tile dropped\n its sparkline and left a small figure in the corner of an empty card. Here the\n figure, its delta and the sparkline sit in a row and the spark fills the width.\n The outer query asks the TILE (host), the inner one the body, so a strip too\n thin for a readable spark (under 24 px) keeps the figure alone. 340 px is what\n a figure, its delta, the gaps and a spark worth reading need \u2014 the builder's\n 236-px tiles got a 40-px wedge of spark beside the delta. */\n@container axdb-tile (max-height: 125px) and (min-width: 340px) {\n .axdb-widget--kpi > .axdb-kpi { flex-direction: row; align-items: center; gap: 14px; }\n .axdb-widget--kpi .axdb-kpi-d { margin-top: 0; }\n @container axdb-kpi (min-height: 24px) {\n .axdb-kpi > .axdb-kpi-s { display: block; flex: 1 1 40%; min-width: 80px; height: 100%; max-height: 100%; margin-top: 0; align-self: stretch; }\n }\n}\n/* THE STRIP (row floor): header and figure on one line, the spark beside them\n when the tile is wide enough. */\n@container axdb-tile (max-height: 46px) {\n .axdb-widget--kpi { flex-direction: row; align-items: center; gap: 12px; }\n .axdb-widget--kpi > .axdb-widget-h { flex: none; padding: 0 0 0 12px; background: none; border-bottom: 0; }\n /* The body is a SIZE container: in a centred row it would collapse to zero\n and every height query would fire. It takes the card's full inner height. */\n .axdb-widget--kpi > .axdb-kpi { flex: 1; align-self: stretch; }\n}\n\n/* SPLIT LAYOUT chrome: a divider sits in the gap between two siblings (a\n 24-px hit zone around a 10-px gap) and shows its line on hover or while\n dragged; the insertion line marks the edge a dragged widget will land on. */\n.grafloria-html-layer > .axdb-div { position: absolute; z-index: 2; pointer-events: auto; background: transparent; }\n.grafloria-html-layer > .axdb-div--row { cursor: col-resize; }\n.grafloria-html-layer > .axdb-div--column { cursor: row-resize; }\n.grafloria-html-layer > .axdb-div::after {\n content: \"\"; position: absolute; border-radius: 2px; background: #3b52d9; opacity: 0; transition: opacity .12s ease;\n}\n.grafloria-html-layer > .axdb-div--row::after { left: 50%; top: 8px; bottom: 8px; width: 4px; margin-left: -2px; }\n.grafloria-html-layer > .axdb-div--column::after { top: 50%; left: 8px; right: 8px; height: 4px; margin-top: -2px; }\n.grafloria-html-layer > .axdb-div:hover::after, .grafloria-html-layer > .axdb-div.axdb-active::after { opacity: .9; }\n.grafloria-html-layer > .axdb-ins {\n position: absolute; z-index: 3; pointer-events: none; border-radius: 2px;\n background: #3b52d9; box-shadow: 0 0 0 3px rgba(59, 82, 217, .22);\n}\n@media (prefers-reduced-motion: reduce) { .grafloria-html-layer > .axdb-div::after { transition: none; } }\n\n/* donut: ring beside its legend. The ring takes the body's height (a tall\n tile gets a bigger ring, not dead card), square, capped so its centre figure\n stays a figure and not a headline. */\n.axdb-widget-b.axdb-donut { display: flex; align-items: center; gap: 10px; }\n/* A legend taller than a SHORT body (a 2-row donut on a squeezed board) is\n clipped at its own foot, not centred over the title above it. */\n.axdb-widget-b.axdb-donut > .axdb-lg--col { max-height: 100%; min-height: 0; overflow: hidden; }\n.axdb-widget-b.axdb-donut > svg {\n flex: 0 0 auto; width: auto; height: 100%; max-height: 260px; max-width: 60%; aspect-ratio: 1 / 1;\n}\n\n/* READABILITY TIERS (widgets.ts chartTier): the text is always in the DOM;\n what a short body cannot afford is hidden, not dropped. */\n.axdb-tier-1 .axdb-yt--q, .axdb-tier-1 .axdb-yl--q,\n.axdb-tier-2 .axdb-yt--q, .axdb-tier-2 .axdb-yl--q,\n.axdb-tier-2 .axdb-yt--h, .axdb-tier-2 .axdb-yl--h,\n.axdb-tier-2 .axdb-xt, .axdb-tier-2 .axdb-vt,\n.axdb-lg--off { display: none; }\n\n/* SECTION CHROME: a pointer-transparent overlay on every member group. It\n wears the selection ring and, while selected, the corner handle. */\n.grafloria-html-layer > .axdb-slab { position: absolute; pointer-events: none; border-radius: var(--axdb-rs-radius, 8px); z-index: 4; }\n/* Selected, the overlay rises above the tiles so ITS corner handle wins a\n corner it shares with a child's; unselected, its handle takes no presses. */\n.grafloria-html-layer > .axdb-slab.axdb-slab--selected { z-index: 6; box-shadow: 0 0 0 1.5px var(--axdb-accent-ring, rgba(59, 82, 217, .55)); }\n.grafloria-html-layer > .axdb-slab > .axdb-rs { pointer-events: none; opacity: 0; }\n.grafloria-html-layer > .axdb-slab.axdb-slab--selected > .axdb-rs { pointer-events: auto; opacity: 1; }\n.grafloria-html-layer > .axdb-slab.axdb-slab--static > .axdb-rs { display: none; }\n\n/* GROUP FRAME (0.4.43): a TAB CONTAINER wears a frame by default \u2014 a bordered\n slab, and a tinted surface UNDER its pages (the strip's own track colour, so\n strip and body read as one panel). The layer is isolated so the surface can\n sit below every tile at z -1 and still paint above the canvas. */\n.grafloria-html-layer { isolation: isolate; }\n.grafloria-html-layer > .axdb-group-bg {\n position: absolute; pointer-events: none; z-index: -1; box-sizing: border-box;\n border-radius: var(--axdb-rs-radius, 8px);\n background: var(--axdb-group-bg, #f1f3f8);\n}\n.grafloria-html-layer > .axdb-slab.axdb-slab--tabs {\n box-sizing: border-box;\n border: 1px solid var(--axdb-group-border, var(--axdb-line, #e5e8ef));\n}\n@media (prefers-color-scheme: dark) {\n .grafloria-html-layer > .axdb-group-bg { background: var(--axdb-group-bg, #1b2029); }\n .grafloria-html-layer > .axdb-slab.axdb-slab--tabs { border-color: var(--axdb-group-border, rgba(236, 238, 244, .12)); }\n}\n\n/* SECTION CAPTION (0.4.22): the band on the slab. Geometry is inline (the\n reserve and the pixels come from one function); everything visual is a\n variable the options write and a theme may override. The band takes the\n pointer \u2014 the slab does not \u2014 so a press on it is the section's. */\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h {\n position: absolute; box-sizing: border-box; pointer-events: auto; z-index: 1;\n display: flex; align-items: center; gap: 6px; min-width: 0;\n padding: var(--axdb-caption-pad, 0 12px);\n background: var(--axdb-caption-bg, rgba(31, 36, 48, .045));\n /* A hairline by default: two bands meeting (a captioned section inside a\n captioned section) read as two headers, not one grey block. */\n border-bottom: var(--axdb-caption-border, 1px solid rgba(31, 36, 48, .1));\n color: var(--axdb-caption-fg, #1f2430);\n font: var(--axdb-caption-font-weight, 600) var(--axdb-caption-font-size, 13px)/1.2 var(--axdb-caption-font-family, system-ui, -apple-system, \"Segoe UI\", sans-serif);\n text-transform: var(--axdb-caption-transform, none);\n letter-spacing: -.005em;\n border-radius: var(--axdb-rs-radius, 8px) var(--axdb-rs-radius, 8px) 0 0;\n cursor: default; user-select: none; -webkit-user-select: none; overflow: hidden;\n transition: opacity .12s;\n}\n/* The modifiers carry the band's own selector so they outrank its defaults. */\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--center { justify-content: center; }\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--end { justify-content: flex-end; }\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--vtop { align-items: flex-start; }\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--vbottom { align-items: flex-end; }\n/* 'tab': the same band sized to its text, a chip at the leading corner. It\n reserves its height like 'inside' \u2014 see captionReserve. */\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--tab {\n max-width: 100%; border-radius: var(--axdb-rs-radius, 8px);\n border-bottom: var(--axdb-caption-border-tab, none);\n box-shadow: inset 0 0 0 1px rgba(31, 36, 48, .08);\n}\n/* 'hover': an overlay. It reserves nothing, so while hidden it must not take\n the pointer (an invisible band swallowed clicks on the content beneath),\n and while shown it paints OPAQUE \u2014 a 5% tint over a chart is a smear. */\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--hover {\n opacity: 0; pointer-events: none;\n background: var(--axdb-caption-bg, #fff);\n box-shadow: 0 1px 4px rgba(31, 36, 48, .16);\n}\n/* .axdb-slab--hot is set by the binder from the live pointer (the overlay\n takes no pointer of its own, so CSS :hover can never fire on it). */\n.grafloria-html-layer > .axdb-slab.axdb-slab--hot > .axdb-slab-h.axdb-slab-h--hover,\n.grafloria-html-layer > .axdb-slab.axdb-slab--selected > .axdb-slab-h.axdb-slab-h--hover { pointer-events: auto; opacity: 1; }\n/* the actions of a hovered or selected section */\n.grafloria-html-layer > .axdb-slab.axdb-slab--hot > .axdb-slab-h > .axdb-slab-h-actions { opacity: 1; }\n/* the tight tier: a section under 90 px */\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--tight { font-size: min(var(--axdb-caption-font-size, 12px), 12px); gap: 4px; }\n.axdb-slab-h--tight .axdb-slab-h-sub, .axdb-slab-h--tight .axdb-slab-h-actions { display: none; }\n.axdb-slab-h-icon { flex: none; }\n.axdb-slab-h-body { display: flex; flex-direction: column; justify-content: center; min-width: 0; flex: 0 1 auto; }\n.axdb-slab-h-text, .axdb-slab-h-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.axdb-slab-h-sub { font-size: .88em; font-weight: 500; opacity: .7; }\n.axdb-slab-h-info { flex: none; opacity: .6; font-size: .9em; cursor: help; }\n.axdb-slab-h-actions { flex: none; display: flex; gap: 2px; margin-inline-start: auto; opacity: 0; transition: opacity .12s; }\n.axdb-slab-h--center .axdb-slab-h-actions, .axdb-slab-h--end .axdb-slab-h-actions { margin-inline-start: 0; }\n.axdb-slab-h:hover > .axdb-slab-h-actions, .axdb-slab--selected > .axdb-slab-h > .axdb-slab-h-actions, .axdb-slab-h:focus-within > .axdb-slab-h-actions { opacity: 1; }\n.axdb-slab-h-action {\n all: unset; box-sizing: border-box; width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center;\n border-radius: 6px; cursor: pointer; font-size: 14px; line-height: 1; color: inherit; transition: background-color .12s;\n}\n.axdb-slab-h-action:hover { background: rgba(31, 36, 48, .08); }\n.axdb-slab-h-action:focus-visible { outline: 2px solid var(--axdb-accent-ring, rgba(59, 82, 217, .55)); outline-offset: -2px; }\n.axdb-slab-h-action[disabled] { opacity: .4; cursor: default; }\n.axdb-slab-h-action[disabled]:hover { background: none; }\n.grafloria-html-layer > .axdb-slab.axdb-slab--static > .axdb-slab-h { cursor: default; }\n@media (prefers-color-scheme: dark) {\n /* The band sits on the PAGE, whose ground the kit does not own (a host that\n stays light while the kit goes dark is common): a solid ink, not a tint,\n so the light caption always reads. */\n .grafloria-html-layer > .axdb-slab > .axdb-slab-h {\n background: var(--axdb-caption-bg, #1b2029);\n color: var(--axdb-caption-fg, #eceef4);\n border-bottom: var(--axdb-caption-border, 1px solid rgba(236, 238, 244, .12));\n }\n .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--tab { box-shadow: inset 0 0 0 1px rgba(236, 238, 244, .12); }\n .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--hover { background: var(--axdb-caption-bg, #1a1d25); box-shadow: 0 1px 4px rgba(0, 0, 0, .5); }\n .axdb-slab-h-action:hover { background: rgba(236, 238, 244, .1); }\n}\n\n/* TAB CONTAINER (0.4.27): a strip of pages across the container's top. The\n strip takes the pointer (the tabs are real buttons); the pages below it are\n ordinary boards. */\n.grafloria-html-layer > .axdb-tabs {\n position: absolute; box-sizing: border-box; pointer-events: auto; z-index: 5;\n display: flex; align-items: center; gap: 2px; padding: 3px 6px; overflow-x: auto; overflow-y: hidden;\n background: var(--axdb-tabs-bg, #f1f3f8);\n border-bottom: 1px solid var(--axdb-tabs-line, #e5e8ef);\n border-radius: var(--axdb-rs-radius, 8px) var(--axdb-rs-radius, 8px) 0 0;\n scrollbar-width: thin;\n}\n.axdb-tabs--center { justify-content: center; }\n.axdb-tabs--end { justify-content: flex-end; }\n.axdb-tabs--stretch > .axdb-tab { flex: 1 1 0; }\n/* A tab is a PILL on the track (a segmented control): quiet until hovered,\n the active one lifted in the card's own ground. */\n.axdb-tab {\n all: unset; box-sizing: border-box; flex: 0 0 auto; max-width: 200px;\n padding: 0 12px; height: 100%; display: inline-flex; align-items: center;\n font: 500 12.5px/1 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n letter-spacing: -.005em;\n color: var(--axdb-tabs-fg, #5a6478); cursor: pointer;\n border-radius: var(--axdb-tab-radius, 6px);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n transition: background-color .12s, color .12s, box-shadow .12s;\n}\n.axdb-tab:hover { background: rgba(31, 36, 48, .06); color: var(--axdb-tabs-on-fg, #1f2430); }\n.axdb-tab:focus-visible { outline: 2px solid var(--axdb-accent-ring, rgba(59, 82, 217, .55)); outline-offset: -2px; }\n.axdb-tab.axdb-tab--on {\n background: var(--axdb-tabs-on-bg, #fff);\n color: var(--axdb-tabs-on-fg, #1f2430);\n font-weight: 600;\n box-shadow: 0 1px 2px rgba(16, 24, 40, .1), 0 0 0 1px rgba(31, 36, 48, .06);\n}\n@media (prefers-reduced-motion: reduce) { .axdb-tab { transition: none; } }\n@media (prefers-color-scheme: dark) {\n /* Solid for the same reason as the caption band: the track sits on the page. */\n .grafloria-html-layer > .axdb-tabs { background: var(--axdb-tabs-bg, #1b2029); border-bottom-color: var(--axdb-tabs-line, rgba(236, 238, 244, .12)); }\n .axdb-tab { color: var(--axdb-tabs-fg, #98a1b4); }\n .axdb-tab:hover { background: rgba(236, 238, 244, .08); color: var(--axdb-tabs-on-fg, #eceef4); }\n .axdb-tab.axdb-tab--on { background: var(--axdb-tabs-on-bg, #232836); color: var(--axdb-tabs-on-fg, #eceef4); box-shadow: 0 1px 2px rgba(0, 0, 0, .45), 0 0 0 1px rgba(236, 238, 244, .1); }\n}\n\n/* legend chips, shared by line and donut */\n.axdb-lg { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 9px; }\n.axdb-lg--col { flex-direction: column; flex-wrap: nowrap; gap: 6px; margin-top: 0; }\n.axdb-lg i {\n display: inline-flex; align-items: center; gap: 5px; font-style: normal;\n font: 500 11px/1.3 system-ui, sans-serif; color: var(--axdb-muted);\n}\n.axdb-lg i b { width: 9px; height: 9px; border-radius: 3px; flex: none; }\n\n/* table */\n.axdb-table { width: 100%; border-collapse: collapse; font-size: 12px; color: var(--axdb-ink); }\n.axdb-table th {\n text-align: left; padding: 4px 8px 7px; border-bottom: 1px solid var(--axdb-line);\n font: 600 10px/1.3 system-ui, sans-serif; letter-spacing: .4px; text-transform: uppercase;\n color: var(--axdb-muted);\n}\n.axdb-table td { padding: 6px 8px; border-bottom: 1px solid var(--axdb-line); white-space: nowrap; }\n.axdb-table td.num { text-align: right; font-variant-numeric: tabular-nums; }\n.axdb-table tr:last-child td { border-bottom: none; }\n\n@media (prefers-color-scheme: dark) {\n .axdb-widget {\n --axdb-ink: #eceef4;\n --axdb-muted: #98a1b4;\n --axdb-grid: rgba(150, 160, 182, .2);\n --axdb-card: #161a22;\n --axdb-line: #2a3040;\n --axdb-head-bg: rgba(255, 255, 255, .035);\n --axdb-head-fg: #e6e9f0;\n --axdb-soft: rgba(150, 160, 182, .16);\n --axdb-up: #4ade80;\n --axdb-down: #fb7185;\n --axdb-c1: #8b9cff; --axdb-c2: #38bdf8; --axdb-c3: #2dd4bf;\n --axdb-c4: #fbbf24; --axdb-c5: #a78bfa; --axdb-c6: #94a3b8;\n }\n}\n";
25
+ export declare const DASHBOARD_KIT_CSS = "\n/* ===== reflow glide (armed on the html layer for the whole gesture) ===== */\n.grafloria-html-layer.axdb-glide > .grafloria-node-host {\n transition: left .28s cubic-bezier(.2, 0, .2, 1), top .28s cubic-bezier(.2, 0, .2, 1),\n width .28s cubic-bezier(.2, 0, .2, 1), height .28s cubic-bezier(.2, 0, .2, 1);\n}\n\n/* Motion is a preference (WCAG 2.3.3): no glide for those who asked for none. */\n@media (prefers-reduced-motion: reduce) {\n .grafloria-html-layer.axdb-glide > .grafloria-node-host { transition: none; }\n}\n\n/* ===== keyboard focus: the roving tab stop shows where it is (WCAG 2.4.7) ===== */\n.grafloria-html-layer > .grafloria-node-host:focus-visible {\n outline: 2px solid var(--axdb-accent, #3b52d9);\n outline-offset: 2px;\n border-radius: var(--axdb-rs-radius, 8px);\n}\n\n/* ===== the held tile: transition-exempt ghost, above everything ===== */\n.grafloria-html-layer > .grafloria-node-host.axdb-ghost,\n.grafloria-html-layer.axdb-glide > .grafloria-node-host.axdb-ghost {\n transition: none;\n z-index: 30;\n opacity: .85;\n cursor: grabbing;\n filter: drop-shadow(0 10px 16px rgba(16, 24, 40, .3));\n}\n/* Outside the board: release will REMOVE \u2014 dim the ghost to say so. */\n.grafloria-html-layer > .grafloria-node-host.axdb-ghost.axdb-out { opacity: .35; filter: grayscale(.6); }\n\n/* ===== the kit's chrome glides WITH the tiles it belongs to (0.4.43) =====\n A pushed group's strip, slab and surface used to jump to the new slot while\n its tiles slid there: one group, two motions. */\n.grafloria-html-layer.axdb-glide > .axdb-tabs,\n.grafloria-html-layer.axdb-glide > .axdb-slab,\n.grafloria-html-layer.axdb-glide > .axdb-group-bg {\n transition: left .28s cubic-bezier(.2, 0, .2, 1), top .28s cubic-bezier(.2, 0, .2, 1),\n width .28s cubic-bezier(.2, 0, .2, 1), height .28s cubic-bezier(.2, 0, .2, 1);\n}\n@media (prefers-reduced-motion: reduce) {\n .grafloria-html-layer.axdb-glide > .axdb-tabs,\n .grafloria-html-layer.axdb-glide > .axdb-slab,\n .grafloria-html-layer.axdb-glide > .axdb-group-bg { transition: none; }\n}\n\n/* ===== CARRIED (0.4.43): a group dragged by its strip or band moves as ONE thing =====\n Its whole subtree \u2014 tiles, strips, slabs, surface \u2014 is transition-exempt like\n the ghost and floats with it; the surface casts the shadow. */\n.grafloria-html-layer > .axdb-carried,\n.grafloria-html-layer.axdb-glide > .axdb-carried { transition: none; cursor: grabbing; }\n.grafloria-html-layer > .grafloria-node-host.axdb-carried { z-index: 30; opacity: .92; }\n.grafloria-html-layer > .axdb-group-bg.axdb-carried { z-index: 29; filter: drop-shadow(0 10px 16px rgba(16, 24, 40, .3)); }\n.grafloria-html-layer > .axdb-slab.axdb-carried { z-index: 31; }\n.grafloria-html-layer > .axdb-tabs.axdb-carried { z-index: 32; }\n\n/* ===== the placeholder: dashed slab, truthful, never animated ===== */\n.grafloria-html-layer > .axdb-ph {\n position: absolute;\n border-radius: var(--axdb-rs-radius, 8px);\n background: rgba(30, 34, 45, .14);\n border: 2px dashed rgba(30, 34, 45, .28);\n box-sizing: border-box;\n pointer-events: none;\n z-index: 0;\n transition: none;\n}\n/* A slab move asked for a cell it cannot have (a locked section in the way): the wanted cell in the danger tone. */\n.grafloria-html-layer > .axdb-ph.axdb-ph--no { border-color: var(--axdb-danger, #b3123c); background: var(--axdb-danger-soft, rgba(179, 18, 60, .07)); }\n\n@media (prefers-color-scheme: dark) {\n .grafloria-html-layer > .axdb-ph { background: rgba(220, 225, 240, .12); border-color: rgba(220, 225, 240, .3); }\n}\n\n/* ===== corner resize handle (hover-revealed, radius-matched) ===== */\n:is(.grafloria-node-host, .axdb-slab) > .axdb-rs {\n position: absolute;\n right: 0;\n bottom: 0;\n width: 24px; /* WCAG 2.5.8: a 24-px minimum target (was 18) */\n height: 24px;\n cursor: nwse-resize;\n border-right: 3px solid rgba(120, 130, 148, .55);\n border-bottom: 3px solid rgba(120, 130, 148, .55);\n border-bottom-right-radius: var(--axdb-rs-radius, 8px);\n opacity: 0;\n transition: opacity .12s;\n z-index: 5;\n}\n.grafloria-node-host:hover > .axdb-rs { opacity: 1; }\n/* A finger never hovers: on touch devices the corner handle is always visible\n (review D8 \u2014 hover-only meant no resize at all on a tablet). */\n@media (hover: none) { .grafloria-node-host > .axdb-rs { opacity: .8; } }\n.grafloria-node-host > .axdb-rs:hover { border-color: #3b52d9; }\n/* RTL boards grow leftwards, so the grab corner mirrors with them. */\n:is(.grafloria-node-host, .axdb-slab) > .axdb-rs.axdb-rs--rtl {\n right: auto;\n left: 0;\n cursor: nesw-resize;\n border-right: none;\n border-left: 3px solid rgba(120, 130, 148, .55);\n border-bottom-right-radius: 0;\n border-bottom-left-radius: var(--axdb-rs-radius, 8px);\n}\n\n/* ===== palette drag-in chip (screen-space clone following the cursor) ===== */\n.axdb-drag-chip {\n position: fixed;\n z-index: 9999;\n pointer-events: none;\n opacity: .9;\n filter: drop-shadow(0 8px 14px rgba(16, 24, 40, .3));\n transition: opacity .12s, filter .12s;\n}\n/* A bounded board with no room refuses the entry: the chip dims to say so\n (the same signal a tile ghost gives outside the board). Without this rule\n the class was set and nothing showed \u2014 a refusal a user could not see. */\n.axdb-drag-chip.axdb-out { opacity: .35; filter: grayscale(.7) drop-shadow(0 8px 14px rgba(16, 24, 40, .2)); }\n/* A tab dragged off its strip: VS Code carries the TAB, not the editor, so\n what follows the pointer is a chip wearing the tab's own label. */\n.axdb-tab-chip {\n padding: 5px 12px;\n max-width: 220px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font: 500 12px/1.4 var(--axdb-font, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n color: var(--axdb-tabs-on-fg, #1f2430);\n background: var(--axdb-tabs-on-bg, #fff);\n border-radius: var(--axdb-tab-radius, 6px);\n box-shadow: 0 1px 2px rgba(16, 24, 40, .1), 0 0 0 1px rgba(31, 36, 48, .08);\n}\n@media (prefers-color-scheme: dark) {\n .axdb-tab-chip {\n color: var(--axdb-tabs-on-fg, #eceef4);\n background: var(--axdb-tabs-on-bg, #232836);\n box-shadow: 0 1px 2px rgba(0, 0, 0, .45), 0 0 0 1px rgba(236, 238, 244, .12);\n }\n}\n/* A tab held over ANOTHER group: it will join that group, not the board. The\n group's frame lights up, and its strip marks the slot the tab will take \u2014\n a bar before the tab it lands in front of, or after the last one. */\n.grafloria-html-layer > .axdb-join {\n position: absolute;\n z-index: 3;\n pointer-events: none;\n box-sizing: border-box;\n border: 2px dashed var(--axdb-accent, #3b52d9);\n background: var(--axdb-accent-soft, rgba(59, 82, 217, .08));\n border-radius: var(--axdb-rs-radius, 8px);\n}\n.axdb-tabs.axdb-tabs--drop { box-shadow: inset 0 -2px 0 var(--axdb-accent, #3b52d9); }\n.axdb-tab.axdb-tab--drop-before { position: relative; }\n.axdb-tab.axdb-tab--drop-before::before {\n content: \"\"; position: absolute; left: -3px; top: 4px; bottom: 4px; width: 3px;\n border-radius: 2px; background: var(--axdb-accent, #3b52d9);\n}\n.axdb-tabs.axdb-tabs--drop-end::after {\n content: \"\"; display: inline-block; width: 3px; height: 18px; margin: 0 4px;\n border-radius: 2px; background: var(--axdb-accent, #3b52d9); vertical-align: middle; align-self: center;\n}\n\n/* ===========================================================================\n BUILT-IN WIDGET CARDS \u2014 what widgets.ts paints when a page writes no\n renderWidget. Class-prefixed (never element selectors), so this can only\n ever style DOM the kit itself produced. Colours are CSS variables on the\n card, so a page re-skins every built-in widget by setting them once; the\n corner radius follows --axdb-rs-radius, which is also the placeholder's and\n the resize handle's, so the three can never drift apart.\n =========================================================================== */\n.axdb-widget {\n --axdb-ink: #1f2430;\n /* 5.95:1 on the card \u2014 WCAG 1.4.3 for the 9\u201311px captions this paints\n (the #7a8496 it replaced sat at 3.77:1, axe-core's first finding). */\n --axdb-muted: #5a6478;\n --axdb-grid: rgba(120, 130, 148, .22);\n --axdb-card: #fff;\n --axdb-line: #e5e8ef;\n --axdb-soft: rgba(120, 130, 148, .14);\n --axdb-up: #0f7a3d; /* 5.42:1 */\n --axdb-down: #be123c; /* 6.29:1 */\n /* The categorical palette, as tokens so the dark card can carry its own\n steps: every entry clears 3:1 against its card (WCAG 1.4.11). */\n --axdb-c1: #3b52d9; --axdb-c2: #0369a1; --axdb-c3: #0f766e;\n --axdb-c4: #b45309; --axdb-c5: #6d28d9; --axdb-c6: #475569;\n /* The header band: a panel wears its title in a band across its top, the\n way DevExpress, Grafana and Metabase panels do \u2014 tinted ground, hairline\n below, sentence case (0.4.36; the tracked uppercase micro-label before). */\n --axdb-head-bg: #f7f8fb;\n --axdb-head-fg: #2a3140;\n box-sizing: border-box;\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n background: var(--axdb-card);\n border: 1px solid var(--axdb-line);\n border-radius: var(--axdb-rs-radius, 8px);\n box-shadow: 0 1px 2px rgba(16, 24, 40, .04);\n font-family: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n color: var(--axdb-ink);\n}\n/* The host is the card's size container (named, so a widget's inner parts can\n ask the TILE's size as well as their own body's), so the card can shed its\n padding as the tile gets short instead of clipping its own content. */\n.grafloria-html-layer > .grafloria-node-host { container: axdb-tile / size; }\n/* Two class hops on the header: these blocks precede the header's own rule\n below and would lose the cascade at equal specificity \u2014 at 54 px the margin\n stayed 8 px, the body shrank to 14 px and the KPI figure hid. */\n@container axdb-tile (max-height: 90px) {\n .axdb-widget > .axdb-widget-h { padding: 5px 12px; font-size: 11.5px; }\n .axdb-widget > .axdb-widget-b { padding: 6px 12px; }\n}\n/* The row floor: the band gives way to an inline label beside the figure. */\n@container axdb-tile (max-height: 46px) {\n .axdb-widget > .axdb-widget-h { padding: 0 0 0 12px; background: none; border-bottom: 0; }\n .axdb-widget > .axdb-widget-b { padding: 3px 12px; }\n}\n@container axdb-tile (max-height: 26px) {\n .axdb-widget > .axdb-widget-h { display: none; }\n .axdb-widget > .axdb-widget-b { padding: 1px 12px; }\n}\n.axdb-widget-h {\n display: flex;\n align-items: center;\n gap: 8px;\n flex: none;\n min-width: 0;\n padding: 9px 14px;\n background: var(--axdb-head-bg);\n border-bottom: 1px solid var(--axdb-line);\n font: 600 12.5px/1.3 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n letter-spacing: -.005em;\n color: var(--axdb-head-fg);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n/* Drag-handle mode (DevExpress drags an item by its caption): the header is\n the grip and says so with a dot pattern and a grab cursor; the body keeps\n its own cursor because a press there starts nothing. */\n.axdb-drag-handle .axdb-widget > .axdb-widget-h { cursor: grab; }\n.axdb-drag-handle .axdb-widget > .axdb-widget-h::before {\n content: ''; flex: none; width: 8px; height: 12px; opacity: .55;\n background: radial-gradient(circle, currentColor 1.1px, transparent 1.5px) 0 0 / 4px 4px;\n}\n/* A PAINTED GRIP (dragHandle: { grip: true }): the only drag zone, a small\n dotted tab along the card's top edge. Inside sits in the header band and\n the header makes room for it; outside is a tab above the card, the\n DevExpress item bar. The host must not clip it. */\n.grafloria-node-host > .axdb-grip {\n position: absolute; z-index: 4; box-sizing: border-box; width: 24px; height: 12px;\n border: 1px solid var(--axdb-line, #e7eaf1); border-radius: 3px; background: var(--axdb-card, #fff);\n color: var(--axdb-muted, #5a6478); cursor: grab;\n /* Shown on the SELECTED widget only (the DevExpress designer shows an item's\n bar on the selected item): a press or keyboard focus selects, a void\n click clears. Hidden, it takes no pointer either. */\n opacity: 0; pointer-events: none; transition: opacity .12s ease;\n}\n.grafloria-node-host.axdb-selected > .axdb-grip, .grafloria-node-host:focus-within > .axdb-grip { opacity: 1; pointer-events: auto; }\n.grafloria-node-host > .axdb-grip::before {\n content: ''; position: absolute; left: 5px; top: 2px; width: 12px; height: 6px;\n background: radial-gradient(circle, currentColor 1px, transparent 1.4px) 0 0 / 4px 3px;\n}\n.grafloria-node-host > .axdb-grip:hover { border-color: var(--axdb-accent, #3b52d9); color: var(--axdb-accent, #3b52d9); }\n/* The selected card says so, quietly. --axdb-accent themes the grip's hover, the\n ring and the focus outline together; --axdb-accent-ring the ring alone. */\n.grafloria-node-host.axdb-selected > .axdb-widget { box-shadow: 0 0 0 1.5px var(--axdb-accent-ring, rgba(59, 82, 217, .55)), 0 1px 2px rgba(16, 24, 40, .05), 0 1px 3px rgba(16, 24, 40, .05); }\n/* While a resize edge is near, the host and everything in it (a chart canvas\n with its own cursor included) show the edge's cursor. */\n.grafloria-node-host[data-axdb-edge=\"ns-resize\"], .grafloria-node-host[data-axdb-edge=\"ns-resize\"] * { cursor: ns-resize !important; }\n.grafloria-node-host[data-axdb-edge=\"ew-resize\"], .grafloria-node-host[data-axdb-edge=\"ew-resize\"] * { cursor: ew-resize !important; }\n.grafloria-node-host[data-axdb-edge=\"nwse-resize\"], .grafloria-node-host[data-axdb-edge=\"nwse-resize\"] * { cursor: nwse-resize !important; }\n.grafloria-node-host[data-axdb-edge=\"nesw-resize\"], .grafloria-node-host[data-axdb-edge=\"nesw-resize\"] * { cursor: nesw-resize !important; }\n/* INSIDE: centred on the header's text line and flush with the card padding,\n per size tier (padding 13/15, then 8/14, 4/12, 2/12). */\n.grafloria-node-host > .axdb-grip--inside { top: 12px; }\n.grafloria-node-host > .axdb-grip--inside.axdb-grip--left { left: 15px; }\n.grafloria-node-host > .axdb-grip--inside.axdb-grip--right { right: 15px; }\n.grafloria-node-host > .axdb-grip--inside.axdb-grip--center { top: 3px; }\n@container axdb-tile (max-height: 90px) {\n .grafloria-node-host > .axdb-grip--inside { top: 9px; }\n .grafloria-node-host > .axdb-grip--inside.axdb-grip--left { left: 14px; }\n .grafloria-node-host > .axdb-grip--inside.axdb-grip--right { right: 14px; }\n .grafloria-node-host > .axdb-grip--inside.axdb-grip--center { top: 2px; }\n .grafloria-node-host.axdb-gp-inside.axdb-gp-left .axdb-widget > .axdb-widget-h { padding-left: 46px; }\n .grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 46px; }\n .grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 15px; }\n}\n@container axdb-tile (max-height: 46px) {\n .grafloria-node-host > .axdb-grip--inside { top: 5px; }\n .grafloria-node-host > .axdb-grip--inside.axdb-grip--left { left: 12px; }\n .grafloria-node-host > .axdb-grip--inside.axdb-grip--right { right: 12px; }\n .grafloria-node-host > .axdb-grip--inside.axdb-grip--center { top: 1px; }\n .grafloria-node-host.axdb-gp-inside.axdb-gp-left .axdb-widget > .axdb-widget-h { padding-left: 44px; }\n .grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 44px; }\n .grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 13px; }\n}\n@container axdb-tile (max-height: 26px) {\n .grafloria-node-host > .axdb-grip--inside { top: 2px; }\n}\n/* OUTSIDE: a tab on the card's top edge, its corners in line with the card's. */\n/* The OUTSIDE tab lives in the gap between tiles: never taller than the gap\n less a pixel (an 11-px tab in a 10-px gap sat on the tile above), never\n shorter than 6 px so the dots still read. */\n.grafloria-node-host > .axdb-grip--outside { top: calc(-1 * clamp(6px, var(--axdb-gap, 11px) - 1px, 11px)); height: clamp(6px, calc(var(--axdb-gap, 11px) - 1px), 11px); border-bottom-left-radius: 0; border-bottom-right-radius: 0; border-bottom: 0; box-shadow: 0 -1px 2px rgba(16, 24, 40, .08); }\n.grafloria-node-host > .axdb-grip--outside.axdb-grip--left { left: 10px; }\n.grafloria-node-host > .axdb-grip--outside.axdb-grip--right { right: 10px; }\n.grafloria-node-host > .axdb-grip--center { left: 50%; transform: translateX(-50%); }\n/* The header makes room for an inside grip on its side \u2014 inset + the 24-px\n grip + an 8-px gap, per tier (32 px under the band's 14-px inset hid the\n first letter of the title) \u2014 and a centred one sits above the text. */\n.grafloria-node-host.axdb-gp-inside.axdb-gp-left .axdb-widget > .axdb-widget-h { padding-left: 47px; }\n.grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 47px; }\n.grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 17px; }\n.axdb-widget-b { flex: 1; min-height: 0; position: relative; padding: 12px 14px; }\n/* A drag across a STATIC board (nothing prevents the press's default there)\n used to select every label on it; kit cards are not prose. Tables stay\n copyable \u2014 a figure in a grid is the one thing a viewer selects. */\n.axdb-widget { user-select: none; -webkit-user-select: none; }\n.axdb-widget .axdb-table { user-select: text; -webkit-user-select: text; }\n.axdb-widget-b > svg { display: block; width: 100%; height: 100%; }\n.axdb-widget-b.axdb-scroll { overflow: auto; }\n/* A chart WITH a legend under it: the plot yields height, the legend keeps its\n own (without this the 100%-tall svg pushes the legend out of the card). */\n.axdb-widget-b.axdb-has-lg { display: flex; flex-direction: column; }\n.axdb-widget-b.axdb-has-lg > svg { flex: 1; height: auto; min-height: 0; }\n/* The data behind a chart, for readers that cannot see the chart: present in\n the accessibility tree, absent from the picture (WCAG 1.1.1). */\n.axdb-sr {\n position: absolute; top: 0; left: 0; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;\n clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;\n}\n.axdb-widget-empty {\n display: flex; align-items: center; justify-content: center; height: 100%;\n font: 500 11.5px/1.3 system-ui, sans-serif; color: var(--axdb-muted);\n}\n\n/* kpi: value + delta stack, the spark yields its height before they do */\n/* A SIZE CONTAINER, so the card's type scales with the tile instead of\n clipping. Fit-mode shrinks tiles when rows are added (dropping a chart above\n the KPI row took these from 122px to 85px) and fixed 30px/12px lines were\n sliced mid-glyph by the card's overflow \u2014 the audited \"trimmed\" widgets.\n cqh = 1% of the body's own height, clamped so full-size tiles look exactly\n as before and short tiles compress instead of cutting. */\n.axdb-widget-b.axdb-kpi { display: flex; flex-direction: column; container: axdb-kpi / size; }\n/* STEP DOWN, NEVER CLIP. As the body gets shorter the sparkline goes first, then the\n delta, then the value \u2014 the header alone at the row floor. Before this a 99-px row\n drew the sparkline as a 13-px sliver and a 28-px row cut the value mid-glyph (the\n fluid board, after a drag pushed the KPI row down). Thresholds are body heights. */\n/* Two class hops, so these outrank \".axdb-widget-b > svg { display: block }\" \u2014\n a bare .axdb-kpi-s lost that cascade and the sparkline stayed. */\n@container (max-height: 78px) { .axdb-kpi > .axdb-kpi-s { display: none; } }\n@container (max-height: 40px) { .axdb-kpi > .axdb-kpi-d { display: none; } }\n@container (max-height: 16px) { .axdb-kpi > .axdb-kpi-v { display: none; } }\n.axdb-kpi-v { font: 700 clamp(15px, 44cqh, 30px)/1.05 system-ui, sans-serif; letter-spacing: -.02em; color: var(--axdb-ink); white-space: nowrap; }\n.axdb-kpi-d { margin-top: clamp(1px, 5cqh, 6px); font: 600 clamp(9px, 19cqh, 12px)/1.2 system-ui, sans-serif; white-space: nowrap; }\n.axdb-kpi-d span { color: var(--axdb-muted); font-weight: 500; }\n.axdb-kpi-d.up { color: var(--axdb-up); }\n.axdb-kpi-d.down { color: var(--axdb-down); }\n/* Grows into a tall tile (a 3-row KPI is not a number over a void) and never\n takes more than two fifths of the body. */\n.axdb-kpi-s { margin-top: auto; height: auto; min-height: 0; flex: 1 1 34px; max-height: 40%; }\n/* SHORT AND WIDE \u2014 a one-row KPI on a fluid board. Stacked, a short tile dropped\n its sparkline and left a small figure in the corner of an empty card. Here the\n figure, its delta and the sparkline sit in a row and the spark fills the width.\n The outer query asks the TILE (host), the inner one the body, so a strip too\n thin for a readable spark (under 24 px) keeps the figure alone. 340 px is what\n a figure, its delta, the gaps and a spark worth reading need \u2014 the builder's\n 236-px tiles got a 40-px wedge of spark beside the delta. */\n@container axdb-tile (max-height: 125px) and (min-width: 340px) {\n .axdb-widget--kpi > .axdb-kpi { flex-direction: row; align-items: center; gap: 14px; }\n .axdb-widget--kpi .axdb-kpi-d { margin-top: 0; }\n @container axdb-kpi (min-height: 24px) {\n .axdb-kpi > .axdb-kpi-s { display: block; flex: 1 1 40%; min-width: 80px; height: 100%; max-height: 100%; margin-top: 0; align-self: stretch; }\n }\n}\n/* THE STRIP (row floor): header and figure on one line, the spark beside them\n when the tile is wide enough. */\n@container axdb-tile (max-height: 46px) {\n .axdb-widget--kpi { flex-direction: row; align-items: center; gap: 12px; }\n .axdb-widget--kpi > .axdb-widget-h { flex: none; padding: 0 0 0 12px; background: none; border-bottom: 0; }\n /* The body is a SIZE container: in a centred row it would collapse to zero\n and every height query would fire. It takes the card's full inner height. */\n .axdb-widget--kpi > .axdb-kpi { flex: 1; align-self: stretch; }\n}\n\n/* SPLIT LAYOUT chrome: a divider sits in the gap between two siblings (a\n 24-px hit zone around a 10-px gap) and shows its line on hover or while\n dragged; the insertion line marks the edge a dragged widget will land on. */\n.grafloria-html-layer > .axdb-div { position: absolute; z-index: 2; pointer-events: auto; background: transparent; }\n.grafloria-html-layer > .axdb-div--row { cursor: col-resize; }\n.grafloria-html-layer > .axdb-div--column { cursor: row-resize; }\n.grafloria-html-layer > .axdb-div::after {\n content: \"\"; position: absolute; border-radius: 2px; background: #3b52d9; opacity: 0; transition: opacity .12s ease;\n}\n.grafloria-html-layer > .axdb-div--row::after { left: 50%; top: 8px; bottom: 8px; width: 4px; margin-left: -2px; }\n.grafloria-html-layer > .axdb-div--column::after { top: 50%; left: 8px; right: 8px; height: 4px; margin-top: -2px; }\n.grafloria-html-layer > .axdb-div:hover::after, .grafloria-html-layer > .axdb-div.axdb-active::after { opacity: .9; }\n.grafloria-html-layer > .axdb-ins {\n position: absolute; z-index: 3; pointer-events: none; border-radius: 2px;\n background: #3b52d9; box-shadow: 0 0 0 3px rgba(59, 82, 217, .22);\n}\n@media (prefers-reduced-motion: reduce) { .grafloria-html-layer > .axdb-div::after { transition: none; } }\n\n/* donut: ring beside its legend. The ring takes the body's height (a tall\n tile gets a bigger ring, not dead card), square, capped so its centre figure\n stays a figure and not a headline. */\n.axdb-widget-b.axdb-donut { display: flex; align-items: center; gap: 10px; }\n/* A legend taller than a SHORT body (a 2-row donut on a squeezed board) is\n clipped at its own foot, not centred over the title above it. */\n.axdb-widget-b.axdb-donut > .axdb-lg--col { max-height: 100%; min-height: 0; overflow: hidden; }\n.axdb-widget-b.axdb-donut > svg {\n flex: 0 0 auto; width: auto; height: 100%; max-height: 260px; max-width: 60%; aspect-ratio: 1 / 1;\n}\n\n/* READABILITY TIERS (widgets.ts chartTier): the text is always in the DOM;\n what a short body cannot afford is hidden, not dropped. */\n.axdb-tier-1 .axdb-yt--q, .axdb-tier-1 .axdb-yl--q,\n.axdb-tier-2 .axdb-yt--q, .axdb-tier-2 .axdb-yl--q,\n.axdb-tier-2 .axdb-yt--h, .axdb-tier-2 .axdb-yl--h,\n.axdb-tier-2 .axdb-xt, .axdb-tier-2 .axdb-vt,\n.axdb-lg--off { display: none; }\n\n/* SECTION CHROME: a pointer-transparent overlay on every member group. It\n wears the selection ring and, while selected, the corner handle. */\n.grafloria-html-layer > .axdb-slab { position: absolute; pointer-events: none; border-radius: var(--axdb-rs-radius, 8px); z-index: 4; }\n/* Selected, the overlay rises above the tiles so ITS corner handle wins a\n corner it shares with a child's; unselected, its handle takes no presses. */\n.grafloria-html-layer > .axdb-slab.axdb-slab--selected { z-index: 6; box-shadow: 0 0 0 1.5px var(--axdb-accent-ring, rgba(59, 82, 217, .55)); }\n.grafloria-html-layer > .axdb-slab > .axdb-rs { pointer-events: none; opacity: 0; }\n.grafloria-html-layer > .axdb-slab.axdb-slab--selected > .axdb-rs { pointer-events: auto; opacity: 1; }\n.grafloria-html-layer > .axdb-slab.axdb-slab--static > .axdb-rs { display: none; }\n\n/* GROUP FRAME (0.4.43): a TAB CONTAINER wears a frame by default \u2014 a bordered\n slab, and a tinted surface UNDER its pages (the strip's own track colour, so\n strip and body read as one panel). The layer is isolated so the surface can\n sit below every tile at z -1 and still paint above the canvas. */\n.grafloria-html-layer { isolation: isolate; }\n.grafloria-html-layer > .axdb-group-bg {\n position: absolute; pointer-events: none; z-index: -1; box-sizing: border-box;\n border-radius: var(--axdb-rs-radius, 8px);\n background: var(--axdb-group-bg, #f1f3f8);\n}\n.grafloria-html-layer > .axdb-slab.axdb-slab--tabs {\n box-sizing: border-box;\n border: 1px solid var(--axdb-group-border, var(--axdb-line, #e5e8ef));\n}\n@media (prefers-color-scheme: dark) {\n .grafloria-html-layer > .axdb-group-bg { background: var(--axdb-group-bg, #1b2029); }\n .grafloria-html-layer > .axdb-slab.axdb-slab--tabs { border-color: var(--axdb-group-border, rgba(236, 238, 244, .12)); }\n}\n\n/* SECTION CAPTION (0.4.22): the band on the slab. Geometry is inline (the\n reserve and the pixels come from one function); everything visual is a\n variable the options write and a theme may override. The band takes the\n pointer \u2014 the slab does not \u2014 so a press on it is the section's. */\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h {\n position: absolute; box-sizing: border-box; pointer-events: auto; z-index: 1;\n display: flex; align-items: center; gap: 6px; min-width: 0;\n padding: var(--axdb-caption-pad, 0 12px);\n background: var(--axdb-caption-bg, rgba(31, 36, 48, .045));\n /* A hairline by default: two bands meeting (a captioned section inside a\n captioned section) read as two headers, not one grey block. */\n border-bottom: var(--axdb-caption-border, 1px solid rgba(31, 36, 48, .1));\n color: var(--axdb-caption-fg, #1f2430);\n font: var(--axdb-caption-font-weight, 600) var(--axdb-caption-font-size, 13px)/1.2 var(--axdb-caption-font-family, system-ui, -apple-system, \"Segoe UI\", sans-serif);\n text-transform: var(--axdb-caption-transform, none);\n letter-spacing: -.005em;\n border-radius: var(--axdb-rs-radius, 8px) var(--axdb-rs-radius, 8px) 0 0;\n cursor: default; user-select: none; -webkit-user-select: none; overflow: hidden;\n transition: opacity .12s;\n}\n/* The modifiers carry the band's own selector so they outrank its defaults. */\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--center { justify-content: center; }\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--end { justify-content: flex-end; }\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--vtop { align-items: flex-start; }\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--vbottom { align-items: flex-end; }\n/* 'tab': the same band sized to its text, a chip at the leading corner. It\n reserves its height like 'inside' \u2014 see captionReserve. */\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--tab {\n max-width: 100%; border-radius: var(--axdb-rs-radius, 8px);\n border-bottom: var(--axdb-caption-border-tab, none);\n box-shadow: inset 0 0 0 1px rgba(31, 36, 48, .08);\n}\n/* 'hover': an overlay. It reserves nothing, so while hidden it must not take\n the pointer (an invisible band swallowed clicks on the content beneath),\n and while shown it paints OPAQUE \u2014 a 5% tint over a chart is a smear. */\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--hover {\n opacity: 0; pointer-events: none;\n background: var(--axdb-caption-bg, #fff);\n box-shadow: 0 1px 4px rgba(31, 36, 48, .16);\n}\n/* .axdb-slab--hot is set by the binder from the live pointer (the overlay\n takes no pointer of its own, so CSS :hover can never fire on it). */\n.grafloria-html-layer > .axdb-slab.axdb-slab--hot > .axdb-slab-h.axdb-slab-h--hover,\n.grafloria-html-layer > .axdb-slab.axdb-slab--selected > .axdb-slab-h.axdb-slab-h--hover { pointer-events: auto; opacity: 1; }\n/* the actions of a hovered or selected section */\n.grafloria-html-layer > .axdb-slab.axdb-slab--hot > .axdb-slab-h > .axdb-slab-h-actions { opacity: 1; }\n/* the tight tier: a section under 90 px */\n.grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--tight { font-size: min(var(--axdb-caption-font-size, 12px), 12px); gap: 4px; }\n.axdb-slab-h--tight .axdb-slab-h-sub, .axdb-slab-h--tight .axdb-slab-h-actions { display: none; }\n.axdb-slab-h-icon { flex: none; }\n.axdb-slab-h-body { display: flex; flex-direction: column; justify-content: center; min-width: 0; flex: 0 1 auto; }\n.axdb-slab-h-text, .axdb-slab-h-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.axdb-slab-h-sub { font-size: .88em; font-weight: 500; opacity: .7; }\n.axdb-slab-h-info { flex: none; opacity: .6; font-size: .9em; cursor: help; }\n.axdb-slab-h-actions { flex: none; display: flex; gap: 2px; margin-inline-start: auto; opacity: 0; transition: opacity .12s; }\n.axdb-slab-h--center .axdb-slab-h-actions, .axdb-slab-h--end .axdb-slab-h-actions { margin-inline-start: 0; }\n.axdb-slab-h:hover > .axdb-slab-h-actions, .axdb-slab--selected > .axdb-slab-h > .axdb-slab-h-actions, .axdb-slab-h:focus-within > .axdb-slab-h-actions { opacity: 1; }\n.axdb-slab-h-action {\n all: unset; box-sizing: border-box; width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center;\n border-radius: 6px; cursor: pointer; font-size: 14px; line-height: 1; color: inherit; transition: background-color .12s;\n}\n.axdb-slab-h-action:hover { background: rgba(31, 36, 48, .08); }\n.axdb-slab-h-action:focus-visible { outline: 2px solid var(--axdb-accent-ring, rgba(59, 82, 217, .55)); outline-offset: -2px; }\n.axdb-slab-h-action[disabled] { opacity: .4; cursor: default; }\n.axdb-slab-h-action[disabled]:hover { background: none; }\n.grafloria-html-layer > .axdb-slab.axdb-slab--static > .axdb-slab-h { cursor: default; }\n@media (prefers-color-scheme: dark) {\n /* The band sits on the PAGE, whose ground the kit does not own (a host that\n stays light while the kit goes dark is common): a solid ink, not a tint,\n so the light caption always reads. */\n .grafloria-html-layer > .axdb-slab > .axdb-slab-h {\n background: var(--axdb-caption-bg, #1b2029);\n color: var(--axdb-caption-fg, #eceef4);\n border-bottom: var(--axdb-caption-border, 1px solid rgba(236, 238, 244, .12));\n }\n .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--tab { box-shadow: inset 0 0 0 1px rgba(236, 238, 244, .12); }\n .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--hover { background: var(--axdb-caption-bg, #1a1d25); box-shadow: 0 1px 4px rgba(0, 0, 0, .5); }\n .axdb-slab-h-action:hover { background: rgba(236, 238, 244, .1); }\n}\n\n/* TAB CONTAINER (0.4.27): a strip of pages across the container's top. The\n strip takes the pointer (the tabs are real buttons); the pages below it are\n ordinary boards. */\n.grafloria-html-layer > .axdb-tabs {\n position: absolute; box-sizing: border-box; pointer-events: auto; z-index: 5;\n display: flex; align-items: center; gap: 2px; padding: 3px 6px; overflow-x: auto; overflow-y: hidden;\n background: var(--axdb-tabs-bg, #f1f3f8);\n border-bottom: 1px solid var(--axdb-tabs-line, #e5e8ef);\n border-radius: var(--axdb-rs-radius, 8px) var(--axdb-rs-radius, 8px) 0 0;\n scrollbar-width: thin;\n}\n.axdb-tabs--center { justify-content: center; }\n.axdb-tabs--end { justify-content: flex-end; }\n.axdb-tabs--stretch > .axdb-tab { flex: 1 1 0; }\n/* A tab is a PILL on the track (a segmented control): quiet until hovered,\n the active one lifted in the card's own ground. */\n.axdb-tab {\n all: unset; box-sizing: border-box; flex: 0 0 auto; max-width: 200px;\n padding: 0 12px; height: 100%; display: inline-flex; align-items: center;\n font: 500 12.5px/1 system-ui, -apple-system, \"Segoe UI\", sans-serif;\n letter-spacing: -.005em;\n color: var(--axdb-tabs-fg, #5a6478); cursor: pointer;\n border-radius: var(--axdb-tab-radius, 6px);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n transition: background-color .12s, color .12s, box-shadow .12s;\n}\n.axdb-tab:hover { background: rgba(31, 36, 48, .06); color: var(--axdb-tabs-on-fg, #1f2430); }\n.axdb-tab:focus-visible { outline: 2px solid var(--axdb-accent-ring, rgba(59, 82, 217, .55)); outline-offset: -2px; }\n.axdb-tab.axdb-tab--on {\n background: var(--axdb-tabs-on-bg, #fff);\n color: var(--axdb-tabs-on-fg, #1f2430);\n font-weight: 600;\n box-shadow: 0 1px 2px rgba(16, 24, 40, .1), 0 0 0 1px rgba(31, 36, 48, .06);\n}\n@media (prefers-reduced-motion: reduce) { .axdb-tab { transition: none; } }\n@media (prefers-color-scheme: dark) {\n /* Solid for the same reason as the caption band: the track sits on the page. */\n .grafloria-html-layer > .axdb-tabs { background: var(--axdb-tabs-bg, #1b2029); border-bottom-color: var(--axdb-tabs-line, rgba(236, 238, 244, .12)); }\n .axdb-tab { color: var(--axdb-tabs-fg, #98a1b4); }\n .axdb-tab:hover { background: rgba(236, 238, 244, .08); color: var(--axdb-tabs-on-fg, #eceef4); }\n .axdb-tab.axdb-tab--on { background: var(--axdb-tabs-on-bg, #232836); color: var(--axdb-tabs-on-fg, #eceef4); box-shadow: 0 1px 2px rgba(0, 0, 0, .45), 0 0 0 1px rgba(236, 238, 244, .1); }\n}\n\n/* legend chips, shared by line and donut */\n.axdb-lg { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 9px; }\n.axdb-lg--col { flex-direction: column; flex-wrap: nowrap; gap: 6px; margin-top: 0; }\n.axdb-lg i {\n display: inline-flex; align-items: center; gap: 5px; font-style: normal;\n font: 500 11px/1.3 system-ui, sans-serif; color: var(--axdb-muted);\n}\n.axdb-lg i b { width: 9px; height: 9px; border-radius: 3px; flex: none; }\n\n/* table */\n.axdb-table { width: 100%; border-collapse: collapse; font-size: 12px; color: var(--axdb-ink); }\n.axdb-table th {\n text-align: left; padding: 4px 8px 7px; border-bottom: 1px solid var(--axdb-line);\n font: 600 10px/1.3 system-ui, sans-serif; letter-spacing: .4px; text-transform: uppercase;\n color: var(--axdb-muted);\n}\n.axdb-table td { padding: 6px 8px; border-bottom: 1px solid var(--axdb-line); white-space: nowrap; }\n.axdb-table td.num { text-align: right; font-variant-numeric: tabular-nums; }\n.axdb-table tr:last-child td { border-bottom: none; }\n\n@media (prefers-color-scheme: dark) {\n .axdb-widget {\n --axdb-ink: #eceef4;\n --axdb-muted: #98a1b4;\n --axdb-grid: rgba(150, 160, 182, .2);\n --axdb-card: #161a22;\n --axdb-line: #2a3040;\n --axdb-head-bg: rgba(255, 255, 255, .035);\n --axdb-head-fg: #e6e9f0;\n --axdb-soft: rgba(150, 160, 182, .16);\n --axdb-up: #4ade80;\n --axdb-down: #fb7185;\n --axdb-c1: #8b9cff; --axdb-c2: #38bdf8; --axdb-c3: #2dd4bf;\n --axdb-c4: #fbbf24; --axdb-c5: #a78bfa; --axdb-c6: #94a3b8;\n }\n}\n";
26
26
  /** Idempotently inject the kit stylesheet (safe to call per binder). */
27
27
  export declare function ensureDashboardKitStyles(doc?: Document): void;
@@ -165,33 +165,13 @@ const CSS = `
165
165
  a bar before the tab it lands in front of, or after the last one. */
166
166
  .grafloria-html-layer > .axdb-join {
167
167
  position: absolute;
168
- z-index: 32;
168
+ z-index: 3;
169
169
  pointer-events: none;
170
170
  box-sizing: border-box;
171
171
  border: 2px dashed var(--axdb-accent, #3b52d9);
172
172
  background: var(--axdb-accent-soft, rgba(59, 82, 217, .08));
173
173
  border-radius: var(--axdb-rs-radius, 8px);
174
174
  }
175
- /* THE LANES: while a widget is held over a container, its four bands are
176
- shown, so "beside it" is a place you can see and aim at rather than a
177
- fixed depth you would have to guess. The band you are IN wears the dashed
178
- mark (.axdb-join) on top; these only say where the edges are. */
179
- /* ABOVE the dragged widget (which sits at 30): the whole point of a lane is to
180
- be seen at the moment the hand is in it, and the widget your hand is carrying
181
- covers the top one almost completely. Both are pointer-events: none, so
182
- nothing about the hit testing changes. */
183
- .grafloria-html-layer > .axdb-lanes { position: absolute; z-index: 31; pointer-events: none; }
184
- .grafloria-html-layer > .axdb-lanes > .axdb-lane {
185
- position: absolute;
186
- box-sizing: border-box;
187
- /* A SHADED MARGIN, never a line: the dashed outline already means "the cell
188
- this will take", and two dashed treatments at once read as one shape. */
189
- background: var(--axdb-lane-bg, rgba(59, 82, 217, .13));
190
- border-radius: 2px;
191
- }
192
- @media (prefers-color-scheme: dark) {
193
- .grafloria-html-layer > .axdb-lanes > .axdb-lane { background: var(--axdb-lane-bg, rgba(134, 155, 255, .17)); }
194
- }
195
175
  .axdb-tabs.axdb-tabs--drop { box-shadow: inset 0 -2px 0 var(--axdb-accent, #3b52d9); }
196
176
  .axdb-tab.axdb-tab--drop-before { position: relative; }
197
177
  .axdb-tab.axdb-tab--drop-before::before {
@@ -59,6 +59,17 @@ export interface ZoneContainer {
59
59
  band: number;
60
60
  /** The TOP and BOTTOM depth in world units, when it is a fixed depth rather than `band` of the body. */
61
61
  bandY?: number;
62
+ /**
63
+ * How deep the "above me" band hangs ABOVE the frame's top edge, in world
64
+ * units. 0 or absent: no such band, and no inside top band either.
65
+ *
66
+ * A hand looking for "above this panel" goes above it. It used to have to go
67
+ * BELOW the panel's header instead, into a 30 px lane inside the body, which
68
+ * is the opposite of where anyone points and reachable only by being told —
69
+ * "try dragging nps above the tab panel, won't work" (0.4.65). The band now
70
+ * hangs off the top edge, and the body means the page, all of it.
71
+ */
72
+ topOutside?: number;
62
73
  /** The board a descent enters: a tab container's ACTIVE page, a section's own board. Null: nothing to enter. */
63
74
  inner: ZoneBoard | null;
64
75
  }
@@ -197,6 +208,7 @@ export interface ContainerBands {
197
208
  stripHeight: number;
198
209
  band: number;
199
210
  bandY?: number;
211
+ topOutside?: number;
200
212
  }
201
213
  /**
202
214
  * The deepest container under a pointer whose EDGES mean something — the one
@@ -211,9 +223,15 @@ export interface ContainerBands {
211
223
  * something on top of the tab group?")
212
224
  */
213
225
  export declare function containerUnder(p: ContainerProbe): ContainerBands | null;
214
- /** Where a container's four bands are, in the same world units as its frame. */
226
+ /**
227
+ * Where a container's bands are, in the same world units as its frame. TOP
228
+ * appears twice: hanging above the frame, where a hand looking for "above it"
229
+ * goes, and under the strip, which is the only one a container pressed against
230
+ * the top of its canvas can offer. Both mean the same thing.
231
+ */
215
232
  export declare function bandRects(c: ContainerBands): {
216
233
  side: BesideSide;
234
+ name: string;
217
235
  rect: ZoneRect;
218
236
  }[];
219
237
  export declare function resolve(input: ResolveInput): Zone;
@@ -102,7 +102,15 @@ export function stripUnder(p) {
102
102
  const ty = atRest ? y : y + dy;
103
103
  const held = c.id === p.held;
104
104
  const pad = held ? p.stay : 0;
105
- if (c.stripHeight > 0 && inRect({ x: frame.x, y: frame.y, width: frame.width, height: c.stripHeight }, tx, ty, pad)) {
105
+ // The stay reaches DOWN into the body and out to the sides, never UP:
106
+ // above the strip is the band that means "above this container" (0.4.65),
107
+ // and a strip that kept the hand 9 px into it would eat a third of it.
108
+ const onStrip = c.stripHeight > 0 &&
109
+ tx >= frame.x - pad &&
110
+ tx <= frame.x + frame.width + pad &&
111
+ ty >= frame.y &&
112
+ ty <= frame.y + c.stripHeight + pad;
113
+ if (onStrip) {
106
114
  if (bestId === null || held || (!bestHeld && b.depth >= bestDepth)) {
107
115
  bestId = c.id;
108
116
  bestDepth = b.depth;
@@ -134,7 +142,7 @@ export function containerUnder(p) {
134
142
  let best = null;
135
143
  let bestDepth = -1;
136
144
  const visit = (b, dx, dy) => {
137
- var _a, _b, _c;
145
+ var _a, _b, _c, _d;
138
146
  for (const c of b.children()) {
139
147
  if ((_a = p.ghostSubtree) === null || _a === void 0 ? void 0 : _a.has(c.id))
140
148
  continue;
@@ -143,10 +151,12 @@ export function containerUnder(p) {
143
151
  const frame = atRest !== null && atRest !== void 0 ? atRest : c.frame;
144
152
  const tx = atRest ? x : x + dx;
145
153
  const ty = atRest ? y : y + dy;
146
- if (!inRect(frame, tx, ty))
154
+ const up = (_c = c.topOutside) !== null && _c !== void 0 ? _c : 0;
155
+ const inOverhang = up > 0 && tx >= frame.x && tx <= frame.x + frame.width && ty < frame.y && ty >= frame.y - up;
156
+ if (!inRect(frame, tx, ty) && !inOverhang)
147
157
  continue;
148
- if (c.band > 0 && !((_c = p.homeChain) === null || _c === void 0 ? void 0 : _c.has(c.id)) && b.depth >= bestDepth) {
149
- best = Object.assign({ containerId: c.id, frame, stripHeight: c.stripHeight, band: c.band }, (c.bandY === undefined ? {} : { bandY: c.bandY }));
158
+ if (c.band > 0 && !((_d = p.homeChain) === null || _d === void 0 ? void 0 : _d.has(c.id)) && b.depth >= bestDepth) {
159
+ best = Object.assign(Object.assign({ containerId: c.id, frame, stripHeight: c.stripHeight, band: c.band }, (c.bandY === undefined ? {} : { bandY: c.bandY })), (c.topOutside === undefined ? {} : { topOutside: c.topOutside }));
150
160
  bestDepth = b.depth;
151
161
  }
152
162
  if (c.inner)
@@ -157,21 +167,41 @@ export function containerUnder(p) {
157
167
  visit(r, 0, 0);
158
168
  return best;
159
169
  }
160
- /** Where a container's four bands are, in the same world units as its frame. */
170
+ /**
171
+ * Where a container's bands are, in the same world units as its frame. TOP
172
+ * appears twice: hanging above the frame, where a hand looking for "above it"
173
+ * goes, and under the strip, which is the only one a container pressed against
174
+ * the top of its canvas can offer. Both mean the same thing.
175
+ */
161
176
  export function bandRects(c) {
177
+ var _a;
162
178
  const bodyY = c.frame.y + c.stripHeight;
163
179
  const bodyH = Math.max(1, c.frame.height - c.stripHeight);
164
180
  const w = Math.max(1, c.frame.width);
165
181
  const side = w * c.band;
166
182
  const depth = c.bandY !== undefined && c.bandY > 0 ? Math.min(c.bandY, bodyH / 2) : c.band * bodyH;
183
+ const up = (_a = c.topOutside) !== null && _a !== void 0 ? _a : 0;
167
184
  return [
168
- { side: 'left', rect: { x: c.frame.x, y: bodyY, width: side, height: bodyH } },
169
- { side: 'right', rect: { x: c.frame.x + w - side, y: bodyY, width: side, height: bodyH } },
170
- // the sides take the corners (bandOf tests rx first), so top and bottom stop short of them
171
- { side: 'top', rect: { x: c.frame.x + side, y: bodyY, width: w - 2 * side, height: depth } },
172
- { side: 'bottom', rect: { x: c.frame.x + side, y: bodyY + bodyH - depth, width: w - 2 * side, height: depth } },
185
+ { side: 'left', name: 'left', rect: { x: c.frame.x, y: bodyY, width: side, height: bodyH } },
186
+ { side: 'right', name: 'right', rect: { x: c.frame.x + w - side, y: bodyY, width: side, height: bodyH } },
187
+ // above the frame: the whole width, since no side band is up there to take the corners
188
+ ...(up > 0 ? [{ side: 'top', name: 'top', rect: { x: c.frame.x, y: c.frame.y - up, width: w, height: up } }] : []),
189
+ // and under the strip; the sides take the corners here (bandOf tests rx first)
190
+ { side: 'top', name: 'top-inside', rect: { x: c.frame.x + side, y: bodyY, width: w - 2 * side, height: depth } },
191
+ { side: 'bottom', name: 'bottom', rect: { x: c.frame.x + side, y: bodyY + bodyH - depth, width: w - 2 * side, height: depth } },
173
192
  ];
174
193
  }
194
+ /**
195
+ * The band a point is in for a TILE drag: the sides and the bottom as
196
+ * `bandOf` gives them, and "top" ONLY from the band hanging above the frame.
197
+ * The rows under the strip are the page.
198
+ */
199
+ function tileBand(f, stripHeight, x, y, band, bandY, topOutside, grow = 1) {
200
+ const up = (topOutside !== null && topOutside !== void 0 ? topOutside : 0) * grow;
201
+ if (up > 0 && x >= f.x && x <= f.x + f.width && y < f.y && y >= f.y - up)
202
+ return 'top';
203
+ return bandOf(f, stripHeight, x, y, band, bandY === undefined ? undefined : bandY * grow);
204
+ }
175
205
  /** The board a container sits on, found by id through the tree. */
176
206
  function boardOf(roots, containerId) {
177
207
  const visit = (b) => {
@@ -206,8 +236,9 @@ export function resolve(input) {
206
236
  const c0 = containerOf(input.roots, p.containerId);
207
237
  const stripH = (_a = c0 === null || c0 === void 0 ? void 0 : c0.stripHeight) !== null && _a !== void 0 ? _a : 0;
208
238
  const depth = c0 === null || c0 === void 0 ? void 0 : c0.bandY;
209
- const stay = bandOf(p.frame0, stripH, x, y, BESIDE_BAND + BESIDE_STAY, depth === undefined ? undefined : depth * (1 + BESIDE_STAY / BESIDE_BAND));
210
- const other = bandOf(p.frame0, stripH, x, y, BESIDE_BAND, depth);
239
+ const grow = 1 + BESIDE_STAY / BESIDE_BAND;
240
+ const stay = tileBand(p.frame0, stripH, x, y, BESIDE_BAND + BESIDE_STAY, depth, c0 === null || c0 === void 0 ? void 0 : c0.topOutside, grow);
241
+ const other = tileBand(p.frame0, stripH, x, y, BESIDE_BAND, depth, c0 === null || c0 === void 0 ? void 0 : c0.topOutside);
211
242
  const onVacated = !!p.vacated && inRect(p.vacated, x, y, input.gap);
212
243
  const held = stay === p.side || (!(other !== null && other !== p.side) && onVacated);
213
244
  if (held) {
@@ -251,7 +282,7 @@ export function resolve(input) {
251
282
  const ndx0 = atRest ? c.frame.x - atRest.x : dx;
252
283
  const ndy0 = atRest ? c.frame.y - atRest.y : dy;
253
284
  const overNested = !!c.inner && c.inner.children().some((cc) => inRect(cc.frame, x + ndx0, y + ndy0));
254
- const side = overNested || input.homeChain.has(c.id) ? null : bandOf(frame, c.stripHeight, tx, ty, c.band, c.bandY);
285
+ const side = overNested || input.homeChain.has(c.id) ? null : tileBand(frame, c.stripHeight, tx, ty, c.band, c.bandY, c.topOutside);
255
286
  if (side)
256
287
  return { kind: 'beside', board, containerId: c.id, side, kept: false };
257
288
  if (opaque(board, c) || !c.inner)
@@ -260,11 +291,44 @@ export function resolve(input) {
260
291
  return { kind: 'plain', board, grace: false }; // the margin: the container's own frame
261
292
  return descend(c.inner, ndx0, ndy0);
262
293
  }
263
- return { kind: 'plain', board, grace: false };
294
+ const over = overhang(board, dx, dy);
295
+ return over !== null && over !== void 0 ? over : { kind: 'plain', board, grace: false };
264
296
  };
297
+ /**
298
+ * The bands that hang ABOVE a container's top edge, once nothing on the
299
+ * board holds the point. Always a second pass: a container whose frame
300
+ * actually holds the hand must never lose to a neighbour's overhang. The
301
+ * band of a container at the board's first row hangs off the board
302
+ * altogether, so the grace path asks for it too.
303
+ */
304
+ function overhang(board, dx, dy) {
305
+ var _a;
306
+ for (const c of board.children()) {
307
+ if (input.ghostSubtree.has(c.id) || input.homeChain.has(c.id) || c.band <= 0)
308
+ continue;
309
+ const rest = (_a = input.restFrames) === null || _a === void 0 ? void 0 : _a.get(c.id);
310
+ const atRest = rest && inRect(rest, x, y) ? rest : null;
311
+ const frame = atRest !== null && atRest !== void 0 ? atRest : c.frame;
312
+ const tx = atRest ? x : x + dx;
313
+ const ty = atRest ? y : y + dy;
314
+ if (tileBand(frame, c.stripHeight, tx, ty, c.band, c.bandY, c.topOutside) === 'top')
315
+ return { kind: 'beside', board, containerId: c.id, side: 'top', kept: false };
316
+ const deeper = c.inner ? overhang(c.inner, atRest ? c.frame.x - atRest.x : dx, atRest ? c.frame.y - atRest.y : dy) : null;
317
+ if (deeper)
318
+ return deeper;
319
+ }
320
+ return null;
321
+ }
265
322
  for (const root of input.roots)
266
323
  if (root.contains(x, y))
267
324
  return descend(root, 0, 0);
325
+ // Off every board — but a container against the board's first row hangs its
326
+ // "above me" band into the margin above it, which is off the board too.
327
+ for (const root of input.roots) {
328
+ const over = overhang(root, 0, 0);
329
+ if (over)
330
+ return over;
331
+ }
268
332
  // Nothing strict matched: the deepest board whose grace row holds the point.
269
333
  let deepest = null;
270
334
  const visit = (b) => {