@helping-ai-workflow/md2doc 3.9.1 → 3.11.0

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/md2doc.js CHANGED
@@ -22,6 +22,7 @@ const fs = require('fs');
22
22
  const path = require('path');
23
23
  const drawio = require('./drawio.js');
24
24
  const { applyReaderTheme } = require('./theme/tokens');
25
+ const { highlightCode } = require('./highlight');
25
26
 
26
27
  // How long to let client-side WaveDrom / Mermaid scripts render before we
27
28
  // snapshot the DOM (PDF print, or --bake-svg inert-SVG bake).
@@ -359,10 +360,14 @@ function renderEditModeList(listToken, blocks, biRef, out) {
359
360
  const runStart = liRunStartsHere(biRef, b) ? ' data-run-start="1"' : '';
360
361
  const listStartAttr = listStart ? ' data-list-start="1"' : '';
361
362
  listStart = false;
363
+ // v3.11 (batch-2 §6): marked's own looseness for this list token, read
364
+ // by the lexer off the source (a blank line between items). The editor
365
+ // gives a NEW neighbour in this list the same spacing.
366
+ const looseAttr = listToken.loose ? ' data-list-loose="1"' : '';
362
367
  parts.push(
363
368
  `<div class="ed-block" data-block-id="${b.id}" data-block-type="li"` +
364
369
  ` data-list-type="${b.listType}" data-task="${b.task ? 1 : 0}" data-indent="${b.indent}"` +
365
- runStart + listStartAttr + ` style="--ed-indent:${b.indent}">` +
370
+ runStart + listStartAttr + looseAttr + ` style="--ed-indent:${b.indent}">` +
366
371
  `<span class="ed-li-marker" aria-hidden="true"></span>` + check +
367
372
  `<div class="ed-li-text">${inner}</div>` +
368
373
  `</div>`
@@ -1033,9 +1038,16 @@ ${itemsHtml}
1033
1038
  const b64 = Buffer.from(code, 'utf8').toString('base64');
1034
1039
  return '\n' + GRAPHVIZ_PLACEHOLDER.html([b64]) + '\n';
1035
1040
  }
1036
- // Default: syntax-highlighted code block
1041
+ // Default: fenced code. v3.10.0 colours registered languages here, at
1042
+ // render time (lib/highlight.js); an unknown or missing language stays
1043
+ // plain escaped text.
1044
+ const coloured = highlightCode(code, lang);
1045
+ // The info string goes into an attribute: escape it (an info string such
1046
+ // as x" onmouseover=… must stay text). Ordinary language names are unchanged.
1047
+ const langAttr = escapeHtml(lang);
1048
+ if (coloured !== null) return `<pre><code class="language-${langAttr} hljs">${coloured}</code></pre>\n`;
1037
1049
  const escaped = code.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1038
- return `<pre><code class="language-${lang}">${escaped}</code></pre>\n`;
1050
+ return `<pre><code class="language-${langAttr}">${escaped}</code></pre>\n`;
1039
1051
  };
1040
1052
 
