@chatcode/cco-market 1.45.1

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.
Files changed (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/README.zh.md +143 -0
  4. package/UPDATE-API-V1.md +132 -0
  5. package/client/client.js +11534 -0
  6. package/cordis.patch.yml +5 -0
  7. package/lib/accelerate.js +184 -0
  8. package/lib/agents.js +36 -0
  9. package/lib/backup.js +572 -0
  10. package/lib/catalog-local-match.js +126 -0
  11. package/lib/catalog-npm.js +111 -0
  12. package/lib/changelog.js +242 -0
  13. package/lib/channels.js +64 -0
  14. package/lib/check.js +1067 -0
  15. package/lib/compatibility.js +198 -0
  16. package/lib/diagnostics.js +49 -0
  17. package/lib/discovery-compatibility.js +260 -0
  18. package/lib/dsh-cli.js +949 -0
  19. package/lib/dsh-install.js +108 -0
  20. package/lib/gist.js +352 -0
  21. package/lib/groups.js +97 -0
  22. package/lib/home-paths.js +42 -0
  23. package/lib/hot.js +519 -0
  24. package/lib/http.js +39 -0
  25. package/lib/index.js +97 -0
  26. package/lib/install.js +387 -0
  27. package/lib/log.js +197 -0
  28. package/lib/ndjson.js +154 -0
  29. package/lib/net.js +98 -0
  30. package/lib/order.js +284 -0
  31. package/lib/patch.js +521 -0
  32. package/lib/pnpm-compat.js +490 -0
  33. package/lib/presets.js +278 -0
  34. package/lib/profile.js +911 -0
  35. package/lib/region-probe.js +93 -0
  36. package/lib/regions.js +253 -0
  37. package/lib/registry.js +204 -0
  38. package/lib/restart.js +373 -0
  39. package/lib/routes.js +4372 -0
  40. package/lib/settings.js +109 -0
  41. package/lib/skill-market/api.js +119 -0
  42. package/lib/skill-market/config.js +29 -0
  43. package/lib/skill-market/installer.js +352 -0
  44. package/lib/skill-market/runtime.js +13 -0
  45. package/lib/skill-market/tui.js +119 -0
  46. package/lib/skill-market/types.js +1 -0
  47. package/lib/skill-market/web-routes.js +123 -0
  48. package/lib/snapshot.js +500 -0
  49. package/lib/source-migration.js +54 -0
  50. package/lib/sources.js +596 -0
  51. package/lib/store.js +91 -0
  52. package/lib/themes.js +102 -0
  53. package/lib/trial.js +116 -0
  54. package/lib/types/accelerate.d.ts +85 -0
  55. package/lib/types/agents.d.ts +22 -0
  56. package/lib/types/backup.d.ts +131 -0
  57. package/lib/types/catalog-local-match.d.ts +50 -0
  58. package/lib/types/catalog-npm.d.ts +47 -0
  59. package/lib/types/changelog.d.ts +102 -0
  60. package/lib/types/channels.d.ts +56 -0
  61. package/lib/types/check.d.ts +266 -0
  62. package/lib/types/compatibility.d.ts +64 -0
  63. package/lib/types/diagnostics.d.ts +29 -0
  64. package/lib/types/discovery-compatibility.d.ts +80 -0
  65. package/lib/types/dsh-cli.d.ts +324 -0
  66. package/lib/types/dsh-install.d.ts +46 -0
  67. package/lib/types/gist.d.ts +53 -0
  68. package/lib/types/groups.d.ts +34 -0
  69. package/lib/types/home-paths.d.ts +16 -0
  70. package/lib/types/hot.d.ts +221 -0
  71. package/lib/types/http.d.ts +12 -0
  72. package/lib/types/index.d.ts +14 -0
  73. package/lib/types/install.d.ts +172 -0
  74. package/lib/types/log.d.ts +43 -0
  75. package/lib/types/ndjson.d.ts +52 -0
  76. package/lib/types/net.d.ts +58 -0
  77. package/lib/types/order.d.ts +100 -0
  78. package/lib/types/patch.d.ts +129 -0
  79. package/lib/types/pnpm-compat.d.ts +93 -0
  80. package/lib/types/presets.d.ts +90 -0
  81. package/lib/types/profile.d.ts +253 -0
  82. package/lib/types/region-probe.d.ts +50 -0
  83. package/lib/types/regions.d.ts +122 -0
  84. package/lib/types/registry.d.ts +86 -0
  85. package/lib/types/restart.d.ts +194 -0
  86. package/lib/types/routes.d.ts +67 -0
  87. package/lib/types/settings.d.ts +78 -0
  88. package/lib/types/skill-market/api.d.ts +10 -0
  89. package/lib/types/skill-market/config.d.ts +3 -0
  90. package/lib/types/skill-market/installer.d.ts +23 -0
  91. package/lib/types/skill-market/runtime.d.ts +10 -0
  92. package/lib/types/skill-market/tui.d.ts +4 -0
  93. package/lib/types/skill-market/types.d.ts +71 -0
  94. package/lib/types/skill-market/web-routes.d.ts +19 -0
  95. package/lib/types/snapshot.d.ts +93 -0
  96. package/lib/types/source-migration.d.ts +11 -0
  97. package/lib/types/sources.d.ts +216 -0
  98. package/lib/types/store.d.ts +30 -0
  99. package/lib/types/themes.d.ts +40 -0
  100. package/lib/types/trial.d.ts +61 -0
  101. package/lib/types/update-api-v1.d.ts +66 -0
  102. package/lib/types/updates.d.ts +123 -0
  103. package/lib/types/verify.d.ts +139 -0
  104. package/lib/update-api-v1.js +215 -0
  105. package/lib/updates.js +361 -0
  106. package/lib/verify.js +453 -0
  107. package/package.json +121 -0
  108. package/src/accelerate.ts +213 -0
  109. package/src/agents.ts +43 -0
  110. package/src/backup.ts +583 -0
  111. package/src/catalog-local-match.ts +144 -0
  112. package/src/catalog-npm.ts +120 -0
  113. package/src/changelog.ts +282 -0
  114. package/src/channels.ts +70 -0
  115. package/src/check.ts +1239 -0
  116. package/src/client/CommentsModal.tsx +119 -0
  117. package/src/client/Diagnostics.tsx +907 -0
  118. package/src/client/ErrorBoundary.tsx +111 -0
  119. package/src/client/InstallToast.tsx +31 -0
  120. package/src/client/Market.module.css +830 -0
  121. package/src/client/MarketSection.tsx +5301 -0
  122. package/src/client/OperationsPanel.tsx +365 -0
  123. package/src/client/SettingsCard.tsx +621 -0
  124. package/src/client/SkillMarket.module.css +21 -0
  125. package/src/client/SkillMarketSection.tsx +162 -0
  126. package/src/client/comments.ts +54 -0
  127. package/src/client/globals.d.ts +13 -0
  128. package/src/client/index.ts +180 -0
  129. package/src/client/locales.ts +1112 -0
  130. package/src/client/market-data.ts +1321 -0
  131. package/src/client/operations.ts +201 -0
  132. package/src/client/preset-panel.tsx +263 -0
  133. package/src/client/primitives.d.ts +140 -0
  134. package/src/client/self-check.ts +147 -0
  135. package/src/client/snapshot-panel.tsx +244 -0
  136. package/src/compatibility.ts +237 -0
  137. package/src/diagnostics.ts +84 -0
  138. package/src/discovery-compatibility.ts +315 -0
  139. package/src/dsh-cli.ts +1126 -0
  140. package/src/dsh-install.ts +118 -0
  141. package/src/gist.ts +362 -0
  142. package/src/groups.ts +111 -0
  143. package/src/home-paths.ts +53 -0
  144. package/src/hot.ts +628 -0
  145. package/src/http.ts +41 -0
  146. package/src/index.ts +128 -0
  147. package/src/install.ts +420 -0
  148. package/src/log.ts +206 -0
  149. package/src/ndjson.ts +185 -0
  150. package/src/net.ts +106 -0
  151. package/src/order.ts +303 -0
  152. package/src/patch.ts +522 -0
  153. package/src/pnpm-compat.ts +527 -0
  154. package/src/presets.ts +344 -0
  155. package/src/profile.ts +940 -0
  156. package/src/region-probe.ts +97 -0
  157. package/src/regions.ts +310 -0
  158. package/src/registry.ts +250 -0
  159. package/src/restart.ts +396 -0
  160. package/src/routes.ts +4506 -0
  161. package/src/settings.ts +141 -0
  162. package/src/skill-market/api.ts +130 -0
  163. package/src/skill-market/config.ts +32 -0
  164. package/src/skill-market/installer.ts +337 -0
  165. package/src/skill-market/runtime.ts +14 -0
  166. package/src/skill-market/tui.ts +157 -0
  167. package/src/skill-market/types.ts +80 -0
  168. package/src/skill-market/web-routes.ts +132 -0
  169. package/src/snapshot.ts +532 -0
  170. package/src/source-migration.ts +61 -0
  171. package/src/sources.ts +565 -0
  172. package/src/store.ts +89 -0
  173. package/src/themes.ts +125 -0
  174. package/src/trial.ts +156 -0
  175. package/src/update-api-v1.ts +277 -0
  176. package/src/updates.ts +400 -0
  177. package/src/verify.ts +492 -0
@@ -0,0 +1,830 @@
1
+ /* Market section + install toast styles. Every color resolves through a
2
+ * --dsw-alias-* design token with the original light-mode literal as
3
+ * fallback. Interactive chrome (buttons, menus, dialogs, pills, inputs,
4
+ * disclosures) comes from @deepseek-ai/dsh-client-ui-primitives; this sheet
5
+ * only carries layout, cards, and the few bespoke surfaces without a
6
+ * primitive (progress bar, banners, tabs, swatches). */
7
+
8
+ .root{height:100%;display:flex;flex-direction:column;min-width:0;color:var(--dsw-alias-label-primary,#1f2328);position:relative;container-type:inline-size}
9
+ .head{display:flex;flex-direction:column;gap:12px;padding:4px 4px 6px}
10
+ .title{font-size:16px;line-height:24px;font-weight:500;margin:0}
11
+ .sub{display:flex;align-items:center;gap:8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#8b93a1);margin:0}
12
+ .submitLink{font-size:11px;line-height:18px;color:var(--dsw-alias-label-tertiary,#8b93a1);text-decoration:none;white-space:nowrap}
13
+ .submitLink:hover{color:var(--dsw-alias-brand-primary,#4f6ef7);text-decoration:underline}
14
+ .tabs{display:flex;gap:2px;border-bottom:1px solid var(--dsw-alias-border-l2,#e5e7eb);align-items:flex-end}
15
+ .tab{border:none;background:none;font:inherit;font-size:13px;color:var(--dsw-alias-label-secondary,#6b7280);padding:7px 12px;cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap}
16
+ .tab.on{color:var(--dsw-alias-brand-primary,#4f6ef7);border-bottom-color:var(--dsw-alias-brand-primary,#4f6ef7);font-weight:600}
17
+ /* Second-level tabs under "Advanced" — same button styling as .tabs so the
18
+ two read as one family, but no full-width bottom border of its own: that
19
+ border is what visually says "top-level tab row", and having two of them
20
+ stacked read as two unrelated tab bars rather than a parent/child pair. */
21
+ .subTabs{display:flex;gap:2px;align-items:flex-end;margin:-4px 0 4px}
22
+ .banner{display:flex;align-items:center;gap:8px;background:var(--dsw-alias-bg-layer-2,#fdf3e3);border:1px solid var(--dsw-alias-border-l2,#f3e3c3);border-radius:8px;padding:8px 12px;font-size:12px;margin:0}
23
+ .bannerIcon{flex-shrink:0;color:var(--dsw-alias-label-secondary,#6b7280)}
24
+ .bannerHint{display:inline-flex;color:var(--dsw-alias-label-tertiary,#8b93a1);cursor:help}
25
+ /* overflow-anchor:none is load-bearing, not a tidy-up (#395 by @zhangyh-x:
26
+ "展开分类,滑动列表时会闪动"). Chrome anchors the scroll position to a node
27
+ below the viewport and silently rewrites scrollTop whenever content ABOVE
28
+ it changes height. The sticky header does exactly that on purpose: leaving
29
+ the top auto-collapses an expanded category row, which is ~120px. So the
30
+ first small scroll collapsed the row, Chrome moved scrollTop back by the
31
+ same 120px to keep the anchored card still, that put the viewport above
32
+ the sentinel again, the row re-expanded, and the cycle repeated for as
33
+ long as the user kept scrolling. Measured in Chrome at 1020x526 with the
34
+ row open: scrollTop 8 -> 134 -> 24 inside 30ms with the row flipping
35
+ 146px <-> 26px each time; parking at scrollTop 20 was yanked back to 0.
36
+ With anchoring off, the same gesture collapses once and scrollTop stays
37
+ where the user put it.
38
+
39
+ Anchoring costs this list nothing to give up: every image it renders is
40
+ sized in CSS (.shot, .cardShot, .av), so nothing above the viewport
41
+ resizes on load, and the banners that can appear live outside this
42
+ scroller entirely. The one thing anchoring had left to compensate for was
43
+ the deliberate collapse — the one adjustment that must not happen. */
44
+ .body{flex:1;overflow-y:auto;padding:12px 4px 24px;overflow-x:hidden;overflow-anchor:none}
45
+ /* Sticky, so the search box and the category row both stay reachable while
46
+ the grid scrolls — they used to stick separately (only .cats did), which
47
+ left the search row to scroll away under .head's fixed title/tabs strip,
48
+ getting sliced off mid-scroll instead of disappearing cleanly. Wrapping
49
+ both in one sticky block makes them move (and stop) together; a second,
50
+ independently-offset sticky element would need its `top` hand-tuned to
51
+ the search row's rendered height, which drifts with locale/font/icon
52
+ changes — this doesn't need that number to exist at all.
53
+ The negative top margin used to pull it 12px up over the search row, and
54
+ with a background and z-index above it that covered the input's lower
55
+ edge — the control looked cut in half at rest, before any scrolling.
56
+ Only the horizontal bleed is wanted; the vertical one was collateral. */
57
+ /* The scroller (.body) carries padding-top:12px, and a sticky child cannot
58
+ rise above its containing block — the parent's CONTENT box. So this row
59
+ stops 12px short of the scrollport and the grid scrolls through the strip
60
+ above it: measured on a real host as cats.top=214 against scroller.top=203.
61
+ Reported twice, first as "a big gap above the sticky row" and then as a
62
+ card clipped into it.
63
+ ::before paints that strip in the same background and inherits the row's
64
+ z-index, so the row covers what it cannot reach. Fixing it by removing the
65
+ scroller's padding would re-space all five tabs to repair one. */
66
+ .stickyHead{position:sticky;top:-1px;z-index:5;background:var(--dsw-alias-bg-layer-2,#f7f8fa)}
67
+ .stickyHead::before{content:'';position:absolute;left:0;right:0;bottom:100%;height:14px;background:inherit;pointer-events:none}
68
+ .cats{padding:12px 4px 4px;margin:0 -4px 2px}
69
+ .catsRow{display:flex;gap:8px;align-items:flex-start;position:relative}
70
+ .star{font-size:11px;color:var(--dsw-alias-label-secondary,#9ca3af);flex:0 0 auto;white-space:nowrap}
71
+ /* Opaque disc behind the outline Button. The host outline fill (incl.
72
+ :hover) is transparent by design — without a surface under it the
73
+ chevron sits on card text. Keep the fill on this wrapper so the
74
+ Button can keep its native hover wash. */
75
+ .top{
76
+ position:absolute;right:18px;bottom:18px;z-index:20;display:inline-flex;
77
+ border-radius:99px;
78
+ background:var(--dsw-alias-bg-layer-1,#fff);
79
+ box-shadow:var(--dsw-shadow-lv1,0 4px 12px rgba(31,35,40,.08));
80
+ }
81
+ .topBtn{width:38px;height:38px;border-radius:99px;padding:0}
82
+ .tag{border:1px solid var(--dsw-alias-border-l3,#d9dde3);color:var(--dsw-alias-label-secondary,#6b7280);border-radius:4px;flex-shrink:0;padding:1px 6px;font-size:11px;line-height:16px}
83
+ .hostRequirement{border:1px solid var(--dsw-alias-border-l3,#d9dde3);color:var(--dsw-alias-label-secondary,#6b7280);border-radius:4px;min-width:0;max-width:100%;padding:1px 6px;font-size:11px;line-height:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
84
+ .hostFilterNote{margin-top:6px;color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:11px;line-height:16px}
85
+ .okState{font-size:12px;font-weight:600;color:var(--dsw-alias-state-success-primary,#16a34a);white-space:nowrap}
86
+ .catsWrap{display:flex;gap:6px;flex-wrap:wrap;flex:1;min-width:0;align-items:center}
87
+ .catsCollapsed{max-height:62px;overflow:hidden}
88
+ .catsToggle.catsToggle{height:26px;min-height:26px;padding:0 6px;color:var(--dsw-alias-label-secondary,#6b7280)}
89
+ .shots{display:flex;gap:8px;overflow-x:auto;padding:2px 0 6px;margin:6px 0 8px;-webkit-overflow-scrolling:touch;scrollbar-width:thin}
90
+ /* width, not just max-width — a slot with only a height reservation still
91
+ collapses to zero width until the image loads, so the strip snaps wider
92
+ the instant each one arrives instead of holding its place from the start. */
93
+ .shot{width:220px;height:150px;flex:0 0 auto;object-fit:cover;border-radius:8px;border:1px solid var(--dsw-alias-border-default,#e5e7eb);background:var(--dsw-alias-bg-layer-2,#f3f4f6);cursor:pointer}
94
+ .cardShots{display:flex;gap:6px;overflow-x:auto;padding:0 0 2px;margin:0 0 6px;-webkit-overflow-scrolling:touch;scrollbar-width:thin}
95
+ /* A fixed box so the strip holds its place: with width:auto the browser had
96
+ no aspect ratio until the bytes arrived, so each thumbnail rendered at its
97
+ min-width and jumped to height x natural-ratio once decoded.
98
+ `contain`, not `cover`: reserving the space is what fixes the jump, and
99
+ cropping to fill would throw away the uncropped thumbnail this strip was
100
+ built for. The letterbox shows through as the tile's own background. */
101
+ .cardShot{width:132px;height:88px;flex:0 0 auto;object-fit:contain;border-radius:8px;
102
+ border:1px solid var(--dsw-alias-border-default,#e5e7eb);
103
+ background:var(--dsw-alias-bg-layer-2,#f3f4f6);cursor:pointer;display:block}
104
+ /* Full-bleed overlay, not the Modal primitive — Modal is sized chrome
105
+ (title/description/footer) for making a decision, this is just an image
106
+ the user came to look at, no decision to make. */
107
+ /* Above the Settings Modal it opens on top of — that Modal's own z-index
108
+ isn't defined in this package (its CSS module ships with the host app),
109
+ so this has to out-rank any plausible dialog layer, not just tie and win
110
+ on DOM order. */
111
+ .lightbox{position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,.85);display:flex;align-items:center;justify-content:center;cursor:zoom-out}
112
+ .lightboxImg{max-width:90vw;max-height:85vh;object-fit:contain;border-radius:4px;cursor:default}
113
+ .lightboxClose{position:absolute;top:16px;right:16px;width:36px;height:36px;border-radius:99px;border:none;background:rgba(255,255,255,.12);color:#fff;font-size:22px;line-height:1;display:grid;place-items:center;cursor:pointer}
114
+ .lightboxClose:hover{background:rgba(255,255,255,.22)}
115
+ .lightboxNav{position:absolute;top:50%;transform:translateY(-50%);width:44px;height:44px;border-radius:99px;border:none;background:rgba(255,255,255,.12);color:#fff;display:grid;place-items:center;cursor:pointer}
116
+ .lightboxNav:hover{background:rgba(255,255,255,.22)}
117
+ .lightboxPrev{left:16px}
118
+ .lightboxNext{right:16px}
119
+ .lightboxDots{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);display:flex;gap:8px}
120
+ .lightboxDot{width:7px;height:7px;border-radius:99px;background:rgba(255,255,255,.4);cursor:pointer}
121
+ .lightboxDotOn{background:#fff}
122
+ .cmd{font-size:11px;line-height:18px;background:var(--dsw-alias-bg-layer-2,#f3f4f6);border-radius:6px;padding:8px 10px;font-family:ui-monospace,Menlo,monospace;margin:8px 0 0;word-break:break-all}
123
+ /* flex-wrap so a long warning + trailing link wraps like normal text — the
124
+ deprecated-plugin warning below (.depLine) is the same icon+text+link
125
+ shape and already gets this right; without it the trailing link is the
126
+ one flex item left with no room once the paragraph text wraps, and gets
127
+ squeezed into a vertical stack instead of dropping to its own line. */
128
+ .warnLine{color:var(--dsw-alias-state-warn-primary,#b45309);font-weight:600;font-size:12px;line-height:18px;margin:0;display:flex;align-items:center;gap:4px;flex-wrap:wrap}
129
+ .modalNote{font-size:12px;color:var(--dsw-alias-label-tertiary,#8b93a1);line-height:18px;margin:12px 0 0;display:flex;align-items:center;gap:4px}
130
+ .grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;gap:10px}
131
+ /* Masonry: two independent columns, each packing its own cards tight.
132
+ A CSS grid cannot do this — its rows are as tall as their tallest cell, so
133
+ a card with screenshots left a matching hole beside a card without them,
134
+ and `align-items:start` turned that hole into visible dead space rather
135
+ than a stretched card. Column-major `column-count` would pack just as
136
+ well but reorders the list down one column and then the other, which
137
+ destroys the ranking the sort just produced. Dealing the cards
138
+ alternately into two flex columns keeps rank reading left-to-right,
139
+ top-to-bottom, and lets each column close its own gaps. */
140
+ .masonry{display:flex;align-items:flex-start;gap:10px}
141
+ .masonryCol{flex:1;min-width:0;display:flex;flex-direction:column;gap:10px}
142
+ /* `.card` carries `align-self:start`, which under CSS grid means "do not
143
+ stretch VERTICALLY to match a taller row-mate". In a flex COLUMN the same
144
+ declaration addresses the other axis: it stops the card stretching
145
+ HORIZONTALLY, so its width becomes max-content and a card with a long
146
+ name grew to 444px inside a 273px column and covered the neighbouring
147
+ column's buttons. Same property, opposite meaning, silent either way.
148
+ Masonry needs the vertical freedom it already gets from the column stack,
149
+ so cards stretch to the column width here. min-width:0 is the flex
150
+ equivalent of the grid's `minmax(0, 1fr)`: without it a card's
151
+ min-content floor would push the column wide again. */
152
+ /* Two class names, not one: `.card` sets `align-self:start` and is declared
153
+ later in this file, so a single-class selector here would tie on
154
+ specificity and lose to source order. */
155
+ .masonry > .masonryCol > *{align-self:stretch;min-width:0}
156
+ @media (max-width:680px){
157
+ /* Stacked, the cross axis is horizontal and `.masonry`'s align-items keeps
158
+ the column shrink-to-fit — so the single column has to claim the width
159
+ explicitly. Both tabs render through the same component, so this belongs
160
+ here rather than as an inline style on one of them. */
161
+ .masonry{flex-direction:column}
162
+ .masonryCol{width:100%}
163
+ }
164
+ @media (max-width:680px){
165
+ .grid{grid-template-columns:minmax(0,1fr)}
166
+ }
167
+ .swatches{display:flex;gap:0;height:34px;border-radius:8px;overflow:hidden;border:1px solid var(--dsw-alias-border-l2,#e5e7eb)}
168
+ .themesGrid{margin-bottom:12px}
169
+ .swatches i{flex:1}
170
+
171
+ /* Themes are a visual choice, not a repository list. Their page therefore
172
+ owns a stable gallery instead of inheriting the generic masonry cards. */
173
+ .themeToolbar{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:8px;padding:0 4px 12px}
174
+ /* border-box, not the sheet's default content-box: the primitives' Input
175
+ renders a span with 8px padding and a 1px border, so `width:100%` resolved
176
+ to the grid cell (438px) and then rendered 18px WIDER than it — straight
177
+ under the filter and full-screen buttons (#305 by @lcthe). The same class
178
+ on the Discover tab never showed it: that row is flex, and a flex item
179
+ shrinks to fit where a grid item does not. */
180
+ .themeSearch{width:100%;min-width:0;box-sizing:border-box}
181
+ .themeToolbarActions{display:flex;align-items:center;justify-content:flex-end;gap:8px}
182
+ .themeFullscreenBtn.themeFullscreenBtn{width:28px;min-width:28px;padding:0}
183
+ .themeResultBar{display:flex;align-items:center;min-height:24px;padding:0 4px 8px;color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:12px;line-height:18px}
184
+ .favoritesSectionHead{margin:16px 0 8px;padding:0 4px;font-size:13px;font-weight:600;line-height:20px;color:var(--dsw-alias-label-primary,#1f2328)}
185
+ .favoritesSectionHead:first-of-type{margin-top:0}
186
+ .favoritesStaleBar{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin:0 0 8px;padding:8px 10px;border:1px solid var(--dsw-alias-state-warn-tertiary,rgba(180,83,9,.2));border-radius:8px;background:var(--dsw-alias-state-warn-tertiary,rgba(180,83,9,.06));color:var(--dsw-alias-state-warn-primary,#b45309);font-size:12px;line-height:18px}
187
+ .favoritesStaleOnly{display:flex;flex-direction:column;align-items:flex-start;gap:10px}
188
+ .themeGallery{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));align-items:stretch;gap:10px}
189
+ .themeCard{display:flex;flex-direction:column;min-width:0;overflow:hidden;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:8px;background:var(--dsw-alias-bg-layer-1,#fff);transition:border-color 160ms cubic-bezier(.16,1,.3,1),box-shadow 160ms cubic-bezier(.16,1,.3,1),transform 160ms cubic-bezier(.16,1,.3,1)}
190
+ .themeCard:hover{border-color:var(--dsw-alias-border-l3,#d9dde3);box-shadow:var(--dsw-shadow-lv1,0 4px 12px rgba(31,35,40,.08));transform:translateY(-1px)}
191
+ .themeCover{position:relative;display:block;width:100%;aspect-ratio:16/10;overflow:hidden;padding:0;border:0;border-bottom:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:0;background:var(--dsw-alias-bg-layer-2,#f3f4f6);color:var(--dsw-alias-label-secondary,#6b7280);cursor:zoom-in}
192
+ .themeCover img{display:block;width:100%;height:100%;object-fit:contain;background:var(--dsw-alias-bg-layer-2,#f3f4f6);transition:transform 180ms cubic-bezier(.16,1,.3,1)}
193
+ .themeCover:hover img{transform:scale(1.012)}
194
+ .themeCover:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4f6ef7);outline-offset:-3px}
195
+ .themeCoverEmpty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;cursor:default;color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:12px;line-height:18px}
196
+ .themePreviewAction,.themePreviewCount{position:absolute;bottom:8px;display:inline-flex;align-items:center;height:24px;padding:0 8px;border:1px solid rgba(255,255,255,.34);border-radius:4px;background:rgba(20,24,31,.78);color:#fff;font-size:11px;line-height:16px;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
197
+ .themePreviewAction{right:8px;gap:4px}
198
+ .themePreviewCount{left:8px}
199
+ .themeCardBody{display:flex;flex:1;flex-direction:column;gap:8px;padding:12px}
200
+ .themeCardHead{display:flex;align-items:flex-start;gap:8px;min-width:0}
201
+ .themeIdentity{min-width:0;flex:1}
202
+ .themeStatus,.themeStatusMuted{flex:none;display:inline-flex;align-items:center;min-height:22px;padding:0 8px;border-radius:4px;font-size:11px;font-weight:600;white-space:nowrap}
203
+ .themeStatus{background:var(--dsw-alias-state-success-tertiary,rgba(22,163,74,.1));color:var(--dsw-alias-state-success-primary,#15803d)}
204
+ .themeStatusMuted{background:var(--dsw-alias-bg-layer-2,#f3f4f6);color:var(--dsw-alias-label-secondary,#6b7280)}
205
+ .themeDescription{display:-webkit-box;min-height:36px;margin:0;overflow:hidden;color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:12px;line-height:18px;-webkit-box-orient:vertical;-webkit-line-clamp:3}
206
+ .themeCardFooter{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:auto;padding-top:4px}
207
+ .themeLifecycle{min-width:0;overflow:hidden;color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:11px;line-height:18px;text-overflow:ellipsis;white-space:nowrap}
208
+ .themeActions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;margin-left:auto}
209
+
210
+ /* The market usually lives inside a settings panel, so viewport media
211
+ queries do not describe the width available to it. Container queries do. */
212
+ @container (max-width:900px){
213
+ .themeGallery{grid-template-columns:repeat(3,minmax(0,1fr))}
214
+ }
215
+ @container (max-width:680px){
216
+ .themeGallery{grid-template-columns:repeat(2,minmax(0,1fr))}
217
+ }
218
+ @container (max-width:460px){
219
+ .themeGallery{grid-template-columns:minmax(0,1fr)}
220
+ }
221
+ @container (max-width:420px){
222
+ .sub{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:2px 8px}
223
+ .sub > span:first-child{grid-column:1;grid-row:1}
224
+ .sub > .grow{display:none}
225
+ .submitLink{grid-column:1;grid-row:2}
226
+ .exportLogBtn{grid-column:2;grid-row:1 / span 2;flex-shrink:0}
227
+ .themeToolbar{grid-template-columns:minmax(0,1fr)}
228
+ .themeCardHead{flex-wrap:wrap}
229
+ .themeStatus,.themeStatusMuted{order:3}
230
+ }
231
+ @container (max-width:280px){
232
+ .themeCardFooter{align-items:flex-start;flex-direction:column}
233
+ .themeActions{width:100%;justify-content:flex-start;margin-left:0}
234
+ }
235
+ /* The host Settings dialog keeps a fixed navigation rail on phone-sized
236
+ viewports. Once that rail leaves less than a card's usable width, the
237
+ market becomes the only settings destination and keeps the host's own
238
+ close button in the content header. */
239
+ @media (max-width:560px){
240
+ [role="dialog"]:has([data-dsh-market-root]) > nav{display:none}
241
+ [role="dialog"]:has([data-dsh-market-root]) > div{width:100%;min-width:0;flex:1 1 100%}
242
+ }
243
+ /* Explicit immersive mode: the normal Settings width remains the default,
244
+ while the user can opt into a Wallpaper-style multi-column gallery. */
245
+ [role="dialog"]:has([data-dsh-market-fullscreen="true"]){position:fixed;inset:0;width:100vw;height:100vh;max-width:none;max-height:none;border-radius:0}
246
+ @media (prefers-reduced-motion:reduce){
247
+ .themeCard,.themeCover img{transition:none}
248
+ .themeCard:hover{transform:none}
249
+ }
250
+ /* align-self:start so a short card doesn't stretch to match a tall
251
+ row-mate — CSS grid rows are equal-height by default, and a stretched
252
+ card with margin-top:auto on its footer used to fill that leftover
253
+ height as a blank band INSIDE the card, between the description and the
254
+ footer, reading as a layout bug rather than "this one's row-mate had a
255
+ longer description". Un-stretched, any leftover row height sits outside
256
+ the card as ordinary grid gutter instead. */
257
+ .card{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:12px;align-self:start}
258
+ .row1{display:flex;align-items:flex-start;gap:10px;min-width:0}
259
+ .cardAction{flex-shrink:0;display:inline-flex;align-items:center}
260
+ .installBtn.installBtn{min-width:64px}
261
+ .av{width:16px;height:16px;border-radius:50%;display:grid;place-items:center;font-weight:700;color:#fff;font-size:9px;flex-shrink:0;object-fit:cover;background:var(--dsw-alias-bg-layer-2,#f3f4f6)}
262
+ /* The plugin name carries the card, so it is the one thing sized up; the
263
+ author sits under it as a signature. Same reading order as a post
264
+ header — you take in who wrote it in one glance and spend the rest on
265
+ what it is. */
266
+ .nm{font-weight:600;font-size:15px;line-height:22px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
267
+ /* display:block is load-bearing, not cosmetic. `.nm` clips with
268
+ overflow:hidden + text-overflow:ellipsis, and neither applies to an
269
+ INLINE box — so once the title became an <a> (it links to the repo), a
270
+ long name stopped ellipsizing and overflowed the card instead, landing on
271
+ top of the Install button and swallowing its clicks. Caught by the e2e
272
+ suite as "the name link intercepts pointer events". */
273
+ .nmLink{display:block;color:inherit;text-decoration:none}
274
+ .nmLink:hover{color:var(--dsw-alias-brand-primary,#4f6ef7);text-decoration:underline}
275
+ /* The title has always opened the repo, but styled `color:inherit` with no
276
+ underline it looked like plain text, so nothing said so until the cursor
277
+ was already on it (#256). The GitHub mark is the standing affordance —
278
+ it names the destination before hover (#365). It rides the title's own
279
+ line rather than taking a row of its own; a second link to the same URL
280
+ would have cost every card ~20px of head, against the density the grid
281
+ was just tuned for. */
282
+ .repoMark{color:var(--dsw-alias-label-secondary,#6b7280);flex-shrink:0;margin-left:5px;vertical-align:-1px;display:inline-block}
283
+ .nmLink:hover .repoMark{color:var(--dsw-alias-brand-primary,#4f6ef7)}
284
+ /* flex-wrap so a long owner name pushes the download/star counts to a
285
+ second line instead of squeezing the name itself toward unreadable: an
286
+ ellipsized span's own min-content width is 0 (overflow:hidden zeroes the
287
+ automatic minimum), so without `.owner`'s explicit min-width below, the
288
+ row never wraps — it just keeps shrinking the name past legibility while
289
+ the fixed-width counts stay whole. */
290
+ .byline{display:flex;align-items:center;gap:6px;min-width:0;margin-top:2px;flex-wrap:wrap}
291
+ /* Second line under the byline: the publish date, moved off the crowded
292
+ owner/downloads/stars row for visual balance (reported once downloads
293
+ joined stars on that line). */
294
+ .meta{font-size:11px;color:var(--dsw-alias-label-secondary,#9ca3af);margin-top:2px}
295
+ .metaInline{font-size:11px;color:var(--dsw-alias-label-secondary,#9ca3af)}
296
+ /* The author name is the only variable-length part, so it is the only part
297
+ that shrinks; the counts stay whole because a truncated one is not a fact. */
298
+ .owner{font-size:11px;color:var(--dsw-alias-label-secondary,#9ca3af);flex:0 1 auto;min-width:44px;
299
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
300
+ .desc{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#8b93a1);margin:0}
301
+ /* A FIXED 5-line slot (not just a cap): a one-line description reserves the
302
+ same 90px as a five-line one, so every card in a row starts its thumbnail
303
+ strip/footer at the same y — a clamp with no floor still let a short card
304
+ end early and a long one run to five lines, which read as the grid itself
305
+ being uneven (reported on a real host: several cards mid-row landing at
306
+ visibly different heights). The toggle in CardDesc removes this class, so
307
+ nothing here caps the expanded height. */
308
+ /* A ceiling, not a fixed slot. The 90px floor existed to make CSS-grid rows
309
+ line up; masonry packs each column independently, so reserving five lines
310
+ for a one-line description now just wastes the space it was buying. */
311
+ .descClamp{display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden}
312
+ .descToggle{display:flex;align-items:center;justify-content:center;width:20px;height:16px;margin-top:2px;
313
+ color:var(--dsw-alias-label-tertiary,#8b93a1);background:none;border:none;padding:0;cursor:pointer}
314
+ .descToggle:hover{color:var(--dsw-alias-brand-primary,#4f6ef7)}
315
+ .foot{display:flex;align-items:flex-end;gap:8px;flex-wrap:nowrap}
316
+ .foot .metaInline,.foot .src{white-space:nowrap}
317
+ /* Tags share one flex child so they wrap among themselves instead of
318
+ kicking favorite/comments onto a lonely second row. flex:1 + min-width:0
319
+ leaves room for .footActions on the right of the same line. */
320
+ .footTags{display:flex;flex-wrap:wrap;gap:8px;flex:1 1 0;min-width:0;align-items:center}
321
+ .grow{flex:1}
322
+ .titleRow{display:flex;align-items:center;gap:10px}
323
+ /* Sits beside the heading so a screenshot of the page carries the
324
+ version — most reports arrive as a photo of the screen. Selectable
325
+ (not user-select:none) so it can also be copied into an issue. */
326
+ .version{font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary,#8b93a1);font-variant-numeric:tabular-nums;flex-shrink:0}
327
+ .repoLink{font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary,#8b93a1);text-decoration:none;flex-shrink:0}
328
+ .repoLink:hover{color:var(--dsw-alias-brand-primary,#4f6ef7)}
329
+ /* The note row (#347). The user's own text reads as theirs — slightly
330
+ stronger than a catalog blurb, which is the point: it is the line they
331
+ wrote for themselves. The original/mine toggle stays quiet text, while
332
+ add/edit is a small outlined action: when both used the description's
333
+ tertiary text treatment, `备注` read like another word in the blurb
334
+ instead of something the user could press (#399). */
335
+ .noteRow{display:flex;align-items:baseline;gap:6px;min-width:0;flex-wrap:wrap}
336
+ /* Below the spec line; #458 can leave this row button-only with no desc
337
+ height, so a little top margin keeps it from hugging the link. */
338
+ .desc.noteRow{margin-top:6px}
339
+ /* The update-preview row (#294) sits right under the note row; without a
340
+ desc line to carry height the two blocks read as one (#458). */
341
+ .desc.noteRow + .noteRow{margin-top:6px}
342
+ .noteEdit + .noteRow{margin-top:6px}
343
+ .noteMine{color:var(--dsw-alias-label-primary,#1f2328)}
344
+ .noteToggle{font:inherit;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#8b93a1);background:0 0;border:none;padding:0;cursor:pointer;flex-shrink:0;white-space:nowrap}
345
+ .noteToggle:hover{color:var(--dsw-alias-brand-primary,#4f6ef7);text-decoration:underline}
346
+ .noteAction{display:inline-flex;align-items:center;color:var(--dsw-alias-label-primary,#1f2328);background:var(--dsw-alias-bg-layer-2,#f3f4f6);border:1px solid var(--dsw-alias-border-l3,#d9dde3);border-radius:4px;padding:0 5px;font-weight:600;text-decoration:none}
347
+ .noteAction:hover{color:var(--dsw-alias-label-primary,#1f2328);border-color:var(--dsw-alias-brand-primary,#4f6ef7);text-decoration:none}
348
+ .noteEdit{display:flex;align-items:center;gap:6px;min-width:0;flex-wrap:wrap;margin-top:6px}
349
+ .noteInput{flex:1;min-width:160px}
350
+ .descTight{min-height:0}
351
+ .src{font-size:11px;color:var(--dsw-alias-label-secondary,#9ca3af);text-decoration:none}
352
+ .src:hover{color:var(--dsw-alias-brand-primary,#4f6ef7)}
353
+ .dot{margin-left:5px;vertical-align:2px}
354
+ .act{display:flex;align-items:center;gap:6px;margin-top:6px;font-size:11px;flex-wrap:wrap}
355
+ .actLive{color:var(--dsw-alias-state-success-primary,#16a34a);font-weight:600;display:inline-flex;align-items:center;gap:4px}
356
+ .actWarn{color:var(--dsw-alias-state-warn-primary,#b45309);font-weight:600;display:inline-flex;align-items:center;gap:4px}
357
+ .actBroken{color:var(--dsw-alias-state-error-primary,#dc2626);font-weight:600;display:inline-flex;align-items:center;gap:4px}
358
+ /* DisclosureRow compacted into the activation chip row. */
359
+ .actWhy{margin-top:2px;color:var(--dsw-alias-label-secondary,#6b7280)}
360
+ .loading{display:flex;flex-direction:column;align-items:center;gap:12px;padding:48px;color:var(--dsw-alias-label-secondary,#9ca3af);font-size:13px}
361
+ .spin{display:inline-flex;flex-shrink:0;color:var(--dsw-alias-brand-primary,#4f6ef7);animation:sp .8s linear infinite}
362
+ /* Brand loading: the loose block plugs itself into the grid's empty corner
363
+ and springs back out. It is the mark's own story — a plugin arriving —
364
+ rather than a generic rotation, and it stays legible at 22px because the
365
+ moving part travels a full block-width against a still grid.
366
+ `transform-box: fill-box` makes the offsets below user units of THIS rect,
367
+ so they are the same (+1.28, -1.27) the static mark is drawn with. */
368
+ .logoMark{display:inline-flex;flex-shrink:0;color:var(--dsw-alias-brand-primary,#4f6ef7)}
369
+ .logoPlug{transform-box:fill-box;transform-origin:50% 50%;animation:dshmPlug 1.5s cubic-bezier(.4,0,.2,1) infinite}
370
+ @keyframes dshmPlug{
371
+ 0%,12%{transform:rotate(9deg)}
372
+ 45%,62%{transform:translate(-1.28px,1.27px) rotate(0)}
373
+ 95%,100%{transform:rotate(9deg)}
374
+ }
375
+ /* Motion is the whole signal here, so with it suppressed the block still has
376
+ to say "working" — it breathes in place instead of sitting inert. */
377
+ @media (prefers-reduced-motion: reduce){
378
+ .logoPlug{animation:dshmPlugFade 1.5s ease-in-out infinite}
379
+ .spin{animation:dshmPlugFade 1.5s ease-in-out infinite}
380
+ }
381
+ @keyframes dshmPlugFade{0%,100%{opacity:1}50%{opacity:.35}}
382
+ @keyframes sp{to{transform:rotate(360deg)}}
383
+ .progress{display:flex;align-items:center;flex-wrap:wrap;gap:9px;background:var(--dsw-alias-bg-layer-2,#f3f4f6);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:8px;padding:8px 12px;font-size:12px;margin:0;color:var(--dsw-alias-label-secondary,#6b7280)}
384
+ .bar{width:100%;height:4px;border-radius:99px;background:var(--dsw-alias-border-l1,#e5e7eb);overflow:hidden}
385
+ .barFill{height:100%;border-radius:99px;background:var(--dsw-alias-brand-primary,#4f6ef7);transition:width .6s ease}
386
+ .barWave{width:30%;animation:dshmSlide 1.2s ease-in-out infinite}
387
+ @keyframes dshmSlide{0%{margin-left:-30%}100%{margin-left:100%}}
388
+ .irow .progress{margin-top:8px}
389
+ .progress code{font-family:ui-monospace,Menlo,monospace;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
390
+ .empty{color:var(--dsw-alias-label-secondary,#9ca3af);font-size:13px;padding:32px;text-align:center}
391
+ .err{color:var(--dsw-alias-state-error-primary,#dc2626);font-size:12px;margin:8px 0;white-space:pre-wrap;word-break:break-all}
392
+ /* Loader-id clash, rendered ON the card the user pressed Install on rather
393
+ than in the page-level .err banner: that banner is shared by a dozen
394
+ unrelated operations, so a failure reported there loses its connection to
395
+ the card that caused it as soon as a list is longer than a screen. */
396
+ .cardBlocked{border-color:var(--dsw-alias-state-error-primary,#dc2626)}
397
+ .conflictHead{display:flex;align-items:flex-start;gap:8px}
398
+ .conflictIcon{flex-shrink:0;margin-top:1px;color:var(--dsw-alias-state-error-primary,#dc2626)}
399
+ .conflictTitle{font-size:13px;font-weight:600;line-height:18px;color:var(--dsw-alias-state-error-primary,#dc2626)}
400
+ .conflictBody{font-size:12px;line-height:19px;margin:0}
401
+ /* One row per OWNER, not per id: the decision the user makes is which
402
+ plugins to uninstall, so the plugin name leads and the ids it clashes on
403
+ ride along as secondary evidence. */
404
+ .roster{display:flex;flex-direction:column;gap:1px;border-radius:8px;overflow:hidden;
405
+ background:var(--dsw-alias-border-l2,#e5e7eb);border:1px solid var(--dsw-alias-border-l2,#e5e7eb)}
406
+ .rosterRow{display:flex;align-items:center;gap:8px;padding:7px 10px;background:var(--dsw-alias-bg-layer-1,#fff)}
407
+ .rosterMain{display:flex;flex-direction:column;min-width:0}
408
+ .rosterName{font-size:12px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
409
+ .rosterAuthor{font-size:10.5px;color:var(--dsw-alias-label-secondary,#9ca3af);
410
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
411
+ .rosterTag{margin-left:auto;flex-shrink:0;font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:4px}
412
+ .rosterTagKeep{color:var(--dsw-alias-state-success-primary,#16a34a);background:rgba(22,163,74,.12)}
413
+ .rosterTagDrop{color:var(--dsw-alias-state-error-primary,#dc2626);background:rgba(220,38,38,.12)}
414
+ /* The losing side is dimmed, so the outcome reads before any label does. */
415
+ .rosterRowOut .rosterName{text-decoration:line-through}
416
+ .rosterRowOut{opacity:.62}
417
+ /* Separates the plugin being installed from the ones already there. */
418
+ .rosterSplit{height:1px;background:var(--dsw-alias-border-l2,#e5e7eb)}
419
+ .reassure{display:flex;align-items:center;gap:5px;font-size:11.5px;line-height:17px;
420
+ color:var(--dsw-alias-label-secondary,#6b7280);margin:0}
421
+ .reassureOk{color:var(--dsw-alias-state-success-primary,#16a34a);flex-shrink:0}
422
+ .conflictWhy{margin-top:2px;font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary,#8b93a1);
423
+ font-family:ui-monospace,Menlo,monospace;overflow-wrap:anywhere}
424
+ .conflictWhyText{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;margin-top:5px}
425
+ /* The clash resolves to a choice between two outcomes, so the options carry
426
+ their own consequence text and the safe one is preselected. */
427
+ .choices{display:flex;flex-direction:column;gap:7px}
428
+ .choice{display:flex;gap:9px;align-items:flex-start;text-align:left;font:inherit;cursor:pointer;
429
+ padding:9px 11px;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#fff);
430
+ border:1px solid var(--dsw-alias-border-l3,#d9dde3)}
431
+ .choice:has(input:disabled){cursor:default;opacity:.6}
432
+ .choiceOn{border-color:var(--dsw-alias-brand-primary,#4f6ef7);
433
+ background:var(--dsw-alias-bg-layer-2,#f5f7ff)}
434
+ /* accent-color, not a redrawn control: the platform's own radio is the one
435
+ users recognise, and it keeps focus ring and arrow-key grouping. */
436
+ .choiceRadio{flex-shrink:0;margin:2px 0 0;width:13px;height:13px;
437
+ accent-color:var(--dsw-alias-brand-primary,#4f6ef7)}
438
+ .choiceMain{display:flex;flex-direction:column;gap:3px;min-width:0}
439
+ .choiceTitle{font-size:12px;font-weight:600;line-height:17px}
440
+ .choiceNote{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#8b93a1)}
441
+ .choiceSafe{color:var(--dsw-alias-state-success-primary,#16a34a)}
442
+
443
+ /* --- activity entry + panel ---------------------------------------------
444
+ The entry sits in the tab row, so no amount of paginating, searching or
445
+ tab switching can take a record — or a pending decision — off screen.
446
+ That was the flaw of reporting an operation on the card that started it. */
447
+ /* Status reads as the dot + tag pair the host's own plugin inventory uses,
448
+ so the two surfaces state the same fact the same way. Every occupant of
449
+ the trailing group is one pill height, which is what lets an enabled row
450
+ and a disabled one line up instead of drifting against each other. */
451
+ .stateTag{display:inline-flex;align-items:center;gap:5px;flex-shrink:0;min-height:20px;
452
+ border-radius:5px;padding:1px 7px;font-size:11px;line-height:16px;white-space:nowrap;
453
+ background:var(--dsw-alias-bg-layer-2,#f3f4f6);color:var(--dsw-alias-label-secondary,#6b7280)}
454
+ .stateTag[data-on='true']{color:var(--dsw-alias-state-success-primary,#16a34a);
455
+ background:color-mix(in srgb, var(--dsw-alias-state-success-primary,#16a34a) 10%, transparent)}
456
+ .stateDot{width:6px;height:6px;flex:none;border-radius:999px;background:var(--dsw-alias-label-tertiary,#8b93a1)}
457
+ .stateDot[data-on='true']{background:var(--dsw-alias-state-success-primary,#16a34a)}
458
+ /* The update slot: a tag when there is nothing to do, a button when there is.
459
+ Bare text here left the slot shorter than its neighbour's button. */
460
+ /* Shrinkable, unlike the buttons beside it: this slot holds the row's only
461
+ variable-length copy ("本地开发链接" / "Local dev link" is three times the
462
+ width of "已是最新"), and as an unshrinkable nowrap tag it pushed Uninstall
463
+ onto a second line (#234 by @Ztyss). The tag is the informational half of
464
+ the row and the buttons are the actionable half, so the tag is what gives
465
+ way — ellipsized, with the full text on hover. */
466
+ .metaTag{display:inline-block;flex-shrink:1;min-width:0;max-width:100%;overflow:hidden;
467
+ text-overflow:ellipsis;min-height:20px;border-radius:5px;
468
+ padding:1px 7px;font-size:11px;line-height:18px;white-space:nowrap;
469
+ background:var(--dsw-alias-bg-layer-2,#f3f4f6);color:var(--dsw-alias-label-tertiary,#8b93a1)}
470
+ .metaTagOk{color:var(--dsw-alias-state-success-primary,#16a34a);
471
+ background:color-mix(in srgb, var(--dsw-alias-state-success-primary,#16a34a) 10%, transparent)}
472
+ /* Local-dev restore in the same status slot as 已是最新 — same pill as
473
+ .metaTag but clickable and never ellipsized. */
474
+ .metaTagAction{font-family:inherit;font-size:11px;line-height:18px;font-weight:inherit;
475
+ cursor:pointer;border:none;flex-shrink:0;padding:1px 7px;border-radius:5px;
476
+ background:var(--dsw-alias-bg-layer-2,#f3f4f6);
477
+ color:var(--dsw-alias-label-secondary,#6b7280)}
478
+ .metaTagAction:hover:not(:disabled){color:var(--dsw-alias-brand-primary,#4f6ef7)}
479
+ .metaTagAction:disabled{opacity:.5;cursor:default}
480
+ /* Installed-row header: title left, local-dev badge top-right. */
481
+ .irowHead{display:flex;align-items:flex-start;gap:8px;min-width:0}
482
+ .irowHead .irowName{flex:1 1 0;min-width:0}
483
+ .irowDevTag{flex-shrink:0;border-radius:99px;padding:2px 8px;font-size:10px;line-height:14px;font-weight:600;
484
+ letter-spacing:0.02em;white-space:nowrap;cursor:default;user-select:none;
485
+ color:var(--dsw-alias-label-tertiary,#8b93a1);
486
+ background:color-mix(in srgb, var(--dsw-alias-brand-primary,#4f6ef7) 9%, var(--dsw-alias-bg-layer-2,#f3f4f6))}
487
+ .nameLink{color:inherit;text-decoration:none}
488
+ /* Installed-row title only. The version is a fact the row exists to state,
489
+ so it never truncates; the name is the variable-length part and is the
490
+ only thing allowed to shrink and ellipsize (#257). Scoped to this row
491
+ rather than added to `.nm`, which six other surfaces share. */
492
+ /* Wraps rather than truncates (#342, #343). A scoped package name is the
493
+ thing that tells two installed plugins apart — `@deepseek-ai/dsh-client-ui-…`
494
+ and `@dsh-external/dsh-sessi…` are both just prefixes — and an ellipsis
495
+ removes exactly the end that distinguishes them. Wrapping costs a line only
496
+ on the names long enough to need one, and the installed list is a masonry,
497
+ so a taller row does not push its neighbours around.
498
+ `anywhere` rather than `break-word`: these names have long unbroken runs
499
+ between the slash and the dashes, which `break-word` leaves overflowing. */
500
+ .irowName{display:flex;align-items:baseline;gap:6px;min-width:0;overflow:visible;text-overflow:clip;flex-wrap:wrap}
501
+ .irowNameText{min-width:0;overflow-wrap:anywhere;white-space:normal}
502
+ .irowName>.owner{flex:0 0 auto}
503
+ .nameLink:hover{color:var(--dsw-alias-brand-primary,#4f6ef7);text-decoration:underline}
504
+ /* Lift the entry off the tab row's bottom rule — a pill button flush on the
505
+ divider reads as clipped, especially when export log sits one row above on
506
+ narrow viewports. */
507
+ .opWrap{position:relative;display:inline-flex;flex-shrink:0;margin-bottom:6px}
508
+ .opEntry{display:inline-flex;align-items:center;gap:6px;font:inherit;font-size:12px;
509
+ padding:4px 10px;border-radius:7px;cursor:pointer;position:relative;white-space:nowrap;
510
+ color:var(--dsw-alias-label-primary,#1f2328);
511
+ background:var(--dsw-alias-bg-layer-1,#fff);
512
+ border:1px solid var(--dsw-alias-border-l3,#d9dde3)}
513
+ /* Idle is not news: no border, no fill, nothing competing with the tabs. */
514
+ .opEntryQuiet{border-color:transparent;background:transparent;color:var(--dsw-alias-label-secondary,#6b7280)}
515
+ .opEntryAlert{border-color:var(--dsw-alias-state-error-primary,#dc2626);color:var(--dsw-alias-state-error-primary,#dc2626)}
516
+ .opDot{position:absolute;top:-3px;right:-3px;width:8px;height:8px;border-radius:99px;
517
+ background:var(--dsw-alias-state-error-primary,#dc2626)}
518
+ .opPanel{position:absolute;top:calc(100% + 6px);right:0;z-index:40;width:460px;max-width:86vw;
519
+ max-height:70vh;overflow-y:auto;border-radius:12px;
520
+ background:var(--dsw-alias-bg-layer-1,#fff);
521
+ border:1px solid var(--dsw-alias-border-l3,#d9dde3);
522
+ box-shadow:0 20px 52px rgba(0,0,0,.28)}
523
+ .opHead{display:flex;align-items:center;gap:8px;padding:9px 14px;
524
+ border-bottom:1px solid var(--dsw-alias-border-l2,#e5e7eb)}
525
+ .opPanelTitle{font-size:12.5px;font-weight:600}
526
+ .opCloseBtn.opCloseBtn{padding:0 6px;min-width:0;color:var(--dsw-alias-label-secondary,#6b7280)}
527
+ .opAggregate{padding:9px 14px;border-bottom:1px solid var(--dsw-alias-border-l2,#e5e7eb);
528
+ background:var(--dsw-alias-bg-layer-2,#f7f8fa)}
529
+ .opAggregateTop{font-size:12px;font-weight:600;font-variant-numeric:tabular-nums}
530
+ .opAggregateHint{font-size:10.5px;color:var(--dsw-alias-label-tertiary,#8b93a1);margin-top:4px}
531
+ .opRow{display:flex;align-items:flex-start;gap:9px;padding:9px 14px;
532
+ border-bottom:1px solid var(--dsw-alias-border-l2,#e5e7eb)}
533
+ .opRow:last-child{border-bottom:none}
534
+ .opRowAlert{background:rgba(220,38,38,.06)}
535
+ .opIcon{flex-shrink:0;margin-top:1px;width:14px;display:grid;place-items:center}
536
+ .opQueuedIcon{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:12px}
537
+ .opMain{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}
538
+ .opTop{display:flex;align-items:baseline;gap:6px;min-width:0}
539
+ .opVerb{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);flex-shrink:0}
540
+ .opName{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
541
+ .opStatus{font-size:10.5px;line-height:16px;color:var(--dsw-alias-label-tertiary,#8b93a1);
542
+ overflow-wrap:anywhere}
543
+ .opStatusBad{color:var(--dsw-alias-state-error-primary,#dc2626)}
544
+ .opActions{flex-shrink:0;display:flex;align-items:center;gap:6px;margin-top:1px}
545
+ .opDecision{margin-top:8px;padding-top:8px;display:flex;flex-direction:column;gap:8px;
546
+ border-top:1px dashed var(--dsw-alias-border-l2,#e5e7eb)}
547
+ .opDecisionFoot{display:flex;align-items:center;gap:8px}
548
+ .conflictDetailsToggle{display:inline-flex;align-items:center;gap:4px;font:inherit;font-size:11px;
549
+ cursor:pointer;padding:2px 0;border:none;background:none;
550
+ color:var(--dsw-alias-label-tertiary,#8b93a1)}
551
+ .conflictDetailsToggle:hover{color:var(--dsw-alias-label-secondary,#6b7280)}
552
+ .opEmpty{padding:30px 14px;text-align:center;font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
553
+ .opEmptyHint{font-size:11px;color:var(--dsw-alias-label-tertiary,#8b93a1);margin-top:4px}
554
+ /* The card keeps a terminal marker for an install that ended without
555
+ installing: a card that looks untouched invites pressing Install again. */
556
+ .cardBlockedMark{display:inline-flex;align-items:center;gap:5px;font:inherit;font-size:11px;
557
+ cursor:pointer;padding:4px 9px;border-radius:7px;
558
+ color:var(--dsw-alias-state-error-primary,#dc2626);
559
+ background:rgba(220,38,38,.08);
560
+ border:1px solid var(--dsw-alias-state-error-primary,#dc2626)}
561
+ /* Destructive actions — uninstall, delete group, and the conflict swap.
562
+ Doubled selectors so these beat the primitive's own variant rules, the
563
+ same way .installBtn and .catsToggle do. */
564
+ .dangerBtn.dangerBtn{color:var(--dsw-alias-state-error-primary,#dc2626);border-color:var(--dsw-alias-state-error-primary,#dc2626)}
565
+ .dangerBtn.dangerBtn:hover:not(:disabled){background:var(--dsw-alias-state-error-primary,#dc2626);color:#fff}
566
+ .dangerArmed.dangerArmed{background:var(--dsw-alias-state-error-primary,#dc2626);border-color:var(--dsw-alias-state-error-primary,#dc2626);color:#fff}
567
+ .retryBtn{margin-top:4px}
568
+ .irow{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);
569
+ border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:10px;min-width:0}
570
+ /* Everything packs left. Pushing the trailing controls right — whether with
571
+ justify-content or an auto margin on the tag — made the switch's position
572
+ depend on whether its neighbour rendered 更新 or 已是最新, so the switch
573
+ sat at a different x on every row. Packed left, state and switch land at
574
+ the same place regardless of what follows them. */
575
+ .irowActions{display:flex;align-items:center;justify-content:flex-start;gap:8px;flex-wrap:wrap;min-width:0}
576
+ /* The status tag and Uninstall travel together: nowrap so they never split
577
+ across lines, min-width:0 so the group can still shrink (the tag inside
578
+ ellipsizes) rather than forcing the whole row wider (#242). */
579
+ .irowTrailing{display:inline-flex;align-items:center;gap:8px;flex-wrap:nowrap;flex-shrink:0;min-width:0}
580
+ .irowTrailing .metaTag{flex-shrink:1;min-width:0}
581
+ .irowTrailing .metaTagAction{flex-shrink:0}
582
+ .irowMissing{filter:grayscale(1);opacity:.5}
583
+ .irow>.src,.irow>.owner{white-space:nowrap;flex-shrink:0}
584
+ .irow button{white-space:nowrap;flex-shrink:0}
585
+ .tabSearchRow{display:flex;padding:0 4px 6px}
586
+ .tabSearch{width:100%}
587
+ /* Buttons sharing the row with a description must keep one line (#26, thanks @yanshuai2002) */
588
+ .spec{font-size:11px;color:var(--dsw-alias-label-secondary,#9ca3af);font-family:ui-monospace,Menlo,monospace;overflow-wrap:anywhere;min-width:0}
589
+ .specTag{display:inline-flex;align-items:center;height:16px;padding:0 5px;border-radius:4px;font-size:10px;font-weight:600;white-space:nowrap;flex-shrink:0}
590
+ .specTagGit{color:#0b7285;background:rgba(18,163,196,.12)}
591
+ .specTagFile{color:#b07d1b;background:rgba(240,180,41,.14)}
592
+ .backupCheckList .grow{flex:1 1 70%;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
593
+ .backupCheckList .spec{flex:0 1 30%;max-width:30%;text-align:right;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
594
+ /* A row, with room between the two buttons. `.err` sets break-all for long
595
+ pnpm paths, which also broke short button labels mid-word once a button
596
+ sat inside it (#410). */
597
+ .staleAction{margin-top:8px;display:flex;flex-wrap:wrap;gap:8px;align-items:center;word-break:normal}
598
+ .pct{font-weight:600;font-size:11px;flex-shrink:0;color:var(--dsw-alias-label-secondary,#6b7280)}
599
+ /* Single-line pager: the host settings dialog's content column is ~556px
600
+ (800px panel − 188px section nav − 48px options padding − 8px body
601
+ padding), so 上一页/下一页 + the numbered window + 第x/y页 + 每页n must
602
+ share ONE row. nowrap is safe because pageItems caps the numbered window
603
+ (five buttons max, 1 and N always present — the pager had no room for the
604
+ «/» skip glyphs: even the compact set measures ~60px over the column in
605
+ English, and a centered overflow clips them at the scroll edge). Compact
606
+ 8px paddings (primitives sm uses 10px) keep the row inside the column.
607
+ Below 544px the pager falls back to two centered lines instead of
608
+ squeezing buttons. */
609
+ .pager{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:16px 0 4px;flex-wrap:nowrap}
610
+ .pagerPages{display:flex;align-items:center;justify-content:center;gap:4px;flex-wrap:nowrap;flex:1;min-width:0}
611
+ .pagerMeta{display:flex;align-items:center;gap:6px;flex-wrap:nowrap;flex-shrink:0}
612
+ .pagerPages button,.pagerMeta button{padding:0 8px;min-width:0}
613
+ .pageEllipsis{color:var(--dsw-alias-label-secondary,#9ca3af);font-size:12px;padding:0 1px}
614
+ .pageInfo{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);white-space:nowrap}
615
+ @container (max-width:544px){.pager{flex-wrap:wrap;justify-content:center;gap:8px}.pagerPages{flex-basis:100%}}
616
+ /* #60: per-plugin enable/disable switches, deprecation badges, custom groups */
617
+ .depBadge{border:1px solid var(--dsw-alias-state-warn-primary,#b45309);color:var(--dsw-alias-state-warn-primary,#b45309);border-radius:4px;flex-shrink:0;padding:1px 6px;font-size:11px;line-height:16px;font-weight:600;white-space:nowrap;margin-left:6px}
618
+ .deprecate{font-size:12px;line-height:18px;color:var(--dsw-alias-state-warn-primary,#b45309);background:var(--dsw-alias-bg-layer-2,#fdf3e3);border:1px solid var(--dsw-alias-border-l2,#f3e3c3);border-radius:8px;padding:8px 10px;margin:0}
619
+ .deprecate a{color:var(--dsw-alias-state-warn-primary,#b45309);text-decoration:underline}
620
+ .deprecate .src{margin-left:8px}
621
+ .depLine{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
622
+ .switch{position:relative;width:38px;height:22px;border-radius:99px;border:1px solid var(--dsw-alias-border-l2,#d9dde3);background:var(--dsw-alias-bg-layer-2,#e5e7eb);cursor:pointer;padding:0;flex-shrink:0;transition:background .15s ease,border-color .15s ease}
623
+ .switchOn{background:var(--dsw-alias-state-success-primary,#16a34a);border-color:var(--dsw-alias-state-success-primary,#16a34a)}
624
+ .switchMixed{background:var(--dsw-alias-state-warn-primary,#b45309);border-color:var(--dsw-alias-state-warn-primary,#b45309)}
625
+ .switchKnob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:99px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:left .15s ease}
626
+ .switchOn .switchKnob,.switchMixed .switchKnob{left:18px}
627
+ .switch:disabled{opacity:.5;cursor:default}
628
+ .viewBar{display:flex;gap:2px;align-items:center;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:8px;padding:2px;margin-bottom:12px;width:fit-content}
629
+ .viewBtn{border:none;background:none;font:inherit;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#6b7280);padding:4px 10px;border-radius:6px;cursor:pointer;white-space:nowrap}
630
+ .viewBtn:hover{color:var(--dsw-alias-brand-primary,#4f6ef7)}
631
+ .viewOn{background:var(--dsw-alias-bg-layer-2,#eef0f4);color:var(--dsw-alias-label-primary,#1f2328);font-weight:600}
632
+ .groupRow{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:12px;padding:12px 14px;margin-bottom:10px}
633
+ .groupHead{display:flex;align-items:center;gap:10px;min-width:0}
634
+ .groupName{font-weight:600;font-size:13px;line-height:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
635
+ .groupActions{display:flex;align-items:center;gap:6px;flex-shrink:0}
636
+ .groupMembers{display:flex;flex-direction:column;gap:6px;margin-top:10px}
637
+ .groupMember{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);font-size:12px;line-height:18px}
638
+ .groupMember .nm{font-size:12px;flex:1;min-width:0}
639
+ .groupAddPanel{display:flex;flex-direction:column;gap:6px;margin-top:10px;padding-top:10px;border-top:1px dashed var(--dsw-alias-border-l2,#e5e7eb)}
640
+ .groupCreate{display:flex;align-items:center;gap:8px;margin-bottom:10px}
641
+ .inlineInput{flex:1;min-width:120px}
642
+ .assignRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
643
+ .assignSelect{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#1f2328);border-radius:6px;padding:3px 6px;font:inherit;font-size:12px;line-height:18px}
644
+ .groupHint{font-size:11px;color:var(--dsw-alias-label-tertiary,#8b93a1)}
645
+ .sectAction{margin:14px 2px 8px;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#6b7280);display:flex;align-items:center;gap:8px}
646
+
647
+ /* #61: profile backup & restore */
648
+ .backupGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}
649
+ .backupCard{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:10px}
650
+ .backupCard h3{font-size:14px;margin:0}
651
+ .backupCard p{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#6b7280);margin:0}
652
+ .backupActions{display:flex;gap:8px;flex-wrap:wrap;position:relative}
653
+ .hiddenFile{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
654
+ .backupInput{width:100%;box-sizing:border-box}
655
+ .backupCheck{display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer}
656
+ .backupWarn{color:var(--dsw-alias-state-warn-primary,#b45309)!important;font-size:12px;line-height:18px;margin:0}
657
+ .backupMessage{grid-column:1/-1;font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
658
+ .backupCheckList{display:flex;flex-direction:column;gap:6px;max-height:260px;overflow-y:auto;margin:10px 0 4px;padding-right:4px}
659
+ .backupCheckList .backupCheck{justify-content:space-between;gap:8px}
660
+
661
+ /* #98: Diagnostics tab — profile composition check report.
662
+ The page fills the settings dialog's content height and scrolls INSIDE
663
+ itself (overflow-y:auto), so the outer dialog never overflows and its own
664
+ overflow-y:auto stays silent (no outer scrollbar). */
665
+ .diagPage{display:flex;flex-direction:column;gap:12px;height:100%;min-height:0;overflow-y:auto}
666
+ .diagSummary{display:flex;flex-wrap:wrap;align-items:center;gap:12px;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:12px;padding:10px 14px;font-size:12px}
667
+ .diagSummaryItem{display:inline-flex;align-items:center;gap:6px;color:var(--dsw-alias-label-secondary,#6b7280);white-space:nowrap}
668
+ .diagSummaryMeta{color:var(--dsw-alias-label-tertiary,#9ca3af);font-family:ui-monospace,Menlo,monospace;font-size:11px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
669
+ .diagSection{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px}
670
+ .diagSection h3{font-size:13px;font-weight:600;margin:0;color:var(--dsw-alias-label-primary,#1f2328)}
671
+ .diagCount{font-size:11px;color:var(--dsw-alias-label-tertiary,#9ca3af);font-weight:400}
672
+ .diagEmpty{color:var(--dsw-alias-label-secondary,#9ca3af);font-size:12px;padding:8px 0}
673
+ .diagBundle{display:flex;flex-direction:column;gap:6px;border-top:1px solid var(--dsw-alias-border-l2,#f0f1f3);padding-top:8px}
674
+ .diagBundle:first-of-type{border-top:none;padding-top:0}
675
+ .diagRow{display:flex;align-items:center;flex-wrap:wrap;gap:8px;font-size:12px;line-height:18px;min-width:0}
676
+ .diagMeta{display:flex;align-items:baseline;gap:8px;font-size:12px;min-width:0}
677
+ .diagKey{color:var(--dsw-alias-label-tertiary,#9ca3af);font-size:11px;flex-shrink:0;min-width:64px}
678
+ .diagVal{font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary,#1f2328);font-family:ui-monospace,Menlo,monospace;overflow-wrap:anywhere;min-width:0}
679
+ .diagIndex{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;border-radius:9px;background:var(--dsw-alias-bg-layer-2,#f3f4f6);color:var(--dsw-alias-label-secondary,#6b7280);font-size:11px;font-weight:600;flex-shrink:0}
680
+ .diagArrow{color:var(--dsw-alias-label-tertiary,#9ca3af);font-size:12px;flex-shrink:0}
681
+ .diagBadgeOfficial{display:inline-flex;align-items:center;padding:0 8px;height:18px;border-radius:9px;background:var(--dsw-alias-brand-primary,#4f6ef7);color:#fff;font-size:11px;font-weight:600;flex-shrink:0}
682
+ .diagBadgeCommunity{display:inline-flex;align-items:center;padding:0 8px;height:18px;border-radius:9px;background:var(--dsw-alias-bg-layer-2,#f3f4f6);color:var(--dsw-alias-label-secondary,#6b7280);font-size:11px;flex-shrink:0}
683
+ .diagBadgeShadow{display:inline-flex;align-items:center;padding:0 8px;height:18px;border-radius:9px;background:var(--dsw-alias-state-error-primary,#dc2626);color:#fff;font-size:11px;font-weight:600;flex-shrink:0}
684
+ .diagBadgeWarn{display:inline-flex;align-items:center;padding:0 8px;height:18px;border-radius:9px;background:var(--dsw-alias-state-warn-primary,#b45309);color:#fff;font-size:11px;font-weight:600;flex-shrink:0;white-space:nowrap}
685
+ .diagBadgeInfo{display:inline-flex;align-items:center;padding:0 8px;height:18px;border-radius:9px;background:var(--dsw-alias-bg-layer-2,#f3f4f6);color:var(--dsw-alias-label-secondary,#6b7280);font-size:11px;flex-shrink:0;white-space:nowrap}
686
+ .diagList{display:flex;flex-direction:column;gap:6px}
687
+
688
+ /* #98 supplement: section overview line, overrides/orphans rows, drag ordering */
689
+ .sectionOverview{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#8b93a1);padding:2px 0 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
690
+ .diagAlert{color:var(--dsw-alias-state-warn-primary,#b45309)}
691
+ .ovRow{display:flex;align-items:center;flex-wrap:wrap;gap:8px;font-size:12px;line-height:18px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);border-radius:8px;padding:6px 10px;min-width:0}
692
+ .ovArrow{color:var(--dsw-alias-label-tertiary,#9ca3af);font-size:12px;flex-shrink:0}
693
+ .ovByTag{display:inline-flex;align-items:center;padding:0 8px;height:18px;border-radius:9px;background:var(--dsw-alias-brand-primary,#4f6ef7);color:#fff;font-size:11px;font-weight:600;flex-shrink:0;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
694
+ .ovFrom{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
695
+ .orphRow{display:flex;align-items:center;flex-wrap:wrap;gap:8px;font-size:12px;line-height:18px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);border-radius:8px;padding:6px 10px;min-width:0}
696
+ .orphBadge{display:inline-flex;align-items:center;padding:0 8px;height:18px;border-radius:9px;background:var(--dsw-alias-state-warn-primary,#b45309);color:#fff;font-size:11px;font-weight:600;flex-shrink:0;white-space:nowrap}
697
+ .dragHandle{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;color:var(--dsw-alias-label-tertiary,#9ca3af);cursor:grab;flex-shrink:0;user-select:none;font-size:12px;line-height:20px}
698
+ .dragOver{outline:2px dashed var(--dsw-alias-brand-primary,#4f6ef7);outline-offset:2px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,#f0f2f8)}
699
+ .dragging{opacity:.45;background:var(--dsw-alias-bg-layer-2,#f3f4f6)}
700
+
701
+ /* #98 phase 3: collapsible snapshots & rollback panel */
702
+ .collapseHead{display:flex;align-items:center;gap:8px;border:none;background:none;font:inherit;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#1f2328);padding:0;cursor:pointer;text-align:left;width:100%}
703
+ .collapseIcon{display:inline-flex;flex-shrink:0;color:var(--dsw-alias-label-secondary,#6b7280)}
704
+ .collapseTitle{flex:1;min-width:0}
705
+ .collapseBody{display:flex;flex-direction:column;gap:10px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2,#f0f1f3);margin-top:8px;min-width:0;overflow-wrap:anywhere}
706
+ .orderPanel{display:flex;flex-direction:column;gap:10px;min-width:0}
707
+ .panelNote{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#6b7280);margin:0}
708
+ .panelActions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
709
+ .presetList{display:flex;flex-direction:column;gap:6px;min-width:0}
710
+ .presetRow{display:flex;flex-direction:column;align-items:stretch;gap:6px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);border-radius:8px;padding:8px 10px;font-size:12px;line-height:18px;min-width:0;max-width:100%}
711
+ .presetName{font-weight:600;font-size:13px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
712
+ .snapList{display:flex;flex-direction:column;gap:6px;min-width:0}
713
+ .snapRow{display:flex;flex-direction:column;align-items:stretch;gap:8px;background:var(--dsw-alias-bg-layer-2,#f7f8fa);border-radius:8px;padding:10px 12px;font-size:12px;line-height:18px}
714
+ .snapMeta{display:flex;flex-wrap:wrap;align-items:center;gap:8px;min-width:0}
715
+ .snapConfirmText{font-size:12px;line-height:18px;color:var(--dsw-alias-state-warn-primary,#b45309);margin:0}
716
+ .confirmRow{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}
717
+
718
+ /* #98: AI-fix clipboard failure fallback — the built prompt as a selectable
719
+ text block, so the user can still copy it by hand. */
720
+ .fixFallback{display:flex;flex-direction:column;gap:6px;margin:8px 0}
721
+ .fixFallbackText{width:100%;box-sizing:border-box;font-family:ui-monospace,Menlo,monospace;font-size:11px;line-height:16px;color:var(--dsw-alias-label-primary,#1f2328);background:var(--dsw-alias-bg-layer-2,#f3f4f6);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:6px;padding:8px 10px;resize:vertical;white-space:pre-wrap;word-break:break-all}
722
+ /* Plugin-configuration card (dsh >= rc.7). Drawn here rather than imported:
723
+ the section's own cards live in a package whose value import would fail
724
+ the client bundle-purity gate, so a card owns its appearance. Matches the
725
+ chrome of the built-in cards beside it. */
726
+ /* The plugin configuration page dispatches a card and lets the plugin draw
727
+ it ("a plugin that ships a browser half owns its own card"). These mirror
728
+ the host's own PluginCard one for one — same tokens, same sizes — so the
729
+ market's card is indistinguishable from the rows above and below it. An
730
+ earlier version invented a flat, always-open box and stood out badly. */
731
+ .setCard{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-3,#fff);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
732
+ .setCard:hover{border-color:var(--dsw-alias-label-dimmed,#c8ccd4)}
733
+ .setCardOpen{background:var(--dsw-alias-bg-layer-2,#f7f8fa);border-color:var(--dsw-alias-label-dimmed,#c8ccd4)}
734
+ .setHeader{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
735
+ .setHeader:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f6ef7);outline-offset:-2px}
736
+ .setHeadText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
737
+ .setName{color:var(--dsw-alias-label-primary,#1f2328);font-size:15px;font-weight:600;line-height:1.4}
738
+ .setDesc{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:13px;line-height:1.5}
739
+ .setChevron{color:var(--dsw-alias-label-tertiary,#8b93a1);flex:none;display:inline-flex;transition:transform .16s}
740
+ .setChevronOpen{transform:rotate(180deg)}
741
+ .setBody{border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb);margin:0 16px;padding-bottom:8px}
742
+ .setRow{display:flex;align-items:center;gap:12px;padding:12px 0}
743
+ .setRow + .setRow{border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb)}
744
+ .setLabelBox{display:flex;flex-direction:column;gap:3px;flex:1;min-width:0}
745
+ .setLabel{font-size:13px;line-height:20px}
746
+ .setHint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#8b93a1)}
747
+ /* The confirm block is deliberately NOT a modal: the consequence copy has to
748
+ stay readable while the checkbox is being decided. */
749
+ .setConfirm{border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb);display:flex;flex-direction:column;gap:8px;padding:12px 0 4px}
750
+ .setCheck{display:flex;align-items:center;gap:8px;font-size:12px;line-height:18px;cursor:pointer}
751
+ .setActions{border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}
752
+ .setInlineActions{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:wrap}
753
+ .setProxyEditor{border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb);display:flex;align-items:flex-end;flex-wrap:wrap;gap:10px;padding:12px 0}
754
+ .setProxyLabel{display:flex;flex:1;min-width:180px;flex-direction:column;gap:5px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#6b7280)}
755
+ .setProxyInput{box-sizing:border-box;width:100%;min-width:0;border:1px solid var(--dsw-alias-border-l2,#d1d5db);border-radius:7px;background:var(--dsw-alias-bg-layer-3,#fff);color:var(--dsw-alias-label-primary,#1f2328);font:inherit;font-size:12px;line-height:18px;padding:6px 8px}
756
+ .setProxyInput:focus{border-color:var(--dsw-alias-brand-primary,#4f6ef7);outline:2px solid color-mix(in srgb,var(--dsw-alias-brand-primary,#4f6ef7) 18%,transparent)}
757
+ .setDanger{color:var(--dsw-alias-state-error-primary,#dc2626)}
758
+ /* A two-state segmented control rather than a switch: "stable / beta" is a
759
+ choice between two named things, and a toggle would force one of them to
760
+ be the unlabelled "off". */
761
+ .setSeg{display:inline-flex;flex-shrink:0;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:8px;padding:2px;gap:2px}
762
+ .setSegBtn{font:inherit;font-size:12px;line-height:18px;padding:3px 10px;border:none;border-radius:6px;background:0 0;color:var(--dsw-alias-label-secondary,#6b7280);cursor:pointer}
763
+ .setSegBtn:disabled{cursor:default;opacity:.5}
764
+ .setSegOn{background:var(--dsw-alias-bg-layer-2,#eef0f4);color:var(--dsw-alias-label-primary,#1f2328);font-weight:600}
765
+ .setBetaTag{margin-left:6px;padding:0 6px;border-radius:9px;font-size:11px;font-weight:600;line-height:17px;background:var(--dsw-alias-bg-module-platform,#eef0f4);color:var(--dsw-alias-label-secondary,#6b7280)}
766
+
767
+ /* ---------------------------------------------------------- comments */
768
+ /* Card footer entry point. Styled as the quiet sibling of .src, not as a
769
+ button: it competes with Install for attention otherwise, and Install is
770
+ what the card is for. */
771
+ .commentsLink{background:none;border:0;padding:0;cursor:pointer;font-size:11px;line-height:16px;white-space:nowrap;color:var(--dsw-alias-label-secondary,#9ca3af)}
772
+ .commentsLink:hover{color:var(--dsw-alias-brand-primary,#4f6ef7)}
773
+ .footActions{display:inline-flex;align-items:center;gap:8px;flex-shrink:0}
774
+ .favoriteBtn{background:none;border:0;padding:0;cursor:pointer;display:inline-flex;align-items:center;color:var(--dsw-alias-label-secondary,#9ca3af)}
775
+ .favoriteBtn:hover{color:var(--dsw-alias-brand-primary,#4f6ef7)}
776
+ .favoriteOn{color:var(--dsw-alias-brand-primary,#4f6ef7)}
777
+ .commentsNote{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary,#9ca3af);margin:0 0 8px}
778
+ .commentsGithubPrompt{padding:10px;margin:0 0 10px;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary,#4f6ef7) 24%,transparent);border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4f6ef7) 6%,transparent)}
779
+ .commentsGithubHint{font-size:12px;line-height:17px;color:var(--dsw-alias-label-primary,#1f2328);margin:0 0 8px}
780
+ .commentsGithubAction{display:inline-flex;align-items:center;justify-content:center;min-height:28px;box-sizing:border-box;padding:5px 10px;border:1px solid var(--dsw-alias-brand-primary,#4f6ef7);border-radius:6px;color:var(--dsw-alias-brand-primary,#4f6ef7);font-size:12px;font-weight:600;line-height:16px;text-decoration:none}
781
+ .commentsGithubAction:hover{background:color-mix(in srgb,var(--dsw-alias-brand-primary,#4f6ef7) 9%,transparent)}
782
+ .commentsStatus{font-size:12px;color:var(--dsw-alias-label-secondary,#9ca3af);margin:0}
783
+ .commentsStatus:empty{display:none}
784
+ .commentsError{font-size:12px;color:var(--dsw-alias-label-primary,#1f2328);margin:0 0 8px}
785
+ .commentsFail{display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:12px 0}
786
+ /* The iframe reports its own height once giscus has the thread. Holding room
787
+ until then keeps the modal from snapping open to full size a beat after the
788
+ reader has already started looking at it. */
789
+ /* The host owns the dialog's width and does not let a plugin change it, so the
790
+ thread is fitted to that width rather than overflowing a box that clips.
791
+ giscus lays out fine this narrow — it is the width it uses on phones. */
792
+ .commentsMount{width:100%;min-height:240px;max-height:56vh;overflow:auto}
793
+ .commentsMount iframe{width:100%}
794
+ /* Update-notes entry + dialog (#294). The entry is a text button, not a
795
+ Button component: it must read as content in the row's flow rather than as
796
+ an action competing with 更新/卸载 on the trailing edge. */
797
+ .notesLink{display:block;background:0 0;border:none;padding:0;margin:2px 0 0;font:inherit;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#6b7280);cursor:pointer;text-align:left}
798
+ .notesLink:hover{color:var(--dsw-alias-label-primary,#1f2328)}
799
+ .notesBody{display:flex;flex-direction:column;gap:8px;min-width:0}
800
+ /* 弹窗内的层级:范围行弱化、区块标题小而重、正文有承载面 */
801
+ .notesRange{display:flex;align-items:center;gap:8px;font-size:12px;line-height:18px;margin-bottom:4px}
802
+ .notesArrow{color:var(--dsw-alias-label-tertiary,#8b93a1)}
803
+ .notesMeta{font-size:12px;line-height:18px;letter-spacing:.02em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,#6b7280);display:flex;gap:6px;flex-wrap:wrap;align-items:baseline;margin-top:12px}
804
+ .notesPre{margin:0;white-space:pre-wrap;word-break:break-word;font-size:13px;line-height:20px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2,#eef0f4);border-radius:8px;max-height:40vh;overflow:auto}
805
+ .notesList{margin:0;padding-left:18px;display:flex;flex-direction:column;gap:6px;font-size:13px;line-height:20px}
806
+ /* markdown轻渲染的层级:标题、段落、行内代码 */
807
+ .notesRendered{display:flex;flex-direction:column;gap:6px;min-width:0}
808
+ .notesH{font-size:13px;font-weight:650;line-height:20px;margin-top:6px}
809
+ .notesP{font-size:13px;line-height:20px}
810
+ .notesCode{font-family:var(--dsw-alias-font-mono,ui-monospace,monospace);font-size:12px;padding:1px 5px;border-radius:5px;background:var(--dsw-alias-bg-layer-2,#eef0f4)}
811
+ /* 列表行:日期一列弱化对齐,内容一列自适应换行 */
812
+ .notesList{margin:0;padding:0;list-style:none;display:flex;flex-direction:column}
813
+ .notesRow{display:flex;gap:12px;align-items:baseline;padding:6px 0;border-bottom:1px solid var(--dsw-alias-border-l2,#e5e7eb)}
814
+ .notesRow:last-child{border-bottom:none}
815
+ .notesDate{flex-shrink:0;color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:12px;font-variant-numeric:tabular-nums}
816
+ .notesMsg{min-width:0;word-break:break-word;font-size:13px;line-height:20px}
817
+ /* commit行:消息限高两行,短sha右对齐可点 */
818
+ .notesMsg{min-width:0;word-break:break-word;font-size:13px;line-height:20px;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
819
+ .notesSha{margin-left:auto;flex-shrink:0;font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);text-decoration:none;font-variant-numeric:tabular-nums}
820
+ .notesSha:hover{color:var(--dsw-alias-label-primary,#1f2328);text-decoration:underline}
821
+ .notesVer{color:inherit;text-decoration:none;font-weight:600}
822
+ .notesVer:hover{text-decoration:underline}
823
+
824
+ /* Git -> npm source confirmation: make the from/to change readable without turning it into an error banner. */
825
+ .migrationSources{display:flex;flex-direction:column;gap:8px;margin-top:4px}
826
+ .migrationSource{display:flex;flex-direction:column;gap:4px;padding:10px 12px;border:1px solid var(--dsw-alias-border-default,#e5e7eb);border-radius:8px}
827
+ .migrationSource code{font-size:12px;font-family:ui-monospace,Menlo,monospace;overflow-wrap:anywhere}
828
+ .migrationLabel{font-size:11px;color:var(--dsw-alias-label-secondary,#9ca3af)}
829
+ .migrationArrow{text-align:center;color:var(--dsw-alias-label-secondary,#9ca3af)}
830
+ .migrationWarning{display:flex;align-items:flex-start;gap:6px;margin:12px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-state-warn-primary,#b45309)}