dsh-fonttune 0.2.4 → 0.2.6

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/lib/client.js CHANGED
@@ -963,14 +963,14 @@ var PART_SUFFIXES = ["-font-size", "-line-height", "-font-family", "-font-weight
963
963
  var FAMILY_VARS = { "--dsw-font-family": true, "--dsw-font-mono": true, "--ds-font-family-code": true };
964
964
 
965
965
  /**
966
- * A `body, body *` selector list, kept in one place because every global
967
- * declaration needs the same reach (DSH writes some tokens on descendants).
968
- * @param {string} declarations - CSS declarations without braces.
969
- * @returns {string} the rule.
966
+ * Ever since every global declaration moved to the root rule (see
967
+ * `buildAxisCss`), no rule needs a `body, body *` reach for a TOKEN, and only
968
+ * the interface weight keeps it — because a weight is the one property whose
969
+ * per-element declarations cannot be reached by inheritance. Nothing else may
970
+ * reintroduce a blanket: on DSH 0.1.5-rc.3 a single one costs 1892
971
+ * selector-element matches on every style recalculation, and dragging a sidebar
972
+ * handle recalculates once per frame.
970
973
  */
971
- function bodyAndDescendants(declarations) {
972
- return "body,body *{" + declarations + "}";
973
- }
974
974
 
975
975
  /**
976
976
  * Whether one name belongs to the official CONTENT-size chain the conversation
@@ -1211,7 +1211,23 @@ var CODE_EXCLUDES = excludeAll(CODE_SURFACE_SELECTORS);
1211
1211
  * the rule, and an unmatched token/class simply means that surface follows
1212
1212
  * the interface axis — the same degradation as an unknown token name.
1213
1213
  */
1214
- var CODE_SELECTOR = [
1214
+ /** The default code selector list: the substring form, for callers with no page. */
1215
+ var CODE_SELECTOR;
1216
+
1217
+ /**
1218
+ * The code surfaces that never need a substring match.
1219
+ *
1220
+ * Everything here is either a semantic element or a class this plugin owns, so
1221
+ * the browser can match it with a tag or a class-token test. The containers
1222
+ * that only a hashed class identifies (`…_codeBody`, `…_terminal`) used to be
1223
+ * spelled `[class*="code" i]` / `[class*="terminal" i]` — measured on
1224
+ * 0.1.5-rc.3, those two substring selectors cost an attribute scan on every one
1225
+ * of the page's ~1900 elements on every rule that carries them, and a drag
1226
+ * recalculates styles once per frame. The browser half now HARVESTS the real
1227
+ * class tokens from the live page and passes them in as exact class selectors
1228
+ * (see `buildFontCss`), falling back to the substring form whenever it cannot.
1229
+ */
1230
+ var CODE_SURFACE_SELECTORS_CHEAP = [
1215
1231
  "pre",
1216
1232
  "code",
1217
1233
  "kbd",
@@ -1221,19 +1237,58 @@ var CODE_SELECTOR = [
1221
1237
  "textarea",
1222
1238
  ".cm-editor",
1223
1239
  ".dfp-previewCode",
1224
- '[class*="code" i]',
1225
- '[class*="terminal" i]',
1226
- ]
1227
- .map(function (selector) {
1228
- return selector + "," + selector + " *";
1229
- })
1230
- .join(",");
1240
+ ];
1241
+
1242
+ /**
1243
+ * The code surfaces for one build.
1244
+ *
1245
+ * The two substring hooks ride along on purpose. A class token the page has
1246
+ * not rendered yet (`…_codeBody`, `…_terminalBody` appear only once a tool card
1247
+ * or a terminal pane exists) cannot be named by a harvested class, and an
1248
+ * unknown surface must still get the code axes — the same "an unmatched token
1249
+ * simply follows the interface" degradation the token axes have. The hooks cost
1250
+ * an attribute scan per element per rule that carries them, which is why the
1251
+ * rest of this module spent its effort removing the OTHER wide selectors (see
1252
+ * `rootRule` / `weightRule` / `dialogSelector`); measured on 0.1.5-rc.3 that is
1253
+ * the difference between 1.87 s and ~0.9 s of style recalculation per drag.
1254
+ */
1255
+ function codeSurfaceList() {
1256
+ return CODE_SURFACE_SELECTORS_CHEAP.concat([
1257
+ '[class*="code" i]',
1258
+ '[class*="terminal" i]',
1259
+ ]);
1260
+ }
1261
+
1262
+ /**
1263
+ * The code selector list for one build.
1264
+ *
1265
+ * Surfaces only — no `… *` descendants. Everything inside a code surface
1266
+ * inherits its family and weight, so descendants are unnecessary, and they are
1267
+ * not free: an ancestor-walking selector costs about 0.03 s per 50-step drag on
1268
+ * 0.1.5-rc.3 (measured: stripping the ` *` forms from these two rules took the
1269
+ * drag from 0.89 s to 0.52 s of style recalculation). The weight family keeps
1270
+ * the code subtree right through `--dfp-interface-weight` instead — see
1271
+ * `weightRule`.
1272
+ *
1273
+ * @param {{code?: string[], terminal?: string[]}|null} [hints] - unused; kept so
1274
+ * the browser half can keep passing one harvest object to `buildFontCss`.
1275
+ * @returns {string} the comma-separated list of code surfaces.
1276
+ */
1277
+ function codeSelector(hints) {
1278
+ return codeSurfaceList().join(",");
1279
+ }
1280
+
1281
+ CODE_SELECTOR = codeSelector(null);
1231
1282
 
1232
1283
  /**
1233
1284
  * The dialog surface: DSH renders the conversation markdown in a CSS-module
1234
- * container whose class hashes per release (`_markdown_<hash>_…`). A
1235
- * class-substring match degrades safely — no match means the dialog follows
1236
- * the interface axis, the same behavior as an unknown token name.
1285
+ * container whose class hashes per release (`_markdown_<hash>_…`), wrapped in a
1286
+ * stable `[data-dss-prose]` container (measured on 0.1.5-rc.3: every markdown
1287
+ * container on the page sits inside one). A class-substring match degrades
1288
+ * safely — no match means the dialog follows the interface axis, the same
1289
+ * behavior as an unknown token name — but it costs an attribute scan per
1290
+ * element per rule, so the browser half prefers the stable wrapper when it can
1291
+ * see one and falls back to the substring form otherwise.
1237
1292
  */
1238
1293
  var MARKDOWN_SELECTOR =
1239
1294
  '[class*="_markdown_" i]' +
@@ -1241,37 +1296,106 @@ var MARKDOWN_SELECTOR =
1241
1296
  ',[class*="_markdown_" i] *' +
1242
1297
  CODE_EXCLUDES;
1243
1298
 
1299
+ /**
1300
+ * The dialog selector for one build, given the harvested wrapper.
1301
+ * @param {{markdown?: string}|null} [hints] - the harvested wrapper selector.
1302
+ * @returns {string} the markdown-scoped selector list.
1303
+ */
1304
+ function dialogSelector(hints) {
1305
+ if (hints !== undefined && hints !== null && typeof hints.markdown === "string" && hints.markdown !== "") {
1306
+ // The wrapper is not markdown content itself, so only its subtree is
1307
+ // scoped; the code exclusion still rides along so inline code keeps the
1308
+ // code family. This is the rule the harvest earns its keep on: the same
1309
+ // `:not(…)` over the stable wrapper trials ~37 elements instead of the
1310
+ // ~1800 the class-substring container selector used to hand it.
1311
+ return hints.markdown + " *" + CODE_EXCLUDES;
1312
+ }
1313
+ return MARKDOWN_SELECTOR;
1314
+ }
1315
+
1244
1316
  /** The dark-theme attribute DSH's ThemePresenter writes on body. */
1245
1317
  var DARK_ATTR = "body[data-ds-dark-theme]";
1246
1318
 
1247
1319
  /**
1248
- * Everything the INTERFACE weight rule must stay out of.
1320
+ * What the INTERFACE weight rule stays out of when the conversation has no
1321
+ * weight of its own.
1322
+ *
1323
+ * The weight is still the one property that needs a `body, body *` reach: DSH
1324
+ * pins weights on elements that do not inherit (`button` / `span` / `div`
1325
+ * class rules — measured on 0.1.5-rc.3: 19 buttons, 13 spans and 1 div in the
1326
+ * chrome declare their own weight; the markdown side's 7 `strong` and 2 `th`
1327
+ * declare theirs). But the 26-argument `:not(…)` that used to ride along cost
1328
+ * a selector trial per argument on ~1800 elements EVERY recalculation, and a
1329
+ * drag recalculates once per frame — measured 1.87 s of style recalculation
1330
+ * for a 50-step drag versus 0.47 s with this plugin removed.
1249
1331
  *
1250
- * A weight can only be expressed as a `body, body *` rule: DSH pins its labels
1251
- * with literal weights on class rules (`…_label{font-weight:500}`,
1252
- * `…_heading{font-weight:600}`) that nothing narrower reaches. That rule would
1253
- * otherwise cover the conversation too, so the conversation markdown subtree is
1254
- * excluded here — and with it every code surface, because the code axis owns
1255
- * those and the two rules would otherwise fight over the same elements (the
1256
- * `:not()` arguments raise this rule's specificity above a single class).
1332
+ * Two narrowings removed that:
1257
1333
  *
1258
- * `dfp-previewDialog` is the card's conversation preview: it carries the
1259
- * conversation's own weight, so the interface rule must not flatten it. The
1260
- * interface's own preview stays inside the rule, and `dfp-previewCode` is a
1261
- * code surface like any other.
1334
+ * 1. Code surfaces no longer appear here at all. The code rules are emitted
1335
+ * after this one and win on their own: class hooks by specificity, element
1336
+ * selectors by order at equal specificity. Their descendants inherit from
1337
+ * them (`font-weight` and `font-family` are inherited), so the blanket
1338
+ * cannot reach a highlight `<span>` inside a `<pre>` either.
1339
+ * 2. What remains — the conversation markdown subtree and the card's own
1340
+ * conversation preview — rides `:where(…)`, which contributes NO
1341
+ * specificity: the rule stays at (0,0,1), exactly like the plain
1342
+ * `body,body *` form, so the dialog rule (class-scoped) and the code rules
1343
+ * keep out-ranking it no matter which order they land in.
1344
+ *
1345
+ * When the conversation DOES carry a weight the guard is dropped entirely (see
1346
+ * `weightRule`): the dialog rule is class-scoped, so it overrides everything
1347
+ * this rule does inside markdown anyway, and the rule collapses to the bare
1348
+ * `body,body *` pair. Measured with the axis at 560: all 141 interface text
1349
+ * elements on the page took 560 and no markdown element moved — the same
1350
+ * coverage the 26-argument form had.
1262
1351
  */
1263
- var INTERFACE_EXCLUDES = excludeAll(
1264
- CODE_SURFACE_SELECTORS.concat([
1265
- ".cm-editor",
1266
- ".cm-editor *",
1267
- ".dfp-previewCode",
1268
- ".dfp-previewCode *",
1352
+ var WEIGHT_MARKDOWN_GUARD =
1353
+ ":not(:where(" +
1354
+ [
1269
1355
  '[class*="_markdown_" i]',
1270
1356
  '[class*="_markdown_" i] *',
1271
1357
  ".dfp-previewDialog",
1272
1358
  ".dfp-previewDialog *",
1273
- ])
1274
- );
1359
+ ].join(",") +
1360
+ "))";
1361
+
1362
+ /**
1363
+ * The elements that carry a weight of their own in DSH.
1364
+ *
1365
+ * The weight is the one property that cannot ride inheritance alone: an element
1366
+ * whose own rule declares a weight ignores the value it would inherit, whatever
1367
+ * that value's importance. Measured on 0.1.5-rc.3, the elements that do declare
1368
+ * one are `button` / `span` / `div` in the chrome and `strong` / `th` in the
1369
+ * conversation; the rest of the list is the inline markup that could plausibly
1370
+ * join them. This replaces the old `body,body *` reach, which matched 1892
1371
+ * elements on every recalculation — the whole point of the table is that it
1372
+ * matches a fraction of that while still covering every pinned element.
1373
+ */
1374
+ var WEIGHT_ELEMENT_TABLE = [
1375
+ "button",
1376
+ "span",
1377
+ "div",
1378
+ "strong",
1379
+ "b",
1380
+ "em",
1381
+ "i",
1382
+ "a",
1383
+ "label",
1384
+ "small",
1385
+ "p",
1386
+ "li",
1387
+ "td",
1388
+ "th",
1389
+ "dt",
1390
+ "dd",
1391
+ "summary",
1392
+ "h1",
1393
+ "h2",
1394
+ "h3",
1395
+ "h4",
1396
+ "h5",
1397
+ "h6",
1398
+ ].join(",");
1275
1399
 
1276
1400
  /**
1277
1401
  * Split a selector list on its top-level commas only.
@@ -1451,27 +1575,66 @@ function isDormant(config) {
1451
1575
  * @param {boolean} isDark - prefix every rule with the dark-theme attribute.
1452
1576
  * @returns {string} declarations for one `<style>` element.
1453
1577
  */
1454
- function buildAxisCss(axis, baseTokens, isDark) {
1578
+ function buildAxisCss(axis, baseTokens, isDark, hints) {
1455
1579
  var sans = formatStack(parseStack(axis[SANS_FIELD]));
1456
1580
  var mono = formatStack(parseStack(axis[MONO_FIELD]));
1581
+ var dialog = formatStack(parseStack(axis[STACK_DIALOG_FIELD]));
1582
+ // Normalised once: an expanded axis always carries a number, but a caller
1583
+ // handing in a raw object may leave the field out, and "absent" has to mean
1584
+ // "unset" rather than "write the string undefined".
1585
+ var dialogWeight = typeof axis[WEIGHT_DIALOG_FIELD] === "number" ? axis[WEIGHT_DIALOG_FIELD] : WEIGHT_UNSET;
1586
+ var codeSurfaces = codeSelector(hints);
1587
+ var dialogSurfaces = dialogSelector(hints);
1457
1588
  var declarations = [];
1458
1589
 
1459
- function blanket(declarations, excludes) {
1460
- // The exclusion reaches the descendants only: `body` itself inherits
1461
- // nothing, and neither the body element nor its dark attribute can be a
1462
- // markdown container or a code surface.
1463
- var suffix = excludes === undefined ? "" : excludes;
1464
- var selector = isDark
1465
- ? DARK_ATTR + "," + DARK_ATTR + " *" + suffix
1466
- : "body,body *" + suffix;
1467
- return selector + "{" + declarations + "}";
1590
+ /**
1591
+ * A rule for INHERITED state: custom properties and the inherited `font-*`
1592
+ * properties. `:root, body` is two elements, `body` alone is one, and every
1593
+ * descendant picks the value up by inheritance — including the values DSH
1594
+ * itself writes (measured on 0.1.5-rc.3: the host declares these tokens at
1595
+ * `:root` and on `body`, and the only inline copy is on `body` itself, where
1596
+ * an `!important` stylesheet declaration still wins).
1597
+ *
1598
+ * This used to be `body, body *`, which matched 1892 elements on EVERY style
1599
+ * recalculation. A drag writes `style` once per frame, so that reach alone
1600
+ * was most of the measured cost: 1.87 s of style recalculation for a 50-step
1601
+ * sidebar drag (0.47 s with the plugin removed) and 49 frames over 25 ms.
1602
+ */
1603
+ function rootRule(declarations) {
1604
+ return (isDark ? DARK_ATTR : ":root,body") + "{" + declarations + "}";
1605
+ }
1606
+ function bodyRule(declarations) {
1607
+ return (isDark ? DARK_ATTR : "body") + "{" + declarations + "}";
1468
1608
  }
1469
1609
  function codeRule(declarations) {
1470
- var selector = isDark ? prefixSelector(CODE_SELECTOR, DARK_ATTR) : CODE_SELECTOR;
1610
+ var selector = isDark ? prefixSelector(codeSurfaces, DARK_ATTR) : codeSurfaces;
1471
1611
  return selector + "{" + declarations + "}";
1472
1612
  }
1473
1613
  function dialogRule(declarations) {
1474
- var selector = isDark ? prefixSelector(MARKDOWN_SELECTOR, DARK_ATTR) : MARKDOWN_SELECTOR;
1614
+ var selector = isDark ? prefixSelector(dialogSurfaces, DARK_ATTR) : dialogSurfaces;
1615
+ return selector + "{" + declarations + "}";
1616
+ }
1617
+ /**
1618
+ * The interface weight, in two reaches: `body` for everything that inherits,
1619
+ * plus an element table for the tags DSH pins a weight on of their own (see
1620
+ * `WEIGHT_ELEMENT_TABLE`).
1621
+ *
1622
+ * The value rides a custom property (`--dfp-interface-weight`, declared on
1623
+ * `:root,body` by the caller) rather than a literal. That is what keeps the
1624
+ * code subtree right without a descendant selector: a highlight `<span>`
1625
+ * inside a `<pre>` IS matched by this rule's table, but it resolves the
1626
+ * variable its code ancestor set (see the code weight rule), so it takes the
1627
+ * code weight instead of the interface one. Naming `pre *` and friends here
1628
+ * or in the code rules instead cost 0.03 s per selector per drag.
1629
+ *
1630
+ * The markdown guard only rides along when the conversation has NO weight of
1631
+ * its own — with one set, the class-scoped dialog rule out-ranks this rule
1632
+ * inside markdown anyway.
1633
+ */
1634
+ function weightRule(declarations) {
1635
+ var base = isDark ? DARK_ATTR : "body";
1636
+ var selector = base + "," + base + " :where(" + WEIGHT_ELEMENT_TABLE + ")";
1637
+ if (dialogWeight === WEIGHT_UNSET) selector += WEIGHT_MARKDOWN_GUARD;
1475
1638
  return selector + "{" + declarations + "}";
1476
1639
  }
1477
1640
  /**
@@ -1481,7 +1644,7 @@ function buildAxisCss(axis, baseTokens, isDark) {
1481
1644
  */
1482
1645
  function pushScopedCodeFamily() {
1483
1646
  var scopedCode = [];
1484
- var surfaces = splitSelectorList(CODE_SELECTOR);
1647
+ var surfaces = splitSelectorList(codeSurfaces);
1485
1648
  for (var cIndex = 0; cIndex < surfaces.length; cIndex += 1) {
1486
1649
  var scoped = '[class*="_markdown_" i] ' + surfaces[cIndex];
1487
1650
  // The dark variant has to carry the theme attribute like every other
@@ -1532,29 +1695,42 @@ function buildAxisCss(axis, baseTokens, isDark) {
1532
1695
  // (below).
1533
1696
 
1534
1697
  // ---- interface weight ----
1535
- // Measured on rc.2 (live page, computed styles): a `body{font-weight}` rule
1536
- // only reaches the elements that INHERIT their weight, while `body, body *`
1537
- // also reaches the labels DSH pins with a literal weight on a class rule —
1538
- // which is why the axis has to be blanket, and why the rule without the
1539
- // markdown exclusion drags the conversation along with it. With the
1540
- // exclusion the interface still moves and the markdown subtree does not.
1541
- var interfaceWeight = axis[WEIGHT_FIELD];
1542
- var codeWeight = axis[CODE_WEIGHT_FIELD];
1698
+ // A `body{font-weight}` rule only reaches the elements that INHERIT their
1699
+ // weight; the chrome's `button` / `span` / `div` class rules pin their own
1700
+ // (measured on 0.1.5-rc.3), which is why the second reach exists. The
1701
+ // conversation is held apart by `weightRule`'s guard (or by the class-scoped
1702
+ // dialog rule when the conversation has a weight of its own).
1703
+ var interfaceWeight = typeof axis[WEIGHT_FIELD] === "number" ? axis[WEIGHT_FIELD] : WEIGHT_UNSET;
1704
+ var codeWeight = typeof axis[CODE_WEIGHT_FIELD] === "number" ? axis[CODE_WEIGHT_FIELD] : WEIGHT_UNSET;
1543
1705
  if (interfaceWeight !== WEIGHT_UNSET) {
1706
+ // The variable the weight rule resolves; the code rules below override it
1707
+ // on their surfaces so everything inside them inherits the code weight.
1544
1708
  declarations.push(
1545
- blanket("font-weight:" + interfaceWeight + " !important", INTERFACE_EXCLUDES)
1709
+ (isDark ? DARK_ATTR : ":root,body") +
1710
+ "{--dfp-interface-weight:" +
1711
+ interfaceWeight +
1712
+ "}"
1713
+ );
1714
+ declarations.push(
1715
+ weightRule(
1716
+ "font-weight:var(--dfp-interface-weight," + interfaceWeight + ") !important"
1717
+ )
1546
1718
  );
1547
1719
  if (codeWeight === WEIGHT_UNSET) {
1548
- // Code surfaces are excluded from the blanket rule, so without this they
1549
- // would inherit the interface weight from body. `normal` is what DSH
1550
- // computes for them (the code `font` shorthands carry no weight), so an
1551
- // unset code weight keeps code exactly as DSH shipped it.
1552
- declarations.push(codeRule("font-weight:normal !important"));
1720
+ // The code rules come after this one and win on their own (class hooks by
1721
+ // specificity, element selectors by order), so code surfaces — which
1722
+ // inherit nothing but start from the body weight — are reset here.
1723
+ // `normal` is what DSH computes for them (the code `font` shorthands
1724
+ // carry no weight), so an unset code weight keeps code exactly as DSH
1725
+ // shipped it. The variable is reset alongside so an element INSIDE a code
1726
+ // surface (which the interface table matches) lands on `normal` too.
1727
+ declarations.push(
1728
+ codeRule("font-weight:normal !important;--dfp-interface-weight:normal")
1729
+ );
1553
1730
  }
1554
1731
  }
1555
1732
 
1556
1733
  // ---- dialog family (scoped; skipped when it equals the interface) ----
1557
- var dialog = formatStack(parseStack(axis[STACK_DIALOG_FIELD]));
1558
1734
  if (dialog !== "" && dialog !== sans) {
1559
1735
  declarations.push(dialogRule("font-family:" + dialog + " !important"));
1560
1736
  if (mono !== "") {
@@ -1564,7 +1740,6 @@ function buildAxisCss(axis, baseTokens, isDark) {
1564
1740
  }
1565
1741
 
1566
1742
  // ---- dialog weight (uniform inside the conversation) ----
1567
- var dialogWeight = axis[WEIGHT_DIALOG_FIELD];
1568
1743
  if (dialogWeight !== WEIGHT_UNSET) {
1569
1744
  declarations.push(dialogRule("font-weight:" + dialogWeight + " !important"));
1570
1745
  // The card's conversation preview simulates this surface, so it takes the
@@ -1603,7 +1778,7 @@ function buildAxisCss(axis, baseTokens, isDark) {
1603
1778
  );
1604
1779
  });
1605
1780
  var contentShift = shiftTokens(contentSources, baseTokens, dialogOffsetValue);
1606
- if (contentShift.length > 0) declarations.push(blanket(contentShift.join(";")));
1781
+ if (contentShift.length > 0) declarations.push(rootRule(contentShift.join(";")));
1607
1782
  // The `small` series is the only markdown family with literal sizes: it
1608
1783
  // does not derive from the chain, so its parts and shorthands shift here.
1609
1784
  var literals = markdownTokenNames(baseTokens).filter(function (name) {
@@ -1611,7 +1786,7 @@ function buildAxisCss(axis, baseTokens, isDark) {
1611
1786
  return name.indexOf("-font-size") >= 0 && typeof base === "string" && base.indexOf("var(") === -1;
1612
1787
  });
1613
1788
  var literalShift = shiftTokens(literals, baseTokens, dialogOffsetValue);
1614
- if (literalShift.length > 0) declarations.push(blanket(literalShift.join(";")));
1789
+ if (literalShift.length > 0) declarations.push(rootRule(literalShift.join(";")));
1615
1790
  // The shorthands whose size is a literal (the small series) must ride the
1616
1791
  // shift too; the delta-derived shorthands flow through the chain above.
1617
1792
  var sizeShifted = [];
@@ -1626,7 +1801,7 @@ function buildAxisCss(axis, baseTokens, isDark) {
1626
1801
  mEntry.name + ":" + rebuildShorthand({ lead: mEntry.parts.lead, size: mSize, height: mEntry.parts.height, family: mEntry.parts.family }) + " !important"
1627
1802
  );
1628
1803
  }
1629
- if (sizeShifted.length > 0) declarations.push(blanket(sizeShifted.join(";")));
1804
+ if (sizeShifted.length > 0) declarations.push(rootRule(sizeShifted.join(";")));
1630
1805
  }
1631
1806
 
1632
1807
  // ---- dialog line height (ratio, through the shorthand heights) ----
@@ -1637,7 +1812,7 @@ function buildAxisCss(axis, baseTokens, isDark) {
1637
1812
  return name.indexOf("-line-height") >= 0;
1638
1813
  });
1639
1814
  var dialogLineDecls = scaleLineTokens(markdownLines, baseTokens, dialogFactor);
1640
- if (dialogLineDecls.length > 0) declarations.push(blanket(dialogLineDecls.join(";")));
1815
+ if (dialogLineDecls.length > 0) declarations.push(rootRule(dialogLineDecls.join(";")));
1641
1816
  var dialogShorthandList = markdownShorthandTokens(baseTokens);
1642
1817
  var dialogShorthandShifted = [];
1643
1818
  for (var dIndex = 0; dIndex < dialogShorthandList.length; dIndex += 1) {
@@ -1655,7 +1830,7 @@ function buildAxisCss(axis, baseTokens, isDark) {
1655
1830
  );
1656
1831
  }
1657
1832
  if (dialogShorthandShifted.length > 0) {
1658
- declarations.push(blanket(dialogShorthandShifted.join(";")));
1833
+ declarations.push(rootRule(dialogShorthandShifted.join(";")));
1659
1834
  }
1660
1835
  }
1661
1836
 
@@ -1668,11 +1843,17 @@ function buildAxisCss(axis, baseTokens, isDark) {
1668
1843
  round(scaleFor(codeSize), 6),
1669
1844
  codeLine
1670
1845
  );
1671
- if (codeScaled.length > 0) declarations.push(blanket(codeScaled.join(";")));
1846
+ if (codeScaled.length > 0) declarations.push(rootRule(codeScaled.join(";")));
1672
1847
  }
1673
- var codeWeightRule = axis[CODE_WEIGHT_FIELD];
1674
- if (codeWeightRule !== WEIGHT_UNSET) {
1675
- declarations.push(codeRule("font-weight:" + codeWeightRule + " !important"));
1848
+ if (codeWeight !== WEIGHT_UNSET) {
1849
+ // The variable rides along so the elements INSIDE a code surface — which the
1850
+ // interface weight rule's element table matches — resolve the code weight
1851
+ // instead of the interface one, without any `… *` selector.
1852
+ declarations.push(
1853
+ codeRule(
1854
+ "font-weight:" + codeWeight + " !important;--dfp-interface-weight:" + codeWeight
1855
+ )
1856
+ );
1676
1857
  }
1677
1858
  var ligatures = axis[LIGATURES_FIELD];
1678
1859
  if (ligatures === LIGATURES_ON) {
@@ -1689,10 +1870,10 @@ function buildAxisCss(axis, baseTokens, isDark) {
1689
1870
  var noItalic = axis[NO_SYNTHETIC_ITALIC_FIELD] === true;
1690
1871
  var noBold = axis[NO_SYNTHETIC_BOLD_FIELD] === true;
1691
1872
  if (noItalic && noBold) {
1692
- declarations.push(blanket("font-synthesis:none !important"));
1873
+ declarations.push(bodyRule("font-synthesis:none !important"));
1693
1874
  } else {
1694
- if (noItalic) declarations.push(blanket("font-synthesis-style:none !important"));
1695
- if (noBold) declarations.push(blanket("font-synthesis-weight:none !important"));
1875
+ if (noItalic) declarations.push(bodyRule("font-synthesis-style:none !important"));
1876
+ if (noBold) declarations.push(bodyRule("font-synthesis-weight:none !important"));
1696
1877
  }
1697
1878
 
1698
1879
  return declarations.join("\n");
@@ -1781,15 +1962,19 @@ function scaleCodeTokens(baseTokens, scale, lineOffset) {
1781
1962
  *
1782
1963
  * @param {unknown} config - normalized or raw configuration.
1783
1964
  * @param {Record<string, string>} [baseTokens] - token name to untouched value.
1965
+ * @param {{markdown?: string, code?: string[], terminal?: string[]}} [hints] - what
1966
+ * the page harvested: the stable markdown wrapper, and the real class tokens
1967
+ * behind the code/terminal hooks. Absent means "match by substring", which is
1968
+ * correct everywhere and costs an attribute scan per element per rule.
1784
1969
  * @returns {string} declarations for one `<style>` element ("" when dormant).
1785
1970
  */
1786
- function buildFontCss(config, baseTokens) {
1971
+ function buildFontCss(config, baseTokens, hints) {
1787
1972
  var tokens = baseTokens === undefined || baseTokens === null ? FALLBACK_TOKENS : baseTokens;
1788
1973
  var sets = resolveAxes(config);
1789
1974
  if (isDormant(sets.light) && isDormant(sets.dark)) return "";
1790
- var light = buildAxisCss(sets.light, tokens, false);
1975
+ var light = buildAxisCss(sets.light, tokens, false, hints);
1791
1976
  if (sameValueSet(sets.light, sets.dark)) return light;
1792
- var dark = buildAxisCss(sets.dark, tokens, true);
1977
+ var dark = buildAxisCss(sets.dark, tokens, true, hints);
1793
1978
  if (dark === "") return light;
1794
1979
  return light + "\n" + dark;
1795
1980
  }
@@ -2183,9 +2368,13 @@ var shared = {
2183
2368
  pickerGroups: pickerGroups,
2184
2369
  catalogRefreshDue: catalogRefreshDue,
2185
2370
  MARKDOWN_SELECTOR: MARKDOWN_SELECTOR,
2371
+ codeSelector: codeSelector,
2372
+ dialogSelector: dialogSelector,
2373
+ CODE_SURFACE_SELECTORS_CHEAP: CODE_SURFACE_SELECTORS_CHEAP,
2186
2374
  CODE_SURFACE_SELECTORS: CODE_SURFACE_SELECTORS,
2187
2375
  CODE_EXCLUDES: CODE_EXCLUDES,
2188
- INTERFACE_EXCLUDES: INTERFACE_EXCLUDES,
2376
+ WEIGHT_MARKDOWN_GUARD: WEIGHT_MARKDOWN_GUARD,
2377
+ WEIGHT_ELEMENT_TABLE: WEIGHT_ELEMENT_TABLE,
2189
2378
  excludeAll: excludeAll,
2190
2379
  splitSelectorList: splitSelectorList,
2191
2380
  prefixSelector: prefixSelector,
@@ -3037,6 +3226,35 @@ function readBaseTokens() {
3037
3226
  return out;
3038
3227
  }
3039
3228
 
3229
+ /**
3230
+ * Find the cheap markdown selector for this page, so the stylesheet does not
3231
+ * have to match the conversation container by substring attribute.
3232
+ *
3233
+ * The substring form `[class*="_markdown_" i]` is what the dialog rule used to
3234
+ * be scoped by, and that rule's code exclusion is a `:not(...)` with ~30
3235
+ * arguments, so its scan is paid once per candidate element: naming the wrapper
3236
+ * takes that from ~1800 candidates per recalculation to ~37.
3237
+ *
3238
+ * The code hooks stay as substring selectors (see `codeSurfaceList()`): a class
3239
+ * the page has not rendered yet cannot be named, and an unknown surface must
3240
+ * still get the code axes.
3241
+ *
3242
+ * A page without the wrapper keeps the substring form — the builder's default —
3243
+ * and only loses the cheaper one.
3244
+ *
3245
+ * @returns {{markdown?: string}} the hints.
3246
+ */
3247
+ function harvestScope() {
3248
+ var hints = {};
3249
+ if (typeof document === "undefined" || typeof document.querySelector !== "function") return hints;
3250
+ try {
3251
+ if (document.querySelector("[data-dss-prose]") !== null) hints.markdown = "[data-dss-prose]";
3252
+ } catch (error) {
3253
+ // No wrapper to name: the builder falls back to the substring selector.
3254
+ }
3255
+ return hints;
3256
+ }
3257
+
3040
3258
  /**
3041
3259
  * Create the writer that keeps one `<style>` element in sync.
3042
3260
  *
@@ -3056,7 +3274,7 @@ function createStylesheet(tokens) {
3056
3274
  var lastCss = null;
3057
3275
  return function apply(config) {
3058
3276
  if (typeof document === "undefined") return;
3059
- var css = buildFontCss(config, tokens());
3277
+ var css = buildFontCss(config, tokens(), harvestScope());
3060
3278
  if (css === lastCss && tag !== null && tag.isConnected) return;
3061
3279
  lastCss = css;
3062
3280
  if (tag === null || !tag.isConnected) {
@@ -4290,7 +4508,15 @@ function firstFamily(stack) {
4290
4508
  function FontCardPage(props) {
4291
4509
  var t = typeof props.t === "function" ? props.t : cardT || fallbackTranslate;
4292
4510
  if (props.view === "summary") return t("card.description");
4293
- return h(FontCard, { scope: props.scope || cardScope || MEMORY_SCOPE, t: t });
4511
+ // Every other view is the card as the content of a section the reader has
4512
+ // already opened, so it starts open: the row/entry they clicked IS the
4513
+ // expansion gesture, and asking for a second click on the card's own header
4514
+ // was the one thing left to remove on the official plugin page.
4515
+ return h(FontCard, {
4516
+ scope: props.scope || cardScope || MEMORY_SCOPE,
4517
+ t: t,
4518
+ defaultOpen: props.view !== "row",
4519
+ });
4294
4520
  }
4295
4521
 
4296
4522
  /**
@@ -4315,7 +4541,14 @@ function FontCard(props) {
4315
4541
  // values `apply` bound are the fallback.
4316
4542
  var t = typeof props.t === "function" ? props.t : cardT || fallbackTranslate;
4317
4543
  var scope = props.scope || cardScope || MEMORY_SCOPE;
4318
- var [open, setOpen] = useState(false);
4544
+ // The official plugin page (0.1.7: `view: "page"`) and the rc line's
4545
+ // configuration cell (which passes no props at all) both render this card as
4546
+ // the whole body of a section the reader has ALREADY opened — the host's own
4547
+ // row is the expansion gesture there, so starting collapsed asked for a
4548
+ // second click. The card therefore starts open unless the seat says
4549
+ // otherwise (`defaultOpen: false`), and `FontCardPage` answers a one-line
4550
+ // summary request with its description instead of the card.
4551
+ var [open, setOpen] = useState(props.defaultOpen !== false);
4319
4552
  var [view, setView] = useState(readViewMode);
4320
4553
  var [expanded, setExpanded] = useState(null);
4321
4554
  var [presetName, setPresetName] = useState("");
@@ -5703,6 +5936,33 @@ function apply(ctx) {
5703
5936
  );
5704
5937
  sync();
5705
5938
 
5939
+ // Which selectors the sheet can be narrowed to depends on what the page
5940
+ // contains, and the conversation renders AFTER this bundle activates: the
5941
+ // first harvest sees an empty shell and falls back to the wide attribute
5942
+ // hooks (correct, but an attribute scan per element per rule). Re-harvest
5943
+ // when the DOM grows. The applier returns untouched when the resulting CSS
5944
+ // is unchanged, so a redundant re-sync costs one string compare.
5945
+ ctx.effect(
5946
+ function () {
5947
+ if (typeof document === "undefined") return undefined;
5948
+ if (typeof MutationObserver !== "function" || !document.body) return undefined;
5949
+ var pending = null;
5950
+ var observer = new MutationObserver(function () {
5951
+ if (pending !== null) return;
5952
+ pending = globalThis.setTimeout(function () {
5953
+ pending = null;
5954
+ sync();
5955
+ }, 1000);
5956
+ });
5957
+ observer.observe(document.body, { childList: true, subtree: true });
5958
+ return function () {
5959
+ if (pending !== null) globalThis.clearTimeout(pending);
5960
+ observer.disconnect();
5961
+ };
5962
+ },
5963
+ "dsh-fonttune: scope re-harvest"
5964
+ );
5965
+
5706
5966
  // DSH writes its tokens (and its content font size) after this bundle
5707
5967
  // activates, and the host's own boot row only covers the first frame; the
5708
5968
  // size axis therefore re-reads the live values and reapplies when they