@grafloria/element 0.4.35 → 0.4.36

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.35",
3
+ "version": "0.4.36",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -626,6 +626,9 @@ export interface DashboardHandleContext {
626
626
  active: string;
627
627
  /** MUTABLE — set by the caller's finalize once the render API exists. */
628
628
  apiRef: DashboardApiRef | null;
629
+ /** MUTABLE — the canvas element, set with apiRef; parking a page or a view
630
+ * reaches the html layer through it (see `teleport`). */
631
+ container: HTMLElement | null;
629
632
  /** The consumer's layout hook, if any (dashboard() passes its option). */
630
633
  onLayoutChange?: (viewId: string, widgets: DashboardWidgetSpec[]) => void;
631
634
  /**
@@ -184,6 +184,29 @@ class SetWidgetLockCommand extends Command {
184
184
  const HISTORY_EVENTS = ['command:executed', 'command:undone', 'command:redone'];
185
185
  const DEFAULTS = { columns: 12, gap: 8, rowHeight: 130, width: 1180, height: 660 };
186
186
  const OFFSCREEN_X = -20000;
187
+ /**
188
+ * Parking is a TELEPORT, never a glide. `.axdb-glide` on the html layer eases
189
+ * every left/top write while a gesture runs and for 400 ms past its drop; a
190
+ * page or a view coming back from OFFSCREEN_X under it SLID in from 20,000 px
191
+ * away over 280 ms (0.4.36). The class comes off for the writes, the styles
192
+ * are flushed so the landing becomes the before-change state, and the class
193
+ * goes back to whichever gesture still holds it.
194
+ */
195
+ const teleport = (container, fn) => {
196
+ const layer = container === null || container === void 0 ? void 0 : container.querySelector('.grafloria-html-layer');
197
+ const held = (layer === null || layer === void 0 ? void 0 : layer.classList.contains('axdb-glide')) === true;
198
+ if (held)
199
+ layer.classList.remove('axdb-glide');
200
+ try {
201
+ fn();
202
+ }
203
+ finally {
204
+ if (held && layer) {
205
+ void layer.offsetWidth; // flush: the new positions are the before-change style now
206
+ layer.classList.add('axdb-glide');
207
+ }
208
+ }
209
+ };
187
210
  let autoId = 0;
188
211
  /** The node a widget spec becomes — one place, so addWidget() and the initial
189
212
  * spec build can never drift apart on metadata. */
@@ -1305,18 +1328,20 @@ export function createDashboardHandle(ctx) {
1305
1328
  return ctx.active;
1306
1329
  },
1307
1330
  showView(id) {
1308
- var _a, _b, _c;
1309
1331
  if (!groups.has(id))
1310
1332
  return;
1311
1333
  ctx.active = id;
1312
- for (const [vid, g] of groups) {
1313
- const x = vid === id ? 0 : OFFSCREEN_X;
1314
- const s = (_a = g.size) !== null && _a !== void 0 ? _a : { width: ctx.boardW, height: ctx.boardH };
1315
- if (g.position.x !== x)
1316
- g.setFrame({ x, y: 0, width: s.width, height: s.height });
1317
- }
1318
- (_b = binders.get(id)) === null || _b === void 0 ? void 0 : _b.sync();
1319
- (_c = ctx.apiRef) === null || _c === void 0 ? void 0 : _c.renderNow();
1334
+ teleport(ctx.container, () => {
1335
+ var _a, _b, _c;
1336
+ for (const [vid, g] of groups) {
1337
+ const x = vid === id ? 0 : OFFSCREEN_X;
1338
+ const s = (_a = g.size) !== null && _a !== void 0 ? _a : { width: ctx.boardW, height: ctx.boardH };
1339
+ if (g.position.x !== x)
1340
+ g.setFrame({ x, y: 0, width: s.width, height: s.height });
1341
+ }
1342
+ (_b = binders.get(id)) === null || _b === void 0 ? void 0 : _b.sync();
1343
+ (_c = ctx.apiRef) === null || _c === void 0 ? void 0 : _c.renderNow();
1344
+ });
1320
1345
  frameView(groups.get(id));
1321
1346
  },
1322
1347
  widget(id) {
@@ -1411,7 +1436,7 @@ export function createDashboardHandle(ctx) {
1411
1436
  },
1412
1437
  getCaption: (id) => { var _a; return (_a = specById.get(id)) === null || _a === void 0 ? void 0 : _a.caption; },
1413
1438
  activateTab(containerId, pageId) {
1414
- var _a, _b, _c, _d, _e, _f, _g;
1439
+ var _a, _b, _c, _d, _e;
1415
1440
  const cg = ctx.boardGroups.get(containerId);
1416
1441
  const w = specById.get(containerId);
1417
1442
  if (!cg || !w || ((_a = ctx.layoutOf.get(containerId)) !== null && _a !== void 0 ? _a : w.layout) !== 'tabs')
@@ -1424,9 +1449,12 @@ export function createDashboardHandle(ctx) {
1424
1449
  w.active = pageId;
1425
1450
  const cw = (_c = cg.getMetadata('containerWidget')) !== null && _c !== void 0 ? _c : {};
1426
1451
  cg.setMetadata('containerWidget', Object.assign(Object.assign({}, cw), { active: pageId }));
1427
- (_d = ctx.syncTabs) === null || _d === void 0 ? void 0 : _d.call(ctx, containerId);
1428
- (_e = ctx.apiRef) === null || _e === void 0 ? void 0 : _e.renderNow();
1429
- (_f = ctx.onTabChange) === null || _f === void 0 ? void 0 : _f.call(ctx, containerId, pageId, (_g = ctx.viewOfBoard.get(containerId)) !== null && _g !== void 0 ? _g : ctx.active);
1452
+ teleport(ctx.container, () => {
1453
+ var _a, _b;
1454
+ (_a = ctx.syncTabs) === null || _a === void 0 ? void 0 : _a.call(ctx, containerId);
1455
+ (_b = ctx.apiRef) === null || _b === void 0 ? void 0 : _b.renderNow();
1456
+ });
1457
+ (_d = ctx.onTabChange) === null || _d === void 0 ? void 0 : _d.call(ctx, containerId, pageId, (_e = ctx.viewOfBoard.get(containerId)) !== null && _e !== void 0 ? _e : ctx.active);
1430
1458
  reportChanged();
1431
1459
  return true;
1432
1460
  },
@@ -1922,6 +1950,7 @@ export function dashboard(options) {
1922
1950
  optionsBase: options,
1923
1951
  active: (_p = (_o = views[0]) === null || _o === void 0 ? void 0 : _o.id) !== null && _p !== void 0 ? _p : 'main',
1924
1952
  apiRef: null,
1953
+ container: null,
1925
1954
  onLayoutChange: options.onLayoutChange,
1926
1955
  };
1927
1956
  const { binders, groups } = ctx;
@@ -1936,24 +1965,25 @@ export function dashboard(options) {
1936
1965
  }, get handle() {
1937
1966
  return handle;
1938
1967
  }, finalize: (api) => {
1939
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
1968
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
1940
1969
  const a = api;
1941
1970
  if (!a)
1942
1971
  return;
1943
1972
  ctx.apiRef = a;
1973
+ ctx.container = (_a = a.container) !== null && _a !== void 0 ? _a : null;
1944
1974
  const model = a.getModel();
1945
1975
  // FLUID: the board starts at the container's box when it can be measured
1946
1976
  // (the binder keeps it there); the authored defaults only fill in for a
1947
1977
  // container with no size yet.
1948
1978
  const box = mode === 'fluid'
1949
- ? { w: ((_a = a.container) === null || _a === void 0 ? void 0 : _a.clientWidth) || 0, h: ((_b = a.container) === null || _b === void 0 ? void 0 : _b.clientHeight) || 0 }
1979
+ ? { w: ((_b = a.container) === null || _b === void 0 ? void 0 : _b.clientWidth) || 0, h: ((_c = a.container) === null || _c === void 0 ? void 0 : _c.clientHeight) || 0 }
1950
1980
  : { w: 0, h: 0 };
1951
1981
  const viewW = (v) => { var _a; return (mode === 'fluid' && box.w > 0 ? box.w : (_a = v.width) !== null && _a !== void 0 ? _a : boardW); };
1952
1982
  const viewH = (v) => { var _a; return (mode === 'fluid' && box.h > 0 ? box.h : (_a = v.height) !== null && _a !== void 0 ? _a : boardH); };
1953
1983
  for (const v of views) {
1954
1984
  // One board per view; the group is a pure LAYOUT CONTAINER, so its
1955
1985
  // chrome is suppressed (frameChrome) exactly as a dashboard needs.
1956
- const g = new GroupModel({ id: v.id, name: (_c = v.name) !== null && _c !== void 0 ? _c : v.id });
1986
+ const g = new GroupModel({ id: v.id, name: (_d = v.name) !== null && _d !== void 0 ? _d : v.id });
1957
1987
  model.addGroup(g);
1958
1988
  g.setMetadata('frameChrome', 'none');
1959
1989
  // THE BOARD'S GEOMETRY, PERSISTED. Cells alone do not describe a board:
@@ -1963,28 +1993,28 @@ export function dashboard(options) {
1963
1993
  // reload would be a picture of a dashboard rather than a dashboard.
1964
1994
  // Serializable values only: the binder's callbacks belong to the app.
1965
1995
  g.setMetadata('dashboardBoard', {
1966
- columns: (_d = v.columns) !== null && _d !== void 0 ? _d : columns,
1996
+ columns: (_e = v.columns) !== null && _e !== void 0 ? _e : columns,
1967
1997
  gap,
1968
1998
  padding: gap,
1969
1999
  sizing,
1970
2000
  baseRowHeight: rowHeight,
1971
2001
  designHeight: viewH(v),
1972
- float: (_e = options.float) !== null && _e !== void 0 ? _e : false,
1973
- rtl: (_f = options.rtl) !== null && _f !== void 0 ? _f : false,
2002
+ float: (_f = options.float) !== null && _f !== void 0 ? _f : false,
2003
+ rtl: (_g = options.rtl) !== null && _g !== void 0 ? _g : false,
1974
2004
  fluid: mode === 'fluid',
1975
2005
  overflow,
1976
- static: (_g = options.static) !== null && _g !== void 0 ? _g : false,
1977
- dragHandle: (_h = options.dragHandle) !== null && _h !== void 0 ? _h : false,
1978
- layout: (_j = ctx.layoutOf.get(v.id)) !== null && _j !== void 0 ? _j : layout,
2006
+ static: (_h = options.static) !== null && _h !== void 0 ? _h : false,
2007
+ dragHandle: (_j = options.dragHandle) !== null && _j !== void 0 ? _j : false,
2008
+ layout: (_k = ctx.layoutOf.get(v.id)) !== null && _k !== void 0 ? _k : layout,
1979
2009
  });
1980
- if (((_k = ctx.layoutOf.get(v.id)) !== null && _k !== void 0 ? _k : layout) === 'split' && v.tree !== undefined)
2010
+ if (((_l = ctx.layoutOf.get(v.id)) !== null && _l !== void 0 ? _l : layout) === 'split' && v.tree !== undefined)
1981
2011
  g.setMetadata(SPLIT_TREE_KEY, v.tree);
1982
2012
  g.size = { width: viewW(v), height: viewH(v), depth: 0 };
1983
2013
  g.position = { x: v.id === ctx.active ? 0 : OFFSCREEN_X, y: 0 };
1984
2014
  groups.set(v.id, g);
1985
2015
  ctx.boardGroups.set(v.id, g);
1986
2016
  mountBoard(v.id, v.id, v.widgets, g);
1987
- binders.set(v.id, bindView(v, g, (_l = ctx.layoutOf.get(v.id)) !== null && _l !== void 0 ? _l : layout));
2017
+ binders.set(v.id, bindView(v, g, (_m = ctx.layoutOf.get(v.id)) !== null && _m !== void 0 ? _m : layout));
1988
2018
  }
1989
2019
  // LIVE LAYOUT SWITCH (setLayout): keep the picture, swap the binder.
1990
2020
  // Either way the outgoing binder's cells are written where the grid
@@ -2100,8 +2130,8 @@ export function dashboard(options) {
2100
2130
  // runs. That cost me a debugging round: `ctx.syncTabs` stayed undefined
2101
2131
  // and every tab switch silently did nothing.)
2102
2132
  ctx.onTabChange = options.onTabChange;
2103
- attachTabsRuntime(ctx, model, (_m = a.container) !== null && _m !== void 0 ? _m : null, handle);
2104
- (_o = ctx.attachHistory) === null || _o === void 0 ? void 0 : _o.call(ctx);
2133
+ attachTabsRuntime(ctx, model, (_o = a.container) !== null && _o !== void 0 ? _o : null, handle);
2134
+ (_p = ctx.attachHistory) === null || _p === void 0 ? void 0 : _p.call(ctx);
2105
2135
  return;
2106
2136
  /**
2107
2137
  * Mount one board's widgets into its group — and recurse for CONTAINERS.
@@ -3038,6 +3038,12 @@ export function bindDashboardGrid(api, group, options = {}) {
3038
3038
  }
3039
3039
  finally {
3040
3040
  flushDeferredRebuild();
3041
+ // EVERY press ends disarmed. The press armed the glide (a task ahead
3042
+ // of any displacement — see onPointerDown); a drop disarms on its own
3043
+ // path, but a press that never travelled — a plain CLICK — left it
3044
+ // armed for good, and every later left/top write eased: a tab switch
3045
+ // slid its page in from 20,000 px off canvas over 280 ms (0.4.36).
3046
+ disarmGlideSoon();
3041
3047
  }
3042
3048
  },
3043
3049
  onCancel() {
@@ -3047,6 +3053,7 @@ export function bindDashboardGrid(api, group, options = {}) {
3047
3053
  }
3048
3054
  finally {
3049
3055
  flushDeferredRebuild();
3056
+ disarmGlideSoon();
3050
3057
  }
3051
3058
  },
3052
3059
  };
@@ -21,5 +21,7 @@
21
21
  * (pages own their card innerHTML).
22
22
  */
23
23
  export declare const DASHBOARD_KIT_STYLE_ID = "grafloria-dashboard-kit-styles";
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 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/* 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";
24
26
  /** Idempotently inject the kit stylesheet (safe to call per binder). */
25
27
  export declare function ensureDashboardKitStyles(doc?: Document): void;
@@ -37,7 +37,7 @@ const CSS = `
37
37
  .grafloria-html-layer > .grafloria-node-host:focus-visible {
38
38
  outline: 2px solid var(--axdb-accent, #3b52d9);
39
39
  outline-offset: 2px;
40
- border-radius: var(--axdb-rs-radius, 3px);
40
+ border-radius: var(--axdb-rs-radius, 8px);
41
41
  }
42
42
 
43
43
  /* ===== the held tile: transition-exempt ghost, above everything ===== */
@@ -55,7 +55,7 @@ const CSS = `
55
55
  /* ===== the placeholder: dashed slab, truthful, never animated ===== */
56
56
  .grafloria-html-layer > .axdb-ph {
57
57
  position: absolute;
58
- border-radius: var(--axdb-rs-radius, 3px);
58
+ border-radius: var(--axdb-rs-radius, 8px);
59
59
  background: rgba(30, 34, 45, .14);
60
60
  border: 2px dashed rgba(30, 34, 45, .28);
61
61
  box-sizing: border-box;
@@ -80,7 +80,7 @@ const CSS = `
80
80
  cursor: nwse-resize;
81
81
  border-right: 3px solid rgba(120, 130, 148, .55);
82
82
  border-bottom: 3px solid rgba(120, 130, 148, .55);
83
- border-bottom-right-radius: var(--axdb-rs-radius, 3px);
83
+ border-bottom-right-radius: var(--axdb-rs-radius, 8px);
84
84
  opacity: 0;
85
85
  transition: opacity .12s;
86
86
  z-index: 5;
@@ -98,7 +98,7 @@ const CSS = `
98
98
  border-right: none;
99
99
  border-left: 3px solid rgba(120, 130, 148, .55);
100
100
  border-bottom-right-radius: 0;
101
- border-bottom-left-radius: var(--axdb-rs-radius, 3px);
101
+ border-bottom-left-radius: var(--axdb-rs-radius, 8px);
102
102
  }
103
103
 
104
104
  /* ===== palette drag-in chip (screen-space clone following the cursor) ===== */
@@ -125,14 +125,14 @@ const CSS = `
125
125
  font: 500 12px/1.4 var(--axdb-font, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
126
126
  color: var(--axdb-tabs-on-fg, #1f2430);
127
127
  background: var(--axdb-tabs-on-bg, #fff);
128
- border-radius: var(--axdb-rs-radius, 3px);
129
- box-shadow: 0 0 0 1px rgba(31, 36, 48, .12);
128
+ border-radius: var(--axdb-tab-radius, 6px);
129
+ box-shadow: 0 1px 2px rgba(16, 24, 40, .1), 0 0 0 1px rgba(31, 36, 48, .08);
130
130
  }
131
131
  @media (prefers-color-scheme: dark) {
132
132
  .axdb-tab-chip {
133
133
  color: var(--axdb-tabs-on-fg, #eceef4);
134
- background: var(--axdb-tabs-on-bg, #1a1d25);
135
- box-shadow: 0 0 0 1px rgba(236, 238, 244, .16);
134
+ background: var(--axdb-tabs-on-bg, #232836);
135
+ box-shadow: 0 1px 2px rgba(0, 0, 0, .45), 0 0 0 1px rgba(236, 238, 244, .12);
136
136
  }
137
137
  }
138
138
  /* A tab held over ANOTHER group: it will join that group, not the board. The
@@ -145,7 +145,7 @@ const CSS = `
145
145
  box-sizing: border-box;
146
146
  border: 2px dashed var(--axdb-accent, #3b52d9);
147
147
  background: var(--axdb-accent-soft, rgba(59, 82, 217, .08));
148
- border-radius: var(--axdb-rs-radius, 3px);
148
+ border-radius: var(--axdb-rs-radius, 8px);
149
149
  }
150
150
  .axdb-tabs.axdb-tabs--drop { box-shadow: inset 0 -2px 0 var(--axdb-accent, #3b52d9); }
151
151
  .axdb-tab.axdb-tab--drop-before { position: relative; }
@@ -173,7 +173,7 @@ const CSS = `
173
173
  --axdb-muted: #5a6478;
174
174
  --axdb-grid: rgba(120, 130, 148, .22);
175
175
  --axdb-card: #fff;
176
- --axdb-line: #e7eaf1;
176
+ --axdb-line: #e5e8ef;
177
177
  --axdb-soft: rgba(120, 130, 148, .14);
178
178
  --axdb-up: #0f7a3d; /* 5.42:1 */
179
179
  --axdb-down: #be123c; /* 6.29:1 */
@@ -181,17 +181,22 @@ const CSS = `
181
181
  steps: every entry clears 3:1 against its card (WCAG 1.4.11). */
182
182
  --axdb-c1: #3b52d9; --axdb-c2: #0369a1; --axdb-c3: #0f766e;
183
183
  --axdb-c4: #b45309; --axdb-c5: #6d28d9; --axdb-c6: #475569;
184
+ /* The header band: a panel wears its title in a band across its top, the
185
+ way DevExpress, Grafana and Metabase panels do — tinted ground, hairline
186
+ below, sentence case (0.4.36; the tracked uppercase micro-label before). */
187
+ --axdb-head-bg: #f7f8fb;
188
+ --axdb-head-fg: #2a3140;
184
189
  box-sizing: border-box;
185
190
  width: 100%;
186
191
  height: 100%;
187
192
  display: flex;
188
193
  flex-direction: column;
189
194
  overflow: hidden;
190
- padding: 13px 15px 12px;
195
+ padding: 0;
191
196
  background: var(--axdb-card);
192
197
  border: 1px solid var(--axdb-line);
193
- border-radius: var(--axdb-rs-radius, 3px);
194
- box-shadow: 0 1px 2px rgba(16, 24, 40, .05), 0 1px 3px rgba(16, 24, 40, .05);
198
+ border-radius: var(--axdb-rs-radius, 8px);
199
+ box-shadow: 0 1px 2px rgba(16, 24, 40, .04);
195
200
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
196
201
  color: var(--axdb-ink);
197
202
  }
@@ -203,25 +208,33 @@ const CSS = `
203
208
  below and would lose the cascade at equal specificity — at 54 px the margin
204
209
  stayed 8 px, the body shrank to 14 px and the KPI figure hid. */
205
210
  @container axdb-tile (max-height: 90px) {
206
- .axdb-widget { padding: 8px 14px 7px; }
207
- .axdb-widget > .axdb-widget-h { margin-bottom: 4px; }
211
+ .axdb-widget > .axdb-widget-h { padding: 5px 12px; font-size: 11.5px; }
212
+ .axdb-widget > .axdb-widget-b { padding: 6px 12px; }
208
213
  }
214
+ /* The row floor: the band gives way to an inline label beside the figure. */
209
215
  @container axdb-tile (max-height: 46px) {
210
- .axdb-widget { padding: 4px 12px 3px; }
211
- .axdb-widget > .axdb-widget-h { margin-bottom: 0; }
216
+ .axdb-widget > .axdb-widget-h { padding: 0 0 0 12px; background: none; border-bottom: 0; }
217
+ .axdb-widget > .axdb-widget-b { padding: 3px 12px; }
212
218
  }
213
219
  @container axdb-tile (max-height: 26px) {
214
- .axdb-widget { padding: 2px 12px 1px; }
220
+ .axdb-widget > .axdb-widget-h { display: none; }
221
+ .axdb-widget > .axdb-widget-b { padding: 1px 12px; }
215
222
  }
216
223
  .axdb-widget-h {
217
224
  display: flex;
218
225
  align-items: center;
219
- gap: 6px;
220
- margin-bottom: 8px;
221
- font: 600 11px/1.3 system-ui, sans-serif;
222
- letter-spacing: .4px;
223
- text-transform: uppercase;
224
- color: var(--axdb-muted);
226
+ gap: 8px;
227
+ flex: none;
228
+ min-width: 0;
229
+ padding: 9px 14px;
230
+ background: var(--axdb-head-bg);
231
+ border-bottom: 1px solid var(--axdb-line);
232
+ font: 600 12.5px/1.3 system-ui, -apple-system, "Segoe UI", sans-serif;
233
+ letter-spacing: -.005em;
234
+ color: var(--axdb-head-fg);
235
+ white-space: nowrap;
236
+ overflow: hidden;
237
+ text-overflow: ellipsis;
225
238
  }
226
239
  /* Drag-handle mode (DevExpress drags an item by its caption): the header is
227
240
  the grip and says so with a dot pattern and a grab cursor; the body keeps
@@ -261,7 +274,7 @@ const CSS = `
261
274
  .grafloria-node-host[data-axdb-edge="nesw-resize"], .grafloria-node-host[data-axdb-edge="nesw-resize"] * { cursor: nesw-resize !important; }
262
275
  /* INSIDE: centred on the header's text line and flush with the card padding,
263
276
  per size tier (padding 13/15, then 8/14, 4/12, 2/12). */
264
- .grafloria-node-host > .axdb-grip--inside { top: 14px; }
277
+ .grafloria-node-host > .axdb-grip--inside { top: 12px; }
265
278
  .grafloria-node-host > .axdb-grip--inside.axdb-grip--left { left: 15px; }
266
279
  .grafloria-node-host > .axdb-grip--inside.axdb-grip--right { right: 15px; }
267
280
  .grafloria-node-host > .axdb-grip--inside.axdb-grip--center { top: 3px; }
@@ -270,12 +283,18 @@ const CSS = `
270
283
  .grafloria-node-host > .axdb-grip--inside.axdb-grip--left { left: 14px; }
271
284
  .grafloria-node-host > .axdb-grip--inside.axdb-grip--right { right: 14px; }
272
285
  .grafloria-node-host > .axdb-grip--inside.axdb-grip--center { top: 2px; }
286
+ .grafloria-node-host.axdb-gp-inside.axdb-gp-left .axdb-widget > .axdb-widget-h { padding-left: 46px; }
287
+ .grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 46px; }
288
+ .grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 15px; }
273
289
  }
274
290
  @container axdb-tile (max-height: 46px) {
275
291
  .grafloria-node-host > .axdb-grip--inside { top: 5px; }
276
292
  .grafloria-node-host > .axdb-grip--inside.axdb-grip--left { left: 12px; }
277
293
  .grafloria-node-host > .axdb-grip--inside.axdb-grip--right { right: 12px; }
278
294
  .grafloria-node-host > .axdb-grip--inside.axdb-grip--center { top: 1px; }
295
+ .grafloria-node-host.axdb-gp-inside.axdb-gp-left .axdb-widget > .axdb-widget-h { padding-left: 44px; }
296
+ .grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 44px; }
297
+ .grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 13px; }
279
298
  }
280
299
  @container axdb-tile (max-height: 26px) {
281
300
  .grafloria-node-host > .axdb-grip--inside { top: 2px; }
@@ -288,11 +307,13 @@ const CSS = `
288
307
  .grafloria-node-host > .axdb-grip--outside.axdb-grip--left { left: 10px; }
289
308
  .grafloria-node-host > .axdb-grip--outside.axdb-grip--right { right: 10px; }
290
309
  .grafloria-node-host > .axdb-grip--center { left: 50%; transform: translateX(-50%); }
291
- /* The header makes room for an inside grip on its side; a centred one sits above it. */
292
- .grafloria-node-host.axdb-gp-inside.axdb-gp-left .axdb-widget > .axdb-widget-h { padding-left: 32px; }
293
- .grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 32px; }
294
- .grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 12px; }
295
- .axdb-widget-b { flex: 1; min-height: 0; position: relative; }
310
+ /* The header makes room for an inside grip on its side inset + the 24-px
311
+ grip + an 8-px gap, per tier (32 px under the band's 14-px inset hid the
312
+ first letter of the title) and a centred one sits above the text. */
313
+ .grafloria-node-host.axdb-gp-inside.axdb-gp-left .axdb-widget > .axdb-widget-h { padding-left: 47px; }
314
+ .grafloria-node-host.axdb-gp-inside.axdb-gp-right .axdb-widget > .axdb-widget-h { padding-right: 47px; }
315
+ .grafloria-node-host.axdb-gp-inside.axdb-gp-center .axdb-widget > .axdb-widget-h { padding-top: 17px; }
316
+ .axdb-widget-b { flex: 1; min-height: 0; position: relative; padding: 12px 14px; }
296
317
  /* A drag across a STATIC board (nothing prevents the press's default there)
297
318
  used to select every label on it; kit cards are not prose. Tables stay
298
319
  copyable — a figure in a grid is the one thing a viewer selects. */
@@ -358,7 +379,7 @@ const CSS = `
358
379
  when the tile is wide enough. */
359
380
  @container axdb-tile (max-height: 46px) {
360
381
  .axdb-widget--kpi { flex-direction: row; align-items: center; gap: 12px; }
361
- .axdb-widget--kpi > .axdb-widget-h { flex: none; }
382
+ .axdb-widget--kpi > .axdb-widget-h { flex: none; padding: 0 0 0 12px; background: none; border-bottom: 0; }
362
383
  /* The body is a SIZE container: in a centred row it would collapse to zero
363
384
  and every height query would fire. It takes the card's full inner height. */
364
385
  .axdb-widget--kpi > .axdb-kpi { flex: 1; align-self: stretch; }
@@ -403,7 +424,7 @@ const CSS = `
403
424
 
404
425
  /* SECTION CHROME: a pointer-transparent overlay on every member group. It
405
426
  wears the selection ring and, while selected, the corner handle. */
406
- .grafloria-html-layer > .axdb-slab { position: absolute; pointer-events: none; border-radius: var(--axdb-rs-radius, 3px); z-index: 4; }
427
+ .grafloria-html-layer > .axdb-slab { position: absolute; pointer-events: none; border-radius: var(--axdb-rs-radius, 8px); z-index: 4; }
407
428
  /* Selected, the overlay rises above the tiles so ITS corner handle wins a
408
429
  corner it shares with a child's; unselected, its handle takes no presses. */
409
430
  .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)); }
@@ -418,16 +439,16 @@ const CSS = `
418
439
  .grafloria-html-layer > .axdb-slab > .axdb-slab-h {
419
440
  position: absolute; box-sizing: border-box; pointer-events: auto; z-index: 1;
420
441
  display: flex; align-items: center; gap: 6px; min-width: 0;
421
- padding: var(--axdb-caption-pad, 0 10px);
422
- background: var(--axdb-caption-bg, rgba(31, 36, 48, .055));
442
+ padding: var(--axdb-caption-pad, 0 12px);
443
+ background: var(--axdb-caption-bg, rgba(31, 36, 48, .045));
423
444
  /* A hairline by default: two bands meeting (a captioned section inside a
424
445
  captioned section) read as two headers, not one grey block. */
425
446
  border-bottom: var(--axdb-caption-border, 1px solid rgba(31, 36, 48, .1));
426
447
  color: var(--axdb-caption-fg, #1f2430);
427
448
  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);
428
449
  text-transform: var(--axdb-caption-transform, none);
429
- letter-spacing: .01em;
430
- border-radius: var(--axdb-rs-radius, 3px) var(--axdb-rs-radius, 3px) 0 0;
450
+ letter-spacing: -.005em;
451
+ border-radius: var(--axdb-rs-radius, 8px) var(--axdb-rs-radius, 8px) 0 0;
431
452
  cursor: default; user-select: none; -webkit-user-select: none; overflow: hidden;
432
453
  transition: opacity .12s;
433
454
  }
@@ -439,7 +460,7 @@ const CSS = `
439
460
  /* 'tab': the same band sized to its text, a chip at the leading corner. It
440
461
  reserves its height like 'inside' — see captionReserve. */
441
462
  .grafloria-html-layer > .axdb-slab > .axdb-slab-h.axdb-slab-h--tab {
442
- max-width: 100%; border-radius: var(--axdb-rs-radius, 3px);
463
+ max-width: 100%; border-radius: var(--axdb-rs-radius, 8px);
443
464
  border-bottom: var(--axdb-caption-border-tab, none);
444
465
  box-shadow: inset 0 0 0 1px rgba(31, 36, 48, .08);
445
466
  }
@@ -463,14 +484,14 @@ const CSS = `
463
484
  .axdb-slab-h-icon { flex: none; }
464
485
  .axdb-slab-h-body { display: flex; flex-direction: column; justify-content: center; min-width: 0; flex: 0 1 auto; }
465
486
  .axdb-slab-h-text, .axdb-slab-h-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
466
- .axdb-slab-h-sub { font-size: .85em; font-weight: 500; opacity: .72; }
487
+ .axdb-slab-h-sub { font-size: .88em; font-weight: 500; opacity: .7; }
467
488
  .axdb-slab-h-info { flex: none; opacity: .6; font-size: .9em; cursor: help; }
468
489
  .axdb-slab-h-actions { flex: none; display: flex; gap: 2px; margin-inline-start: auto; opacity: 0; transition: opacity .12s; }
469
490
  .axdb-slab-h--center .axdb-slab-h-actions, .axdb-slab-h--end .axdb-slab-h-actions { margin-inline-start: 0; }
470
491
  .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; }
471
492
  .axdb-slab-h-action {
472
- all: unset; box-sizing: border-box; width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center;
473
- border-radius: 4px; cursor: pointer; font-size: 14px; line-height: 1; color: inherit;
493
+ all: unset; box-sizing: border-box; width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
494
+ border-radius: 6px; cursor: pointer; font-size: 14px; line-height: 1; color: inherit; transition: background-color .12s;
474
495
  }
475
496
  .axdb-slab-h-action:hover { background: rgba(31, 36, 48, .08); }
476
497
  .axdb-slab-h-action:focus-visible { outline: 2px solid var(--axdb-accent-ring, rgba(59, 82, 217, .55)); outline-offset: -2px; }
@@ -478,8 +499,11 @@ const CSS = `
478
499
  .axdb-slab-h-action[disabled]:hover { background: none; }
479
500
  .grafloria-html-layer > .axdb-slab.axdb-slab--static > .axdb-slab-h { cursor: default; }
480
501
  @media (prefers-color-scheme: dark) {
502
+ /* The band sits on the PAGE, whose ground the kit does not own (a host that
503
+ stays light while the kit goes dark is common): a solid ink, not a tint,
504
+ so the light caption always reads. */
481
505
  .grafloria-html-layer > .axdb-slab > .axdb-slab-h {
482
- background: var(--axdb-caption-bg, rgba(236, 238, 244, .07));
506
+ background: var(--axdb-caption-bg, #1b2029);
483
507
  color: var(--axdb-caption-fg, #eceef4);
484
508
  border-bottom: var(--axdb-caption-border, 1px solid rgba(236, 238, 244, .12));
485
509
  }
@@ -493,36 +517,42 @@ const CSS = `
493
517
  ordinary boards. */
494
518
  .grafloria-html-layer > .axdb-tabs {
495
519
  position: absolute; box-sizing: border-box; pointer-events: auto; z-index: 5;
496
- display: flex; align-items: flex-end; gap: 2px; padding: 0 6px; overflow-x: auto; overflow-y: hidden;
497
- background: var(--axdb-tabs-bg, rgba(31, 36, 48, .05));
498
- border-bottom: 1px solid var(--axdb-tabs-line, rgba(31, 36, 48, .12));
499
- border-radius: var(--axdb-rs-radius, 3px) var(--axdb-rs-radius, 3px) 0 0;
520
+ display: flex; align-items: center; gap: 2px; padding: 3px 6px; overflow-x: auto; overflow-y: hidden;
521
+ background: var(--axdb-tabs-bg, #f1f3f8);
522
+ border-bottom: 1px solid var(--axdb-tabs-line, #e5e8ef);
523
+ border-radius: var(--axdb-rs-radius, 8px) var(--axdb-rs-radius, 8px) 0 0;
500
524
  scrollbar-width: thin;
501
525
  }
502
526
  .axdb-tabs--center { justify-content: center; }
503
527
  .axdb-tabs--end { justify-content: flex-end; }
504
528
  .axdb-tabs--stretch > .axdb-tab { flex: 1 1 0; }
529
+ /* A tab is a PILL on the track (a segmented control): quiet until hovered,
530
+ the active one lifted in the card's own ground. */
505
531
  .axdb-tab {
506
532
  all: unset; box-sizing: border-box; flex: 0 0 auto; max-width: 200px;
507
- padding: 0 12px; height: calc(100% - 4px); display: inline-flex; align-items: center;
508
- font: 600 12px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
533
+ padding: 0 12px; height: 100%; display: inline-flex; align-items: center;
534
+ font: 500 12.5px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
535
+ letter-spacing: -.005em;
509
536
  color: var(--axdb-tabs-fg, #5a6478); cursor: pointer;
510
- border-radius: var(--axdb-rs-radius, 3px) var(--axdb-rs-radius, 3px) 0 0;
537
+ border-radius: var(--axdb-tab-radius, 6px);
511
538
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
539
+ transition: background-color .12s, color .12s, box-shadow .12s;
512
540
  }
513
- .axdb-tab:hover { background: rgba(31, 36, 48, .06); }
541
+ .axdb-tab:hover { background: rgba(31, 36, 48, .06); color: var(--axdb-tabs-on-fg, #1f2430); }
514
542
  .axdb-tab:focus-visible { outline: 2px solid var(--axdb-accent-ring, rgba(59, 82, 217, .55)); outline-offset: -2px; }
515
- /* The active tab reads as the front page: the card's own ground, lifted. */
516
543
  .axdb-tab.axdb-tab--on {
517
544
  background: var(--axdb-tabs-on-bg, #fff);
518
545
  color: var(--axdb-tabs-on-fg, #1f2430);
519
- box-shadow: 0 -1px 0 var(--axdb-accent, #3b52d9) inset, 0 0 0 1px rgba(31, 36, 48, .1);
546
+ font-weight: 600;
547
+ box-shadow: 0 1px 2px rgba(16, 24, 40, .1), 0 0 0 1px rgba(31, 36, 48, .06);
520
548
  }
549
+ @media (prefers-reduced-motion: reduce) { .axdb-tab { transition: none; } }
521
550
  @media (prefers-color-scheme: dark) {
522
- .grafloria-html-layer > .axdb-tabs { background: var(--axdb-tabs-bg, rgba(236, 238, 244, .06)); border-bottom-color: var(--axdb-tabs-line, rgba(236, 238, 244, .14)); }
551
+ /* Solid for the same reason as the caption band: the track sits on the page. */
552
+ .grafloria-html-layer > .axdb-tabs { background: var(--axdb-tabs-bg, #1b2029); border-bottom-color: var(--axdb-tabs-line, rgba(236, 238, 244, .12)); }
523
553
  .axdb-tab { color: var(--axdb-tabs-fg, #98a1b4); }
524
- .axdb-tab:hover { background: rgba(236, 238, 244, .08); }
525
- .axdb-tab.axdb-tab--on { background: var(--axdb-tabs-on-bg, #1a1d25); color: var(--axdb-tabs-on-fg, #eceef4); box-shadow: 0 -1px 0 var(--axdb-accent, #7d8ff0) inset, 0 0 0 1px rgba(236, 238, 244, .14); }
554
+ .axdb-tab:hover { background: rgba(236, 238, 244, .08); color: var(--axdb-tabs-on-fg, #eceef4); }
555
+ .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); }
526
556
  }
527
557
 
528
558
  /* legend chips, shared by line and donut */
@@ -550,8 +580,10 @@ const CSS = `
550
580
  --axdb-ink: #eceef4;
551
581
  --axdb-muted: #98a1b4;
552
582
  --axdb-grid: rgba(150, 160, 182, .2);
553
- --axdb-card: #1a1d25;
554
- --axdb-line: #2b3040;
583
+ --axdb-card: #161a22;
584
+ --axdb-line: #2a3040;
585
+ --axdb-head-bg: rgba(255, 255, 255, .035);
586
+ --axdb-head-fg: #e6e9f0;
555
587
  --axdb-soft: rgba(150, 160, 182, .16);
556
588
  --axdb-up: #4ade80;
557
589
  --axdb-down: #fb7185;
@@ -560,6 +592,8 @@ const CSS = `
560
592
  }
561
593
  }
562
594
  `;
595
+ /** The kit stylesheet as text — for a host that bundles its own CSS, and for the styling spec. */
596
+ export const DASHBOARD_KIT_CSS = CSS;
563
597
  /** Idempotently inject the kit stylesheet (safe to call per binder). */
564
598
  export function ensureDashboardKitStyles(doc) {
565
599
  // SERVER-SAFE: `dashboard()` is a pure spec builder and runs where there is
package/src/lib/load.js CHANGED
@@ -238,6 +238,7 @@ export function fromDocument(document, options = {}) {
238
238
  : {},
239
239
  active: (_x = activeGroup === null || activeGroup === void 0 ? void 0 : activeGroup.id) !== null && _x !== void 0 ? _x : 'main',
240
240
  apiRef: null,
241
+ container: null,
241
242
  };
242
243
  const handle = createDashboardHandle(ctx);
243
244
  const renderCustomNode = (node, host) => {
@@ -255,7 +256,7 @@ export function fromDocument(document, options = {}) {
255
256
  (_b = getNodeType(node.type)) === null || _b === void 0 ? void 0 : _b(node, host);
256
257
  };
257
258
  const finalize = (api) => {
258
- var _a, _b, _c, _d, _e, _f;
259
+ var _a, _b, _c, _d, _e, _f, _g;
259
260
  const a = api;
260
261
  if (!a)
261
262
  return;
@@ -296,6 +297,7 @@ export function fromDocument(document, options = {}) {
296
297
  // work immediately — no camera move, no showView, so the paint is byte-for-
297
298
  // byte what a boards-only load produced.
298
299
  ctx.apiRef = a;
300
+ ctx.container = (_e = a.container) !== null && _e !== void 0 ? _e : null;
299
301
  for (const group of groups) {
300
302
  const board = group.getMetadata('dashboardBoard');
301
303
  if (!board)
@@ -308,7 +310,7 @@ export function fromDocument(document, options = {}) {
308
310
  }
309
311
  // Tab containers, with the same runtime dashboard() uses — a reloaded
310
312
  // document must switch pages exactly like a board built from a literal.
311
- attachTabsRuntime(ctx, model, (_e = a.container) !== null && _e !== void 0 ? _e : null, handle);
313
+ attachTabsRuntime(ctx, model, (_f = a.container) !== null && _f !== void 0 ? _f : null, handle);
312
314
  // LIVE LAYOUT SWITCH on a loaded document — the same contract dashboard()
313
315
  // finalize offers: cells persisted where the grid reads them, any tree and
314
316
  // column cache cleared, the board's `layout` flag flipped, a fresh binder.
@@ -355,7 +357,7 @@ export function fromDocument(document, options = {}) {
355
357
  }
356
358
  // A loaded board follows the history exactly as an authored one does: an
357
359
  // undo re-syncs every binder without the consumer calling refresh().
358
- (_f = ctx.attachHistory) === null || _f === void 0 ? void 0 : _f.call(ctx);
360
+ (_g = ctx.attachHistory) === null || _g === void 0 ? void 0 : _g.call(ctx);
359
361
  };
360
362
  return Object.assign({ nodes, edges: model.getLinks(), renderCustomNode,
361
363
  finalize,