1041
1053
  renderer.heading = function(token) {
@@ -1070,7 +1082,18 @@ ${itemsHtml}
1070
1082
  const baseListitem = renderer.listitem.bind(renderer);
1071
1083
  renderer.listitem = function(token) {
1072
1084
  appendSectionText(flattenTokenText(token.tokens));
1073
- return baseListitem(token);
1085
+ const html = baseListitem(token);
1086
+ if (!token.task) return html;
1087
+ // v3.10.0: a task item drops its bullet and keeps its own text in a span,
1088
+ // so a checked item's strike-through stops at its own words — CSS
1089
+ // text-decoration cannot be undone on a nested child list. Shapes: tight
1090
+ // (text then an optional block child — a nested list, blockquote, code,
1091
+ // table…), loose (inside a <p>), leaf. The span ends at whichever comes
1092
+ // first: a block child's opening tag, the loose item's </p>, or </li>.
1093
+ const cls = token.checked ? 'task-item is-checked' : 'task-item';
1094
+ const m = html.match(/^<li>([\s\S]*?<input[^>]*>)([\s\S]*?)(<(?:ul|ol|blockquote|pre|table|div|p|h[1-6])[\s>][\s\S]*|<\/p>[\s\S]*|<\/li>\s*)$/);
1095
+ if (!m) return html.replace(/^<li>/, '<li class="' + cls + '">');
1096
+ return '<li class="' + cls + '">' + m[1] + ' <span class="task-text">' + m[2].trim() + '</span>' + m[3];
1074
1097
  };
1075
1098
 
1076
1099
  const baseBlockquote = renderer.blockquote.bind(renderer);
@@ -1305,7 +1328,10 @@ ${itemsHtml}
1305
1328
  var __md2docWavedromSeq = 0;
1306
1329
  window.__md2docInitDiagrams = function (rootEl) {
1307
1330
  rootEl = rootEl || document;
1308
- if (typeof mermaid !== 'undefined') {
1331
+ // v3.10.0: with the theme runtime on the page (lib/theme/runtime.js), new
1332
+ // mermaid hosts are drawn through it, in the theme on screen, so a block
1333
+ // rebuilt after an edit does not come back light (or recoloured) in dark.
1334
+ if (typeof mermaid !== 'undefined' && !(window.__md2docThemeMermaid && window.__md2docThemeMermaid())) {
1309
1335
  mermaid.initialize({ startOnLoad: false, ${MERMAID_CONFIG_JS} });
1310
1336
  var mermaidNodes = rootEl.querySelectorAll ? rootEl.querySelectorAll('.mermaid') : [];
1311
1337
  var pending = [];
@@ -1434,6 +1460,17 @@ ${itemsHtml}
1434
1460
  '\n .ed-block[data-list-type="ol"][data-indent="' + d + '"] > .ed-li-marker::before' +
1435
1461
  ' { content: counter(ed-ol-' + d + ') "."; }';
1436
1462
  }
1463
+ // v3.10.0: bullets change by depth like the reader's disc / circle / square,
1464
+ // cycling every three levels. Task items keep their own content:none rule
1465
+ // (same specificity, later in the sheet, so it still wins).
1466
+ const ED_UL_GLYPHS = ['\\2022', '\\25E6', '\\25AA'];
1467
+ // Depth-agnostic fallback first (deeper than ED_OL_MAX_DEPTH keeps a •
1468
+ // rather than going blank); the per-depth rules outrank it.
1469
+ let edUlMarkerCss = '\n .ed-block[data-list-type="ul"] > .ed-li-marker::before { content: "' + ED_UL_GLYPHS[0] + '"; }';
1470
+ for (let d = 0; d <= ED_OL_MAX_DEPTH; d++) {
1471
+ edUlMarkerCss += '\n .ed-block[data-list-type="ul"][data-indent="' + d + '"] > .ed-li-marker::before' +
1472
+ ' { content: "' + ED_UL_GLYPHS[d % 3] + '"; }';
1473
+ }
1437
1474
 
1438
1475
  const editModeLayoutCss = `
1439
1476
  /* 56px, not 48px — spec §4.2's hit-test conflict 1 (gutter vs
@@ -1463,8 +1500,16 @@ ${itemsHtml}
1463
1500
  comment above .ed-handle below. That still leaves 6px of clearance from
1464
1501
  splitterRight (contentLeft-56), so the conflict-1 guard still holds.
1465
1502
  Guarded by the elementFromPoint(splitter.right - 2, y) assertion §4.2
1466
- asks for, in test/editor-client-runtime.test.js. */
1467
- .content { padding-left: 56px; }
1503
+ asks for, in test/editor-client-runtime.test.js.
1504
+
1505
+ v3.10.0: 62px. The B look makes the buttons 24px; with --ed-gutter-gap
1506
+ 2px and the 10px shift the pair spans [contentLeft-60, contentLeft-12],
1507
+ and at 56px of padding the + hung back over the splitter again (the
1508
+ spec's gap 0 already did: measured 1400x900, splitter [324, 356], +
1509
+ [354, 378], elementFromPoint(355, +'s y) === .ed-insert). 62px restores
1510
+ 2px of clearance: re-measured + [358, 382], ⠿ [382, 406], row grip
1511
+ [408, 428], contentLeft 418. */
1512
+ .content { padding-left: 62px; }
1468
1513
 
1469
1514
  /* S1: every block is one full-width row, whatever its type or depth. The
1470
1515
  indent is a property of the item's own MARKER, never of its box, so all
@@ -1510,7 +1555,7 @@ ${itemsHtml}
1510
1555
  margin-left: calc(var(--ed-indent, 0) * 1.6em);
1511
1556
  width: 1.2em;
1512
1557
  display: flex; justify-content: flex-end;
1513
- color: #6b7280; -webkit-user-select: none; user-select: none;
1558
+ color: var(--md-muted); -webkit-user-select: none; user-select: none;
1514
1559
  }
1515
1560
  /* min-width: 0 so an unbreakable token inside an item cannot widen the row
1516
1561
  past its siblings — that would break the equal-width guarantee above. */
@@ -1525,7 +1570,7 @@ ${itemsHtml}
1525
1570
  .ed-block[data-block-type="li"] > .ed-editing { flex: 1 1 auto; min-width: 0; }
1526
1571
  /* column-gap already provides the marker/checkbox separation. */
1527
1572
  .ed-block[data-block-type="li"] > .ed-li-check { flex: 0 0 auto; margin-right: 0; }
1528
- .ed-block[data-list-type="ul"] > .ed-li-marker::before { content: "\\2022"; }${edOlCounterCss}
1573
+ ${edUlMarkerCss}${edOlCounterCss}
1529
1574
  /* A BULLETED task item's checkbox IS its marker, so the • is suppressed and
1530
1575
  the marker box collapses (the indent lives on its margin and survives).
1531
1576
  An ORDERED task item keeps its number — that is what GFM renders — and
@@ -1554,8 +1599,8 @@ ${itemsHtml}
1554
1599
  whatever --ed-gutter-shift moved the pair by. Writing any of them as a
1555
1600
  second literal is how the corridor comes back. */
1556
1601
  :root {
1557
- --ed-gutter-btn: 18px;
1558
- --ed-gutter-gap: 4px;
1602
+ --ed-gutter-btn: 24px;
1603
+ --ed-gutter-gap: 2px;
1559
1604
  --ed-gutter-w: calc(var(--ed-gutter-btn) * 2 + var(--ed-gutter-gap));
1560
1605
  }
1561
1606
  /* v3.0.1: the pair moves LEFT by --ed-gutter-shift so the table row grip
@@ -1567,15 +1612,21 @@ ${itemsHtml}
1567
1612
  tableLeft=412; a 10px shift lands ⠿=[380,398] +=[362,380], grip
1568
1613
  (straddling, 20px wide, centred on tableLeft) = [402,422], leaving
1569
1614
  exactly --ed-gutter-gap (4px) between the ⠿'s right edge (398) and the
1570
- grip's left edge (402), and +'s left edge (362) stays well clear of 0. */
1615
+ grip's left edge (402), and +'s left edge (362) stays well clear of 0.
1616
+ v3.10.0 re-measured (same page, 24px buttons, --ed-gutter-gap 2px, 62px
1617
+ content padding): ⠿=[382,406] +=[358,382] tableLeft=418, grip=[408,428].
1618
+ The spec asked for gap 0; that left the ⠿ abutting the grip, and on CI's
1619
+ fractional layout the grip took the ⠿'s last half-pixel column (§4.2
1620
+ conflict 2, editor-client-runtime: x 73.5 in a [50, 74] box answered
1621
+ ROW-GRIP). 2px is the clearance that survives sub-pixel snapping. */
1571
1622
  :root { --ed-gutter-shift: 10px; }
1572
1623
  .ed-handle {
1573
1624
  left: calc(-1 * (var(--ed-gutter-btn) + var(--ed-gutter-gap) + var(--ed-gutter-shift)));
1574
- top: 0;
1625
+ top: var(--ed-pair-top, 0px);
1575
1626
  width: var(--ed-gutter-btn);
1576
1627
  }
1577
1628
  .ed-insert {
1578
- left: calc(-1 * (var(--ed-gutter-w) + var(--ed-gutter-shift))); top: 0;
1629
+ left: calc(-1 * (var(--ed-gutter-w) + var(--ed-gutter-shift))); top: var(--ed-pair-top, 0px);
1579
1630
  width: var(--ed-gutter-btn);
1580
1631
  }
1581
1632
  /* v2.11.1: the gutter's HOVER ZONE, and it is the reason the pair could move
@@ -1705,22 +1756,6 @@ ${itemsHtml}
1705
1756
  /* The reader bar is gone in edit mode, so its 52px anchor offset goes too
1706
1757
  (it moved scrollIntoView landings under the edit toolbar). */
1707
1758
  .content [id] { scroll-margin-top: 0; }
1708
- /* T11-2 step 5: enterSourceMode() (lib/editor/client.js) sets
1709
- contentEl.hidden = true and mounts a plain textarea in its place, and
1710
- setDocMode() writes docMode onto body as data-ed-mode. Measured on that
1711
- build: with ten TOC links listed, clicking the last one moved neither
1712
- the (hidden) reading position nor the textarea's own scrollTop
1713
- (3924 before and after) — the click target was a normal anchor jump /
1714
- scrollIntoView() reaching into a subtree rooted at .content, and
1715
- .content's own display:none is what absorbed it; the same subtree
1716
- relationship holds for a search result's jump target. Hiding the TOC
1717
- nav and the search-results panel while data-ed-mode="source" removes
1718
- the two controls whose only reachable target sits in that hidden
1719
- subtree; the search input row (.reader-tools) is left as-is — driving
1720
- it against the textarea's own line numbers instead is a separate
1721
- feasibility question, not answered here. */
1722
- body[data-ed-mode="source"] .toc,
1723
- body[data-ed-mode="source"] .search-results { display: none !important; }
1724
1759
 
1725
1760
  /* ── v3.4.0 batch3 Task 6: the wavedrom editing surface ───────────────
1726
1761
  Both of these are mounted on document.body by lib/editor/client.js, and
@@ -1735,10 +1770,10 @@ ${itemsHtml}
1735
1770
  toolbar's own band, because the editor is modal over the document. */
1736
1771
  .ed-wave-edit-btn {
1737
1772
  position: fixed; z-index: 12; padding: 3px 10px; font-size: 12px;
1738
- border: 1px solid #bbb; border-radius: 4px; background: #ffffffee;
1739
- color: #222; cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,.2);
1773
+ border: 1px solid var(--md-border); border-radius: 4px; background: var(--md-ed-surface);
1774
+ color: var(--md-strong); cursor: pointer; box-shadow: 0 1px 3px var(--md-ed-shadow);
1740
1775
  }
1741
- .ed-wave-edit-btn:hover { background: #fff; border-color: #888; }
1776
+ .ed-wave-edit-btn:hover { background: var(--md-ed-surface); border-color: var(--md-border); }
1742
1777
  .ed-wave-overlay {
1743
1778
  position: fixed; inset: 0; z-index: 998; background: rgba(0,0,0,.45);
1744
1779
  display: flex; align-items: center; justify-content: center;
@@ -2783,6 +2818,24 @@ const html = `<!DOCTYPE html>
2783
2818
  overflow-wrap: anywhere;
2784
2819
  word-break: break-word;
2785
2820
  }
2821
+ /* v3.10.0: task items. The checkbox is the marker, so no bullet; a checked
2822
+ item's OWN text is struck — child selectors only, because a descendant
2823
+ rule would strike the nested child items too (text-decoration cannot be
2824
+ undone further down). */
2825
+ .content ul > li.task-item { list-style: none; }
2826
+ .content li.task-item > input[type="checkbox"],
2827
+ .content li.task-item > p > input[type="checkbox"] {
2828
+ appearance: none; -webkit-appearance: none; width: 16px; height: 16px; margin: 0 6px 0 0;
2829
+ vertical-align: -3px; border: 1.5px solid var(--md-muted); border-radius: 4px; background: transparent; }
2830
+ /* In a bulleted list the checkbox takes the bullet's place; an ORDERED task
2831
+ item keeps its number (what GFM renders, and what the editor does). */
2832
+ .content ul > li.task-item > input[type="checkbox"],
2833
+ .content ul > li.task-item > p > input[type="checkbox"] { margin-left: -22px; }
2834
+ .content li.task-item.is-checked > input[type="checkbox"],
2835
+ .content li.task-item.is-checked > p > input[type="checkbox"] {
2836
+ border-color: var(--md-accent); background: var(--md-accent) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / 12px no-repeat; }
2837
+ .content li.task-item.is-checked > .task-text,
2838
+ .content li.task-item.is-checked > p > .task-text { color: var(--md-muted); text-decoration: line-through; }
2786
2839
  .content > * { max-width: 100%; }
2787
2840
  .content iframe, .content video, .content canvas { max-width: 100%; height: auto; }
2788
2841
  .doc-meta {
@@ -2837,6 +2890,16 @@ const html = `<!DOCTYPE html>
2837
2890
  line-height: 1.45;
2838
2891
  }
2839
2892
  pre code { background: none; padding: 0; font-size: 0.875em; }
2893
+ /* v3.10.0: fenced-code syntax colour, written at render time by
2894
+ lib/highlight.js (no script on the page). Colours are the syn-* roles. */
2895
+ .hljs-keyword, .hljs-selector-tag, .hljs-literal { color: var(--md-syn-keyword); }
2896
+ .hljs-number, .hljs-attr, .hljs-attribute { color: var(--md-syn-number); }
2897
+ .hljs-comment, .hljs-quote { color: var(--md-syn-comment); font-style: italic; }
2898
+ .hljs-string, .hljs-regexp { color: var(--md-syn-string); }
2899
+ .hljs-type, .hljs-built_in, .hljs-meta { color: var(--md-syn-type); }
2900
+ .hljs-title, .hljs-variable, .hljs-params, .hljs-section { color: var(--md-syn-title); }
2901
+ .hljs-addition { color: var(--md-syn-string); }
2902
+ .hljs-deletion { color: var(--md-syn-keyword); }
2840
2903
  .content :not(pre) > code {
2841
2904
  background: none;
2842
2905
  padding: 0;
@@ -3222,11 +3285,10 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3222
3285
  above), so shipping this unconditionally is inert on normal HTML
3223
3286
  output, same precedent as the lightbox selectors above. */
3224
3287
  .ed-block { position: relative; cursor: pointer; }
3225
- .ed-block:hover { outline: 1px dashed #b0b0b0; }
3226
3288
  /* S3: the block-level multi-select tint (spec §3.6). rgba, never opaque —
3227
- §3.6 says the text underneath stays readable. #3b82f6 is the editor's
3228
- established blue (.ed-wys-armed:focus, .ed-li-check[data-checked="1"],
3229
- .ed-te-hl).
3289
+ §3.6 says the text underneath stays readable. Since v3.10.0 the tint is
3290
+ the --md-ed-sel role, the reader accent at low alpha, shared with
3291
+ .ed-te-hl (focus rings and the checked box use --md-accent itself).
3230
3292
 
3231
3293
  The descendant rules and their !important are not decoration, they are
3232
3294
  the whole problem: <pre> (#f6f8fa) and the sticky first column (#ffffff)
@@ -3245,12 +3307,12 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3245
3307
  :focus outline is suppressed because the roving tabindex below focuses a
3246
3308
  .ed-block for real (spec §4.4 wants a real focus holder, not <body>) and
3247
3309
  the tint — not a second ring — is the selection's affordance. */
3248
- .ed-block.ed-selected { background: rgba(59, 130, 246, 0.15); }
3310
+ .ed-block.ed-selected { background: var(--md-ed-sel); }
3249
3311
  .ed-block.ed-selected pre,
3250
3312
  .ed-block.ed-selected th,
3251
3313
  .ed-block.ed-selected tr:nth-child(even),
3252
3314
  .ed-block.ed-selected tbody td:first-child,
3253
- .ed-block.ed-selected thead th:first-child { background: rgba(59, 130, 246, 0.15) !important; }
3315
+ .ed-block.ed-selected thead th:first-child { background: var(--md-ed-sel) !important; }
3254
3316
  .ed-block.ed-selected:focus { outline: none; }
3255
3317
  /* white-space: pre-line is LOAD-BEARING, not styling — do not relax it.
3256
3318
  A hard-wrapped ("lazy continuation") list item's own content contains a
@@ -3270,9 +3332,10 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3270
3332
  Task 5's and live in editModeLayoutCss (edit-mode only), fed by the hooks
3271
3333
  renderEditModeList() emits here. */
3272
3334
  .ed-block[data-block-type="li"] { cursor: text; }
3273
- .ed-li-check { display: inline-block; width: 14px; height: 14px; margin-right: 6px;
3274
- border: 1px solid #8a8a8a; border-radius: 3px; vertical-align: middle; cursor: pointer; }
3275
- .ed-li-check[data-checked="1"] { background: #3b82f6; border-color: #3b82f6; }
3335
+ .ed-li-check { display: inline-block; width: 16px; height: 16px; margin-right: 6px; box-sizing: border-box;
3336
+ border: 1.5px solid var(--md-ed-glyph); border-radius: 4px; vertical-align: middle; cursor: pointer; }
3337
+ .ed-li-check[data-checked="1"] { border-color: var(--md-accent); background: var(--md-accent) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") center / 12px no-repeat; }
3338
+ .ed-block[data-task="1"]:has(> .ed-li-check[data-checked="1"]) > .ed-li-text { color: var(--md-muted); text-decoration: line-through; }
3276
3339
  /* Phase 3 Task 2: always-on paragraph/heading/list editing. Every eligible
3277
3340
  block's content element is contenteditable from the moment it renders
3278
3341
  (armEditables() in the client runtime) — no persistent outline (that
@@ -3283,8 +3346,8 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3283
3346
  toggling. */
3284
3347
  .ed-wys-armed { cursor: text; }
3285
3348
  .ed-wys-armed:focus {
3286
- outline: 2px solid #3b82f6; outline-offset: 2px;
3287
- caret-color: #3b82f6;
3349
+ outline: 2px solid var(--md-accent); outline-offset: 2px;
3350
+ caret-color: var(--md-accent);
3288
3351
  }
3289
3352
  /* Task 5: table cells armed PERMANENTLY (one edit surface per CELL, not
3290
3353
  per table root — see armEditables()'s 'table' branch in the client
@@ -3295,8 +3358,8 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3295
3358
  table's horizontal edges. */
3296
3359
  .ed-wys-cell { cursor: text; }
3297
3360
  .ed-wys-cell:focus {
3298
- outline: 2px solid #3b82f6; outline-offset: -2px;
3299
- caret-color: #3b82f6;
3361
+ outline: 2px solid var(--md-accent); outline-offset: -2px;
3362
+ caret-color: var(--md-accent);
3300
3363
  }
3301
3364
  /* Phase 3 Task 2: the ⠿ block-actions handle — one real per-block node
3302
3365
  (not a floating, JS-repositioned element like .ed-seltb/.ed-tb-insert),
@@ -3304,10 +3367,10 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3304
3367
  visibility toggle (opacity), never display or pointer-events, so a
3305
3368
  script-driven click still reaches it even without a real hover. */
3306
3369
  .ed-handle {
3307
- position: absolute; left: -22px; top: 0; width: 18px; height: 20px;
3370
+ position: absolute; left: -22px; top: 0; width: 24px; height: 24px;
3308
3371
  display: flex; align-items: center; justify-content: center;
3309
3372
  padding: 0; margin: 0; border: none; border-radius: 4px;
3310
- background: transparent; color: #8a8a8a; font-size: 13px; line-height: 1;
3373
+ background: transparent; color: var(--md-ed-glyph); font-size: 13px; line-height: 1;
3311
3374
  cursor: pointer; opacity: 0; transition: opacity .12s ease, background .12s ease;
3312
3375
  }
3313
3376
  .ed-block:hover .ed-handle,
@@ -3323,7 +3386,7 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3323
3386
  a recorded, unfixed residue. */
3324
3387
  .ed-block.ed-keep-lit .ed-handle,
3325
3388
  .ed-block.ed-keep-lit .ed-insert { opacity: 1; }
3326
- .ed-handle:hover { background: rgba(0, 0, 0, 0.08); }
3389
+ .ed-handle:hover { background: var(--md-ed-hover); color: var(--md-strong); }
3327
3390
  /* The ⠿ handle's menu: 轉換成 › / 建立副本 / 刪除 / MD 原始碼 (spec §3.7),
3328
3391
  each led by a 16px currentColor icon (v2.12.0; drawn in
3329
3392
  lib/editor/client.js's MENU_ICON_PATHS).
@@ -3341,8 +3404,8 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3341
3404
  position: absolute; top: -4px; left: -4px; z-index: 6;
3342
3405
  display: flex; flex-direction: column; align-items: stretch; gap: 2px;
3343
3406
  padding: 4px; border-radius: 8px;
3344
- background: rgba(16, 18, 21, 0.92); color: #e6edf3;
3345
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
3407
+ background: var(--md-ed-surface); color: var(--md-strong);
3408
+ box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
3346
3409
  }
3347
3410
  .ed-handle-menu-btn {
3348
3411
  /* v3.0.1: the icon sits FLUSH LEFT and the label sits one space-width
@@ -3350,8 +3413,8 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3350
3413
  The right padding is untouched — it is what keeps the 轉換成 row's
3351
3414
  chevron off the panel edge. */
3352
3415
  min-width: 132px; height: 26px; padding: 0 10px 0 6px;
3353
- border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 6px;
3354
- background: rgba(255, 255, 255, 0.08); color: inherit;
3416
+ border: 1px solid transparent; border-radius: 6px;
3417
+ background: transparent; color: inherit;
3355
3418
  font: inherit; font-size: 12px; line-height: 1;
3356
3419
  text-align: left; white-space: nowrap; cursor: pointer;
3357
3420
  /* v2.12.0: a flex ROW so the icon and the label share a baseline-free
@@ -3406,7 +3469,7 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3406
3469
  tighter than the list at 18px per row. (No backticks in this comment:
3407
3470
  it lives inside a JS template literal.) */
3408
3471
  .ed-handle-submenu { left: 100%; margin-left: 4px; max-height: calc(100vh - 8px); overflow-y: auto; }
3409
- .ed-handle-menu-btn:hover { background: rgba(255, 255, 255, 0.18); }
3472
+ .ed-handle-menu-btn:hover { background: var(--md-ed-hover); }
3410
3473
  .ed-handle-menu-btn[hidden] { display: none; }
3411
3474
  /* §10-gap fix: block-level INSERT — the + button sits NEXT TO the ⠿
3412
3475
  handle in the left gutter, same node/visibility-toggle shape as
@@ -3430,32 +3493,32 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3430
3493
  than deleted because it is the correct fallback if the gutter padding is
3431
3494
  ever conditioned differently. */
3432
3495
  .ed-insert {
3433
- position: absolute; left: -22px; top: -22px; width: 18px; height: 20px;
3496
+ position: absolute; left: -22px; width: 24px; height: 24px;
3434
3497
  display: flex; align-items: center; justify-content: center;
3435
3498
  padding: 0; margin: 0; border: none; border-radius: 4px;
3436
- background: transparent; color: #8a8a8a; font-size: 13px; line-height: 1;
3499
+ background: transparent; color: var(--md-ed-glyph); font-size: 13px; line-height: 1;
3437
3500
  cursor: pointer; opacity: 0; transition: opacity .12s ease, background .12s ease;
3438
3501
  }
3439
3502
  .ed-block:hover .ed-insert,
3440
3503
  .ed-insert:focus { opacity: 1; }
3441
- .ed-insert:hover { background: rgba(0, 0, 0, 0.08); }
3504
+ .ed-insert:hover { background: var(--md-ed-hover); color: var(--md-strong); }
3442
3505
  /* The + button's small menu: 段落/標題/清單/表格/程式碼 — same dark
3443
3506
  translucent pill language as .ed-handle-menu above. */
3444
3507
  .ed-insert-menu {
3445
3508
  position: absolute; top: -4px; left: -4px; z-index: 6;
3446
3509
  display: flex; align-items: center; gap: 4px;
3447
3510
  padding: 4px 6px; border-radius: 8px;
3448
- background: rgba(16, 18, 21, 0.92); color: #e6edf3;
3449
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
3511
+ background: var(--md-ed-surface); color: var(--md-strong);
3512
+ box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
3450
3513
  }
3451
3514
  .ed-insert-menu-btn {
3452
3515
  min-width: 28px; height: 26px; padding: 0 8px;
3453
- border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 6px;
3454
- background: rgba(255, 255, 255, 0.08); color: inherit;
3516
+ border: 1px solid transparent; border-radius: 6px;
3517
+ background: transparent; color: inherit;
3455
3518
  font: inherit; font-size: 12px; line-height: 1;
3456
3519
  white-space: nowrap; cursor: pointer;
3457
3520
  }
3458
- .ed-insert-menu-btn:hover { background: rgba(255, 255, 255, 0.18); }
3521
+ .ed-insert-menu-btn:hover { background: var(--md-ed-hover); }
3459
3522
  /* Task 5: hover-edge column/row insert bubbles — a SINGLETON pair of "+"
3460
3523
  buttons (client runtime repositions them via getBoundingClientRect(),
3461
3524
  never creates more than these two). Visual language deliberately
@@ -3468,11 +3531,11 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3468
3531
  position: fixed; z-index: 8;
3469
3532
  width: 18px; height: 18px; padding: 0; margin: 0;
3470
3533
  display: flex; align-items: center; justify-content: center;
3471
- border: 1px solid #3b82f6; border-radius: 50%;
3472
- background: #fff; color: #3b82f6; font-size: 13px; line-height: 1;
3473
- cursor: pointer; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
3534
+ border: 1px solid var(--md-accent); border-radius: 50%;
3535
+ background: var(--md-ed-surface); color: var(--md-accent); font-size: 13px; line-height: 1;
3536
+ cursor: pointer; box-shadow: 0 0 0 1px var(--md-ed-ring), 0 2px 6px var(--md-ed-shadow);
3474
3537
  }
3475
- .ed-tb-insert:hover { background: #3b82f6; color: #fff; }
3538
+ .ed-tb-insert:hover { background: var(--md-accent); color: var(--md-ed-surface); }
3476
3539
  .ed-tb-insert[hidden] { display: none; }
3477
3540
  /* Task 6: table edge-click menus (delete/align) + row drag-reorder.
3478
3541
  '.ed-te-hl' marks the currently-selected column's or row's CELLS
@@ -3487,32 +3550,32 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3487
3550
  relabeled/repositioned per click rather than rebuilt. '.ed-te-drop-
3488
3551
  indicator' is the singleton line shown while dragging a row.
3489
3552
  '.ed-te-row-dragging' dims the row actually being dragged. */
3490
- .ed-te-hl { background: rgba(59, 130, 246, 0.15) !important; }
3553
+ .ed-te-hl { background: var(--md-ed-sel) !important; }
3491
3554
  /* Table cell-range selection: the painted rectangle, and the table that
3492
3555
  holds one (its caret and native selection are hidden while it stands). */
3493
- .ed-cell-range { background: rgba(59, 130, 246, 0.18) !important; }
3556
+ .ed-cell-range { background: var(--md-ed-range) !important; }
3494
3557
  .ed-cell-range-active { caret-color: transparent; }
3495
3558
  .ed-cell-range-active ::selection { background: transparent; }
3496
3559
  .ed-te-menu {
3497
3560
  position: fixed; z-index: 12;
3498
3561
  display: flex; align-items: center; gap: 4px;
3499
3562
  padding: 4px 6px; border-radius: 8px;
3500
- background: rgba(16, 18, 21, 0.92); color: #e6edf3;
3501
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
3563
+ background: var(--md-ed-surface); color: var(--md-strong);
3564
+ box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
3502
3565
  }
3503
3566
  .ed-te-menu[hidden] { display: none; }
3504
3567
  .ed-te-menu-btn {
3505
3568
  min-width: 28px; height: 26px; padding: 0 8px;
3506
- border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 6px;
3507
- background: rgba(255, 255, 255, 0.08); color: inherit;
3569
+ border: 1px solid transparent; border-radius: 6px;
3570
+ background: transparent; color: inherit;
3508
3571
  font: inherit; font-size: 12px; line-height: 1;
3509
3572
  white-space: nowrap; cursor: pointer;
3510
3573
  }
3511
- .ed-te-menu-btn:hover { background: rgba(255, 255, 255, 0.18); }
3574
+ .ed-te-menu-btn:hover { background: var(--md-ed-hover); }
3512
3575
  .ed-te-menu-btn[hidden] { display: none; }
3513
3576
  .ed-te-drop-indicator {
3514
3577
  position: fixed; z-index: 8; height: 3px; border-radius: 2px;
3515
- background: #3b82f6; pointer-events: none;
3578
+ background: var(--md-accent); pointer-events: none;
3516
3579
  }
3517
3580
  .ed-te-drop-indicator[hidden] { display: none; }
3518
3581
  .ed-te-row-dragging { opacity: 0.4; }
@@ -3527,7 +3590,7 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3527
3590
  the BLOCK line rather than table chrome that happened to be on screen. */
3528
3591
  .ed-block-drop-indicator {
3529
3592
  position: fixed; z-index: 8; height: 3px; border-radius: 2px;
3530
- background: #3b82f6; pointer-events: none;
3593
+ background: var(--md-accent); pointer-events: none;
3531
3594
  }
3532
3595
  .ed-block-drop-indicator[hidden] { display: none; }
3533
3596
  /* S4 Task 2b (user request, 2026-09-01): the drag's feedback on the SOURCE
@@ -3579,9 +3642,9 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3579
3642
  transition: background .12s ease;
3580
3643
  }
3581
3644
  .ed-te-grip[hidden] { display: none; }
3582
- .ed-te-grip:hover { background: rgba(0, 0, 0, 0.06); }
3583
- .ed-te-grip-dot { width: 3px; height: 3px; border-radius: 50%; background: #9ca3af; }
3584
- .ed-te-grip:hover .ed-te-grip-dot { background: #6b7280; }
3645
+ .ed-te-grip:hover { background: var(--md-ed-hover); }
3646
+ .ed-te-grip-dot { width: 3px; height: 3px; border-radius: 50%; background: var(--md-ed-glyph); }
3647
+ .ed-te-grip:hover .ed-te-grip-dot { background: var(--md-muted); }
3585
3648
  /* spec §4.2 hit-test conflict 2: the row grip's width is a TOKEN because
3586
3649
  lib/editor/client.js's updateTableEdgeGrips() reads it (via
3587
3650
  gripRowWidthPx()) to CENTRE the grip on the table's left border —
@@ -3611,17 +3674,17 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3611
3674
  position: fixed; z-index: 15;
3612
3675
  display: flex; align-items: center; gap: 4px;
3613
3676
  padding: 4px 6px; border-radius: 8px;
3614
- background: rgba(16, 18, 21, 0.92); color: #e6edf3;
3615
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
3677
+ background: var(--md-ed-surface); color: var(--md-strong);
3678
+ box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
3616
3679
  }
3617
3680
  .ed-seltb-btn {
3618
3681
  min-width: 28px; height: 26px; padding: 0 8px;
3619
- border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 6px;
3620
- background: rgba(255, 255, 255, 0.08); color: inherit;
3682
+ border: 1px solid transparent; border-radius: 6px;
3683
+ background: transparent; color: inherit;
3621
3684
  font: inherit; font-size: 13px; line-height: 1;
3622
3685
  white-space: nowrap; cursor: pointer;
3623
3686
  }
3624
- .ed-seltb-btn:hover:not([aria-disabled="true"]) { background: rgba(255, 255, 255, 0.18); }
3687
+ .ed-seltb-btn:hover:not([aria-disabled="true"]) { background: var(--md-ed-hover); }
3625
3688
  /* v3.3.0 (F2): the four states these buttons report about the selection.
3626
3689
  'true' (the selection is entirely inside a mark of this type) is the
3627
3690
  filled treatment the fixed toolbar already uses. 'mixed' (the selection
@@ -3631,17 +3694,18 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3631
3694
  rule above. INERT is aria-disabled, never the disabled DOM property; see
3632
3695
  paintSelToolbarMarks() in lib/editor/client.js for why. */
3633
3696
  .ed-seltb-btn[aria-pressed="true"] {
3634
- background: #e6edf3; color: #101215; border-color: #e6edf3;
3697
+ background: var(--md-active-bg); color: var(--md-active-fg); border-color: transparent;
3635
3698
  }
3636
3699
  .ed-seltb-btn[aria-pressed="mixed"] {
3637
- background: rgba(230, 237, 243, 0.3);
3638
- border-style: dashed; border-color: rgba(230, 237, 243, 0.8);
3700
+ background: transparent;
3701
+ border-style: dashed; border-color: var(--md-border);
3639
3702
  }
3640
3703
  .ed-seltb-btn[aria-disabled="true"] { opacity: 0.38; cursor: default; }
3641
- .ed-seltb-b { font-weight: bold; }
3642
- .ed-seltb-i { font-style: italic; }
3643
- .ed-seltb-s { text-decoration: line-through; }
3644
- .ed-seltb-u { text-decoration: underline; }
3704
+ .ed-seltb { padding: 3px; gap: 2px; }
3705
+ .ed-seltb-btn { width: 30px; height: 30px; min-width: 0; padding: 0; display: inline-grid; place-items: center; color: var(--md-muted); }
3706
+ .ed-seltb-btn:hover:not([aria-disabled="true"]) { color: var(--md-strong); }
3707
+ .ed-seltb-sep { width: 1px; height: 18px; margin: 0 3px; background: var(--md-rule); align-self: center; }
3708
+ .ed-ico { display: block; flex: none; }
3645
3709
  /* v3.1.0 §4: the always-visible editing toolbar. Its numeric language is
3646
3710
  .lightbox-bar's (8px/12px padding, 6px gap, 13px type) over .ed-seltb's
3647
3711
  dark pill, so the two floating surfaces read as one family.
@@ -3684,7 +3748,9 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3684
3748
  save included, still reaches an empty unreachable list and an empty
3685
3749
  neverClear list at all three — this change does not trade the T11-2
3686
3750
  no-scroll guarantee for the F12 with-scroll one. */
3687
- gap: 3px; padding: 8px 12px;
3751
+ /* v3.10.0 B 外觀: buttons grew to 32x32 (+~4px each) and separators to
3752
+ 6px margins, so the gap went 3px -> 0 to keep outline (hamburger) inside 800px. */
3753
+ gap: 0; padding: 8px 12px;
3688
3754
  /* F12, both halves measured on this build at 900px viewport height.
3689
3755
  justify-content was center. Centering an overflowing flex row splits
3690
3756
  the overflow across BOTH ends, and the leading end of a scroll
@@ -3704,9 +3770,13 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3704
3770
  scrollLeft 0 after this change, [outline, preview] still overlap the
3705
3771
  slot at a 900px viewport and [check, bold, italic, strike] do at 420px.
3706
3772
  What makes that harmless is the slot's own pointer-events: none, in
3707
- the rule further down — the load-bearing line, not this one. */
3708
- padding-right: 124px;
3709
- background: rgba(16, 18, 21, 0.92); color: #e6edf3;
3773
+ the rule further down — the load-bearing line, not this one.
3774
+ v3.10.0: 260px. The slot now carries the save state, and its longest
3775
+ text (the dirty one, "有未儲存的變更 · Ctrl+S 儲存" after a dot) is far
3776
+ wider than the old 7rem mode name. */
3777
+ padding-right: 260px;
3778
+ background: var(--md-ed-chrome); color: var(--md-muted);
3779
+ border-bottom: 1px solid var(--md-rule);
3710
3780
  /* F12, second half: before this the bar scrolled but nothing on screen
3711
3781
  said so, so a button parked outside the viewport looked absent rather
3712
3782
  than off to one side. Two edge gradients, painted only on the side
@@ -3730,32 +3800,34 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3730
3800
  than light — darkening the ground a translucent button sits on is what
3731
3801
  reads as content passing under an edge, where a light wash behind the
3732
3802
  same button would not. */
3733
- --ed-tb-edge-l: rgba(0, 0, 0, 0);
3734
- --ed-tb-edge-r: rgba(0, 0, 0, 0);
3803
+ --ed-tb-edge-l: transparent;
3804
+ --ed-tb-edge-r: transparent;
3735
3805
  background-image:
3736
- linear-gradient(to right, var(--ed-tb-edge-l), rgba(0, 0, 0, 0)),
3737
- linear-gradient(to left, var(--ed-tb-edge-r), rgba(0, 0, 0, 0));
3806
+ linear-gradient(to right, var(--ed-tb-edge-l), transparent),
3807
+ linear-gradient(to left, var(--ed-tb-edge-r), transparent);
3738
3808
  background-repeat: no-repeat;
3739
3809
  background-position: left center, right center;
3740
3810
  background-size: 26px 100%, 26px 100%;
3741
3811
  font-size: 13px;
3742
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
3812
+ box-shadow: none;
3743
3813
  }
3744
- .ed-toolbar[data-ed-tb-overflow~="left"] { --ed-tb-edge-l: rgba(0, 0, 0, 0.55); }
3745
- .ed-toolbar[data-ed-tb-overflow~="right"] { --ed-tb-edge-r: rgba(0, 0, 0, 0.55); }
3814
+ .ed-toolbar[data-ed-tb-overflow~="left"] { --ed-tb-edge-l: var(--md-ed-shadow); }
3815
+ .ed-toolbar[data-ed-tb-overflow~="right"] { --ed-tb-edge-r: var(--md-ed-shadow); }
3746
3816
  .ed-toolbar-sep {
3747
- flex: 0 0 auto; width: 1px; height: 18px; margin: 0 2px;
3748
- background: rgba(255, 255, 255, 0.25);
3817
+ flex: 0 0 auto; width: 1px; height: 20px; margin: 0 6px;
3818
+ background: var(--md-rule);
3749
3819
  }
3750
3820
  .ed-toolbar-btn {
3751
3821
  flex: 0 0 auto;
3752
- min-width: 28px; height: 26px; padding: 0 8px;
3753
- border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 6px;
3754
- background: rgba(255, 255, 255, 0.08); color: inherit;
3822
+ min-width: 32px; height: 32px; padding: 0;
3823
+ display: inline-grid; place-items: center; color: var(--md-muted);
3824
+ border: 0 solid transparent; border-radius: 6px;
3825
+ background: transparent;
3755
3826
  font: inherit; font-size: 13px; line-height: 1;
3756
3827
  white-space: nowrap; cursor: pointer;
3757
3828
  }
3758
- .ed-toolbar-btn:hover:not(:disabled) { background: rgba(255, 255, 255, 0.18); }
3829
+ .ed-toolbar-btn:hover:not(:disabled) { background: var(--md-ed-hover); color: var(--md-strong); }
3830
+ .ed-toolbar-btn .ed-ico { width: 18px; height: 18px; }
3759
3831
  /* F12: where the toolbar's keyboard cursor is. An outline rather than a
3760
3832
  background so it stays legible on a button that is already pressed-in
3761
3833
  (aria-pressed paints that one nearly white). Claim no more for it than a
@@ -3764,21 +3836,18 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3764
3836
  follow, and what this cue does for a screen reader has not been measured
3765
3837
  here. What it does do is let the row be walked without a mouse. */
3766
3838
  .ed-toolbar-btn[data-ed-tb-cursor] {
3767
- outline: 2px solid #6ea8fe; outline-offset: 1px;
3839
+ outline: 2px solid var(--md-accent); outline-offset: 1px;
3768
3840
  }
3769
3841
  .ed-toolbar-btn:disabled { opacity: 0.38; cursor: default; }
3770
3842
  .ed-toolbar-btn[aria-pressed="true"] {
3771
- background: #e6edf3; color: #101215; border-color: #e6edf3;
3843
+ background: var(--md-active-bg); color: var(--md-active-fg); border-color: transparent;
3772
3844
  }
3773
3845
  /* v3.3.0 (F2): the middle state, matching .ed-seltb-btn[aria-pressed="mixed"]
3774
3846
  above so the two bars answer the same question the same way. */
3775
3847
  .ed-toolbar-btn[aria-pressed="mixed"] {
3776
- background: rgba(230, 237, 243, 0.3);
3777
- border-style: dashed; border-color: rgba(230, 237, 243, 0.8);
3848
+ background: transparent;
3849
+ border-width: 1px; border-style: dashed; border-color: var(--md-border);
3778
3850
  }
3779
- .ed-toolbar-b { font-weight: bold; }
3780
- .ed-toolbar-i { font-style: italic; }
3781
- .ed-toolbar-s { text-decoration: line-through; }
3782
3851
  /* H1 fix — and its reason has since moved, so both readings are kept here
3783
3852
  rather than one silently standing in for the other. The brief's literal
3784
3853
  margin-left: auto (a flex-flow item) was measured on v3.2.0 to break
@@ -3824,9 +3893,13 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3824
3893
  regression, this time on content someone can actually see and try to
3825
3894
  click. */
3826
3895
  .ed-toolbar-status { position: fixed; top: 0; right: 12px;
3827
- height: var(--ed-toolbar-h); line-height: var(--ed-toolbar-h);
3828
- min-width: 7rem; text-align: right; font-size: 12px; opacity: .8;
3896
+ height: var(--ed-toolbar-h); display: flex; align-items: center; gap: 6px;
3897
+ min-width: 7rem; justify-content: flex-end; font-size: 13px; color: var(--md-muted);
3829
3898
  pointer-events: none; }
3899
+ .ed-toolbar-status[data-state="clean"] .ed-ico { color: var(--md-ed-ok); }
3900
+ .ed-toolbar-status[data-state="failed"] { color: var(--md-ed-err); }
3901
+ .ed-status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--md-ed-dirty); display: inline-block; }
3902
+ .ed-toolbar-btn[data-ed-tb="save"][aria-pressed="true"] { color: var(--md-accent); background: var(--md-active-bg); }
3830
3903
  /* The H▾ dropdown: same singleton-panel idiom as .ed-insert-menu, but
3831
3904
  anchored to a fixed bar rather than moved into a block, so it is
3832
3905
  positioned in viewport coordinates by the client runtime.
@@ -3839,8 +3912,8 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3839
3912
  position: fixed; z-index: 102;
3840
3913
  display: flex; flex-direction: column; gap: 2px;
3841
3914
  padding: 4px; border-radius: 8px;
3842
- background: rgba(16, 18, 21, 0.96); color: #e6edf3;
3843
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
3915
+ background: var(--md-ed-surface); color: var(--md-strong);
3916
+ box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
3844
3917
  max-height: calc(100vh - 8px); overflow-y: auto;
3845
3918
  }
3846
3919
  .ed-toolbar-menu[hidden] { display: none; }
@@ -3849,7 +3922,7 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3849
3922
  border: none; border-radius: 4px; background: transparent; color: inherit;
3850
3923
  font: inherit; font-size: 13px; line-height: 1.4; cursor: pointer;
3851
3924
  }
3852
- .ed-toolbar-menu-btn:hover { background: rgba(255, 255, 255, 0.18); }
3925
+ .ed-toolbar-menu-btn:hover { background: var(--md-ed-hover); }
3853
3926
  /* backlog #7 (Task 9 review, M1): the panel's OWN keyboard cursor
3854
3927
  (lib/editor/client.js's toolbarMenuIndex / data-ed-tb-menu-cursor) had
3855
3928
  no matching rule at all — the fix made the six items operable by
@@ -3859,32 +3932,24 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3859
3932
  over :hover's own background, and over nothing at all when neither
3860
3933
  applies). */
3861
3934
  .ed-toolbar-menu-btn[data-ed-tb-menu-cursor] {
3862
- outline: 2px solid #6ea8fe; outline-offset: -2px;
3863
- background: rgba(255, 255, 255, 0.18);
3864
- }
3865
- /* 追加 4: the whole-document source escape hatch. Replaces the rendered
3866
- content column for the duration of source mode; the document's own
3867
- lines[] is untouched until the mode is left (see docsource.js). */
3868
- .ed-source {
3869
- display: block; width: 100%; box-sizing: border-box;
3870
- min-height: 70vh; padding: 12px;
3871
- font-family: monospace; font-size: 13px; line-height: 1.6;
3872
- border: 1px solid #d0d7de; border-radius: 8px; resize: vertical;
3873
- white-space: pre; overflow-wrap: normal;
3935
+ outline: 2px solid var(--md-accent); outline-offset: -2px;
3936
+ background: var(--md-ed-hover);
3874
3937
  }
3875
3938
  .ed-editing { position: relative; }
3876
3939
  .ed-raw {
3877
- display: block; width: 100%; font-family: monospace; font-size: 13px;
3940
+ display: block; width: 100%; font-family: "Cascadia Mono", Consolas, ui-monospace, monospace; font-size: 13px;
3878
3941
  min-height: 3em; box-sizing: border-box; padding: 6px;
3879
- border: 1px solid #808080; resize: vertical;
3942
+ border: 1px solid var(--md-border); border-radius: 6px;
3943
+ background: var(--md-ed-field); color: var(--md-strong); resize: vertical;
3880
3944
  }
3881
3945
  .ed-controls { display: flex; gap: 6px; margin-top: 4px; }
3882
3946
  .ed-controls button {
3883
3947
  font-size: 13px; padding: 2px 10px; cursor: pointer;
3884
- border: 1px solid #b0b0b0; border-radius: 4px; background: #fff;
3948
+ border: 1px solid var(--md-border); border-radius: 6px;
3949
+ background: var(--md-ed-surface); color: var(--md-strong);
3885
3950
  }
3886
- .ed-commit { color: #0a7a0a; }
3887
- .ed-cancel { color: #b00020; }
3951
+ .ed-commit { color: var(--md-ed-ok); }
3952
+ .ed-cancel { color: var(--md-ed-err); }
3888
3953
  /* backlog #8: 'top: 0' used to put this banner directly OVER .ed-toolbar
3889
3954
  (same top-of-viewport band, and z-index 999 against the bar's 101 wins
3890
3955
  the paint) — MEASURED (task-10-toolbar-probe.js): all 23 toolbar
@@ -3921,16 +3986,38 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3921
3986
  land nearby as a side effect of their own trigger's position, not a
3922
3987
  shared clamp — see showBanner()'s parallel comment in
3923
3988
  lib/editor/client.js. Closed regardless, so a banner can never land on
3924
- top of one of those instead. */
3989
+ top of one of those instead.
3990
+ v3.10.0 (two message levels): only the ERROR card floors at the token
3991
+ now — its top is --ed-toolbar-h plus 10px, a centred card rather than a
3992
+ full-width band — and a NOTICE sits at the bottom of the viewport, so it
3993
+ never reaches the toolbar's band at all. */
3994
+ /* Centred with left/right + auto margins and a fit-content width, NOT
3995
+ left: 50% + translateX(-50%): a fixed box anchored at left: 50% may only
3996
+ grow into the right half of the viewport, so the 760px cap never applied
3997
+ and a long message wrapped into a card half the viewport wide and several
3998
+ times as tall (measured 800x600: 400px wide, y 54-284, covering the
3999
+ waveform editor's group buttons). */
3925
4000
  .ed-conflict {
3926
- position: fixed; top: var(--ed-toolbar-h); left: 0; right: 0; padding: 10px;
3927
- background: #b00020; color: #fff; z-index: 999; text-align: center;
3928
- display: flex; align-items: center; justify-content: center; gap: 12px;
3929
- }
3930
- .ed-conflict button {
3931
- background: #fff; color: #b00020; border: none; border-radius: 4px;
3932
- padding: 4px 12px; cursor: pointer; font-weight: bold;
3933
- }
4001
+ position: fixed; left: 16px; right: 16px; margin: 0 auto; width: fit-content; z-index: 999;
4002
+ display: flex; align-items: center; gap: 10px;
4003
+ max-width: 760px; box-sizing: border-box;
4004
+ padding: 10px 10px 10px 14px; border-radius: 8px; font-size: 14px; line-height: 1.5;
4005
+ }
4006
+ .ed-conflict[data-level="error"] {
4007
+ top: calc(var(--ed-toolbar-h) + 10px);
4008
+ background: var(--md-ed-err-bg); color: var(--md-ed-err-ink);
4009
+ box-shadow: inset 3px 0 0 var(--md-ed-err), 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
4010
+ }
4011
+ .ed-conflict[data-level="notice"] {
4012
+ bottom: 28px; background: var(--md-ed-surface); color: var(--md-strong);
4013
+ box-shadow: 0 0 0 1px var(--md-ed-ring), 0 8px 24px var(--md-ed-shadow);
4014
+ }
4015
+ .ed-msg-ico { display: grid; color: var(--md-muted); }
4016
+ .ed-conflict[data-level="error"] .ed-msg-ico { color: var(--md-ed-err); }
4017
+ .ed-msg-text { flex: 1 1 auto; }
4018
+ .ed-conflict button { font: inherit; font-size: 13px; font-weight: 600; border: 0; border-radius: 6px;
4019
+ padding: 4px 12px; cursor: pointer; background: var(--md-ed-err); color: var(--md-ed-surface); }
4020
+ .ed-conflict button.ed-msg-x { background: transparent; color: inherit; padding: 4px; display: grid; }
3934
4021
  ${opts.editMode ? editModeLayoutCss : ''}
3935
4022
  </style>
3936
4023
  ${usesMath ? buildKatexStyleTag() : ''}
@@ -5949,8 +6036,8 @@ ${usesDrawioMultiPage ? ` // v3.4.0 batch2 Task 6: wired through a NAMED, reusa
5949
6036
  await drawioBrowser.closeIfOpen();
5950
6037
  }
5951
6038
  // v3.9.0 dark mode: a post-pass over the finished reader HTML (lib/theme/).
5952
- // Edit mode is light-only; noTheme is an internal switch for the light-lock test.
5953
- if (!opts.editMode && !opts.noTheme) bakedHtml = applyReaderTheme(bakedHtml);
6039
+ // v3.10.0: edit mode is themed too. noTheme is an internal switch for the light-lock test.
6040
+ if (!opts.noTheme) bakedHtml = applyReaderTheme(bakedHtml);
5954
6041
  return {
5955
6042
  html: bakedHtml, bodyHtml: bakedBody, parts, blocks,
5956
6043
  // v3.4.0 batch2 Task 6: an added field, not a shape change to the four