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