privateboard 0.1.3 → 0.1.5

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/public/index.html CHANGED
@@ -2434,9 +2434,18 @@
2434
2434
  the SSE `room-paused` event arrives. */
2435
2435
  html.pause-pending .input-bar .input-wrap { display: none; }
2436
2436
  html.pause-pending .input-bar .speaking-queue { display: none; }
2437
+ /* Hide the session-control toolbar mid-transition · prevents double-
2438
+ clicking pause while the previous pause is still resolving (and
2439
+ adjourn while we're stopping cleanly is also confusing UX). */
2440
+ html.pause-pending .input-bar .input-bar-actions { display: none; }
2437
2441
  html.pause-pending .input-bar::before {
2438
2442
  content: "⌛ pausing after the current turn finishes…";
2439
2443
  display: block;
2444
+ /* `.input-bar` is now `display: flex` (for the new icon toolbar),
2445
+ so this pseudo is a flex child. Without `flex: 1` it shrinks
2446
+ to its text width instead of spanning the bar. The grow-to-fill
2447
+ restores the pre-flex banner behaviour. */
2448
+ flex: 1 1 auto;
2440
2449
  text-align: center;
2441
2450
  font-family: var(--mono);
2442
2451
  font-size: 11px;
@@ -2487,6 +2496,39 @@
2487
2496
  }
2488
2497
  .resume-btn-lg:hover { background: var(--bg); color: var(--lime); }
2489
2498
 
2499
+ /* Adjourn button on the paused-bar · same shape as resume-btn-lg
2500
+ but secondary (ghost) treatment so resume reads as the primary
2501
+ action when both are present. Goes warn-coloured on hover for
2502
+ the destructive cue (matches the input-bar's ib-adjourn). */
2503
+ .adjourn-btn-lg {
2504
+ display: inline-flex;
2505
+ align-items: center;
2506
+ gap: 5px;
2507
+ padding: 6px 12px;
2508
+ background: transparent;
2509
+ color: var(--text-soft);
2510
+ border: 0.5px solid var(--line-bright);
2511
+ font-family: var(--mono);
2512
+ font-size: 10px;
2513
+ font-weight: 700;
2514
+ line-height: 1.2;
2515
+ cursor: pointer;
2516
+ text-decoration: none;
2517
+ text-transform: uppercase;
2518
+ letter-spacing: 0.1em;
2519
+ transition: color 0.12s, border-color 0.12s, background 0.12s;
2520
+ }
2521
+ .adjourn-btn-lg .adjourn-glyph {
2522
+ color: var(--text-faint);
2523
+ letter-spacing: -0.04em;
2524
+ }
2525
+ .adjourn-btn-lg:hover {
2526
+ color: var(--red, #C75450);
2527
+ border-color: var(--red, #C75450);
2528
+ background: var(--bg);
2529
+ }
2530
+ .adjourn-btn-lg:hover .adjourn-glyph { color: var(--red, #C75450); }
2531
+
2490
2532
  /* ─── Adjourned-state controls (room is archived) ─── */
2491
2533
  .adjourned-pill {
2492
2534
  display: inline-flex;
@@ -2739,10 +2781,26 @@
2739
2781
  }
2740
2782
  /* Word / character count · same register as `.brief-meta-line` but
2741
2783
  keep numerals tabular so the figure doesn't jitter while the
2742
- parent flex-row gets re-rendered (e.g. on tab switch). */
2784
+ parent flex-row gets re-rendered (e.g. on tab switch).
2785
+ Tier colour communicates length-fit for the room's tone:
2786
+ · thin · text-faint · "could be fuller"
2787
+ · sweet · lime · bullseye
2788
+ · dense · amber · past sweet zone, still readable
2789
+ · long · red soft · approaching "filed not read"
2790
+ · too-long · red strong · likely skimmed, not read
2791
+ The sweet band itself is tone-aware (brainstorm < standard <
2792
+ research/critique) — see `_briefWordCount` in app.js. Hover
2793
+ surfaces a tone-aware tip via `title`. */
2743
2794
  .brief-meta-words {
2744
2795
  font-variant-numeric: tabular-nums;
2796
+ cursor: default;
2797
+ transition: color 0.12s;
2745
2798
  }
2799
+ .brief-meta-words.is-thin { color: var(--text-faint); }
2800
+ .brief-meta-words.is-sweet { color: var(--lime); }
2801
+ .brief-meta-words.is-dense { color: var(--amber); }
2802
+ .brief-meta-words.is-long { color: var(--red); opacity: 0.85; }
2803
+ .brief-meta-words.is-too-long { color: var(--red); font-weight: 700; }
2746
2804
 
2747
2805
  /* Signatures · folded into the meta line, no top border */
2748
2806
  .brief-signed {
@@ -3871,6 +3929,27 @@
3871
3929
  background: var(--bg);
3872
3930
  border: 0.5px solid var(--line);
3873
3931
  }
3932
+ /* Items past index 1 hide by default; the [+ show N more] toggle
3933
+ adds .sa-points-expanded to the parent <ul> to reveal them. */
3934
+ .sa-points .sa-point-extra { display: none; }
3935
+ .sa-points.sa-points-expanded .sa-point-extra { display: grid; }
3936
+ .sa-points-toggle {
3937
+ align-self: flex-start;
3938
+ margin: 6px 0 0;
3939
+ padding: 2px 0;
3940
+ background: transparent;
3941
+ border: none;
3942
+ font-family: var(--mono);
3943
+ font-size: 10px;
3944
+ letter-spacing: 0.04em;
3945
+ color: var(--text-faint);
3946
+ cursor: pointer;
3947
+ }
3948
+ .sa-points-toggle:hover { color: var(--text-soft); }
3949
+ .sa-points-toggle:focus-visible {
3950
+ outline: 1px dotted var(--text-faint);
3951
+ outline-offset: 2px;
3952
+ }
3874
3953
  .sa-point-mark {
3875
3954
  color: var(--lime);
3876
3955
  font-size: 9.5px;
@@ -8811,14 +8890,123 @@
8811
8890
  background: var(--panel);
8812
8891
  }
8813
8892
 
8814
- /* Input bar — sits inside the chat column */
8893
+ /* Input bar — sits inside the chat column. Live state hosts a
8894
+ small action toolbar (pause / adjourn) on the left, then the
8895
+ input pill which grows to fill remaining width. */
8815
8896
  .input-bar {
8816
8897
  flex: 0 0 auto;
8817
8898
  border-top: 0.5px solid var(--line-bright);
8818
8899
  padding: 8px 14px;
8819
8900
  background: var(--panel-2);
8901
+ display: flex;
8902
+ align-items: stretch;
8903
+ gap: 10px;
8904
+ }
8905
+ /* Session-control icons · sit to the LEFT of the input pill as
8906
+ bare glyph buttons. Borderless in the resting state so they
8907
+ don't compete with the input pill's hairline frame.
8908
+ ──────────────────────────────────────────────────────────────
8909
+ Visual vocabulary mirrors the sidebar's nav icons (Lucide-style
8910
+ stroked SVG, 16px, mask-image fill via currentColor). Hover
8911
+ state matches the sidebar's `.new-btn:hover` — panel-2 bg + text
8912
+ colour shift, no accent tint. The destructive cue for adjourn
8913
+ lives in the confirm overlay, not in the icon, so neither
8914
+ button needs an accent colour to broadcast severity here. Hit
8915
+ target 32×32; icon 16×16 sits centred inside it. */
8916
+ .input-bar-actions {
8917
+ display: flex;
8918
+ align-items: center;
8919
+ gap: 2px;
8920
+ }
8921
+ .ib-action {
8922
+ position: relative;
8923
+ width: 32px;
8924
+ height: 32px;
8925
+ display: inline-flex;
8926
+ align-items: center;
8927
+ justify-content: center;
8928
+ padding: 0;
8929
+ background: transparent;
8930
+ border: none;
8931
+ color: var(--text-faint);
8932
+ cursor: pointer;
8933
+ transition: color 0.12s, background 0.12s;
8934
+ }
8935
+ .ib-action::before {
8936
+ content: "";
8937
+ width: 16px;
8938
+ height: 16px;
8939
+ background-color: currentColor;
8940
+ -webkit-mask-image: var(--icon, none);
8941
+ mask-image: var(--icon, none);
8942
+ -webkit-mask-repeat: no-repeat;
8943
+ mask-repeat: no-repeat;
8944
+ -webkit-mask-position: center;
8945
+ mask-position: center;
8946
+ -webkit-mask-size: 16px 16px;
8947
+ mask-size: 16px 16px;
8948
+ }
8949
+ .ib-action:hover { background: var(--panel-2); color: var(--text); }
8950
+ /* Hover tooltip · CSS-only via ::after + data-tip. The native
8951
+ `title` attribute pops after a 1-2s OS delay, which is sluggish;
8952
+ this version reveals at ~300ms — slow enough not to be noisy
8953
+ when the cursor only passes through, fast enough to feel
8954
+ responsive on intentional dwell. Mirrors the .note-tip visual
8955
+ register (panel-2 surface, mono micro-type, hairline frame).
8956
+ Pointer-events:none so the tooltip never blocks the hover that
8957
+ triggers it. */
8958
+ .ib-action::after {
8959
+ content: attr(data-tip);
8960
+ position: absolute;
8961
+ bottom: calc(100% + 8px);
8962
+ /* Anchor the tooltip's LEFT edge to the button's left edge so
8963
+ the tip extends RIGHTWARD into the chat column. Centered
8964
+ anchoring would push the left half past the chat-column edge
8965
+ where `.body-grid { overflow: hidden }` (line ~215) clips it
8966
+ behind the sidebar — the user reported this as "tip gets cut
8967
+ off by the sidebar". Right-anchored gives clearance because
8968
+ the chat column has plenty of width to the right. */
8969
+ left: 0;
8970
+ transform: translateY(3px);
8971
+ background: var(--panel-2);
8972
+ border: 0.5px solid var(--line-strong);
8973
+ padding: 5px 9px;
8974
+ font-family: var(--mono);
8975
+ font-size: 10px;
8976
+ letter-spacing: 0.04em;
8977
+ color: var(--text);
8978
+ white-space: nowrap;
8979
+ pointer-events: none;
8980
+ opacity: 0;
8981
+ visibility: hidden;
8982
+ box-shadow: 0 4px 14px -6px rgba(0, 0, 0, 0.55);
8983
+ /* On exit the visibility flip is delayed past the opacity fade
8984
+ so the tooltip stays interactable until it has fully
8985
+ disappeared visually. */
8986
+ transition: opacity 0.14s ease, transform 0.14s ease, visibility 0s linear 0.18s;
8987
+ z-index: 50;
8988
+ }
8989
+ .ib-action:hover::after,
8990
+ .ib-action:focus-visible::after {
8991
+ opacity: 1;
8992
+ visibility: visible;
8993
+ transform: translateY(0);
8994
+ /* On entry · 0.3s delay before the fade starts; visibility
8995
+ flips immediately so the transform animates from offset. */
8996
+ transition: opacity 0.14s ease 0.3s, transform 0.14s ease 0.3s, visibility 0s linear 0.3s;
8997
+ }
8998
+ /* Pause · Lucide pause (two rounded vertical bars). */
8999
+ .ib-pause {
9000
+ --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect x='14' y='3' width='5' height='18' rx='1'/><rect x='5' y='3' width='5' height='18' rx='1'/></svg>");
9001
+ }
9002
+ /* Adjourn · Lucide log-out (door + arrow exit). Reads as "leave
9003
+ this session"; the confirm overlay clarifies brief filing. */
9004
+ .ib-adjourn {
9005
+ --icon: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'/><polyline points='16 17 21 12 16 7'/><line x1='21' x2='9' y1='12' y2='12'/></svg>");
8820
9006
  }
8821
9007
  .input-wrap {
9008
+ flex: 1 1 auto;
9009
+ min-width: 0;
8822
9010
  background: var(--bg);
8823
9011
  border: 0.5px solid var(--line-strong);
8824
9012
  padding: 0 8px;
@@ -9073,6 +9261,17 @@
9073
9261
  </div>
9074
9262
 
9075
9263
  <footer class="input-bar">
9264
+ <!-- Session-control toolbar · pause + adjourn live left of
9265
+ the input pill so they're reachable without leaving
9266
+ the typing area. Both reuse the existing data-pause /
9267
+ data-adjourn handlers; no new backend routes. The
9268
+ visual grammar matches head-actions (mono caps,
9269
+ brackets) but at a tighter scale. Adjourn turns warn
9270
+ on hover to flag its destructiveness. -->
9271
+ <div class="input-bar-actions">
9272
+ <button type="button" class="ib-action ib-pause" data-pause aria-label="Pause discussion" data-tip="Pause · you can resume later"></button>
9273
+ <button type="button" class="ib-action ib-adjourn" data-adjourn aria-label="Adjourn the room" data-tip="Adjourn · file the report and end"></button>
9274
+ </div>
9076
9275
  <div class="input-wrap">
9077
9276
  <input type="text" placeholder="interject anytime · @first_p to direct a director..." data-send-input>
9078
9277
  <button class="send-btn" data-send-button>[ Send ]</button>
@@ -9085,6 +9284,7 @@
9085
9284
  </div>
9086
9285
  <div class="paused-bar-actions">
9087
9286
  <a href="?status=live" class="resume-btn-lg">[ ▶ Resume Discussion ]</a>
9287
+ <a href="#" class="adjourn-btn-lg" data-adjourn>[ <span class="adjourn-glyph">⏏</span> Adjourn ]</a>
9088
9288
  </div>
9089
9289
  </footer>
9090
9290
 
@@ -8,9 +8,11 @@
8
8
  (function () {
9
9
  const MODEL_GROUPS = [
10
10
  { provider: "anthropic", models: [
11
- { v: "sonnet-4-6", name: "Sonnet 4.6", deck: "balanced · default" },
12
- { v: "opus-4-7", name: "Opus 4.7", deck: "deep reasoning" },
13
- { v: "haiku-4-5", name: "Haiku 4.5", deck: "fast · low-cost" }
11
+ { v: "sonnet-4-6", name: "Sonnet 4.6", deck: "balanced · default" },
12
+ { v: "opus-4-7", name: "Opus 4.7", deck: "deep reasoning" },
13
+ { v: "opus-4-6", name: "Opus 4.6", deck: "prior-gen flagship" },
14
+ { v: "opus-4-6-fast", name: "Opus 4.6 Fast", deck: "faster 4.6 · same intelligence" },
15
+ { v: "haiku-4-5", name: "Haiku 4.5", deck: "fast · low-cost" }
14
16
  ]},
15
17
  { provider: "openai", models: [
16
18
  { v: "gpt-5-5", name: "GPT-5.5", deck: "flagship · 1M ctx" },
@@ -49,15 +49,21 @@
49
49
  as PingFang globally (user preference). Songti SC remains as a
50
50
  last-resort fallback for headless contexts that can't reach
51
51
  PingFang's font directory. */
52
- --serif: "Charter", "Source Serif Pro", "Iowan Old Style", Georgia,
52
+ /* CJK dispatch handles lead each cascade · see report.html
53
+ @font-face block. Without "cjk-*" first, Chrome's print pipeline
54
+ lands on Arial mid-cascade and renders Chinese as .notdef. */
55
+ --serif: "cjk-serif",
56
+ "Charter", "Source Serif Pro", "Iowan Old Style", Georgia,
53
57
  "PingFang SC", "PingFang TC", "Hiragino Sans GB",
54
58
  "Source Han Sans CN", "Noto Sans CJK SC",
55
59
  "Songti SC", "STSong", serif;
56
- --sans: "Inter", "Helvetica Neue", "Arial", -apple-system, BlinkMacSystemFont,
60
+ --sans: "cjk-sans",
61
+ "Inter", "Helvetica Neue", "Arial", -apple-system, BlinkMacSystemFont,
57
62
  "PingFang SC", "PingFang TC", "Hiragino Sans GB",
58
63
  "Source Han Sans CN", "Noto Sans CJK SC",
59
64
  "Songti SC", "STSong", sans-serif;
60
- --mono: "SF Mono", "JetBrains Mono", "Menlo",
65
+ --mono: "cjk-mono",
66
+ "SF Mono", "JetBrains Mono", "Menlo",
61
67
  "PingFang SC", "PingFang TC", "Hiragino Sans GB",
62
68
  "Source Han Sans CN", "Songti SC", "STSong", monospace;
63
69
  }
@@ -1086,9 +1092,8 @@ html, body {
1086
1092
  background: transparent;
1087
1093
  border-top: 1px solid var(--gold);
1088
1094
  border-bottom: 1px solid var(--rule);
1089
- padding: 22px 0 18px;
1095
+ padding: 18px 0 14px;
1090
1096
  margin: 20px 0 26px;
1091
- min-height: 360px;
1092
1097
  text-align: center;
1093
1098
  font-family: var(--mono);
1094
1099
  font-size: 11px;
@@ -32,15 +32,22 @@
32
32
  /* Latin remains serif (Tiempos / Charter — anthropic signature).
33
33
  CJK falls to PingFang SC first per global preference; Songti
34
34
  remains as a last-resort fallback. */
35
- --serif: "Tiempos Headline", "Tiempos", "Source Serif Pro", "Charter",
35
+ /* CJK dispatch handles (cjk-serif/sans/mono) lead each cascade so
36
+ Chrome's print pipeline routes CJK code points to PingFang/Songti
37
+ via @font-face unicode-range. Without these, Chrome lands on
38
+ Arial mid-cascade and renders Chinese as .notdef glyphs in PDFs. */
39
+ --serif: "cjk-serif",
40
+ "Tiempos Headline", "Tiempos", "Source Serif Pro", "Charter",
36
41
  "Iowan Old Style", "Palatino", Georgia,
37
42
  "PingFang SC", "PingFang TC", "Hiragino Sans GB",
38
43
  "Source Han Sans CN", "Noto Sans CJK SC",
39
44
  "Songti SC", "STSong", serif;
40
- --sans: "Söhne", "Styrene B", "Inter", "Helvetica Neue", -apple-system,
45
+ --sans: "cjk-sans",
46
+ "Söhne", "Styrene B", "Inter", "Helvetica Neue", -apple-system,
41
47
  BlinkMacSystemFont, system-ui,
42
48
  "PingFang SC", "Hiragino Sans GB", "Source Han Sans CN", "Noto Sans CJK SC", sans-serif;
43
- --mono: "Söhne Mono", "JetBrains Mono", "SF Mono",
49
+ --mono: "cjk-mono",
50
+ "Söhne Mono", "JetBrains Mono", "SF Mono",
44
51
  "PingFang SC", "Source Han Sans CN", monospace;
45
52
  }
46
53
 
@@ -602,7 +609,7 @@ html, body {
602
609
  .body section.section-methodology strong { color: var(--ink); font-family: var(--sans); font-size: 15px; font-weight: 600; }
603
610
 
604
611
  .body pre.codeblock { background: var(--surface); border: 1px solid var(--rule); padding: 14px; font-family: var(--mono); font-size: 14px; color: var(--ink-mid); border-radius: 3px; }
605
- .body pre.mermaid { background: var(--surface); border-top: 2px solid var(--clay); border-bottom: 1px solid var(--rule); padding: 28px 24px 24px; margin: 22px 0 28px; min-height: 380px; text-align: center; font-family: var(--mono); font-size: 12px; color: var(--ink-faint); }
612
+ .body pre.mermaid { background: var(--surface); border-top: 2px solid var(--clay); border-bottom: 1px solid var(--rule); padding: 20px 18px 18px; margin: 22px 0 28px; text-align: center; font-family: var(--mono); font-size: 12px; color: var(--ink-faint); }
606
613
  .body pre.mermaid svg text { font-family: var(--serif) !important; }
607
614
 
608
615
  .foot-rule {
@@ -51,12 +51,17 @@
51
51
  of every cascade — they're guaranteed-present on Mac and serve
52
52
  as a last-resort glyph source if the preferred CJK families
53
53
  aren't available (some print / headless contexts). */
54
- --mono: "Human Sans", "Inter", -apple-system, BlinkMacSystemFont,
54
+ /* CJK dispatch handles lead each cascade · see report.html
55
+ @font-face block. Without "cjk-*" first, Chrome's print pipeline
56
+ lands on Arial mid-cascade and renders Chinese as .notdef. */
57
+ --mono: "cjk-mono",
58
+ "Human Sans", "Inter", -apple-system, BlinkMacSystemFont,
55
59
  "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
56
60
  "Source Han Sans CN", "Noto Sans CJK SC",
57
61
  "Songti SC", "STSong",
58
62
  "Segoe UI", system-ui, sans-serif;
59
- --sans: "Human Sans", "Inter", -apple-system, BlinkMacSystemFont,
63
+ --sans: "cjk-sans",
64
+ "Human Sans", "Inter", -apple-system, BlinkMacSystemFont,
60
65
  "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
61
66
  "Source Han Sans CN", "Noto Sans CJK SC",
62
67
  "Songti SC", "STSong",
@@ -69,7 +74,8 @@
69
74
  fall through to PingFang SC first (sans) — user preference is
70
75
  that Chinese reads in PingFang globally. Songti / Source Han
71
76
  Serif sit deep as last-resort fallbacks. */
72
- --serif: "Charter", "Source Serif Pro", "Iowan Old Style",
77
+ --serif: "cjk-serif",
78
+ "Charter", "Source Serif Pro", "Iowan Old Style",
73
79
  "Georgia",
74
80
  "PingFang SC", "PingFang TC", "Hiragino Sans GB",
75
81
  "Source Han Sans CN", "Noto Sans CJK SC",
@@ -1047,7 +1053,7 @@ html, body {
1047
1053
  background: var(--panel-2);
1048
1054
  border-top: 2px solid var(--text);
1049
1055
  border-bottom: 1px solid var(--line-bright);
1050
- padding: 28px 24px 24px;
1056
+ padding: 20px 18px 18px;
1051
1057
  margin: 22px 0 28px;
1052
1058
  text-align: center;
1053
1059
  overflow-x: auto;
@@ -1055,7 +1061,11 @@ html, body {
1055
1061
  font-size: 11px;
1056
1062
  color: var(--text-faint);
1057
1063
  line-height: 1.4;
1058
- min-height: 360px;
1064
+ /* No min-height · the chart's own dimensions (set in mermaid init)
1065
+ drive the frame size. Forcing 360px left empty bands above /
1066
+ below smaller diagrams (sequence, journey, mindmap) and pushed
1067
+ the design toward "presentation slide" rather than "inline
1068
+ exhibit". Refined-compact = the frame sizes to its content. */
1059
1069
  }
1060
1070
  .body pre.mermaid svg {
1061
1071
  max-width: 100%;
@@ -28,10 +28,15 @@
28
28
  /* Cross-spine token aliases · used by the unified design system. */
29
29
  --accent: var(--brand);
30
30
  --warn: var(--red);
31
- --sans: "Inter", "Helvetica Neue", "Arial", -apple-system, BlinkMacSystemFont,
31
+ /* CJK dispatch handles lead each cascade · see report.html
32
+ @font-face block. Without "cjk-*" first, Chrome's print pipeline
33
+ lands on Arial mid-cascade and renders Chinese as .notdef. */
34
+ --sans: "cjk-sans",
35
+ "Inter", "Helvetica Neue", "Arial", -apple-system, BlinkMacSystemFont,
32
36
  "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Source Han Sans CN",
33
37
  "Noto Sans CJK SC", sans-serif;
34
- --mono: "SF Mono", "JetBrains Mono", "Menlo",
38
+ --mono: "cjk-mono",
39
+ "SF Mono", "JetBrains Mono", "Menlo",
35
40
  "PingFang SC", "Source Han Sans CN", monospace;
36
41
  }
37
42
 
@@ -431,7 +436,7 @@ html, body {
431
436
  .body section.section-methodology strong { color: var(--ink-soft); font-family: var(--mono); font-size: 12px; }
432
437
 
433
438
  .body pre.codeblock { background: var(--bg-soft); border: 1px solid var(--rule); padding: 12px 14px; font-family: var(--mono); font-size: 13px; color: var(--ink-soft); }
434
- .body pre.mermaid { background: var(--bg); border-top: 2px solid var(--brand); border-bottom: 1px solid var(--rule); padding: 28px 24px 24px; margin: 22px 0 28px; min-height: 380px; text-align: center; font-family: var(--mono); font-size: 11px; color: var(--ink-faint); }
439
+ .body pre.mermaid { background: var(--bg); border-top: 2px solid var(--brand); border-bottom: 1px solid var(--rule); padding: 20px 18px 18px; margin: 22px 0 28px; text-align: center; font-family: var(--mono); font-size: 11px; color: var(--ink-faint); }
435
440
  .body pre.mermaid svg text { font-family: var(--sans) !important; }
436
441
 
437
442
  .foot-rule {
@@ -30,10 +30,15 @@
30
30
  report.html. */
31
31
  --accent: var(--blue);
32
32
  --warn: var(--red);
33
- --sans: "Inter", "Helvetica Neue", "Arial", -apple-system, BlinkMacSystemFont,
33
+ /* CJK dispatch handles lead each cascade · see report.html
34
+ @font-face block. Without "cjk-*" first, Chrome's print pipeline
35
+ lands on Arial mid-cascade and renders Chinese as .notdef. */
36
+ --sans: "cjk-sans",
37
+ "Inter", "Helvetica Neue", "Arial", -apple-system, BlinkMacSystemFont,
34
38
  "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Source Han Sans CN",
35
39
  "Noto Sans CJK SC", sans-serif;
36
- --mono: "SF Mono", "JetBrains Mono", "Menlo",
40
+ --mono: "cjk-mono",
41
+ "SF Mono", "JetBrains Mono", "Menlo",
37
42
  "PingFang SC", "Source Han Sans CN", monospace;
38
43
  }
39
44
 
@@ -419,7 +424,7 @@ html, body {
419
424
  .body section.section-methodology strong { color: var(--ink-soft); font-family: var(--mono); font-size: 12px; }
420
425
 
421
426
  .body pre.codeblock { background: var(--soft); border: 1px solid var(--rule); padding: 14px; font-family: var(--mono); font-size: 13px; color: var(--ink-soft); }
422
- .body pre.mermaid { background: var(--bg); border-top: 2px solid var(--navy); border-bottom: 1px solid var(--rule); padding: 28px 24px 24px; margin: 22px 0 28px; min-height: 380px; text-align: center; font-family: var(--mono); font-size: 11px; color: var(--ink-faint); }
427
+ .body pre.mermaid { background: var(--bg); border-top: 2px solid var(--navy); border-bottom: 1px solid var(--rule); padding: 20px 18px 18px; margin: 22px 0 28px; text-align: center; font-family: var(--mono); font-size: 11px; color: var(--ink-faint); }
423
428
  .body pre.mermaid svg text { font-family: var(--sans) !important; }
424
429
 
425
430
  .foot-rule {
@@ -21,10 +21,15 @@
21
21
  /* Cross-spine token aliases · used by the unified design system. */
22
22
  --accent: var(--teal);
23
23
  --warn: var(--red);
24
- --sans: "Söhne", "Inter", "Helvetica Neue", -apple-system, BlinkMacSystemFont, system-ui,
24
+ /* CJK dispatch handles lead each cascade · see report.html
25
+ @font-face block. Without "cjk-*" first, Chrome's print pipeline
26
+ lands on Arial mid-cascade and renders Chinese as .notdef. */
27
+ --sans: "cjk-sans",
28
+ "Söhne", "Inter", "Helvetica Neue", -apple-system, BlinkMacSystemFont, system-ui,
25
29
  "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Source Han Sans CN",
26
30
  "Noto Sans CJK SC", sans-serif;
27
- --mono: "Söhne Mono", "JetBrains Mono", "SF Mono", "Menlo",
31
+ --mono: "cjk-mono",
32
+ "Söhne Mono", "JetBrains Mono", "SF Mono", "Menlo",
28
33
  "PingFang SC", "Source Han Sans CN", monospace;
29
34
  }
30
35
 
@@ -412,7 +417,7 @@ html, body {
412
417
  .body section.section-methodology strong { color: var(--ink-soft); font-family: var(--mono); }
413
418
 
414
419
  .body pre.codeblock { background: var(--paper); border: 1px solid var(--rule); padding: 14px 16px; font-family: var(--mono); font-size: 13px; color: var(--ink-soft); border-radius: 4px; }
415
- .body pre.mermaid { background: var(--paper); border-top: 2px solid var(--ink); border-bottom: 1px solid var(--rule); border-radius: 4px; padding: 28px 24px 24px; margin: 22px 0 28px; min-height: 380px; text-align: center; font-family: var(--mono); font-size: 12px; color: var(--ink-faint); }
420
+ .body pre.mermaid { background: var(--paper); border-top: 2px solid var(--ink); border-bottom: 1px solid var(--rule); border-radius: 4px; padding: 20px 18px 18px; margin: 22px 0 28px; text-align: center; font-family: var(--mono); font-size: 12px; color: var(--ink-faint); }
416
421
  .body pre.mermaid svg text { font-family: var(--sans) !important; }
417
422
 
418
423
  .foot-rule {