@jant/core 0.6.14 → 0.6.16

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 (350) hide show
  1. package/bin/commands/import-site.js +88 -13
  2. package/bin/commands/migrate.js +55 -0
  3. package/bin/commands/posts/rebuild-html.js +184 -0
  4. package/bin/commands/site/snapshot/import.js +14 -10
  5. package/bin/lib/d1-query.js +23 -5
  6. package/bin/lib/migration-runner.js +29 -7
  7. package/bin/lib/node-database.js +32 -0
  8. package/bin/lib/site-snapshot.js +347 -46
  9. package/bin/lib/sql-export.js +1 -1
  10. package/bin/lib/thread-collection-migration.js +326 -0
  11. package/dist/app-C75Zc8dD.js +6 -0
  12. package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
  13. package/dist/client/.vite/manifest.json +8 -8
  14. package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
  15. package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
  16. package/dist/client/_assets/client-D3VZyG4L.css +2 -0
  17. package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
  18. package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
  19. package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
  20. package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
  21. package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
  22. package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
  23. package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
  24. package/dist/index.js +5 -5
  25. package/dist/node.js +7 -7
  26. package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
  27. package/package.json +11 -10
  28. package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
  29. package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
  30. package/src/__tests__/export-hugo-build.test.ts +63 -4
  31. package/src/__tests__/export-import-roundtrip.test.ts +6 -4
  32. package/src/__tests__/export-service.test.ts +142 -9
  33. package/src/__tests__/helpers/app.ts +3 -0
  34. package/src/__tests__/helpers/export-fixtures.ts +2 -0
  35. package/src/__tests__/import-site-command.test.ts +65 -0
  36. package/src/__tests__/mise-config.test.ts +22 -0
  37. package/src/__tests__/vite-config.test.ts +45 -0
  38. package/src/app.tsx +16 -3
  39. package/src/client/__tests__/collection-created-notice.test.ts +36 -0
  40. package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
  41. package/src/client/__tests__/collection-page-actions.test.ts +71 -2
  42. package/src/client/__tests__/compose-bridge.test.ts +316 -2
  43. package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
  44. package/src/client/__tests__/feed-video-player.test.ts +16 -1
  45. package/src/client/__tests__/footnote-rail.test.ts +472 -0
  46. package/src/client/__tests__/hydrate-partial.test.ts +3 -0
  47. package/src/client/__tests__/toast.test.ts +82 -1
  48. package/src/client/collection-created-notice.ts +66 -0
  49. package/src/client/collection-form-bridge.ts +17 -0
  50. package/src/client/collection-navigation.ts +44 -0
  51. package/src/client/collection-page-actions.ts +57 -1
  52. package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
  53. package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
  54. package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
  55. package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
  56. package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
  57. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
  58. package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
  59. package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
  60. package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
  61. package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
  62. package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
  63. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
  64. package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
  65. package/src/client/components/collection-manager-types.ts +9 -3
  66. package/src/client/components/compose-format-convert.ts +5 -1
  67. package/src/client/components/compose-types.ts +55 -3
  68. package/src/client/components/jant-collection-directory.ts +165 -30
  69. package/src/client/components/jant-command-palette.ts +14 -7
  70. package/src/client/components/jant-compose-dialog.ts +1542 -694
  71. package/src/client/components/jant-compose-editor.ts +237 -99
  72. package/src/client/components/jant-compose-fullscreen.ts +27 -9
  73. package/src/client/components/jant-config-editor.ts +722 -0
  74. package/src/client/components/jant-media-lightbox.ts +100 -18
  75. package/src/client/components/jant-nav-manager.ts +1283 -54
  76. package/src/client/components/jant-post-form.ts +3 -3
  77. package/src/client/components/jant-post-menu.ts +177 -95
  78. package/src/client/components/nav-manager-types.ts +51 -2
  79. package/src/client/components/post-form-types.ts +1 -1
  80. package/src/client/compose-bridge.ts +147 -123
  81. package/src/client/compose-launch.ts +13 -0
  82. package/src/client/compose-shortcuts.ts +5 -1
  83. package/src/client/feed-video-player.ts +62 -2
  84. package/src/client/footnote-rail.ts +478 -0
  85. package/src/client/hydrate-partial.ts +2 -0
  86. package/src/client/media-lightbox-events.ts +63 -0
  87. package/src/client/post-refresh.ts +135 -0
  88. package/src/client/site-header-fragment.ts +38 -0
  89. package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
  90. package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
  91. package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
  92. package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
  93. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
  94. package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
  95. package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
  96. package/src/client/tiptap/bubble-menu.ts +88 -10
  97. package/src/client/tiptap/continuous-lists.ts +102 -0
  98. package/src/client/tiptap/create-editor.ts +3 -0
  99. package/src/client/tiptap/extensions.ts +14 -5
  100. package/src/client/tiptap/link-input-rules.ts +4 -4
  101. package/src/client/tiptap/markdown-clipboard.ts +608 -11
  102. package/src/client/tiptap/slash-commands.ts +8 -1
  103. package/src/client/tiptap/structural-keymap.ts +211 -0
  104. package/src/client/tiptap/tab-indent.ts +8 -22
  105. package/src/client/tiptap/table-control-labels.ts +30 -0
  106. package/src/client/tiptap/table-controls.ts +537 -0
  107. package/src/client/tiptap/table-size-picker.ts +254 -0
  108. package/src/client/toast.ts +133 -39
  109. package/src/client-auth.ts +1 -0
  110. package/src/client.ts +1 -0
  111. package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
  112. package/src/db/__tests__/d1-query.test.ts +43 -1
  113. package/src/db/__tests__/migration-runner.test.ts +63 -0
  114. package/src/db/__tests__/migrations.test.ts +469 -0
  115. package/src/db/__tests__/thread-activity.test.ts +113 -0
  116. package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
  117. package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
  118. package/src/db/migrations/0027_old_blue_blade.sql +39 -0
  119. package/src/db/migrations/0028_superb_post.sql +2 -0
  120. package/src/db/migrations/0029_boring_magma.sql +1 -0
  121. package/src/db/migrations/0030_typical_vivisector.sql +50 -0
  122. package/src/db/migrations/0031_many_ego.sql +3 -0
  123. package/src/db/migrations/meta/0027_snapshot.json +2511 -0
  124. package/src/db/migrations/meta/0028_snapshot.json +2511 -0
  125. package/src/db/migrations/meta/0029_snapshot.json +2519 -0
  126. package/src/db/migrations/meta/0030_snapshot.json +2541 -0
  127. package/src/db/migrations/meta/0031_snapshot.json +2562 -0
  128. package/src/db/migrations/meta/_journal.json +35 -0
  129. package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
  130. package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
  131. package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
  132. package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
  133. package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
  134. package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
  135. package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
  136. package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
  137. package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
  138. package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
  139. package/src/db/migrations/pg/meta/_journal.json +35 -0
  140. package/src/db/pg/schema.ts +46 -20
  141. package/src/db/rehearsal-fixtures/demo-current.json +39 -4
  142. package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
  143. package/src/db/schema.ts +48 -20
  144. package/src/db/thread-activity.ts +101 -0
  145. package/src/i18n/__tests__/detect.test.ts +8 -2
  146. package/src/i18n/detect.ts +14 -5
  147. package/src/i18n/locales/public/en.po +222 -51
  148. package/src/i18n/locales/public/en.ts +1 -1
  149. package/src/i18n/locales/public/zh-Hans.po +216 -45
  150. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  151. package/src/i18n/locales/public/zh-Hant.po +216 -45
  152. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  153. package/src/i18n/locales/settings/en.po +513 -10
  154. package/src/i18n/locales/settings/en.ts +1 -1
  155. package/src/i18n/locales/settings/zh-Hans.po +515 -12
  156. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  157. package/src/i18n/locales/settings/zh-Hant.po +515 -12
  158. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  159. package/src/lib/__tests__/api-settings.test.ts +299 -0
  160. package/src/lib/__tests__/constants.test.ts +5 -0
  161. package/src/lib/__tests__/feed-policy.test.ts +25 -0
  162. package/src/lib/__tests__/feed.test.ts +168 -0
  163. package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
  164. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
  165. package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
  166. package/src/lib/__tests__/markdown.test.ts +24 -6
  167. package/src/lib/__tests__/navigation.test.ts +59 -2
  168. package/src/lib/__tests__/post-body-html.test.ts +176 -0
  169. package/src/lib/__tests__/post-display.test.ts +64 -0
  170. package/src/lib/__tests__/resolve-config.test.ts +113 -1
  171. package/src/lib/__tests__/slug.test.ts +18 -0
  172. package/src/lib/__tests__/summary.test.ts +82 -0
  173. package/src/lib/__tests__/timeline.test.ts +109 -70
  174. package/src/lib/__tests__/timezones.test.ts +31 -3
  175. package/src/lib/__tests__/tiptap-render.test.ts +268 -9
  176. package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
  177. package/src/lib/__tests__/translit.test.ts +87 -0
  178. package/src/lib/__tests__/url.test.ts +53 -0
  179. package/src/lib/__tests__/view.test.ts +53 -0
  180. package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
  181. package/src/lib/api-settings.ts +239 -12
  182. package/src/lib/collection-sort.ts +7 -5
  183. package/src/lib/constants.ts +2 -0
  184. package/src/lib/feed-path.ts +25 -0
  185. package/src/lib/feed-policy.ts +64 -0
  186. package/src/lib/feed.ts +113 -11
  187. package/src/lib/footnotes.ts +264 -0
  188. package/src/lib/github-sync-site-config.ts +2 -0
  189. package/src/lib/hugo-markdown.ts +15 -3
  190. package/src/lib/jant-branding.ts +1 -0
  191. package/src/lib/link-preview.ts +25 -0
  192. package/src/lib/markdown-manager.ts +193 -5
  193. package/src/lib/markdown.ts +10 -3
  194. package/src/lib/navigation.ts +16 -3
  195. package/src/lib/post-body-html.ts +131 -0
  196. package/src/lib/post-display.ts +47 -8
  197. package/src/lib/render.tsx +9 -2
  198. package/src/lib/resolve-config.ts +100 -16
  199. package/src/lib/schemas.ts +133 -1
  200. package/src/lib/settings-paths.ts +5 -0
  201. package/src/lib/site-skill.ts +78 -0
  202. package/src/lib/summary.ts +16 -5
  203. package/src/lib/timeline.ts +137 -131
  204. package/src/lib/timezones.ts +54 -14
  205. package/src/lib/tiptap-render.ts +359 -83
  206. package/src/lib/translit.ts +288 -0
  207. package/src/lib/url.ts +123 -12
  208. package/src/lib/view.ts +58 -20
  209. package/src/lib/worker-response-cache.ts +11 -18
  210. package/src/middleware/__tests__/public-content-access.test.ts +91 -0
  211. package/src/middleware/config.ts +2 -2
  212. package/src/middleware/public-content-access.ts +87 -0
  213. package/src/node/__tests__/cli-export.test.ts +14 -0
  214. package/src/node/__tests__/cli-migrate.test.ts +6 -0
  215. package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
  216. package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
  217. package/src/node/index.ts +2 -1
  218. package/src/preset.css +16 -2
  219. package/src/routes/__tests__/compose.test.ts +51 -0
  220. package/src/routes/__tests__/site-skill.test.ts +112 -0
  221. package/src/routes/api/__tests__/collections.test.ts +91 -19
  222. package/src/routes/api/__tests__/mcp.test.ts +65 -1
  223. package/src/routes/api/__tests__/nav-items.test.ts +91 -0
  224. package/src/routes/api/__tests__/palette.test.ts +44 -0
  225. package/src/routes/api/__tests__/public-access.test.ts +81 -0
  226. package/src/routes/api/__tests__/settings.test.ts +217 -0
  227. package/src/routes/api/collections.ts +22 -26
  228. package/src/routes/api/export.ts +2 -0
  229. package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
  230. package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
  231. package/src/routes/api/internal/post-body-html.ts +43 -0
  232. package/src/routes/api/internal/sites.ts +24 -0
  233. package/src/routes/api/nav-items.ts +24 -1
  234. package/src/routes/api/posts.ts +14 -10
  235. package/src/routes/api/public/__tests__/archive.test.ts +35 -0
  236. package/src/routes/api/public/__tests__/posts.test.ts +246 -0
  237. package/src/routes/api/public/archive.ts +4 -0
  238. package/src/routes/api/public/posts.ts +48 -24
  239. package/src/routes/api/search.ts +3 -0
  240. package/src/routes/api/settings.ts +33 -0
  241. package/src/routes/auth/__tests__/setup.test.ts +7 -4
  242. package/src/routes/compose.tsx +8 -3
  243. package/src/routes/dash/settings.tsx +45 -2
  244. package/src/routes/feed/__tests__/feed.test.ts +210 -6
  245. package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
  246. package/src/routes/feed/feed.ts +23 -14
  247. package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
  248. package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
  249. package/src/routes/pages/__tests__/collections.test.ts +9 -9
  250. package/src/routes/pages/__tests__/featured.test.ts +6 -2
  251. package/src/routes/pages/__tests__/preview.test.ts +176 -0
  252. package/src/routes/pages/__tests__/search.test.ts +69 -0
  253. package/src/routes/pages/archive.tsx +90 -22
  254. package/src/routes/pages/collection.tsx +32 -19
  255. package/src/routes/pages/collections.tsx +6 -1
  256. package/src/routes/pages/page.tsx +86 -11
  257. package/src/routes/site-skill.ts +18 -0
  258. package/src/services/__tests__/auth.test.ts +7 -5
  259. package/src/services/__tests__/collection.test.ts +190 -44
  260. package/src/services/__tests__/navigation.test.ts +229 -21
  261. package/src/services/__tests__/post-timeline.test.ts +249 -39
  262. package/src/services/__tests__/post.test.ts +844 -5
  263. package/src/services/__tests__/search.test.ts +48 -1
  264. package/src/services/__tests__/settings.test.ts +8 -0
  265. package/src/services/__tests__/site-admin.test.ts +121 -1
  266. package/src/services/auth.ts +2 -2
  267. package/src/services/bootstrap.ts +3 -2
  268. package/src/services/collection.ts +224 -167
  269. package/src/services/export-theme/assets/client-site.css +1 -1
  270. package/src/services/export-theme/assets/client-site.js +46 -25
  271. package/src/services/export-theme/layouts/_default/list.html +18 -4
  272. package/src/services/export-theme/layouts/_default/rss.xml +8 -12
  273. package/src/services/export-theme/layouts/featured/list.html +5 -4
  274. package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
  275. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  276. package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
  277. package/src/services/export-theme/styles/main.css +30 -2
  278. package/src/services/export.ts +94 -85
  279. package/src/services/mcp.ts +37 -27
  280. package/src/services/media.ts +1 -1
  281. package/src/services/navigation.ts +185 -46
  282. package/src/services/path.ts +4 -1
  283. package/src/services/post.ts +1142 -342
  284. package/src/services/search.ts +17 -2
  285. package/src/services/settings.ts +2 -10
  286. package/src/services/site-admin.ts +63 -43
  287. package/src/style-cjk-jp.css +1 -1
  288. package/src/style-cjk-kr.css +1 -1
  289. package/src/style-cjk-tc.css +1 -1
  290. package/src/style-cjk.css +1 -1
  291. package/src/styles/components.css +540 -14
  292. package/src/styles/site-media.css +111 -19
  293. package/src/styles/tokens.css +76 -10
  294. package/src/styles/ui.css +2227 -1347
  295. package/src/types/bindings.ts +3 -0
  296. package/src/types/config.ts +243 -12
  297. package/src/types/constants.ts +22 -1
  298. package/src/types/entities.ts +14 -8
  299. package/src/types/operations.ts +18 -10
  300. package/src/types/props.ts +20 -2
  301. package/src/types/raw-assets.d.ts +5 -0
  302. package/src/types/views.ts +22 -4
  303. package/src/ui/__tests__/font-themes.test.ts +42 -30
  304. package/src/ui/compose/ComposeDialog.tsx +109 -19
  305. package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
  306. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
  307. package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
  308. package/src/ui/dash/settings/GeneralContent.tsx +15 -5
  309. package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
  310. package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
  311. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
  312. package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
  313. package/src/ui/feed/LinkCard.tsx +2 -12
  314. package/src/ui/feed/LinkPreview.tsx +2 -7
  315. package/src/ui/feed/NoteCard.tsx +10 -20
  316. package/src/ui/feed/PostStatusBadges.tsx +42 -3
  317. package/src/ui/feed/QuoteCard.tsx +2 -12
  318. package/src/ui/feed/ThreadPreview.tsx +26 -34
  319. package/src/ui/feed/TimelineFeed.tsx +2 -2
  320. package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
  321. package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
  322. package/src/ui/feed/thread-preview-state.ts +5 -7
  323. package/src/ui/font-themes.ts +74 -26
  324. package/src/ui/layouts/BaseLayout.tsx +22 -8
  325. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
  326. package/src/ui/pages/ArchivePage.tsx +131 -24
  327. package/src/ui/pages/CollectionPage.tsx +101 -22
  328. package/src/ui/pages/CollectionsPage.tsx +3 -1
  329. package/src/ui/pages/PostPage.tsx +39 -12
  330. package/src/ui/pages/SearchPage.tsx +6 -14
  331. package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
  332. package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
  333. package/src/ui/shared/CollectionDirectory.tsx +13 -9
  334. package/src/ui/shared/CollectionsManager.tsx +17 -7
  335. package/src/ui/shared/DraftPreviewBar.tsx +49 -0
  336. package/src/ui/shared/HomePageBranding.tsx +2 -2
  337. package/src/ui/shared/MediaGallery.tsx +1 -0
  338. package/src/ui/shared/PostFooter.tsx +38 -13
  339. package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
  340. package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
  341. package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
  342. package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
  343. package/src/ui/shared/collection-management-labels.ts +33 -2
  344. package/src/ui/shared/custom-icons.ts +5 -0
  345. package/src/ui/shared/navigation-labels.ts +2 -4
  346. package/src/ui/shared/post-article-attributes.ts +52 -0
  347. package/dist/app-DZDlmh7C.js +0 -6
  348. package/dist/client/_assets/client-Cevfpm8H.css +0 -2
  349. package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
  350. package/src/client/tiptap/wrapping-input-rules.ts +0 -102
package/src/styles/ui.css CHANGED
@@ -30,6 +30,134 @@
30
30
  counter-reset: sidenote-counter;
31
31
  }
32
32
 
33
+ /* Authenticated utility chrome lives outside .site-page so the previewed
34
+ theme layout remains the same subtree visitors receive after publishing. */
35
+ .draft-preview-bar {
36
+ position: sticky;
37
+ top: 0;
38
+ z-index: 40;
39
+ width: 100%;
40
+ border-bottom: 1px solid
41
+ color-mix(in srgb, var(--site-divider) 78%, transparent);
42
+ background: color-mix(
43
+ in srgb,
44
+ var(--site-elevated-bg) 94%,
45
+ var(--site-nav-hover-bg)
46
+ );
47
+ color: var(--site-text-secondary);
48
+ font-family: var(--font-ui);
49
+ box-shadow: 0 1px 0
50
+ color-mix(in srgb, var(--site-elevated-bg) 70%, transparent);
51
+ backdrop-filter: blur(14px);
52
+ -webkit-backdrop-filter: blur(14px);
53
+ }
54
+
55
+ .draft-preview-bar-inner {
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: space-between;
59
+ gap: 0.7rem;
60
+ min-height: 2.5rem;
61
+ max-width: var(--layout-body-max-width);
62
+ margin: 0 auto;
63
+ padding: 0.45rem var(--site-padding);
64
+ font-size: var(--type-ui-caption);
65
+ line-height: 1.35;
66
+ text-align: center;
67
+ }
68
+
69
+ .draft-preview-bar-copy {
70
+ display: inline-flex;
71
+ align-items: center;
72
+ gap: 0.7rem;
73
+ min-width: 0;
74
+ }
75
+
76
+ .draft-preview-bar-label {
77
+ display: inline-flex;
78
+ align-items: center;
79
+ gap: 0.42rem;
80
+ flex: none;
81
+ color: var(--site-text-primary);
82
+ font-weight: var(--fw-semibold);
83
+ letter-spacing: var(--type-label-tracking);
84
+ text-transform: uppercase;
85
+ }
86
+
87
+ .draft-preview-bar-dot {
88
+ width: 0.42rem;
89
+ height: 0.42rem;
90
+ border-radius: 999px;
91
+ background: var(--site-accent);
92
+ box-shadow: 0 0 0 3px
93
+ color-mix(in srgb, var(--site-accent) 12%, transparent);
94
+ }
95
+
96
+ .draft-preview-bar-description {
97
+ color: var(--site-text-secondary);
98
+ }
99
+
100
+ .draft-preview-bar-action {
101
+ display: inline-flex;
102
+ flex: none;
103
+ align-items: center;
104
+ justify-content: center;
105
+ min-height: 1.9rem;
106
+ padding: 0.28rem 0.72rem;
107
+ border: 1px solid
108
+ color-mix(in srgb, var(--site-text-primary) 18%, transparent);
109
+ border-radius: 999px;
110
+ background: color-mix(
111
+ in srgb,
112
+ var(--site-elevated-bg) 78%,
113
+ var(--site-nav-hover-bg)
114
+ );
115
+ color: var(--site-text-primary);
116
+ font-size: var(--type-ui-caption);
117
+ font-weight: var(--fw-medium);
118
+ line-height: 1;
119
+ text-decoration: none;
120
+ cursor: pointer;
121
+ transition:
122
+ border-color 150ms ease,
123
+ background-color 150ms ease,
124
+ color 150ms ease,
125
+ transform 150ms ease;
126
+ }
127
+
128
+ .draft-preview-bar-action:hover {
129
+ border-color: color-mix(
130
+ in srgb,
131
+ var(--site-accent) 48%,
132
+ var(--site-divider)
133
+ );
134
+ background: var(--site-nav-hover-bg);
135
+ color: var(--site-accent);
136
+ }
137
+
138
+ .draft-preview-bar-action:active {
139
+ transform: translateY(1px);
140
+ }
141
+
142
+ .draft-preview-bar-action:focus-visible {
143
+ outline: 2px solid var(--site-accent);
144
+ outline-offset: 2px;
145
+ }
146
+
147
+ @media (max-width: 480px) {
148
+ .draft-preview-bar-inner {
149
+ align-items: center;
150
+ gap: 0.65rem;
151
+ text-align: left;
152
+ }
153
+
154
+ .draft-preview-bar-copy {
155
+ flex-direction: column;
156
+ align-items: flex-start;
157
+ gap: 0.12rem;
158
+ }
159
+ }
160
+
33
161
  /*
34
162
  * Tufte horizontal frame — single source of truth for left/right positioning.
35
163
  * Every page-level section gets the same asymmetric padding.
@@ -1127,6 +1255,148 @@
1127
1255
  align-items: center;
1128
1256
  }
1129
1257
 
1258
+ .collection-created-notice {
1259
+ display: flex;
1260
+ align-items: center;
1261
+ flex-wrap: wrap;
1262
+ gap: 0.4rem 0.7rem;
1263
+ width: fit-content;
1264
+ max-width: 100%;
1265
+ min-height: 2rem;
1266
+ margin-bottom: 0.7rem;
1267
+ padding: 0.35rem 0.45rem 0.35rem 0.65rem;
1268
+ border-radius: 0.55rem;
1269
+ background: color-mix(in srgb, var(--site-accent) 5%, var(--site-page-bg));
1270
+ box-shadow: inset 2px 0 0
1271
+ color-mix(in srgb, var(--site-accent) 30%, transparent);
1272
+ }
1273
+
1274
+ .collection-created-notice-message {
1275
+ margin: 0;
1276
+ color: color-mix(
1277
+ in srgb,
1278
+ var(--site-text-secondary) 82%,
1279
+ var(--site-text-primary)
1280
+ );
1281
+ font-family: var(--font-ui);
1282
+ font-size: var(--type-sm);
1283
+ line-height: 1.35;
1284
+ }
1285
+
1286
+ @keyframes collection-created-notice-enter {
1287
+ from {
1288
+ opacity: 0;
1289
+ transform: translateY(0.25rem);
1290
+ }
1291
+
1292
+ to {
1293
+ opacity: 1;
1294
+ transform: translateY(0);
1295
+ }
1296
+ }
1297
+
1298
+ @keyframes collection-created-notice-attention {
1299
+ from {
1300
+ background: color-mix(
1301
+ in srgb,
1302
+ var(--site-accent) 11%,
1303
+ var(--site-page-bg)
1304
+ );
1305
+ box-shadow: inset 2px 0 0
1306
+ color-mix(in srgb, var(--site-accent) 52%, transparent);
1307
+ }
1308
+
1309
+ to {
1310
+ background: color-mix(
1311
+ in srgb,
1312
+ var(--site-accent) 5%,
1313
+ var(--site-page-bg)
1314
+ );
1315
+ box-shadow: inset 2px 0 0
1316
+ color-mix(in srgb, var(--site-accent) 30%, transparent);
1317
+ }
1318
+ }
1319
+
1320
+ @media (prefers-reduced-motion: no-preference) {
1321
+ .collection-created-notice {
1322
+ animation:
1323
+ collection-created-notice-enter 360ms cubic-bezier(0.22, 1, 0.36, 1)
1324
+ both,
1325
+ collection-created-notice-attention 1.8s 120ms ease-out both;
1326
+ }
1327
+ }
1328
+
1329
+ .collection-created-notice-actions {
1330
+ display: flex;
1331
+ align-items: center;
1332
+ flex: 0 0 auto;
1333
+ gap: 0.35rem;
1334
+ }
1335
+
1336
+ .collection-created-notice-action {
1337
+ appearance: none;
1338
+ display: inline-flex;
1339
+ align-items: center;
1340
+ min-height: 2rem;
1341
+ padding: 0.2rem 0;
1342
+ border: 0;
1343
+ background: transparent;
1344
+ font-family: var(--font-ui);
1345
+ font-size: var(--type-sm);
1346
+ line-height: 1.35;
1347
+ text-decoration: none;
1348
+ text-underline-offset: 0.22em;
1349
+ cursor: pointer;
1350
+ }
1351
+
1352
+ .collection-created-notice-action-primary {
1353
+ color: var(--site-text-primary);
1354
+ font-weight: var(--fw-medium);
1355
+ }
1356
+
1357
+ .collection-created-notice-action-primary:hover {
1358
+ color: var(--site-text-primary);
1359
+ text-decoration: underline;
1360
+ }
1361
+
1362
+ .collection-created-notice-action:focus-visible {
1363
+ outline: 2px solid color-mix(in srgb, var(--site-accent) 28%, transparent);
1364
+ outline-offset: 3px;
1365
+ border-radius: var(--radius-sm, 0.25rem);
1366
+ }
1367
+
1368
+ .collection-created-notice-action:disabled {
1369
+ opacity: 0.5;
1370
+ cursor: wait;
1371
+ }
1372
+
1373
+ .collection-created-notice-dismiss {
1374
+ appearance: none;
1375
+ display: inline-flex;
1376
+ align-items: center;
1377
+ justify-content: center;
1378
+ width: 1.75rem;
1379
+ height: 1.75rem;
1380
+ padding: 0;
1381
+ border: 0;
1382
+ border-radius: 999px;
1383
+ background: transparent;
1384
+ color: var(--site-text-secondary);
1385
+ opacity: 0.65;
1386
+ cursor: pointer;
1387
+ }
1388
+
1389
+ .collection-created-notice-dismiss:hover {
1390
+ background: color-mix(in srgb, var(--site-accent) 6%, transparent);
1391
+ color: var(--site-text-primary);
1392
+ opacity: 1;
1393
+ }
1394
+
1395
+ .collection-created-notice-dismiss:focus-visible {
1396
+ outline: 2px solid color-mix(in srgb, var(--site-accent) 28%, transparent);
1397
+ outline-offset: 2px;
1398
+ }
1399
+
1130
1400
  .collections-page-actions {
1131
1401
  display: flex;
1132
1402
  align-items: center;
@@ -1440,6 +1710,15 @@
1440
1710
  position: absolute;
1441
1711
  top: 0.7rem;
1442
1712
  right: 0;
1713
+ opacity: 0;
1714
+ pointer-events: none;
1715
+ }
1716
+
1717
+ .collection-directory-managed-row:hover .collection-directory-item-menu,
1718
+ .collection-directory-managed-row:focus-within
1719
+ .collection-directory-item-menu {
1720
+ opacity: 1;
1721
+ pointer-events: auto;
1443
1722
  }
1444
1723
 
1445
1724
  .collections-link-create,
@@ -2080,6 +2359,13 @@
2080
2359
  }
2081
2360
  }
2082
2361
 
2362
+ @media (hover: none) {
2363
+ .collection-directory-item-menu {
2364
+ opacity: 1;
2365
+ pointer-events: auto;
2366
+ }
2367
+ }
2368
+
2083
2369
  /* =========================================================================
2084
2370
  * Timeline & Post Components
2085
2371
  * ========================================================================= */
@@ -2285,7 +2571,7 @@
2285
2571
  .post-header-meta-link {
2286
2572
  color: var(--site-text-secondary);
2287
2573
  font-family: var(--font-ui);
2288
- font-size: var(--type-ui-meta);
2574
+ font-size: var(--type-ui-hint);
2289
2575
  text-decoration: none;
2290
2576
  white-space: nowrap;
2291
2577
  }
@@ -2332,19 +2618,13 @@
2332
2618
  [data-page="post"] .feed-quote-attribution,
2333
2619
  [data-page="post"] .post-header-meta-row,
2334
2620
  [data-page="post"] .post-header-meta-link,
2335
- [data-page="post"] .post-header-meta-link time,
2336
- [data-page="post"] [data-post-meta],
2337
- [data-page="post"] [data-post-meta] .post-footer-meta,
2338
- [data-page="post"] [data-post-meta] a,
2339
- [data-page="post"] [data-post-meta] time {
2621
+ [data-page="post"] .post-header-meta-link time {
2340
2622
  color: var(--site-reading-meta);
2341
2623
  }
2342
2624
 
2343
2625
  [data-page="post"] .feed-link-domain:hover,
2344
2626
  [data-page="post"] .feed-quote-source:hover,
2345
- [data-page="post"] .post-header-meta-link:hover,
2346
- [data-page="post"] [data-post-meta] a:hover,
2347
- [data-page="post"] [data-post-meta] .post-footer-external-link:hover {
2627
+ [data-page="post"] .post-header-meta-link:hover {
2348
2628
  color: var(--site-reading-body);
2349
2629
  }
2350
2630
 
@@ -2356,35 +2636,33 @@
2356
2636
  );
2357
2637
  }
2358
2638
 
2359
- /* --- Sidenotes (Tufte-style margin notes) ------------------------------ */
2639
+ /* --- Semantic footnotes with Tufte presentation ------------------------ */
2360
2640
 
2361
- .sidenote,
2362
- .marginnote {
2363
- /* Layout: float into the right margin. A floated note anchors to its containing
2364
- block's right *content* edge, so if a theme gives that block inline-end padding
2365
- (e.g. a padded blockquote card) the anchor is inset and the note drifts left.
2366
- Themes expose that inset via --sidenote-anchor-inset (default 0) — best derived
2367
- from the same token that sets the padding — so the note still reaches the gutter
2368
- no matter how the container is indented. Core's own blocks add no inset. */
2369
- float: right;
2370
- clear: right;
2371
- margin-right: calc(
2372
- var(--layout-sidenote-margin) - var(--sidenote-anchor-inset, 0px)
2373
- );
2374
- width: var(--layout-sidenote-width);
2375
- margin-top: 0.3rem;
2376
- /* Breathing room so multiple notes from one paragraph don't touch when they stack. */
2377
- margin-bottom: 1.4rem;
2378
- vertical-align: baseline;
2379
- position: relative;
2641
+ /* The published DOM stays semantic and readable without CSS: body blocks
2642
+ come first, followed by doc-endnotes containing one native ordered list.
2643
+ Detail and timeline posts use their real container width to opt into the
2644
+ Tufte rail; narrow containers, print, feeds, and readers retain endnote
2645
+ order. */
2646
+ [data-post-view],
2647
+ [data-timeline-item] {
2648
+ container: post-view / inline-size;
2649
+ }
2380
2650
 
2381
- /* Typographic isolation. A footnote is an annotation, not part of the text it
2382
- hangs off of, so it must render identically no matter what its reference sits
2383
- inside. The body span is emitted at the reference position (see tiptap-render
2384
- renderSidenoteReference), so when the [^n] is inside a blockquote / bold run /
2385
- heading the span inherits that context. Pin every inheritable type property to
2386
- the canonical note look to cut every such leak — not just the blockquote
2387
- serif+italic case. */
2651
+ .footnote-endnotes {
2652
+ margin-block: 1.5rem 0;
2653
+ padding-block-start: 0.85rem;
2654
+ border-block-start: 1px solid var(--site-divider);
2655
+ }
2656
+
2657
+ .footnote-list {
2658
+ margin-block: 0;
2659
+ padding-inline-start: 1.5rem;
2660
+ }
2661
+
2662
+ .footnote {
2663
+ scroll-margin-block-start: 2rem;
2664
+ padding-inline-start: 0.15rem;
2665
+ overflow-wrap: anywhere;
2388
2666
  font-family: var(--font-body);
2389
2667
  font-size: var(--type-sm);
2390
2668
  font-style: normal;
@@ -2393,37 +2671,158 @@
2393
2671
  line-height: 1.3;
2394
2672
  letter-spacing: var(--type-body-tracking, normal);
2395
2673
  text-align: left;
2396
- color: var(--site-reading-meta);
2674
+ color: var(--site-footnote-text);
2397
2675
  }
2398
2676
 
2399
- .sidenote-number {
2400
- counter-increment: sidenote-counter;
2677
+ .footnote::marker {
2678
+ color: inherit;
2679
+ font-size: var(--type-xs);
2680
+ font-variant-numeric: tabular-nums;
2401
2681
  }
2402
2682
 
2403
- .sidenote-number::after,
2404
- .sidenote::before {
2405
- position: relative;
2406
- vertical-align: baseline;
2683
+ .footnote:target {
2684
+ border-radius: var(--radius-sm, 0.25rem);
2685
+ background: color-mix(in srgb, var(--site-reading-meta) 8%, transparent);
2407
2686
  }
2408
2687
 
2409
- .sidenote-number::after {
2410
- content: counter(sidenote-counter);
2411
- font-size: var(--type-code);
2412
- top: -0.5rem;
2413
- /* Real margins (not `left`, which reserves no space) so the superscript
2414
- number keeps a gap from the words on both sides — matters for CJK, which
2415
- has no inter-word spacing. */
2416
- margin-left: 0.1rem;
2417
- margin-right: 0.25rem;
2688
+ .footnote > :first-child {
2689
+ margin-block-start: 0;
2418
2690
  }
2419
2691
 
2420
- .sidenote::before {
2421
- content: counter(sidenote-counter) " ";
2422
- font-size: var(--type-xs);
2692
+ .footnote > :last-child {
2693
+ margin-block-end: 0;
2694
+ }
2695
+
2696
+ .footnote code {
2697
+ font-size: var(--type-code);
2698
+ }
2699
+
2700
+ .footnote-backlinks {
2701
+ display: inline-flex;
2702
+ flex-wrap: wrap;
2703
+ align-items: baseline;
2704
+ gap: 0.35rem;
2705
+ font-size: var(--type-xs);
2706
+ }
2707
+
2708
+ span.footnote-backlinks {
2709
+ margin-inline-start: 0.35rem;
2710
+ vertical-align: baseline;
2711
+ }
2712
+
2713
+ p.footnote-backlinks {
2714
+ display: flex;
2715
+ margin-block: 0.45rem 0;
2716
+ }
2717
+
2718
+ .footnote + .footnote {
2719
+ margin-block-start: 0.75rem;
2720
+ }
2721
+
2722
+ .footnote-backref,
2723
+ .footnote-ref > a {
2724
+ color: inherit;
2725
+ text-decoration: none;
2726
+ }
2727
+
2728
+ .footnote-backref:hover,
2729
+ .footnote-ref > a:hover {
2730
+ text-decoration: underline;
2731
+ }
2732
+
2733
+ .footnote-backref:focus-visible,
2734
+ .footnote-ref > a:focus-visible {
2735
+ border-radius: var(--radius-sm, 0.25rem);
2736
+ outline: 1px solid currentColor;
2737
+ outline-offset: 2px;
2738
+ }
2739
+
2740
+ .footnote-ref {
2741
+ --site-prose-link-color: var(--site-footnote-marker);
2742
+ --site-prose-link-hover: var(--site-footnote-marker);
2743
+ --site-prose-link-underline: transparent;
2744
+
2745
+ margin-inline: 0.25rem;
2746
+ vertical-align: super;
2747
+ line-height: 0;
2748
+ font-size: var(--type-footnote-ref);
2749
+ font-variant-numeric: tabular-nums;
2750
+ white-space: nowrap;
2751
+ color: var(--site-footnote-marker);
2752
+ }
2753
+
2754
+ /* --- Legacy v1 sidenotes ----------------------------------------------- */
2755
+
2756
+ /* Kept for one compatibility window so imported snapshots or cached HTML
2757
+ from an older runtime remain readable during rollout. Product reads render
2758
+ stale post projections as v3 before the database rebuild runs. */
2759
+
2760
+ .sidenote:not([role="doc-footnote"]),
2761
+ .marginnote {
2762
+ /* Layout: float into the right margin. A floated note anchors to its containing
2763
+ block's right *content* edge, so if a theme gives that block inline-end padding
2764
+ (e.g. a padded blockquote card) the anchor is inset and the note drifts left.
2765
+ Themes expose that inset via --sidenote-anchor-inset (default 0) — best derived
2766
+ from the same token that sets the padding — so the note still reaches the gutter
2767
+ no matter how the container is indented. Core's own blocks add no inset. */
2768
+ float: right;
2769
+ clear: right;
2770
+ margin-right: calc(
2771
+ var(--layout-sidenote-margin) - var(--sidenote-anchor-inset, 0px)
2772
+ );
2773
+ width: var(--layout-sidenote-width);
2774
+ margin-top: 0.3rem;
2775
+ /* Breathing room so multiple notes from one paragraph don't touch when they stack. */
2776
+ margin-bottom: 1.4rem;
2777
+ vertical-align: baseline;
2778
+ position: relative;
2779
+
2780
+ /* Typographic isolation. A footnote is an annotation, not part of the text it
2781
+ hangs off of, so it must render identically no matter what its reference sits
2782
+ inside. The body span is emitted at the reference position (see tiptap-render
2783
+ renderSidenoteReference), so when the [^n] is inside a blockquote / bold run /
2784
+ heading the span inherits that context. Pin every inheritable type property to
2785
+ the canonical note look to cut every such leak — not just the blockquote
2786
+ serif+italic case. */
2787
+ font-family: var(--font-body);
2788
+ font-size: var(--type-sm);
2789
+ font-style: normal;
2790
+ font-weight: 400;
2791
+ font-variant: normal;
2792
+ line-height: 1.3;
2793
+ letter-spacing: var(--type-body-tracking, normal);
2794
+ text-align: left;
2795
+ color: var(--site-reading-meta);
2796
+ }
2797
+
2798
+ .sidenote-number {
2799
+ counter-increment: sidenote-counter;
2800
+ }
2801
+
2802
+ .sidenote-number::after,
2803
+ .sidenote:not([role="doc-footnote"])::before {
2804
+ position: relative;
2805
+ vertical-align: baseline;
2806
+ }
2807
+
2808
+ .sidenote-number::after {
2809
+ content: counter(sidenote-counter);
2810
+ font-size: var(--type-code);
2811
+ top: -0.5rem;
2812
+ /* Real margins (not `left`, which reserves no space) so the superscript
2813
+ number keeps a gap from the words on both sides — matters for CJK, which
2814
+ has no inter-word spacing. */
2815
+ margin-left: 0.1rem;
2816
+ margin-right: 0.25rem;
2817
+ }
2818
+
2819
+ .sidenote:not([role="doc-footnote"])::before {
2820
+ content: counter(sidenote-counter) " ";
2821
+ font-size: var(--type-xs);
2423
2822
  top: -0.5rem;
2424
2823
  }
2425
2824
 
2426
- .sidenote > code,
2825
+ .sidenote:not([role="doc-footnote"]) > code,
2427
2826
  .marginnote > code {
2428
2827
  font-size: var(--type-code);
2429
2828
  }
@@ -2452,12 +2851,12 @@
2452
2851
  cursor: pointer;
2453
2852
  }
2454
2853
 
2455
- .sidenote,
2854
+ .sidenote:not([role="doc-footnote"]),
2456
2855
  .marginnote {
2457
2856
  display: none;
2458
2857
  }
2459
2858
 
2460
- .margin-toggle:checked + .sidenote,
2859
+ .margin-toggle:checked + .sidenote:not([role="doc-footnote"]),
2461
2860
  .margin-toggle:checked + .marginnote {
2462
2861
  display: block;
2463
2862
  float: left;
@@ -2540,21 +2939,52 @@
2540
2939
  flex-shrink: 0;
2541
2940
  }
2542
2941
 
2942
+ /* One dot on both rails: a flat fill with a surface-coloured gap punched
2943
+ around it, so it reads as a bead threaded on the rail rather than a bulge in
2944
+ it. Same size, same colours, same punch — reading then rings it (below) and
2945
+ compose stops at the fill. What a post *is* comes from its card; the dot
2946
+ only says where it sits on the chain, and an accent fill says which one the
2947
+ view is about. */
2543
2948
  .thread-group,
2544
2949
  .compose-thread-layout {
2545
- --site-thread-marker-size: 10px;
2546
- --site-thread-marker-border-width: 2px;
2547
- --site-thread-marker-ring-width: 2px;
2548
- --site-thread-marker-hero-size: 14px;
2549
- --site-thread-marker-hero-border-width: 3px;
2950
+ --site-thread-marker-size: 7px;
2951
+ /* Kept at 7:1.5 with the marker. A 7px dot on a 1px hairline reads as a bead
2952
+ dropped onto a thread rather than the same stroke thickening; the timeline
2953
+ components this pattern comes from (Ant, CodyHouse) all run a 2px rail. */
2954
+ --site-thread-rail-line-width: 1.5px;
2955
+ /* The gap's ceiling is set by whichever dot has the rail on one side only —
2956
+ in compose that's the last post, where the rail is trimmed. One line end
2957
+ holds it instead of two, and around 12px it reads as floating loose. */
2958
+ --site-thread-marker-gap: 9px;
2550
2959
  --site-thread-marker-surface: var(--site-page-bg);
2960
+ /* "This is the post the view is about." Tinted toward the rail rather than
2961
+ the raw accent: every other accent-derived token in the system sits
2962
+ between 6% and 24% (see tokens.css), and a 7px shape at full accent was
2963
+ the single highest-contrast thing in an otherwise near-empty column. */
2964
+ --site-thread-marker-current: color-mix(
2965
+ in srgb,
2966
+ var(--site-accent) 90%,
2967
+ var(--site-threadline)
2968
+ );
2969
+ }
2970
+
2971
+ .compose-thread-layout {
2972
+ --site-thread-marker-surface: var(--compose-paper-bg);
2551
2973
  }
2552
2974
 
2553
- /* Thread group: timeline layout — vertical line with dot markers */
2975
+ /* Thread group: timeline layout — vertical line with dot markers.
2976
+ Reading rings its dots; compose doesn't. A compose dot sits beside a card
2977
+ edge, a format switcher and a "1/3", so a joint in the line is all it has to
2978
+ be. Reading's rail runs down an otherwise empty column, where a bare fill
2979
+ reads as a speck of stray threadline — the ring gives it a silhouette
2980
+ without making it any louder, since the tint is the same 16% accent every
2981
+ other halo in the system uses. Held off the fill by a band of surface so
2982
+ the two stay separate marks rather than one fuzzy dot. */
2554
2983
  .thread-group {
2555
2984
  --site-thread-rail-indent: 24px;
2556
2985
  --site-thread-rail-line-left: 5px;
2557
- --site-thread-rail-line-width: 2px;
2986
+ --site-thread-marker-ring-inset: 2px;
2987
+ --site-thread-marker-ring-width: 2px;
2558
2988
  position: relative;
2559
2989
  padding-left: var(--site-thread-rail-indent);
2560
2990
  }
@@ -2588,28 +3018,55 @@
2588
3018
 
2589
3019
  /* Dot marker at each post position — auto-centered on the rail line.
2590
3020
  Line ::before is relative to .thread-group, but dot ::before is relative
2591
- to .thread-item which sits inside the padding (indent), so we subtract it. */
3021
+ to .thread-item which sits inside the padding (indent), so we subtract it.
3022
+ Every length here is derived: a hardcoded half-marker is what once left the
3023
+ compose rail running down the edge of its own dots instead of through them. */
2592
3024
  .thread-item::before {
2593
3025
  content: "";
2594
3026
  position: absolute;
2595
3027
  left: calc(
2596
3028
  var(--site-thread-rail-line-left) + var(--site-thread-rail-line-width) /
2597
- 2 - 6px - var(--site-thread-rail-indent)
3029
+ 2 - var(--site-thread-marker-size) / 2 - var(--site-thread-rail-indent)
2598
3030
  );
2599
3031
  top: 50%;
2600
3032
  transform: translateY(-50%);
2601
- width: 12px;
2602
- height: 12px;
3033
+ width: var(--site-thread-marker-size);
3034
+ height: var(--site-thread-marker-size);
2603
3035
  border-radius: 50%;
2604
3036
  background-color: var(--site-threadline);
3037
+ /* Three concentric layers off one border box, painted first-on-top: a band
3038
+ of surface, the accent ring, then the rest of the punch-out. The punch's
3039
+ radius is unchanged, so the ring sits inside the gap the rail line was
3040
+ already broken for and nothing downstream (`.thread-context-shell`'s
3041
+ inset, the compose switcher's overhang) has to move for it. */
3042
+ box-shadow:
3043
+ 0 0 0 var(--site-thread-marker-ring-inset)
3044
+ var(--site-thread-marker-surface),
3045
+ 0 0 0
3046
+ calc(
3047
+ var(--site-thread-marker-ring-inset) +
3048
+ var(--site-thread-marker-ring-width)
3049
+ )
3050
+ var(--site-thread-dot-ring),
3051
+ 0 0 0 var(--site-thread-marker-gap) var(--site-thread-marker-surface);
2605
3052
  z-index: 1;
2606
3053
  }
2607
3054
 
3055
+ /* "This is the post the view is about": the feed preview's latest reply, a
3056
+ curated preview's highlighted segment, and the post you opened on a detail
3057
+ page. One signal for all three — accent fill on the same dot. Hero and
3058
+ curated also ran 4px larger; the full card beside them already says it, so
3059
+ size was a second signal doing the first one's job. */
3060
+ .thread-item-hero::before,
3061
+ .thread-item-curated::before,
3062
+ .thread-item-current::before {
3063
+ background-color: var(--site-thread-marker-current);
3064
+ }
3065
+
2608
3066
  .thread-group-preview,
2609
3067
  .thread-group-detail {
2610
3068
  --site-thread-rail-indent: 0px;
2611
3069
  --site-thread-rail-line-left: -27px;
2612
- --site-thread-rail-line-width: 1px;
2613
3070
  }
2614
3071
 
2615
3072
  @media (max-width: 760px) {
@@ -2620,6 +3077,12 @@
2620
3077
  }
2621
3078
  }
2622
3079
 
3080
+ /* The rail fades in at the top, where collapsed ancestor context begins: the
3081
+ thread continues above what this view shows, so the line shouldn't start on
3082
+ a hard edge. The bottom needs no matching fade — the line is cut at the last
3083
+ dot instead. Compose doesn't share the gradient: its rail is drawn per row
3084
+ so both ends can be trimmed, and a per-row gradient would fade once per
3085
+ post rather than once per thread. */
2623
3086
  .thread-group-preview::before,
2624
3087
  .thread-group-detail::before {
2625
3088
  background: linear-gradient(
@@ -2630,39 +3093,44 @@
2630
3093
  );
2631
3094
  }
2632
3095
 
3096
+ /* The rail ends at the last dot. Below it the line pointed at posts that
3097
+ aren't there — the thread is over, and compose already trims its own rail
3098
+ the same way. Painted over rather than shortened: the line is one box on the
3099
+ group so it can carry a single gradient, and only the last post knows where
3100
+ its own dot sits (its own 50%). `> ` scopes this to the group's own children
3101
+ — the ancestors inside `.thread-context-shell` have a last child too, and
3102
+ the rail must run straight through it. */
3103
+ .thread-group > .thread-item:last-child::after {
3104
+ content: "";
3105
+ position: absolute;
3106
+ left: calc(
3107
+ var(--site-thread-rail-line-left) - var(--site-thread-rail-indent)
3108
+ );
3109
+ top: 50%;
3110
+ bottom: 0;
3111
+ width: var(--site-thread-rail-line-width);
3112
+ background-color: var(--site-thread-marker-surface);
3113
+ }
3114
+
2633
3115
  .thread-group-preview .thread-item,
2634
3116
  .thread-group-detail .thread-item {
2635
3117
  padding: var(--site-thread-item-spacing) 0;
2636
3118
  }
2637
3119
 
2638
- .thread-group-preview .thread-item::before,
2639
- .thread-group-detail .thread-item::before {
2640
- left: calc(
2641
- var(--site-thread-rail-line-left) + var(--site-thread-rail-line-width) /
2642
- 2 - var(--site-thread-marker-size) / 2 - var(--site-thread-rail-indent)
2643
- );
2644
- width: var(--site-thread-marker-size);
2645
- height: var(--site-thread-marker-size);
2646
- border: var(--site-thread-marker-border-width) solid
2647
- var(--site-thread-marker-surface);
2648
- background-color: var(--site-threadline);
2649
- box-shadow: 0 0 0 var(--site-thread-marker-ring-width)
2650
- var(--site-thread-dot-ring);
2651
- }
2652
-
2653
3120
  /* Collapsible ancestor context wrapper.
2654
3121
  Server-rendered with `data-collapsed`. Crop and fade are always active
2655
3122
  while data-collapsed is set; JS only toggles the Show more/less button
2656
3123
  visibility based on real overflow. Negative left margin + matching
2657
3124
  padding extends the shell's box past the content edge so the dot
2658
- markers (which live at left:-31.5px on desktop) survive
2659
- `overflow: hidden`. `box-sizing: content-box` keeps the inner content
2660
- width identical to before. */
3125
+ markers survive `overflow: hidden`. `box-sizing: content-box` keeps the
3126
+ inner content width identical to before. The inset has to clear the dot's
3127
+ punched gap, not just its fill: on desktop the fill starts at -30px but the
3128
+ gap reaches -39px, so the old 40px cleared it by a single pixel. */
2661
3129
  .thread-context-shell {
2662
3130
  position: relative;
2663
3131
  box-sizing: content-box;
2664
- margin-left: -40px;
2665
- padding-left: 40px;
3132
+ margin-left: -48px;
3133
+ padding-left: 48px;
2666
3134
  transition: max-height 0.32s cubic-bezier(0.4, 0, 0.2, 1);
2667
3135
  }
2668
3136
 
@@ -2853,37 +3321,6 @@
2853
3321
  padding-top: 0.95rem;
2854
3322
  }
2855
3323
 
2856
- .thread-group-preview .thread-item-hero::before,
2857
- .compose-editor-row .compose-thread-dot {
2858
- left: calc(
2859
- var(--site-thread-rail-line-left) + var(--site-thread-rail-line-width) /
2860
- 2 - var(--site-thread-marker-hero-size) / 2 -
2861
- var(--site-thread-rail-indent)
2862
- );
2863
- width: var(--site-thread-marker-hero-size);
2864
- height: var(--site-thread-marker-hero-size);
2865
- border: var(--site-thread-marker-hero-border-width) solid
2866
- var(--site-thread-marker-surface);
2867
- background-color: var(--site-accent);
2868
- box-shadow: 0 0 0 var(--site-thread-marker-ring-width)
2869
- var(--site-thread-dot-ring);
2870
- }
2871
-
2872
- .thread-group-curated .thread-item-curated::before {
2873
- left: calc(
2874
- var(--site-thread-rail-line-left) + var(--site-thread-rail-line-width) /
2875
- 2 - var(--site-thread-marker-hero-size) / 2 -
2876
- var(--site-thread-rail-indent)
2877
- );
2878
- width: var(--site-thread-marker-hero-size);
2879
- height: var(--site-thread-marker-hero-size);
2880
- border: var(--site-thread-marker-hero-border-width) solid
2881
- var(--site-thread-marker-surface);
2882
- background-color: var(--site-accent);
2883
- box-shadow: 0 0 0 var(--site-thread-marker-ring-width)
2884
- var(--site-thread-dot-ring);
2885
- }
2886
-
2887
3324
  /* Compact text for thread replies */
2888
3325
  .feed-compact {
2889
3326
  @apply text-sm;
@@ -2894,6 +3331,28 @@
2894
3331
  scroll-margin-top: 80px;
2895
3332
  }
2896
3333
 
3334
+ /* --- Trailing draft in a thread (author-only) ---------------------------
3335
+ Rendered so the author sees the same thread end the server does. Reads as
3336
+ provisional — present enough to act on, quiet enough not to pass for
3337
+ published writing. */
3338
+
3339
+ .thread-detail-item article[data-post-draft] {
3340
+ opacity: 0.72;
3341
+ transition: opacity 0.18s ease;
3342
+ }
3343
+
3344
+ .thread-detail-item article[data-post-draft]:hover,
3345
+ .thread-detail-item article[data-post-draft]:focus-within {
3346
+ opacity: 1;
3347
+ }
3348
+
3349
+ /* A draft is not part of the published spine, so its marker is hollow: the
3350
+ rail reaches it, but the dot reads as an empty slot rather than another
3351
+ published stop. */
3352
+ .thread-item:has(article[data-post-draft])::before {
3353
+ background-color: var(--site-thread-marker-surface);
3354
+ }
3355
+
2897
3356
  /* When a child post has an ancestor shell above it (toggle button sits
2898
3357
  directly before the current post), leave enough room above the scroll
2899
3358
  landing so the user can see the Show more button + a peek of the
@@ -2904,23 +3363,6 @@
2904
3363
  scroll-margin-top: 120px;
2905
3364
  }
2906
3365
 
2907
- .thread-group-detail .thread-item-current::before {
2908
- background-color: var(--site-accent);
2909
- box-shadow:
2910
- 0 0 0 var(--site-thread-marker-border-width)
2911
- var(--site-thread-marker-surface),
2912
- 0 0 0
2913
- calc(
2914
- var(--site-thread-marker-border-width) +
2915
- var(--site-thread-marker-ring-width)
2916
- )
2917
- color-mix(
2918
- in srgb,
2919
- var(--site-accent) 18%,
2920
- var(--site-thread-marker-surface)
2921
- );
2922
- }
2923
-
2924
3366
  /* Quote block styling */
2925
3367
  .feed-quote {
2926
3368
  margin: 0;
@@ -3005,6 +3447,10 @@
3005
3447
  line-height: 1.3;
3006
3448
  }
3007
3449
 
3450
+ .feed-quote-attribution:has(+ .post-menu-footer) {
3451
+ margin-bottom: 0.45rem;
3452
+ }
3453
+
3008
3454
  .feed-quote-attribution::before {
3009
3455
  content: "";
3010
3456
  width: 0.9rem;
@@ -3135,6 +3581,7 @@
3135
3581
  /* Show the container when at least one status is active */
3136
3582
  article[data-post-pinned] .post-status-badges,
3137
3583
  article[data-post-pinned-in-collection] .post-status-badges,
3584
+ article[data-post-draft] .post-status-badges,
3138
3585
  article[data-post-visibility="private"] .post-status-badges {
3139
3586
  display: flex;
3140
3587
  }
@@ -3165,6 +3612,38 @@
3165
3612
  display: inline-flex;
3166
3613
  }
3167
3614
 
3615
+ article[data-post-draft] .post-status-draft {
3616
+ display: inline-flex;
3617
+ }
3618
+
3619
+ /* The draft badge is a button — the shortest path from "I see an unfinished
3620
+ post" to "I'm finishing it". */
3621
+ .post-status-draft {
3622
+ padding: 0;
3623
+ border: 0;
3624
+ background: transparent;
3625
+ font: inherit;
3626
+ letter-spacing: inherit;
3627
+ cursor: pointer;
3628
+ transition: color 0.18s ease;
3629
+ }
3630
+
3631
+ .post-status-draft:hover,
3632
+ .post-status-draft:focus-visible {
3633
+ color: var(--site-text-primary);
3634
+ }
3635
+
3636
+ .post-status-draft:hover svg,
3637
+ .post-status-draft:focus-visible svg {
3638
+ opacity: 1;
3639
+ }
3640
+
3641
+ .post-status-draft:focus-visible {
3642
+ outline: 2px solid var(--site-accent);
3643
+ outline-offset: 3px;
3644
+ border-radius: 3px;
3645
+ }
3646
+
3168
3647
  .post-status-badge svg {
3169
3648
  width: 11px;
3170
3649
  height: 11px;
@@ -3183,10 +3662,17 @@
3183
3662
 
3184
3663
  .post-footer-detail {
3185
3664
  margin-top: 24px;
3186
- font-size: var(--type-ui-meta);
3665
+ font-size: var(--type-ui-hint);
3187
3666
  color: var(--site-text-secondary);
3188
3667
  }
3189
3668
 
3669
+ /* A standalone detail footer needs extra breathing room to close the
3670
+ reading surface. Thread entries keep a smaller content gap so their
3671
+ metadata stays connected without crowding the post body. */
3672
+ .thread-group-detail .post-footer-detail {
3673
+ margin-top: var(--content-gap);
3674
+ }
3675
+
3190
3676
  .post-footer-meta {
3191
3677
  display: flex;
3192
3678
  flex: 1 1 auto;
@@ -3605,6 +4091,14 @@
3605
4091
  padding: 0 0.22rem;
3606
4092
  }
3607
4093
 
4094
+ /* Every item in a group is conditional, so a group can end up with none —
4095
+ a draft reply hides both Featured and Pin. Match on element children so
4096
+ Lit's binding comments and the template's whitespace don't count as
4097
+ content, otherwise the empty group still draws its divider and padding. */
4098
+ .post-menu-section:not(:has(> *)) {
4099
+ display: none;
4100
+ }
4101
+
3608
4102
  .post-menu-section + .post-menu-section {
3609
4103
  margin-top: 0.08rem;
3610
4104
  padding-top: 0.3rem;
@@ -4355,7 +4849,12 @@
4355
4849
  }
4356
4850
  }
4357
4851
 
4852
+ /* The one composer that is not a floating sheet: it fills the viewport, so
4853
+ its paper is the page's, not the elevated surface's. Nothing sits behind
4854
+ it to be lifted away from. */
4358
4855
  .compose-page {
4856
+ --compose-paper-bg: var(--site-page-bg);
4857
+
4359
4858
  position: fixed;
4360
4859
  inset: 0;
4361
4860
  z-index: 50;
@@ -4424,19 +4923,18 @@
4424
4923
  .compose-page-shell .compose-dialog-inner {
4425
4924
  min-height: 0;
4426
4925
  max-height: none;
4926
+ padding-top: 0;
4427
4927
  border-radius: inherit;
4428
4928
  }
4429
4929
 
4430
- .compose-page-shell .compose-dialog-inner > jant-compose-editor,
4431
- .compose-page-shell .compose-thread-layout,
4432
- .compose-page-shell .compose-editor-row,
4433
- .compose-page-shell .compose-editor-row > jant-compose-editor {
4930
+ /* The page itself is the scroller here, so the composer's region gives up
4931
+ its own and grows to whatever it holds. */
4932
+ .compose-page-shell .compose-scroll {
4434
4933
  flex: 0 1 auto;
4435
- min-height: 0;
4934
+ overflow: visible;
4436
4935
  }
4437
4936
 
4438
4937
  .compose-page-shell .compose-body {
4439
- flex: 0 1 auto;
4440
4938
  min-height: clamp(8rem, 22vh, 12rem);
4441
4939
  }
4442
4940
 
@@ -4463,6 +4961,10 @@
4463
4961
  }
4464
4962
 
4465
4963
  .compose-page-shell .compose-dialog-inner-page {
4964
+ /* The card's own padding is the text column here, so nothing inside it
4965
+ adds a gutter of its own. */
4966
+ --compose-gutter: 0px;
4967
+
4466
4968
  padding: 20px 24px 16px;
4467
4969
  background-color: var(--compose-floating-bg);
4468
4970
  border: 1px solid var(--compose-control-border);
@@ -4473,40 +4975,25 @@
4473
4975
  overflow: visible;
4474
4976
  }
4475
4977
 
4476
- .compose-page-shell .compose-dialog-inner-page .compose-dialog-header {
4477
- display: grid;
4478
- grid-template-columns: 1fr auto;
4479
- align-items: center;
4480
- gap: 12px;
4481
- padding: 0 0 16px;
4482
- }
4483
-
4484
- .compose-page-shell
4485
- .compose-dialog-inner-page
4486
- .compose-dialog-header-center {
4487
- position: static;
4488
- transform: none;
4489
- justify-self: start;
4490
- min-width: 0;
4491
- }
4492
-
4493
- .compose-page-shell .compose-dialog-inner-page .compose-dialog-cancel {
4494
- display: none;
4495
- }
4496
-
4497
4978
  .compose-page-shell .compose-dialog-inner-page .compose-body {
4498
- padding: 22px 0 12px;
4979
+ padding: 22px 0 12px var(--compose-gutter);
4499
4980
  min-height: clamp(16rem, 36vh, 24rem);
4500
4981
  }
4501
4982
 
4983
+ /* Only the right is restated: the left comes from the card's own gutter,
4984
+ which is what every one of these rows already reads. */
4502
4985
  .compose-page-shell .compose-dialog-inner-page .compose-tools-row,
4503
4986
  .compose-page-shell .compose-dialog-inner-page .compose-add-thread-trigger {
4504
- padding-inline: 0;
4987
+ padding-inline-end: 0;
4505
4988
  background: transparent;
4506
4989
  }
4507
4990
 
4991
+ .compose-page-shell .compose-dialog-inner-page .compose-thread-post-header {
4992
+ padding-inline-end: 0;
4993
+ }
4994
+
4508
4995
  .compose-page-shell .compose-dialog-inner-page .compose-attachments-dock {
4509
- padding-inline: 0;
4996
+ padding-inline-end: 0;
4510
4997
  background: transparent;
4511
4998
  }
4512
4999
 
@@ -4515,7 +5002,15 @@
4515
5002
  bottom: 0;
4516
5003
  z-index: 5;
4517
5004
  margin-inline: 0;
4518
- padding: 14px 0 0;
5005
+ padding: 14px 0 0 var(--compose-gutter);
5006
+ background-color: inherit;
5007
+ }
5008
+
5009
+ /* Takes the page's gutter rather than the dialog's, so the toggles line up
5010
+ under Publish instead of sitting 12px inside it. */
5011
+ .compose-page-shell .compose-dialog-inner-page .compose-quick-actions-row {
5012
+ padding-inline: 0;
5013
+ padding-bottom: 2px;
4519
5014
  background-color: inherit;
4520
5015
  }
4521
5016
 
@@ -4540,8 +5035,8 @@
4540
5035
 
4541
5036
  .compose-dialog,
4542
5037
  .compose-page-shell > jant-compose-dialog {
4543
- --compose-title-input-size: var(--type-content-subtitle);
4544
- --compose-title-input-leading: var(--type-display-leading);
5038
+ --compose-title-input-size: calc(var(--type-content-body) * 1.2);
5039
+ --compose-title-input-leading: 1.35;
4545
5040
  --compose-quote-input-size: var(--type-content-subtitle);
4546
5041
  --compose-quote-input-leading: 1.32;
4547
5042
  --compose-inline-input-size: var(--type-ui-input);
@@ -4550,12 +5045,40 @@
4550
5045
  --compose-panel-control-size: var(--type-ui-control);
4551
5046
  --compose-summary-size: var(--type-ui-meta);
4552
5047
  --compose-primary-button-size: var(--type-ui-meta);
5048
+
5049
+ /* How far the format switcher is pulled left of the text column. Declared
5050
+ up here with the other compose tunables so one value covers the dialog,
5051
+ the /new page and every thread row at once. See `.compose-segmented` for
5052
+ what the endpoints look like and why 12px is the ceiling. */
5053
+ --compose-switcher-hang: 8px;
5054
+
5055
+ /* The composer's text column, measured from each row's own container.
5056
+ Every row starts its padding here and the control inside it hangs its
5057
+ box back out by its own optical inset, so what lands on the column is
5058
+ the glyph or the label rather than the box around it — the switcher's
5059
+ hang above is the same idea, named per control because each one is
5060
+ inset by a different amount.
5061
+
5062
+ It drops to 0 wherever something outside the editor already supplies the
5063
+ indent: a thread row's rail, or the /new card's own padding. Only the
5064
+ left is expressed this way; the right paddings stay per-row, because a
5065
+ thread row is indented on one side only. */
5066
+ --compose-gutter: 20px;
5067
+ }
5068
+
5069
+ /* The fullscreen sheet renders none of these rows today, but it does share
5070
+ `.compose-editor-row`. Give it the column too rather than leave the value
5071
+ undefined there: an undefined custom property drops the whole padding
5072
+ declaration that reads it instead of falling back to a sane number. */
5073
+ .compose-fullscreen-dialog {
5074
+ --compose-gutter: 20px;
4553
5075
  }
4554
5076
 
4555
5077
  @media (min-width: 700px) {
4556
5078
  .compose-dialog,
5079
+ .compose-fullscreen-dialog,
4557
5080
  .compose-page-shell > jant-compose-dialog {
4558
- --compose-title-input-size: var(--feed-note-title-size);
5081
+ --compose-gutter: 24px;
4559
5082
  }
4560
5083
  }
4561
5084
 
@@ -4575,7 +5098,7 @@
4575
5098
  box-shadow:
4576
5099
  0 8px 40px rgba(0, 0, 0, 0.08),
4577
5100
  0 1px 3px rgba(0, 0, 0, 0.04);
4578
- border: 1px solid var(--compose-control-border);
5101
+ border: 1px solid var(--site-raised-border);
4579
5102
  overflow: visible;
4580
5103
  }
4581
5104
 
@@ -4583,8 +5106,15 @@
4583
5106
  animation: compose-modal-in 0.32s cubic-bezier(0.22, 1, 0.36, 1) both;
4584
5107
  }
4585
5108
 
5109
+ /* The dim is only half of it. On a dark page there is no luminance left to
5110
+ take away — 0.3 → 0.7 alpha moves the dialog's contrast against its own
5111
+ backdrop by 0.05 — so the blur carries the separation instead. It reads as
5112
+ depth rather than darkness, which is the one cue that still works when the
5113
+ page and the sheet are three percent apart. */
4586
5114
  .compose-dialog::backdrop {
4587
5115
  background-color: rgba(0, 0, 0, 0.3);
5116
+ backdrop-filter: blur(3px);
5117
+ -webkit-backdrop-filter: blur(3px);
4588
5118
  }
4589
5119
 
4590
5120
  .compose-dialog jant-compose-dialog {
@@ -4604,10 +5134,15 @@
4604
5134
  background: transparent;
4605
5135
  }
4606
5136
 
5137
+ /* The composer opens straight onto the first post's header row now that the
5138
+ title bar is gone, so the top gutter it used to provide lives here. It
5139
+ cannot go on the row itself: single-post mode's wrapper is
5140
+ `display: contents` and generates no box to pad. */
4607
5141
  .compose-dialog-inner {
4608
5142
  display: flex;
4609
5143
  flex-direction: column;
4610
5144
  max-height: calc(100dvh - var(--compose-margin-top) - 20px);
5145
+ padding-top: 10px;
4611
5146
  position: relative;
4612
5147
  background-color: var(--compose-paper-bg);
4613
5148
  border-radius: inherit;
@@ -4627,12 +5162,31 @@
4627
5162
  pointer-events: none;
4628
5163
  }
4629
5164
 
4630
- .compose-dialog-inner > jant-compose-editor {
5165
+ /* The composer's one scroll region. Every block inside it — editor body,
5166
+ attachments, per-post toolbar — flows at its natural height and scrolls
5167
+ together, so there is a single answer to "what scrolls?" in every mode.
5168
+ Only the action row below it stays pinned. Nesting a second scroller in
5169
+ here (a fixed-height body, a capped attachment dock) is what forced the
5170
+ old previews to be small; don't reintroduce one. */
5171
+ .compose-scroll {
4631
5172
  flex: 1;
4632
5173
  min-height: 0;
5174
+ overflow-y: auto;
5175
+ overflow-x: hidden;
5176
+ }
5177
+
5178
+ .compose-scroll::-webkit-scrollbar {
5179
+ width: 3px;
5180
+ }
5181
+
5182
+ .compose-scroll::-webkit-scrollbar-thumb {
5183
+ background-color: var(--site-divider);
5184
+ border-radius: 4px;
5185
+ }
5186
+
5187
+ .compose-scroll > jant-compose-editor {
4633
5188
  display: flex;
4634
5189
  flex-direction: column;
4635
- overflow: hidden;
4636
5190
  position: relative;
4637
5191
  }
4638
5192
 
@@ -4710,120 +5264,39 @@
4710
5264
 
4711
5265
  /* --- Header ------------------------------------------------------------- */
4712
5266
 
4713
- .compose-dialog-header {
4714
- display: flex;
4715
- align-items: center;
4716
- justify-content: space-between;
4717
- min-height: 3.6rem;
4718
- padding: 12px 14px;
4719
- background-color: var(--compose-paper-bg);
4720
- border-top-left-radius: inherit;
4721
- border-top-right-radius: inherit;
4722
- position: relative;
4723
- z-index: 4;
4724
- }
4725
-
4726
- @media (min-width: 700px) {
4727
- .compose-dialog-header {
4728
- padding: 14px 18px;
4729
- }
4730
- }
5267
+ /* Leaves the composer, so unlike the per-post × it never hides until hover:
5268
+ on a phone this and the options sheet are the only ways out. */
5269
+ .compose-close-btn {
5270
+ --compose-dialog-icon-paper-fill: var(--compose-paper-bg);
4731
5271
 
4732
- .compose-dialog-cancel {
4733
- min-height: 44px;
4734
- min-width: 72px;
4735
- padding: 10px 14px;
4736
- border: none;
4737
- background: none;
4738
- border-radius: 10px;
4739
- color: var(--site-text-secondary);
4740
- font-size: var(--type-ui-control);
4741
- font-weight: var(--fw-medium);
4742
- cursor: pointer;
4743
- transition: color 0.15s ease-out;
4744
- flex-shrink: 0;
4745
- text-align: left;
4746
- }
4747
-
4748
- .compose-dialog-cancel:hover {
4749
- color: var(--site-text-primary);
4750
- }
4751
-
4752
- .compose-dialog-header-center {
4753
- position: absolute;
4754
- left: 50%;
4755
- transform: translateX(-50%);
4756
- width: max-content;
4757
- max-width: calc(100% - 9.5rem);
4758
- }
4759
-
4760
- .compose-dialog-header-actions {
4761
- display: flex;
4762
- align-items: center;
4763
- gap: 0;
4764
- flex-shrink: 0;
4765
- min-width: 44px;
4766
- justify-content: flex-end;
4767
- }
4768
-
4769
- .compose-dialog-title {
4770
- font-weight: var(--fw-semibold);
4771
- font-size: var(--type-ui-control);
4772
- line-height: 1.2;
4773
- }
4774
-
4775
- .compose-dialog-header-btn {
4776
- --compose-dialog-icon-paper-fill: var(--compose-paper-bg);
4777
-
4778
- display: flex;
5272
+ display: inline-flex;
4779
5273
  align-items: center;
4780
5274
  justify-content: center;
4781
- width: 44px;
4782
- height: 44px;
4783
- border-radius: 12px;
5275
+ margin-left: auto;
5276
+ width: 28px;
5277
+ height: 28px;
5278
+ border-radius: 8px;
4784
5279
  border: none;
4785
- background: none;
5280
+ background: transparent;
4786
5281
  color: var(--site-text-secondary);
4787
5282
  cursor: pointer;
5283
+ flex-shrink: 0;
5284
+ opacity: 0.6;
4788
5285
  transition:
4789
- background-color 0.15s ease-out,
4790
- color 0.15s ease-out;
5286
+ opacity 0.15s ease,
5287
+ background-color 0.13s ease,
5288
+ color 0.13s ease;
4791
5289
  }
4792
5290
 
4793
- .compose-dialog-header-btn:hover {
4794
- --compose-dialog-icon-paper-fill: var(--compose-control-bg);
4795
- color: var(--site-text-primary);
5291
+ .compose-close-btn:hover {
5292
+ opacity: 1;
4796
5293
  background-color: var(--compose-control-bg);
4797
- }
4798
-
4799
- .compose-dialog-draft-btn {
4800
- color: color-mix(
4801
- in srgb,
4802
- var(--site-text-primary) 48%,
4803
- var(--site-text-secondary)
4804
- );
4805
- }
4806
-
4807
- .compose-dialog-header-icon {
4808
- display: block;
4809
- width: 26px;
4810
- height: 26px;
4811
- --icon-stroke: 1.32;
4812
- flex-shrink: 0;
4813
- overflow: visible;
4814
- }
4815
-
4816
- .compose-dialog-draft-btn .compose-dialog-header-icon {
4817
- --icon-stroke: 1.24;
4818
- }
4819
-
4820
- .compose-dialog-draft-btn:hover {
4821
5294
  color: var(--site-text-primary);
4822
5295
  }
4823
5296
 
4824
- .compose-dialog-header-btn:disabled {
4825
- opacity: 0.3;
4826
- cursor: not-allowed;
5297
+ .compose-edit-loading-close {
5298
+ display: flex;
5299
+ justify-content: flex-end;
4827
5300
  }
4828
5301
 
4829
5302
  /* Treat touch-first browsers as mobile here too. iOS Safari can expose a
@@ -4863,29 +5336,13 @@
4863
5336
  padding-left: calc(20px + env(safe-area-inset-left));
4864
5337
  }
4865
5338
 
4866
- .compose-dialog-inner > jant-compose-editor,
4867
- .compose-thread-layout,
4868
- .compose-editor-row,
4869
- .compose-editor-row > jant-compose-editor {
4870
- flex: 0 1 auto;
4871
- }
4872
-
4873
- .compose-dialog-inner > jant-compose-editor,
4874
- .compose-editor-row > jant-compose-editor {
4875
- max-height: calc(
4876
- 100dvh - 13rem - env(safe-area-inset-top) - env(safe-area-inset-bottom)
4877
- );
4878
- }
4879
-
4880
- .compose-dialog-header {
4881
- padding-top: calc(12px + env(safe-area-inset-top));
4882
- padding-right: calc(14px + env(safe-area-inset-right));
4883
- padding-left: calc(14px + env(safe-area-inset-left));
4884
- border-bottom: 1px solid var(--compose-control-border);
5339
+ /* Fullscreen on a phone, so the composer's own first row sits under the
5340
+ notch — it takes the inset the title bar used to. */
5341
+ .compose-dialog-inner {
5342
+ padding-top: calc(10px + env(safe-area-inset-top));
4885
5343
  }
4886
5344
 
4887
5345
  .compose-body {
4888
- flex: 0 1 auto;
4889
5346
  min-height: clamp(8rem, 22vh, 12rem);
4890
5347
  padding-right: calc(20px + env(safe-area-inset-right));
4891
5348
  padding-left: calc(20px + env(safe-area-inset-left));
@@ -4912,92 +5369,100 @@
4912
5369
  }
4913
5370
  }
4914
5371
 
4915
- /* --- Format switcher with animated pill --------------------------------- */
5372
+ /* --- Format switcher ----------------------------------------------------- */
4916
5373
 
5374
+ /* One grouped control — a tinted track with a pill on the active format.
5375
+ The strip's own inset (track padding + item padding) is 16px, so its box
5376
+ and its first label can never both sit on the text column: whatever the
5377
+ strip is pulled left by, the other edge is 16px away from the text. There
5378
+ is no value that aligns both, only a choice of which one to favour, so the
5379
+ amount is a knob rather than a derived number:
5380
+
5381
+ 0px strip's tinted box on the text edge, "Note" 16px inside it
5382
+ 8px neither edge exact; the tint is light enough that the perceived
5383
+ edge sits between them, which is why this is the default
5384
+ 16px "Note" on the text edge, the tint hanging a full inset out
5385
+
5386
+ 12px is the hard ceiling: in a thread row the rail dot ends 12px short of
5387
+ the content edge, and past that the tint runs over the dot. */
4917
5388
  .compose-segmented {
5389
+ --compose-switcher-track-pad: 3px;
5390
+ --compose-switcher-item-pad: 13px;
5391
+
4918
5392
  display: inline-flex;
4919
- background-color: var(--compose-control-bg);
4920
- border-radius: 10px;
4921
- padding: 3px;
5393
+ flex: none;
5394
+ gap: 1px;
5395
+ padding: var(--compose-switcher-track-pad);
5396
+ margin-inline-start: calc(-1 * var(--compose-switcher-hang));
5397
+ /* Paints over the thread rail dot, not under it. The dot carries a
5398
+ surface-coloured ring (`--site-thread-marker-gap`) to break the rail line
5399
+ around itself, and that ring reaches further right than the dot does —
5400
+ far enough to clip the strip's left edge once it hangs. Both sit at
5401
+ z-index 1, so the strip wins on document order. Covering ring pixels is
5402
+ harmless here: the line it protects runs down the dot's centre, well left
5403
+ of anywhere the strip reaches. */
4922
5404
  position: relative;
4923
- box-shadow: inset 0 0 0 1px var(--compose-control-border);
5405
+ z-index: 1;
5406
+ border-radius: calc(var(--compose-switcher-track-pad) + 8px);
5407
+ background-color: var(--compose-control-bg);
4924
5408
  font-family: var(--font-ui);
4925
5409
  }
4926
5410
 
4927
- .compose-format-pill {
4928
- position: absolute;
4929
- top: 3px;
4930
- left: 3px;
4931
- width: calc((100% - 6px) / 3);
4932
- height: calc(100% - 6px);
4933
- background-color: var(--compose-paper-bg);
4934
- border-radius: 7px;
4935
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
4936
- transition: left 0.28s cubic-bezier(0.22, 1, 0.36, 1);
4937
- z-index: 0;
4938
- }
4939
-
4940
- .compose-format-pill-link {
4941
- left: calc(3px + (100% - 6px) / 3);
4942
- }
4943
-
4944
- .compose-format-pill-quote {
4945
- left: calc(3px + (100% - 6px) * 2 / 3);
4946
- }
4947
-
4948
5411
  .compose-segmented-item {
4949
- position: relative;
4950
- z-index: 1;
4951
- flex: 1;
4952
- padding: 5px 0.95rem;
4953
- border-radius: 8px;
5412
+ flex: none;
5413
+ padding: 5px var(--compose-switcher-item-pad);
4954
5414
  border: none;
5415
+ border-radius: 8px;
4955
5416
  background: transparent;
4956
5417
  color: var(--site-text-secondary);
4957
5418
  font-size: var(--type-ui-hint);
4958
5419
  font-weight: var(--fw-medium);
4959
5420
  line-height: 1.2;
4960
- cursor: pointer;
4961
- transition: color 0.2s ease;
4962
5421
  letter-spacing: 0.01em;
5422
+ cursor: pointer;
5423
+ transition:
5424
+ color 0.15s ease,
5425
+ background-color 0.15s ease,
5426
+ box-shadow 0.15s ease;
4963
5427
  }
4964
5428
 
4965
- @media (min-width: 700px) {
4966
- .compose-segmented-item {
4967
- padding: 0.4rem 1.15rem;
4968
- font-size: var(--type-ui-control);
4969
- }
5429
+ .compose-segmented-item:hover:not(.compose-segmented-item-active) {
5430
+ color: var(--site-text-primary);
4970
5431
  }
4971
5432
 
4972
5433
  .compose-segmented-item-active {
4973
5434
  color: var(--site-text-primary);
4974
- font-weight: var(--fw-medium);
5435
+ background-color: var(--compose-paper-bg);
5436
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
4975
5437
  }
4976
5438
 
4977
5439
  /* --- Body --------------------------------------------------------------- */
4978
5440
 
5441
+ /* Grows with its content — `.compose-scroll` does the scrolling. The floor is
5442
+ a target to click into on an empty post, so it yields once the post has an
5443
+ attachment: below a full-size preview the same space reads as a gap between
5444
+ the text and the picture. */
4979
5445
  .compose-body {
4980
- flex: 1;
4981
- min-height: 0;
4982
- overflow-y: auto;
4983
- padding: 16px 20px 12px;
5446
+ min-height: 72px;
5447
+ padding: 16px 20px 12px var(--compose-gutter);
4984
5448
  display: flex;
4985
5449
  flex-direction: column;
4986
5450
  }
4987
5451
 
4988
- @media (min-width: 700px) {
4989
- .compose-body {
4990
- padding: 18px 24px 12px;
4991
- }
5452
+ jant-compose-editor:has(.compose-attachments-dock) .compose-body {
5453
+ min-height: 0;
4992
5454
  }
4993
5455
 
4994
- .compose-body::-webkit-scrollbar {
4995
- width: 3px;
5456
+ jant-compose-editor:has(.compose-attachments-dock)
5457
+ .compose-tiptap-body
5458
+ .tiptap {
5459
+ min-height: 0;
4996
5460
  }
4997
5461
 
4998
- .compose-body::-webkit-scrollbar-thumb {
4999
- background-color: var(--site-divider);
5000
- border-radius: 4px;
5462
+ @media (min-width: 700px) {
5463
+ .compose-body {
5464
+ padding: 18px 24px 12px var(--compose-gutter);
5465
+ }
5001
5466
  }
5002
5467
 
5003
5468
  .compose-field-enter {
@@ -5023,22 +5488,25 @@
5023
5488
  color: var(--site-text-secondary);
5024
5489
  }
5025
5490
 
5491
+ /* An optional field sitting directly above the one people came to type in:
5492
+ the empty state should recede, not announce itself. */
5493
+ .compose-note-title::placeholder,
5494
+ .compose-link-title::placeholder {
5495
+ color: color-mix(in srgb, var(--site-text-secondary) 55%, transparent);
5496
+ font-weight: 400;
5497
+ }
5498
+
5026
5499
  .compose-input:focus::placeholder {
5027
5500
  opacity: 0.6;
5028
5501
  }
5029
5502
 
5030
5503
  /* --- Note mode ---------------------------------------------------------- */
5031
5504
 
5032
- .compose-note-title-row {
5033
- display: flex;
5034
- align-items: center;
5035
- gap: 10px;
5036
- margin-bottom: 14px;
5037
- animation: compose-fade-up 0.28s cubic-bezier(0.22, 1, 0.36, 1) both;
5038
- }
5039
-
5040
5505
  .compose-note-title {
5041
- flex: 1;
5506
+ width: 100%;
5507
+ /* Sole owner of the title-to-body gap now that the first block's lead-in
5508
+ margin is gone — one place to tune instead of two that stack. */
5509
+ margin-bottom: 12px;
5042
5510
  font-family: var(--font-heading);
5043
5511
  font-size: var(--compose-title-input-size);
5044
5512
  font-weight: var(--type-heading-weight);
@@ -5047,30 +5515,6 @@
5047
5515
  text-wrap: pretty;
5048
5516
  }
5049
5517
 
5050
- @media (min-width: 700px) {
5051
- .compose-note-title {
5052
- font-size: var(--compose-title-input-size);
5053
- }
5054
- }
5055
-
5056
- .compose-note-title-dismiss {
5057
- border: none;
5058
- background: transparent;
5059
- color: var(--site-text-secondary);
5060
- cursor: pointer;
5061
- font-size: var(--type-ui-hint);
5062
- padding: 6px 8px;
5063
- border-radius: 8px;
5064
- flex-shrink: 0;
5065
- line-height: 1;
5066
- opacity: 0.6;
5067
- transition: opacity 0.15s ease;
5068
- }
5069
-
5070
- .compose-note-title-dismiss:hover {
5071
- opacity: 1;
5072
- }
5073
-
5074
5518
  .compose-body-input {
5075
5519
  width: 100%;
5076
5520
  padding: 0;
@@ -5131,17 +5575,11 @@
5131
5575
  font-weight: var(--type-heading-weight);
5132
5576
  letter-spacing: var(--type-heading-tracking);
5133
5577
  line-height: var(--compose-title-input-leading);
5134
- margin-bottom: 16px;
5578
+ margin-bottom: 10px;
5135
5579
  padding-left: 2px;
5136
5580
  text-wrap: pretty;
5137
5581
  }
5138
5582
 
5139
- @media (min-width: 700px) {
5140
- .compose-link-title {
5141
- font-size: var(--compose-title-input-size);
5142
- }
5143
- }
5144
-
5145
5583
  /* --- Quote mode --------------------------------------------------------- */
5146
5584
 
5147
5585
  .compose-quote-wrap {
@@ -5212,15 +5650,17 @@
5212
5650
  font-weight: var(--fw-regular);
5213
5651
  line-height: var(--compose-quote-input-leading);
5214
5652
  letter-spacing: -0.02em;
5215
- min-height: 9rem;
5653
+ /* Four lines of the quote's own type — enough to hold most pulled passages
5654
+ without opening a well. Stated in lines so it tracks the two leadings
5655
+ this input takes (1.32 in compose, 1.42 in a reply) instead of needing a
5656
+ pixel value per breakpoint. The `rows` attribute is set to match. */
5657
+ min-height: calc(var(--compose-quote-input-leading) * 4em);
5216
5658
  padding-top: 0.5rem;
5217
5659
  text-wrap: pretty;
5218
5660
  }
5219
5661
 
5220
5662
  @media (min-width: 700px) {
5221
5663
  .compose-quote-text {
5222
- font-size: var(--compose-quote-input-size);
5223
- min-height: 9.5rem;
5224
5664
  padding-top: 0.55rem;
5225
5665
  }
5226
5666
  }
@@ -5289,7 +5729,9 @@
5289
5729
 
5290
5730
  .compose-url-input {
5291
5731
  border-bottom: 1px solid transparent;
5732
+ font-family: var(--font-mono);
5292
5733
  font-size: var(--compose-inline-input-size);
5734
+ color: var(--site-accent);
5293
5735
  line-height: 1.4;
5294
5736
  padding-block: 0.1rem;
5295
5737
  transition:
@@ -5386,16 +5828,11 @@
5386
5828
  margin-left: 4px;
5387
5829
  }
5388
5830
 
5831
+ /* Plain background, no top fade: nothing scrolls underneath it any more —
5832
+ the dock travels with its post inside `.compose-scroll`. */
5389
5833
  .compose-attachments-dock {
5390
- flex-shrink: 0;
5391
- padding: 8px 20px 4px;
5392
- background:
5393
- linear-gradient(
5394
- 180deg,
5395
- color-mix(in srgb, var(--compose-paper-bg) 0%, transparent) 0%,
5396
- var(--compose-paper-bg) 18px
5397
- ),
5398
- var(--compose-paper-bg);
5834
+ padding: 8px 20px 4px var(--compose-gutter);
5835
+ background-color: var(--compose-paper-bg);
5399
5836
  }
5400
5837
 
5401
5838
  @media (min-width: 700px) {
@@ -5483,26 +5920,31 @@
5483
5920
 
5484
5921
  /* --- Tools row ---------------------------------------------------------- */
5485
5922
 
5923
+ /* Belongs to its post, not to the dialog: it sits under that post's content
5924
+ and scrolls away with it, so a thread shows one toolbar per block. */
5486
5925
  .compose-tools-row {
5487
- padding: 6px 10px;
5926
+ /* Half the difference between the 44px button and the 20px glyph inside
5927
+ it. The row starts on the text column and its first control hangs back
5928
+ out by this much, so the icon lands on the column while the button keeps
5929
+ its full touch target. 12px is also the ceiling for anything hanging out
5930
+ of a thread row — see `.compose-segmented`, whose knob has the same job
5931
+ for a control with a different inset. */
5932
+ --compose-tool-hang: 12px;
5933
+
5934
+ padding: 6px 10px 6px var(--compose-gutter);
5488
5935
  display: flex;
5489
5936
  align-items: center;
5490
- flex-shrink: 0;
5491
5937
  gap: 4px;
5492
5938
  background-color: var(--compose-paper-bg);
5493
5939
  }
5494
5940
 
5495
- .compose-thread-layout .compose-tools-row {
5496
- padding-inline-start: 0;
5497
- }
5498
-
5499
- .compose-thread-layout .compose-tools-row .compose-tool-btn:first-child {
5500
- margin-inline-start: -10px;
5941
+ .compose-tools-row > :first-child {
5942
+ margin-inline-start: calc(-1 * var(--compose-tool-hang));
5501
5943
  }
5502
5944
 
5503
5945
  @media (min-width: 700px) {
5504
5946
  .compose-tools-row {
5505
- padding: 6px 16px;
5947
+ padding: 6px 16px 6px var(--compose-gutter);
5506
5948
  gap: 4px;
5507
5949
  }
5508
5950
  }
@@ -5514,37 +5956,64 @@
5514
5956
  justify-content: center;
5515
5957
  width: 44px;
5516
5958
  height: 44px;
5959
+ /* The button keeps its 44px touch target; only the paint is inset, so the
5960
+ chip a hovered or switched-on tool wears is 36px and sits closer to the
5961
+ glyph than to its neighbours. `background-clip` shrinks the corner radii
5962
+ with the box, which is why 12px here reads as 8px on the chip. */
5963
+ padding: 4px;
5964
+ background-clip: content-box;
5517
5965
  border-radius: 12px;
5518
5966
  border: none;
5519
- background: transparent;
5967
+ background-color: transparent;
5520
5968
  cursor: pointer;
5521
5969
  transition: all 0.18s ease;
5522
5970
  flex-shrink: 0;
5523
- color: var(--site-text-secondary);
5971
+ color: var(--compose-tool-ink);
5524
5972
  }
5525
5973
 
5974
+ /* One weight for the whole row, stated here rather than per icon, so the
5975
+ stroked glyphs and the star's outline read as one set. */
5526
5976
  .compose-tool-icon {
5527
- --icon-stroke: 1.46;
5528
5977
  display: block;
5529
5978
  width: 20px;
5530
5979
  height: 20px;
5531
5980
  flex-shrink: 0;
5532
5981
  overflow: visible;
5982
+ stroke-width: 1.42;
5533
5983
  }
5534
5984
 
5535
5985
  .compose-tool-btn:hover {
5536
5986
  background-color: var(--compose-control-bg);
5537
5987
  }
5538
5988
 
5989
+ /* On is a real chip — the shape people already read as "this control is
5990
+ switched on" — just quieter than the one that was tried first. That one
5991
+ paired the chip with full-strength ink, so the whole button lit up at
5992
+ once; here the glyph is left in the row's faded ink and the chip does the
5993
+ talking on its own, a little under the row's hover tint. It is the only
5994
+ edge in the row, so it needs no help to be found. */
5539
5995
  .compose-tool-btn-active {
5540
- color: var(--site-text-primary);
5996
+ background-color: color-mix(
5997
+ in srgb,
5998
+ var(--compose-control-bg) 85%,
5999
+ var(--compose-paper-bg)
6000
+ );
6001
+ }
6002
+
6003
+ /* Hover has to stay a step above a button that already carries a chip. */
6004
+ .compose-tool-btn-active:hover {
5541
6005
  background-color: var(--compose-control-bg-strong);
5542
6006
  }
5543
6007
 
6008
+ /* A tool that has grown a label is reading matter now, so it carries text's
6009
+ weight — the row's faded ink would put "Add more" under the 4.5:1 floor. */
5544
6010
  .compose-tool-btn-add {
5545
6011
  width: auto;
5546
6012
  gap: 4px;
5547
- padding: 0 10px 0 8px;
6013
+ /* Same 4px of inset paint as the icon-only buttons; only the sides open up
6014
+ to make room for the label. */
6015
+ padding: 4px 10px 4px 8px;
6016
+ color: var(--site-text-secondary);
5548
6017
  }
5549
6018
 
5550
6019
  .compose-tool-label {
@@ -5553,12 +6022,11 @@
5553
6022
  white-space: nowrap;
5554
6023
  }
5555
6024
 
5556
- .compose-tool-sep {
5557
- width: 1px;
5558
- height: 16px;
5559
- background-color: var(--site-divider);
5560
- margin: 0 2px;
5561
- border-radius: 1px;
6025
+ /* Was a hairline. The two groups (insert something / mark something up) are
6026
+ still worth telling apart, but air does it without drawing another edge
6027
+ into the paper. */
6028
+ .compose-tool-gap {
6029
+ width: 8px;
5562
6030
  flex-shrink: 0;
5563
6031
  }
5564
6032
 
@@ -5572,7 +6040,6 @@
5572
6040
  width: 44px;
5573
6041
  height: 44px;
5574
6042
  border-radius: 12px;
5575
- color: var(--site-text-secondary);
5576
6043
  }
5577
6044
 
5578
6045
  .compose-tool-btn-view:hover,
@@ -5601,7 +6068,7 @@
5601
6068
  /* --- Action row --------------------------------------------------------- */
5602
6069
 
5603
6070
  .compose-action-row {
5604
- padding: 10px 12px 14px;
6071
+ padding: 10px 12px 14px var(--compose-gutter);
5605
6072
  display: flex;
5606
6073
  align-items: center;
5607
6074
  gap: 10px;
@@ -5612,7 +6079,10 @@
5612
6079
  border-bottom-right-radius: inherit;
5613
6080
  }
5614
6081
 
6082
+ /* The quick toggles take over the action row's bottom edge when present, so
6083
+ the two read as one block rather than a strip stuck underneath. */
5615
6084
  .compose-action-row:has(+ .compose-quick-actions-row) {
6085
+ padding-bottom: 4px;
5616
6086
  border-bottom-left-radius: 0;
5617
6087
  border-bottom-right-radius: 0;
5618
6088
  }
@@ -5621,17 +6091,54 @@
5621
6091
  z-index: 6;
5622
6092
  }
5623
6093
 
6094
+ .compose-action-row-without-collection {
6095
+ justify-content: flex-end;
6096
+ }
6097
+
6098
+ .compose-quick-actions-row {
6099
+ display: flex;
6100
+ justify-content: flex-end;
6101
+ gap: 0.5rem;
6102
+ padding: 2px 12px 12px;
6103
+ background-color: var(--compose-paper-bg);
6104
+ border-bottom-left-radius: inherit;
6105
+ border-bottom-right-radius: inherit;
6106
+ }
6107
+
5624
6108
  @media (min-width: 700px) {
5625
6109
  .compose-action-row {
5626
- padding: 12px 18px 16px;
6110
+ padding: 12px 18px 16px var(--compose-gutter);
6111
+ }
6112
+
6113
+ .compose-action-row:has(+ .compose-quick-actions-row) {
6114
+ padding-bottom: 4px;
6115
+ }
6116
+
6117
+ .compose-quick-actions-row {
6118
+ padding: 2px 18px 14px;
5627
6119
  }
5628
6120
  }
5629
6121
 
5630
6122
  .compose-collection-select {
6123
+ /* Optically, not geometrically, centered: the glyph meets the left padding
6124
+ at a vertex and sits inset in its own box, so equal padding leaves the
6125
+ icon side looking open and the label side pinched against the edge.
6126
+ Tune the pill's side padding here and in the touch block below — both
6127
+ set these two values rather than re-declaring `padding`, so neither
6128
+ silently shadows the other. They live on the wrapper because the hang
6129
+ below is measured from them and has to apply to the flex item the action
6130
+ row lays out, not to the trigger inside it. */
6131
+ --compose-collection-pad-icon: 0.56rem;
6132
+ --compose-collection-pad-label: 0.85rem;
6133
+
5631
6134
  display: inline-flex;
5632
6135
  position: relative;
5633
6136
  min-width: 0;
5634
6137
  max-width: 100%;
6138
+ /* Hangs back by the trigger's icon-side inset plus its 1px transparent
6139
+ border, so it is the layers glyph that lands on the text column rather
6140
+ than the pill's edge. */
6141
+ margin-inline-start: calc(-1 * (var(--compose-collection-pad-icon) + 1px));
5635
6142
  }
5636
6143
 
5637
6144
  .compose-collection-select[data-open] {
@@ -5641,14 +6148,18 @@
5641
6148
  .compose-collection-trigger {
5642
6149
  display: flex;
5643
6150
  align-items: center;
5644
- gap: 0.62rem;
6151
+ gap: 0.38rem;
5645
6152
  min-width: 0;
5646
6153
  max-width: 100%;
5647
- min-height: 2.45rem;
5648
- border: 1px solid var(--compose-control-border);
5649
- background: var(--compose-control-bg);
6154
+ min-height: 1.95rem;
6155
+ /* No fill and no edge at rest — it was the one bordered pill left in the
6156
+ composer, which made picking a collection look like the thing to do. It
6157
+ keeps a transparent border so the box does not resize on hover. */
6158
+ border: 1px solid transparent;
6159
+ background: transparent;
5650
6160
  border-radius: 999px;
5651
- padding: 0.24rem 0.72rem 0.24rem 0.28rem;
6161
+ padding: 0.2rem var(--compose-collection-pad-label) 0.2rem
6162
+ var(--compose-collection-pad-icon);
5652
6163
  cursor: pointer;
5653
6164
  transition:
5654
6165
  background-color 0.18s ease,
@@ -5659,12 +6170,12 @@
5659
6170
  font-size: var(--type-ui-control);
5660
6171
  }
5661
6172
 
6173
+ /* The same flat wash the tools row uses on hover — no lift, no edge. A chip
6174
+ that rises off the paper is what made this read as the loud control. */
5662
6175
  .compose-collection-trigger:hover,
5663
6176
  .compose-collection-trigger[data-open="true"] {
5664
6177
  color: var(--site-text-primary);
5665
- background: var(--compose-control-bg-strong);
5666
- border-color: color-mix(in srgb, var(--site-text-primary) 10%, transparent);
5667
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
6178
+ background: var(--compose-control-bg);
5668
6179
  }
5669
6180
 
5670
6181
  .compose-collection-trigger[data-selected="true"] {
@@ -5675,39 +6186,16 @@
5675
6186
  );
5676
6187
  }
5677
6188
 
5678
- .compose-collection-trigger-icon {
5679
- display: inline-flex;
5680
- align-items: center;
5681
- justify-content: center;
5682
- width: 1.65rem;
5683
- height: 1.65rem;
5684
- border-radius: 999px;
5685
- background: color-mix(
5686
- in srgb,
5687
- var(--compose-paper-bg) 72%,
5688
- var(--compose-control-bg)
5689
- );
5690
- color: color-mix(
5691
- in srgb,
5692
- var(--site-text-primary) 58%,
5693
- var(--site-text-secondary)
5694
- );
5695
- flex-shrink: 0;
5696
- }
5697
-
5698
- .compose-collection-trigger-svg,
5699
6189
  .compose-collection-search-icon,
5700
6190
  .compose-collection-add-svg,
5701
6191
  .compose-collection-option-check-circle,
5702
6192
  .compose-collection-option-plus-circle,
5703
6193
  .compose-collection-option-svg,
5704
- .compose-collection-chevron,
5705
6194
  .compose-collection-option-check {
5706
6195
  display: block;
5707
6196
  overflow: visible;
5708
6197
  }
5709
6198
 
5710
- .compose-collection-trigger-svg,
5711
6199
  .compose-collection-option-svg {
5712
6200
  width: 0.92rem;
5713
6201
  height: 0.92rem;
@@ -5715,6 +6203,26 @@
5715
6203
  stroke-width: var(--icon-stroke);
5716
6204
  }
5717
6205
 
6206
+ /* Bare on the pill rather than in its own tinted circle: the pill is already
6207
+ a container, and a chip inside it read as a button inside a button. */
6208
+ .compose-collection-trigger-svg {
6209
+ display: block;
6210
+ overflow: visible;
6211
+ width: 0.86rem;
6212
+ height: 0.86rem;
6213
+ flex-shrink: 0;
6214
+ opacity: 0.75;
6215
+ --icon-stroke: 1.4;
6216
+ stroke-width: var(--icon-stroke);
6217
+ }
6218
+
6219
+ .compose-collection-trigger:hover .compose-collection-trigger-svg,
6220
+ .compose-collection-trigger[data-open="true"] .compose-collection-trigger-svg,
6221
+ .compose-collection-trigger[data-selected="true"]
6222
+ .compose-collection-trigger-svg {
6223
+ opacity: 1;
6224
+ }
6225
+
5718
6226
  .compose-collection-label {
5719
6227
  display: block;
5720
6228
  flex: 1 1 auto;
@@ -5726,32 +6234,18 @@
5726
6234
  font-weight: var(--fw-medium);
5727
6235
  }
5728
6236
 
5729
- .compose-collection-chevron {
5730
- width: 0.78rem;
5731
- height: 0.78rem;
5732
- flex-shrink: 0;
5733
- opacity: 0.46;
5734
- --icon-stroke: 1.38;
5735
- stroke-width: var(--icon-stroke);
5736
- transition:
5737
- transform 0.18s ease,
5738
- opacity 0.18s ease;
5739
- }
5740
-
5741
- .compose-collection-trigger[data-open="true"] .compose-collection-chevron {
5742
- transform: rotate(180deg);
5743
- opacity: 0.74;
5744
- }
5745
-
6237
+ /* Desktop sizing. Every dimension here has a roomier counterpart in the
6238
+ touch block near the end of the file — a picker aimed with a cursor can be
6239
+ tighter than one aimed with a thumb. */
5746
6240
  .compose-collection-select [data-popover] {
5747
6241
  z-index: 100;
5748
- min-width: 16rem;
5749
- max-width: 20rem;
6242
+ min-width: 13.5rem;
6243
+ max-width: 17rem;
5750
6244
  }
5751
6245
 
5752
6246
  .compose-collection-popover {
5753
- padding: 0.56rem 0.62rem 0.5rem;
5754
- border-radius: 1.08rem;
6247
+ padding: 0.44rem 0.48rem 0.4rem;
6248
+ border-radius: 0.95rem;
5755
6249
  border: 1px solid var(--compose-control-border);
5756
6250
  background: var(--compose-floating-bg);
5757
6251
  box-shadow:
@@ -5761,16 +6255,16 @@
5761
6255
  }
5762
6256
 
5763
6257
  .compose-collection-popover-header {
5764
- padding: 0.24rem 0 0.6rem;
6258
+ padding: 0.18rem 0 0.44rem;
5765
6259
  }
5766
6260
 
5767
6261
  .compose-collection-search-shell {
5768
6262
  display: flex;
5769
6263
  align-items: center;
5770
- gap: 0.52rem;
5771
- min-height: 2.8rem;
5772
- padding: 0 0.95rem;
5773
- border-radius: 0.92rem;
6264
+ gap: 0.45rem;
6265
+ min-height: 2.25rem;
6266
+ padding: 0 0.75rem;
6267
+ border-radius: 0.75rem;
5774
6268
  border: 1px solid var(--compose-control-border);
5775
6269
  background: var(--compose-control-bg);
5776
6270
  color: var(--site-text-secondary);
@@ -5793,8 +6287,8 @@
5793
6287
  }
5794
6288
 
5795
6289
  .compose-collection-search-icon {
5796
- width: 1.15rem;
5797
- height: 1.15rem;
6290
+ width: 0.98rem;
6291
+ height: 0.98rem;
5798
6292
  flex-shrink: 0;
5799
6293
  --icon-stroke: 1.3;
5800
6294
  stroke-width: var(--icon-stroke);
@@ -5821,7 +6315,7 @@
5821
6315
  display: flex;
5822
6316
  flex-direction: column;
5823
6317
  gap: 0.06rem;
5824
- max-height: 15rem;
6318
+ max-height: 12.5rem;
5825
6319
  overflow-y: auto;
5826
6320
  padding: 0.08rem;
5827
6321
  }
@@ -5830,15 +6324,15 @@
5830
6324
  width: 100%;
5831
6325
  display: flex;
5832
6326
  align-items: center;
5833
- gap: 0.75rem;
6327
+ gap: 0.6rem;
5834
6328
  min-width: 0;
5835
- min-height: 3rem;
6329
+ min-height: 2.35rem;
5836
6330
  padding-top: 0;
5837
6331
  padding-bottom: 0;
5838
- padding-left: 0.8rem !important;
5839
- padding-right: 0.46rem !important;
6332
+ padding-left: 0.62rem !important;
6333
+ padding-right: 0.4rem !important;
5840
6334
  border: none;
5841
- border-radius: 0.88rem;
6335
+ border-radius: 0.7rem;
5842
6336
  background: transparent;
5843
6337
  background-image: none !important;
5844
6338
  cursor: pointer !important;
@@ -5878,8 +6372,8 @@
5878
6372
  display: inline-flex;
5879
6373
  align-items: center;
5880
6374
  justify-content: center;
5881
- width: 1.32rem;
5882
- height: 1.32rem;
6375
+ width: 1.12rem;
6376
+ height: 1.12rem;
5883
6377
  flex-shrink: 0;
5884
6378
  }
5885
6379
 
@@ -5930,7 +6424,7 @@
5930
6424
  }
5931
6425
 
5932
6426
  .compose-collection-empty {
5933
- padding: 0.95rem 0.7rem 1.05rem;
6427
+ padding: 0.75rem 0.6rem 0.85rem;
5934
6428
  color: var(--site-text-secondary);
5935
6429
  font-size: var(--type-ui-caption);
5936
6430
  line-height: 1.5;
@@ -5938,8 +6432,8 @@
5938
6432
  }
5939
6433
 
5940
6434
  .compose-collection-footer {
5941
- margin-top: 0.35rem;
5942
- padding: 0.35rem 0.22rem 0.12rem;
6435
+ margin-top: 0.28rem;
6436
+ padding: 0.28rem 0.18rem 0.1rem;
5943
6437
  border-top: 1px solid
5944
6438
  color-mix(in srgb, var(--site-divider) 50%, transparent);
5945
6439
  }
@@ -5947,12 +6441,12 @@
5947
6441
  .compose-collection-add-action {
5948
6442
  display: flex;
5949
6443
  align-items: center;
5950
- gap: 0.75rem;
6444
+ gap: 0.6rem;
5951
6445
  width: 100%;
5952
- min-height: 3rem;
5953
- padding: 0 0.8rem;
6446
+ min-height: 2.35rem;
6447
+ padding: 0 0.62rem;
5954
6448
  border: none;
5955
- border-radius: 1rem;
6449
+ border-radius: 0.7rem;
5956
6450
  background: transparent;
5957
6451
  cursor: pointer;
5958
6452
  text-align: left;
@@ -6033,28 +6527,7 @@
6033
6527
  min-width: 0;
6034
6528
  }
6035
6529
 
6036
- .compose-quick-actions-row {
6037
- display: flex;
6038
- justify-content: flex-end;
6039
- gap: 0.5rem;
6040
- padding: 2px 12px 12px;
6041
- background-color: var(--compose-paper-bg);
6042
- border-bottom-left-radius: inherit;
6043
- border-bottom-right-radius: inherit;
6044
- }
6045
-
6046
- .compose-action-row:has(+ .compose-quick-actions-row) {
6047
- padding-bottom: 4px;
6048
- }
6049
-
6050
6530
  @media (min-width: 700px) {
6051
- .compose-quick-actions-row {
6052
- padding: 2px 18px 14px;
6053
- }
6054
-
6055
- .compose-action-row:has(+ .compose-quick-actions-row) {
6056
- padding-bottom: 4px;
6057
- }
6058
6531
  }
6059
6532
 
6060
6533
  .compose-publish-quick-toggle {
@@ -6117,83 +6590,6 @@
6117
6590
  cursor: not-allowed;
6118
6591
  }
6119
6592
 
6120
- .compose-publish-summaries {
6121
- display: flex;
6122
- flex-wrap: wrap;
6123
- justify-content: flex-end;
6124
- gap: 0.5rem;
6125
- width: 100%;
6126
- }
6127
-
6128
- .compose-publish-summary {
6129
- display: inline-flex;
6130
- align-items: center;
6131
- gap: 0.48rem;
6132
- min-height: 2.15rem;
6133
- max-width: min(100%, 17rem);
6134
- padding: 0.38rem 0.74rem;
6135
- border: 1px solid var(--compose-control-border);
6136
- border-radius: 999px;
6137
- background-color: color-mix(
6138
- in srgb,
6139
- var(--compose-control-bg) 82%,
6140
- var(--compose-paper-bg)
6141
- );
6142
- color: color-mix(
6143
- in srgb,
6144
- var(--site-text-primary) 70%,
6145
- var(--site-text-secondary)
6146
- );
6147
- font-size: var(--compose-summary-size);
6148
- font-weight: var(--fw-medium);
6149
- line-height: 1.1;
6150
- cursor: pointer;
6151
- white-space: nowrap;
6152
- transition:
6153
- background-color 0.18s ease,
6154
- border-color 0.18s ease,
6155
- color 0.18s ease,
6156
- transform 0.18s ease;
6157
- }
6158
-
6159
- .compose-publish-summary:hover:not(:disabled) {
6160
- border-color: color-mix(in srgb, var(--site-text-primary) 12%, transparent);
6161
- background-color: color-mix(
6162
- in srgb,
6163
- var(--compose-control-bg-strong) 84%,
6164
- var(--compose-paper-bg)
6165
- );
6166
- color: var(--site-text-primary);
6167
- transform: translateY(-1px);
6168
- }
6169
-
6170
- .compose-publish-summary:disabled {
6171
- opacity: 0.6;
6172
- cursor: progress;
6173
- }
6174
-
6175
- .compose-publish-summary-icon {
6176
- display: inline-flex;
6177
- align-items: center;
6178
- justify-content: center;
6179
- width: 1rem;
6180
- height: 1rem;
6181
- flex-shrink: 0;
6182
- }
6183
-
6184
- .compose-publish-summary-icon svg {
6185
- display: block;
6186
- width: 100%;
6187
- height: 100%;
6188
- overflow: visible;
6189
- }
6190
-
6191
- .compose-publish-summary-text {
6192
- min-width: 0;
6193
- overflow: hidden;
6194
- text-overflow: ellipsis;
6195
- }
6196
-
6197
6593
  .compose-publish-group {
6198
6594
  position: relative;
6199
6595
  display: inline-flex;
@@ -6206,10 +6602,17 @@
6206
6602
  z-index: 100;
6207
6603
  }
6208
6604
 
6209
- .compose-publish-single,
6210
- .compose-publish-main,
6211
- .compose-publish-toggle {
6212
- min-height: 2.8rem;
6605
+ /* Sized against the row it leads, not in isolation. Measured: Publish 33px,
6606
+ the options trigger 33px, the collection pill 30.5px — Publish and the
6607
+ options circle share a height so they read as one pair at the end of the
6608
+ row, and the collection pill sits just under them. Touch gets the roomier
6609
+ set back further down: 33px is a fine mouse target and a poor thumb one. */
6610
+ .compose-publish-main {
6611
+ /* `em`, not `rem`: the box is a multiple of its own label, so the ratio
6612
+ holds if the button's type ever scales. The reference is 2.4em (36px
6613
+ around a 15px label); ours sits a step under, which is the only place we
6614
+ leave it — every other number in this button is traced. */
6615
+ min-height: 2.2em;
6213
6616
  border: none;
6214
6617
  background: transparent;
6215
6618
  cursor: pointer;
@@ -6219,28 +6622,36 @@
6219
6622
  box-shadow 0.18s ease;
6220
6623
  }
6221
6624
 
6222
- .compose-publish-single,
6625
+ /* Shape traced from threads.com's Post button, measured off the reference at
6626
+ 2x: 1px border, 8px radius, 18px side padding, ~15px bold label, content
6627
+ width with no `min-width` — the reference sizes to its word. Height is the
6628
+ one number we take a step under; see above.
6629
+
6630
+ Where we part from it: threads outlines Post in *both* states, and we only
6631
+ borrow that for disabled (below). Once there is something to post the
6632
+ button fills with ink rather than firming up an outline — a composer this
6633
+ flat can carry one solid object, and it should be the send. Weight and
6634
+ tracking hold across both states so the box does not resize the moment the
6635
+ post becomes postable. */
6223
6636
  .compose-publish-main {
6224
6637
  display: inline-flex;
6225
6638
  align-items: center;
6226
6639
  justify-content: center;
6227
6640
  gap: 0.3rem;
6228
6641
  white-space: nowrap;
6229
- min-width: 6rem;
6230
6642
  padding: 0 18px;
6231
6643
  background-color: var(--site-text-primary);
6232
6644
  color: var(--site-elevated-bg);
6233
6645
  font-size: var(--compose-primary-button-size);
6234
- font-weight: var(--fw-semibold);
6235
- letter-spacing: 0.01em;
6646
+ font-weight: var(--fw-bold);
6647
+ letter-spacing: 0;
6236
6648
  box-shadow: none;
6237
6649
  }
6238
6650
 
6239
- .compose-publish-single:hover:not(:disabled),
6240
6651
  .compose-publish-main:hover:not(:disabled) {
6241
6652
  background-color: color-mix(
6242
6653
  in srgb,
6243
- var(--site-text-primary) 94%,
6654
+ var(--site-text-primary) 92%,
6244
6655
  var(--site-elevated-bg)
6245
6656
  );
6246
6657
  }
@@ -6249,138 +6660,32 @@
6249
6660
  display: inline-flex;
6250
6661
  align-items: stretch;
6251
6662
  overflow: hidden;
6252
- border-radius: 999px;
6253
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
6663
+ border-radius: 8px;
6254
6664
  }
6255
6665
 
6256
6666
  .compose-publish-buttons .compose-publish-main {
6257
- border-radius: 999px 0 0 999px;
6258
- }
6259
-
6260
- .compose-publish-buttons-inactive .compose-publish-toggle:not(:disabled) {
6261
- color: color-mix(
6262
- in srgb,
6263
- var(--site-text-secondary) 92%,
6264
- var(--site-elevated-bg)
6265
- );
6266
- background-color: color-mix(
6267
- in srgb,
6268
- var(--compose-control-bg) 88%,
6269
- var(--compose-paper-bg)
6270
- );
6271
- --compose-publish-divider-color: color-mix(
6272
- in srgb,
6273
- var(--site-divider) 72%,
6274
- transparent
6275
- );
6276
- }
6277
-
6278
- .compose-publish-buttons-inactive
6279
- .compose-publish-toggle:not(:disabled):hover,
6280
- .compose-publish-buttons-inactive
6281
- .compose-publish-toggle[aria-expanded="true"]:not(:disabled) {
6282
- color: color-mix(
6283
- in srgb,
6284
- var(--site-text-primary) 68%,
6285
- var(--site-text-secondary)
6286
- );
6287
- background-color: color-mix(
6288
- in srgb,
6289
- var(--compose-control-bg-strong) 88%,
6290
- var(--compose-paper-bg)
6291
- );
6292
- }
6293
-
6294
- .compose-publish-buttons-inactive .compose-publish-toggle-chevron {
6295
- opacity: 0.62;
6296
- }
6297
-
6298
- .compose-publish-toggle {
6299
- display: inline-flex;
6300
- align-items: center;
6301
- justify-content: center;
6302
- position: relative;
6303
- width: 2.75rem;
6304
- color: var(--site-elevated-bg);
6305
- background-color: var(--site-text-primary);
6306
- border-radius: 0 999px 999px 0;
6307
- flex-shrink: 0;
6308
- --compose-publish-divider-color: color-mix(
6309
- in srgb,
6310
- var(--site-elevated-bg) 14%,
6311
- transparent
6312
- );
6313
- }
6314
-
6315
- .compose-publish-toggle::before {
6316
- content: "";
6317
- position: absolute;
6318
- left: 0;
6319
- top: 50%;
6320
- width: 1px;
6321
- height: 1rem;
6322
- transform: translateY(-50%);
6323
- background-color: var(--compose-publish-divider-color);
6324
- opacity: 0.7;
6325
- }
6326
-
6327
- .compose-publish-toggle:hover:not(:disabled),
6328
- .compose-publish-toggle[aria-expanded="true"]:not(:disabled) {
6329
- background-color: color-mix(
6330
- in srgb,
6331
- var(--site-text-primary) 94%,
6332
- var(--site-elevated-bg)
6333
- );
6334
- }
6335
-
6336
- .compose-publish-toggle-chevron {
6337
- opacity: 0.78;
6338
- display: block;
6339
- width: 0.92rem;
6340
- height: 0.92rem;
6341
- overflow: visible;
6342
- --icon-stroke: 1.46;
6343
- stroke-width: var(--icon-stroke);
6344
- }
6345
-
6346
- .compose-publish-single:focus-visible,
6347
- .compose-publish-main:focus-visible,
6348
- .compose-publish-toggle:focus-visible,
6349
- .compose-publish-summary:focus-visible,
6350
- .compose-publish-chip:focus-visible {
6351
- outline: 2px solid
6352
- color-mix(in srgb, var(--site-text-primary) 22%, transparent);
6353
- outline-offset: 2px;
6667
+ border-radius: 8px;
6354
6668
  }
6355
6669
 
6356
- .compose-publish-single:disabled:not(.compose-publish-single-loading),
6670
+ /* Nothing postable yet. Both channels step back together: the frame to
6671
+ 7% (1.10:1, the reference's #f4f4f4 on white) and the label to 46%
6672
+ (2.12:1, its #b2b2b2). Inactive controls are exempt from the contrast
6673
+ floors, which is the only reason a label may sit this far under 4.5:1. */
6357
6674
  .compose-publish-main:disabled:not(.compose-publish-main-loading) {
6358
6675
  opacity: 1;
6359
- color: var(--site-text-secondary);
6360
- background-color: color-mix(
6361
- in srgb,
6362
- var(--compose-control-bg) 88%,
6363
- var(--compose-paper-bg)
6364
- );
6365
- box-shadow: none;
6366
- }
6367
-
6368
- .compose-publish-toggle:disabled:not(.compose-publish-toggle-loading) {
6369
- opacity: 1;
6370
- color: var(--site-text-secondary);
6371
- background-color: color-mix(
6372
- in srgb,
6373
- var(--compose-control-bg) 88%,
6374
- var(--compose-paper-bg)
6375
- );
6376
- border-left-color: color-mix(in srgb, var(--site-divider) 80%, transparent);
6676
+ color: color-mix(in srgb, var(--site-text-secondary) 46%, transparent);
6677
+ background-color: transparent;
6678
+ box-shadow: inset 0 0 0 1px
6679
+ color-mix(in srgb, var(--site-text-secondary) 7%, transparent);
6680
+ cursor: not-allowed;
6377
6681
  }
6378
6682
 
6379
- .compose-publish-single-loading:disabled,
6380
- .compose-publish-main-loading:disabled,
6381
- .compose-publish-toggle-loading:disabled {
6382
- opacity: 1;
6383
- cursor: progress;
6683
+ /* Pointing at it says so rather than doing nothing: the frame comes up a
6684
+ step, but the label stays faded and the cursor stays barred, so the hover
6685
+ never promises a press it will not accept. */
6686
+ .compose-publish-main:disabled:not(.compose-publish-main-loading):hover {
6687
+ box-shadow: inset 0 0 0 1px
6688
+ color-mix(in srgb, var(--site-text-secondary) 18%, transparent);
6384
6689
  }
6385
6690
 
6386
6691
  .compose-publish-panel {
@@ -6457,163 +6762,398 @@
6457
6762
  padding-left: calc(1rem + env(safe-area-inset-left));
6458
6763
  }
6459
6764
 
6460
- .compose-publish-section {
6765
+ /* ── Options sheet ──────────────────────────────────────────────────
6766
+ A vertical list read top to bottom, not a toolbar of chips: muted
6767
+ group label, one full-width row per setting, value or control on the
6768
+ right. Every choice carries its own hint so they can be compared
6769
+ before picking one. */
6770
+
6771
+ .compose-sheet {
6461
6772
  display: flex;
6462
6773
  flex-direction: column;
6463
- gap: 0.625rem;
6464
6774
  }
6465
6775
 
6466
- .compose-publish-section-header {
6776
+ .compose-sheet-label {
6777
+ margin: 0;
6778
+ padding: 0.35rem 0.35rem 0.3rem;
6779
+ color: var(--site-text-secondary);
6780
+ font-size: 0.79rem;
6781
+ font-weight: var(--fw-semibold);
6782
+ }
6783
+
6784
+ .compose-sheet-row {
6785
+ --compose-icon-paper-fill: var(--compose-paper-bg);
6786
+
6467
6787
  display: flex;
6468
- align-items: flex-start;
6469
- justify-content: space-between;
6470
- gap: 0.75rem;
6788
+ align-items: center;
6789
+ gap: 0.7rem;
6790
+ width: 100%;
6791
+ padding: 0.5rem 0.45rem;
6792
+ border: none;
6793
+ border-radius: 10px;
6794
+ background: transparent;
6795
+ color: var(--site-text-primary);
6796
+ text-align: left;
6797
+ cursor: pointer;
6798
+ transition: background-color 0.15s ease;
6799
+ }
6800
+
6801
+ .compose-sheet-row:hover:not(:disabled) {
6802
+ --compose-icon-paper-fill: var(--compose-control-bg);
6803
+ background-color: var(--compose-control-bg);
6471
6804
  }
6472
6805
 
6473
- .compose-publish-section-copy {
6806
+ /* Sized to the visibility glyphs it sits in a list with, so the session rows
6807
+ at the foot of the panel read as the same kind of row. */
6808
+ .compose-sheet-row-icon {
6809
+ display: block;
6810
+ width: 0.98rem;
6811
+ height: 0.98rem;
6812
+ --icon-stroke: 1.3;
6813
+ stroke-width: var(--icon-stroke);
6814
+ flex-shrink: 0;
6815
+ overflow: visible;
6816
+ color: var(--site-text-secondary);
6817
+ }
6818
+
6819
+ .compose-sheet-row:disabled {
6820
+ cursor: not-allowed;
6821
+ opacity: 0.55;
6822
+ }
6823
+
6824
+ .compose-sheet-row:focus-visible {
6825
+ outline: 2px solid
6826
+ color-mix(in srgb, var(--site-text-primary) 22%, transparent);
6827
+ outline-offset: 1px;
6828
+ }
6829
+
6830
+ .compose-sheet-main {
6831
+ display: flex;
6832
+ flex-direction: column;
6833
+ gap: 0.08rem;
6834
+ flex: 1;
6474
6835
  min-width: 0;
6475
6836
  }
6476
6837
 
6477
- .compose-publish-section-label {
6478
- color: color-mix(
6479
- in srgb,
6480
- var(--site-text-primary) 84%,
6481
- var(--site-text-secondary)
6482
- );
6483
- font-size: var(--compose-panel-label-size);
6838
+ .compose-sheet-title {
6839
+ font-size: 0.9rem;
6484
6840
  font-weight: var(--fw-medium);
6485
- letter-spacing: 0.01em;
6486
- line-height: 1.24;
6487
6841
  }
6488
6842
 
6489
- .compose-publish-section-hint {
6490
- margin-top: 0.25rem;
6843
+ .compose-sheet-sub {
6491
6844
  color: var(--site-text-secondary);
6492
- font-size: var(--compose-panel-hint-size);
6845
+ font-size: 0.75rem;
6846
+ line-height: 1.4;
6847
+ }
6848
+
6849
+ .compose-sheet-check,
6850
+ .compose-sheet-caret {
6851
+ display: inline-flex;
6852
+ flex-shrink: 0;
6853
+ align-items: center;
6854
+ }
6855
+
6856
+ .compose-sheet-caret {
6857
+ color: var(--site-text-secondary);
6858
+ opacity: 0.75;
6859
+ }
6860
+
6861
+ .compose-sheet-hint {
6862
+ margin: 0;
6863
+ color: var(--site-text-secondary);
6864
+ font-size: 0.75rem;
6493
6865
  line-height: 1.45;
6494
- text-wrap: pretty;
6495
6866
  }
6496
6867
 
6497
- .compose-publish-section-action {
6868
+ .compose-sheet-divider {
6869
+ height: 1px;
6870
+ margin: 0.45rem 0.1rem;
6871
+ background-color: var(--site-divider);
6872
+ }
6873
+
6874
+ .compose-sheet-row-switch {
6875
+ cursor: default;
6876
+ }
6877
+
6878
+ .compose-sheet-row-switch > .input[role="switch"] {
6498
6879
  flex-shrink: 0;
6499
- min-height: 2.1rem;
6500
- padding: 0.42rem 0.78rem;
6501
- border: 1px solid var(--compose-control-border);
6880
+ }
6881
+
6882
+ /* ── Per-post date / permalink ──────────────────────────────────────
6883
+ Sits on the post it describes, not in the publish panel. Quiet until the
6884
+ post is hovered or focused, or the values are non-default. Uses
6885
+ margin-left:auto so appearing and disappearing never shifts the × next
6886
+ to it. */
6887
+
6888
+ .compose-post-meta {
6889
+ position: relative;
6890
+ margin-left: auto;
6891
+ /* Without this the flex item cannot shrink past its content's min-content
6892
+ width, so the header row overflows on narrow screens instead of
6893
+ ellipsising the permalink. */
6894
+ min-width: 0;
6895
+ opacity: 0;
6896
+ transition: opacity 0.18s ease;
6897
+ }
6898
+
6899
+ /* Narrow screens show the pill outright instead of revealing it. "Quiet until
6900
+ approached" needs a pointer to approach with, and this width is mostly
6901
+ touch, where there is none — an invisible-until-focused control is just a
6902
+ missing one.
6903
+
6904
+ It must also stay in the layout. Gating `display` on
6905
+ `jant-compose-editor:focus-within` made pressing the pill destroy it:
6906
+ pointerdown moves focus off the editor, the pill unmounts before mouseup,
6907
+ and a press that starts and ends on different elements never becomes a
6908
+ click. The pill was unclickable at this width — only `.click()` from script
6909
+ could open the panel. */
6910
+ @media (max-width: 760px) {
6911
+ .compose-post-meta {
6912
+ opacity: 1;
6913
+ }
6914
+ }
6915
+
6916
+ /* Only the first trailing control claims the free space. Two `auto` margins
6917
+ would split it and strand the pill mid-row. */
6918
+ .compose-post-meta ~ .compose-thread-post-remove,
6919
+ .compose-post-meta ~ .compose-close-btn {
6920
+ margin-left: 0;
6921
+ }
6922
+
6923
+ /* Scoped to the editor element rather than a layout row: single-post mode
6924
+ has no `.compose-editor-row` wrapper to hang this off. */
6925
+ jant-compose-editor:hover .compose-post-meta,
6926
+ jant-compose-editor:focus-within .compose-post-meta,
6927
+ .compose-post-meta-set,
6928
+ .compose-post-meta-open,
6929
+ .compose-post-meta:hover,
6930
+ .compose-post-meta:focus-within {
6931
+ opacity: 1;
6932
+ }
6933
+
6934
+ /* A lone post shows its pill outright. "Quiet until approached" only pays
6935
+ off when several rows would each carry one — with a single pill there is
6936
+ no noise to save, and hiding it made the date/permalink fade out exactly
6937
+ when focus moved to the publish controls, where that metadata matters
6938
+ most. The child combinator is the single-post detector: only that mode
6939
+ renders the editor as a direct child of `.compose-scroll`; reply and
6940
+ thread rows sit inside `.compose-editor-row` and stay quiet. */
6941
+ .compose-scroll > jant-compose-editor .compose-post-meta {
6942
+ opacity: 1;
6943
+ }
6944
+
6945
+ /* Defaults are the common case, so at rest this is a caption rather than a
6946
+ control: no chrome, and dimmer than the format selector it shares the row
6947
+ with. The outline arrives on hover, once it is being aimed at. */
6948
+ .compose-post-meta-pill {
6949
+ display: inline-flex;
6950
+ align-items: center;
6951
+ gap: 5px;
6952
+ max-width: min(17rem, 52vw);
6953
+ min-width: 0;
6954
+ padding: 3px 10px 3px 8px;
6955
+ border: 1px solid transparent;
6502
6956
  border-radius: 999px;
6503
- background-color: var(--compose-control-bg);
6504
- color: color-mix(
6505
- in srgb,
6506
- var(--site-text-primary) 74%,
6507
- var(--site-text-secondary)
6508
- );
6509
- font-size: var(--compose-panel-control-size);
6510
- font-weight: var(--fw-medium);
6511
- line-height: 1;
6957
+ background-color: transparent;
6958
+ color: color-mix(in srgb, var(--site-text-secondary) 62%, transparent);
6959
+ font-family: var(--font-ui);
6960
+ font-size: var(--type-ui-caption);
6961
+ white-space: nowrap;
6512
6962
  cursor: pointer;
6513
6963
  transition:
6514
- color 0.16s ease,
6515
- background-color 0.16s ease,
6516
- border-color 0.16s ease,
6517
- transform 0.16s ease;
6964
+ background-color 0.15s ease,
6965
+ border-color 0.15s ease,
6966
+ color 0.15s ease;
6518
6967
  }
6519
6968
 
6520
- .compose-publish-section-action:hover {
6969
+ .compose-post-meta-pill:hover,
6970
+ .compose-post-meta-pill[aria-expanded="true"] {
6971
+ border-color: var(--compose-control-border);
6972
+ background-color: var(--compose-control-bg);
6521
6973
  color: var(--site-text-primary);
6522
- border-color: color-mix(in srgb, var(--site-text-primary) 14%, transparent);
6523
- background-color: var(--compose-control-bg-strong);
6524
- transform: translateY(-1px);
6525
6974
  }
6526
6975
 
6527
- .compose-publish-section-action:focus-visible {
6528
- outline: 2px solid
6529
- color-mix(in srgb, var(--site-text-primary) 18%, transparent);
6530
- outline-offset: 2px;
6976
+ /* A date or permalink the author chose is real information, not a default —
6977
+ it gets the chrome back and full contrast. */
6978
+ .compose-post-meta-set .compose-post-meta-pill {
6979
+ border-color: color-mix(in srgb, var(--site-text-primary) 32%, transparent);
6980
+ background-color: var(--compose-paper-bg);
6981
+ color: var(--site-text-primary);
6982
+ font-weight: var(--fw-medium);
6531
6983
  }
6532
6984
 
6533
- .compose-publish-switch-row {
6534
- display: flex;
6535
- align-items: flex-start;
6536
- justify-content: space-between;
6537
- gap: 0.75rem;
6538
- cursor: pointer;
6985
+ .compose-post-meta-icon {
6986
+ flex-shrink: 0;
6987
+ opacity: 0.85;
6539
6988
  }
6540
6989
 
6541
- .compose-publish-switch-row > .input[role="switch"] {
6542
- flex-shrink: 0;
6543
- margin-top: 0.1rem;
6990
+ .compose-post-meta-sep {
6991
+ color: color-mix(in srgb, currentcolor 45%, transparent);
6544
6992
  }
6545
6993
 
6546
- .compose-publish-divider {
6547
- height: 1px;
6548
- margin: 0.75rem 0;
6549
- background-color: color-mix(
6550
- in srgb,
6551
- var(--site-text-primary) 8%,
6552
- transparent
6553
- );
6994
+ /* Only the permalink gives ground when space runs out — the date is short and
6995
+ already as terse as it gets. Without `overflow: hidden` a shrunk flex item
6996
+ still paints its full text, so a long slug used to run under the date. */
6997
+ .compose-post-meta-value {
6998
+ flex-shrink: 0;
6999
+ min-width: 0;
7000
+ overflow: hidden;
6554
7001
  }
6555
7002
 
6556
- /* --- Visibility segmented control --------------------------------------- */
7003
+ .compose-post-meta-value-slug {
7004
+ flex-shrink: 1;
7005
+ text-overflow: ellipsis;
7006
+ }
6557
7007
 
6558
- .compose-publish-chips {
7008
+ /* Fixed, not absolute. The pill it hangs from lives inside `.compose-scroll`,
7009
+ and an absolute panel is clipped by that box — an empty post makes the box
7010
+ shorter than the panel every time, so the footer went missing and focusing
7011
+ the panel scrolled the format switcher off the top to compensate. Fixed
7012
+ leaves the scroller behind; `_updatePostMetaPanelLayout` supplies top/left
7013
+ and the max-height, since only script can know where the pill ended up. */
7014
+ .compose-post-meta-panel {
7015
+ position: fixed;
7016
+ z-index: 100;
7017
+ top: 0;
7018
+ left: 0;
6559
7019
  display: flex;
7020
+ flex-direction: column;
7021
+ gap: 0.85rem;
7022
+ width: min(20rem, calc(100vw - 3rem));
7023
+ /* A window too short for the whole panel scrolls the panel, never crops it.
7024
+ Unset until script measures, so the panel is never briefly 1px tall. */
7025
+ max-height: var(--compose-post-meta-panel-max-height, none);
7026
+ overflow-y: auto;
7027
+ overscroll-behavior: contain;
7028
+ padding: 0.8rem 0.85rem 0.85rem;
7029
+ border: 1px solid var(--compose-control-border);
6560
7030
  border-radius: 12px;
6561
- padding: 4px;
6562
- background-color: var(--compose-control-bg);
6563
- box-shadow: inset 0 0 0 1px var(--compose-control-border);
7031
+ /* Same surface treatment as .compose-publish-panel — it is the same kind
7032
+ of floating panel, and --compose-floating-bg is what adapts to dark. */
7033
+ background-color: var(--compose-floating-bg);
7034
+ box-shadow:
7035
+ 0 10px 28px rgba(0, 0, 0, 0.08),
7036
+ 0 1px 3px rgba(0, 0, 0, 0.05);
6564
7037
  }
6565
7038
 
6566
- .compose-publish-chip {
6567
- flex: 1;
6568
- display: inline-flex;
6569
- align-items: center;
6570
- justify-content: center;
6571
- gap: 0.3rem;
6572
- min-height: 2.45rem;
6573
- padding: 0.46rem 0.5rem;
6574
- border-radius: 10px;
7039
+ /* It takes focus when it opens so the dialog is announced and Tab lands in
7040
+ the fields, but it is a container, not a control — no focus ring. */
7041
+ .compose-post-meta-panel:focus {
7042
+ outline: none;
7043
+ }
7044
+
7045
+ /* ── Date / permalink form ──────────────────────────────────────────
7046
+ Two settings, so they are both just open: a drill row per field meant
7047
+ the panel restated each value directly above the input showing it, and
7048
+ changed height every time one was opened. */
7049
+
7050
+ .compose-meta-field {
7051
+ display: flex;
7052
+ flex-direction: column;
7053
+ gap: 0.4rem;
7054
+ }
7055
+
7056
+ .compose-meta-field-head {
7057
+ display: flex;
7058
+ align-items: baseline;
7059
+ justify-content: space-between;
7060
+ gap: 0.6rem;
7061
+ min-height: 1.15rem;
7062
+ }
7063
+
7064
+ .compose-meta-label {
7065
+ color: var(--site-text-primary);
7066
+ font-size: 0.82rem;
7067
+ font-weight: var(--fw-medium);
7068
+ }
7069
+
7070
+ /* The panel's own way out. Escape and a click outside both close it, but
7071
+ neither announces itself, and on touch there is no Escape key at all. It
7072
+ spans the panel's padding so the divider reads as the foot of the panel
7073
+ rather than a line drawn inside it. */
7074
+ .compose-post-meta-footer {
7075
+ display: flex;
7076
+ justify-content: flex-end;
7077
+ margin: -0.1rem -0.85rem -0.85rem;
7078
+ padding: 0.4rem 0.6rem 0.5rem;
7079
+ border-top: 1px solid
7080
+ color-mix(in srgb, var(--site-divider) 60%, transparent);
7081
+ }
7082
+
7083
+ .compose-post-meta-open {
7084
+ z-index: 100;
7085
+ }
7086
+
7087
+ /* Options trigger: its own button past Publish, never a chevron welded to
7088
+ it. A split button reads as one button. */
7089
+ .compose-options-trigger {
7090
+ display: inline-grid;
7091
+ flex-shrink: 0;
7092
+ place-items: center;
7093
+ width: 2.2rem;
7094
+ height: 2.2rem;
6575
7095
  border: none;
7096
+ border-radius: 999px;
6576
7097
  background: transparent;
6577
- cursor: pointer;
6578
- white-space: nowrap;
6579
7098
  color: var(--site-text-secondary);
7099
+ cursor: pointer;
6580
7100
  transition:
6581
7101
  background-color 0.18s ease,
6582
- box-shadow 0.18s ease,
6583
7102
  color 0.18s ease;
6584
7103
  }
6585
7104
 
6586
- .compose-publish-chip:hover:not(:disabled):not(
6587
- .compose-publish-chip-selected
6588
- ) {
7105
+ .compose-options-trigger:hover:not(:disabled),
7106
+ .compose-options-trigger-open {
7107
+ background-color: var(--compose-control-bg);
6589
7108
  color: var(--site-text-primary);
6590
7109
  }
6591
7110
 
6592
- .compose-publish-chip:disabled {
6593
- cursor: default;
6594
- opacity: 0.72;
7111
+ .compose-options-trigger:disabled {
7112
+ cursor: not-allowed;
7113
+ opacity: 0.5;
6595
7114
  }
6596
7115
 
6597
- .compose-publish-chip-selected {
6598
- background-color: var(--compose-paper-bg);
6599
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
6600
- color: var(--site-text-primary);
7116
+ .compose-options-trigger:focus-visible {
7117
+ outline: 2px solid
7118
+ color-mix(in srgb, var(--site-text-primary) 22%, transparent);
7119
+ outline-offset: 2px;
6601
7120
  }
6602
7121
 
6603
- .compose-publish-chip-label {
6604
- font-size: 0.875rem;
6605
- font-weight: var(--fw-medium);
6606
- letter-spacing: 0.005em;
6607
- line-height: 1.28;
7122
+ .compose-options-trigger-icon {
7123
+ width: 1.35rem;
7124
+ height: 1.35rem;
6608
7125
  }
6609
7126
 
6610
- .compose-publish-chip-hint {
7127
+ /* Sits on the field's label row, so it reads as an aside to that label rather
7128
+ than a second thing to do. It only appears once there is a value to undo,
7129
+ which is why it can afford to be this quiet. */
7130
+ .compose-publish-section-action {
7131
+ flex-shrink: 0;
7132
+ padding: 0;
7133
+ border: none;
7134
+ background: none;
6611
7135
  color: var(--site-text-secondary);
6612
- font-size: var(--compose-panel-hint-size);
6613
- line-height: 1.45;
6614
- text-wrap: pretty;
7136
+ font-size: 0.75rem;
7137
+ line-height: 1.2;
7138
+ text-underline-offset: 2px;
7139
+ cursor: pointer;
7140
+ transition: color 0.16s ease;
7141
+ }
7142
+
7143
+ .compose-publish-section-action:hover {
7144
+ color: var(--site-text-primary);
7145
+ text-decoration: underline;
7146
+ }
7147
+
7148
+ .compose-publish-section-action:focus-visible {
7149
+ outline: 2px solid
7150
+ color-mix(in srgb, var(--site-text-primary) 18%, transparent);
7151
+ outline-offset: 3px;
7152
+ border-radius: 4px;
6615
7153
  }
6616
7154
 
7155
+ /* --- Visibility segmented control --------------------------------------- */
7156
+
6617
7157
  .compose-publish-visibility-icon {
6618
7158
  display: inline-flex;
6619
7159
  align-items: center;
@@ -6712,6 +7252,7 @@
6712
7252
  text-align: left;
6713
7253
  font-size: var(--compose-panel-control-size);
6714
7254
  color: var(--site-text-primary);
7255
+ text-decoration: none;
6715
7256
  transition: background-color 0.12s ease;
6716
7257
  }
6717
7258
 
@@ -6719,90 +7260,52 @@
6719
7260
  background-color: var(--compose-control-bg);
6720
7261
  }
6721
7262
 
7263
+ .compose-dropdown-separator {
7264
+ height: 1px;
7265
+ margin: 4px 8px;
7266
+ background-color: var(--compose-control-border);
7267
+ }
7268
+
7269
+ /* One line, no card: a card holding a chip holding the slug was three nested
7270
+ frames for what is a single tap-to-accept. */
6722
7271
  .compose-slug-suggestion {
6723
- display: grid;
6724
- grid-template-columns: minmax(0, 1fr) auto;
6725
- align-items: center;
6726
- gap: 0.65rem;
7272
+ display: flex;
7273
+ align-items: baseline;
7274
+ gap: 0.4rem;
6727
7275
  width: 100%;
6728
- padding: 0.62rem 0.68rem;
6729
- border: 1px solid var(--compose-control-border);
6730
- border-radius: 10px;
6731
- background-color: color-mix(
6732
- in srgb,
6733
- var(--compose-control-bg) 72%,
6734
- var(--compose-paper-bg)
6735
- );
6736
- box-shadow:
6737
- inset 0 1px 0 color-mix(in srgb, var(--compose-paper-bg) 88%, transparent),
6738
- 0 1px 2px rgba(0, 0, 0, 0.03);
7276
+ margin: -0.05rem -0.35rem;
7277
+ padding: 0.3rem 0.35rem;
7278
+ border: none;
7279
+ border-radius: 7px;
7280
+ background: none;
6739
7281
  cursor: pointer;
6740
7282
  text-align: left;
6741
- transition:
6742
- background-color 0.16s ease,
6743
- border-color 0.16s ease,
6744
- transform 0.16s ease,
6745
- box-shadow 0.16s ease;
7283
+ transition: background-color 0.16s ease;
6746
7284
  }
6747
7285
 
6748
7286
  .compose-slug-suggestion:hover {
6749
- border-color: color-mix(in srgb, var(--site-text-primary) 14%, transparent);
6750
- background-color: color-mix(
6751
- in srgb,
6752
- var(--compose-control-bg-strong) 78%,
6753
- var(--compose-paper-bg)
6754
- );
6755
- transform: translateY(-1px);
6756
- box-shadow:
6757
- inset 0 1px 0 color-mix(in srgb, var(--compose-paper-bg) 88%, transparent),
6758
- 0 4px 14px rgba(0, 0, 0, 0.045);
7287
+ background-color: var(--compose-control-bg);
6759
7288
  }
6760
7289
 
6761
7290
  .compose-slug-suggestion:focus-visible {
6762
7291
  outline: 2px solid
6763
7292
  color-mix(in srgb, var(--site-text-primary) 18%, transparent);
6764
- outline-offset: 2px;
6765
- }
6766
-
6767
- .compose-slug-suggestion-copy {
6768
- min-width: 0;
6769
- display: flex;
6770
- align-items: center;
6771
- flex-wrap: wrap;
6772
- gap: 0.45rem;
7293
+ outline-offset: 1px;
6773
7294
  }
6774
7295
 
6775
7296
  .compose-slug-suggestion-label {
7297
+ flex-shrink: 0;
6776
7298
  color: var(--site-text-secondary);
6777
- font-size: var(--type-ui-micro);
6778
- font-weight: var(--fw-medium);
6779
- letter-spacing: 0.01em;
6780
- line-height: 1.2;
6781
- }
6782
-
6783
- .compose-slug-suggestion-chip {
6784
- display: inline-flex;
6785
- align-items: center;
6786
- min-width: 0;
6787
- max-width: 100%;
6788
- padding: 0.25rem 0.48rem;
6789
- border-radius: 999px;
6790
- border: 1px solid var(--compose-control-border);
6791
- background-color: color-mix(
6792
- in srgb,
6793
- var(--compose-paper-bg) 92%,
6794
- var(--compose-control-bg)
6795
- );
6796
- box-shadow: inset 0 1px 0
6797
- color-mix(in srgb, var(--compose-paper-bg) 88%, transparent);
7299
+ font-size: 0.75rem;
7300
+ line-height: 1.3;
6798
7301
  }
6799
7302
 
6800
7303
  .compose-slug-suggestion-value {
6801
- display: block;
7304
+ min-width: 0;
6802
7305
  color: var(--site-text-primary);
6803
7306
  font-size: var(--type-ui-caption);
6804
7307
  font-weight: var(--fw-medium);
6805
- line-height: 1.2;
7308
+ line-height: 1.3;
6806
7309
  font-family:
6807
7310
  ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
6808
7311
  "Courier New", monospace;
@@ -6813,101 +7316,61 @@
6813
7316
 
6814
7317
  .compose-slug-suggestion-icon {
6815
7318
  display: inline-flex;
7319
+ flex-shrink: 0;
6816
7320
  align-items: center;
6817
- justify-content: center;
6818
- width: 1.65rem;
6819
- height: 1.65rem;
6820
- border-radius: 999px;
6821
- background-color: color-mix(
6822
- in srgb,
6823
- var(--compose-paper-bg) 88%,
6824
- var(--compose-control-bg)
6825
- );
6826
- color: color-mix(
6827
- in srgb,
6828
- var(--site-text-primary) 72%,
6829
- var(--site-text-secondary)
6830
- );
7321
+ margin-left: auto;
7322
+ color: var(--site-text-secondary);
7323
+ transform: translateY(0.1em);
6831
7324
  transition:
6832
7325
  transform 0.16s ease,
6833
- background-color 0.16s ease,
6834
7326
  color 0.16s ease;
6835
7327
  }
6836
7328
 
6837
7329
  .compose-slug-suggestion:hover .compose-slug-suggestion-icon {
6838
- transform: translateX(1px);
6839
- background-color: color-mix(
6840
- in srgb,
6841
- var(--compose-paper-bg) 66%,
6842
- var(--compose-control-bg-strong)
6843
- );
7330
+ transform: translate(2px, 0.1em);
6844
7331
  color: var(--site-text-primary);
6845
7332
  }
6846
7333
 
6847
- .compose-publish-slug-field,
6848
- .compose-publish-date-field {
6849
- display: flex;
6850
- flex-direction: column;
6851
- gap: 0.55rem;
6852
- }
6853
-
7334
+ /* Scaled to the panel it sits in. The old field was 3rem tall with a 14px
7335
+ radius, a gradient, an inset highlight and a drop shadow — more chrome than
7336
+ the dialog's own controls carry. */
6854
7337
  .compose-publish-slug-input-wrap,
6855
7338
  .compose-publish-date-input-wrap {
6856
7339
  display: flex;
6857
7340
  align-items: center;
6858
- gap: 0.78rem;
6859
- min-height: 3rem;
6860
- padding: 0 0.95rem;
7341
+ gap: 0.5rem;
7342
+ min-height: 2.3rem;
7343
+ padding: 0 0.6rem;
6861
7344
  border: 1px solid var(--compose-control-border);
6862
- border-radius: 14px;
6863
- background:
6864
- linear-gradient(
6865
- 180deg,
6866
- color-mix(in srgb, var(--compose-paper-bg) 94%, transparent),
6867
- color-mix(in srgb, var(--compose-control-bg) 16%, transparent)
6868
- ),
6869
- color-mix(in srgb, var(--compose-paper-bg) 97%, var(--compose-control-bg));
6870
- box-shadow:
6871
- inset 0 1px 0 color-mix(in srgb, var(--compose-paper-bg) 92%, transparent),
6872
- 0 1px 2px rgba(0, 0, 0, 0.02);
7345
+ border-radius: 9px;
7346
+ background-color: color-mix(
7347
+ in srgb,
7348
+ var(--compose-control-bg) 55%,
7349
+ var(--compose-paper-bg)
7350
+ );
6873
7351
  transition:
6874
7352
  border-color 0.16s ease,
6875
- background-color 0.16s ease,
6876
7353
  box-shadow 0.16s ease;
6877
7354
  }
6878
7355
 
6879
7356
  .compose-publish-slug-input-wrap:focus-within,
6880
7357
  .compose-publish-date-input-wrap:focus-within {
6881
- border-color: color-mix(in srgb, var(--site-text-primary) 16%, transparent);
6882
- background:
6883
- linear-gradient(
6884
- 180deg,
6885
- color-mix(in srgb, var(--compose-paper-bg) 96%, transparent),
6886
- color-mix(in srgb, var(--compose-control-bg-strong) 24%, transparent)
6887
- ),
6888
- color-mix(
6889
- in srgb,
6890
- var(--compose-paper-bg) 96%,
6891
- var(--compose-control-bg-strong)
6892
- );
6893
- box-shadow:
6894
- inset 0 1px 0 color-mix(in srgb, var(--compose-paper-bg) 94%, transparent),
6895
- 0 0 0 3px color-mix(in srgb, var(--site-text-primary) 6%, transparent);
7358
+ border-color: color-mix(in srgb, var(--site-text-primary) 22%, transparent);
7359
+ box-shadow: 0 0 0 3px
7360
+ color-mix(in srgb, var(--site-text-primary) 7%, transparent);
7361
+ }
7362
+
7363
+ /* The message under the field is red; the field it belongs to should say so
7364
+ too, rather than leaving the reader to pair them up. */
7365
+ .compose-publish-slug-input-wrap:has([aria-invalid="true"]),
7366
+ .compose-publish-date-input-wrap:has([aria-invalid="true"]) {
7367
+ border-color: color-mix(in srgb, var(--destructive) 42%, transparent);
6896
7368
  }
6897
7369
 
6898
7370
  .compose-publish-slug-prefix {
6899
7371
  flex-shrink: 0;
6900
- min-width: 1.25rem;
6901
- padding-right: 0.78rem;
6902
- border-right: 1px solid
6903
- color-mix(in srgb, var(--site-text-primary) 8%, transparent);
6904
- color: color-mix(
6905
- in srgb,
6906
- var(--site-text-primary) 52%,
6907
- var(--site-text-secondary)
6908
- );
6909
- font-size: var(--compose-panel-control-size);
6910
- font-weight: var(--fw-medium);
7372
+ color: var(--site-text-secondary);
7373
+ font-size: var(--type-ui-caption);
6911
7374
  line-height: 1;
6912
7375
  font-family:
6913
7376
  ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
@@ -6923,9 +7386,8 @@
6923
7386
  padding: 0;
6924
7387
  box-shadow: none;
6925
7388
  color: var(--site-text-primary);
6926
- font-size: var(--compose-inline-input-size);
6927
- font-weight: var(--fw-medium);
6928
- line-height: 1.35;
7389
+ font-size: var(--type-ui-caption);
7390
+ line-height: 1.4;
6929
7391
  font-family:
6930
7392
  ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
6931
7393
  "Courier New", monospace;
@@ -6935,13 +7397,56 @@
6935
7397
  font-family: inherit;
6936
7398
  }
6937
7399
 
7400
+ /* The browser paints `yyyy-mm-dd` in the value's own colour, so an untouched
7401
+ field reads as a date that was chosen. Only the empty state is dimmed. */
7402
+ .compose-publish-date-input-empty::-webkit-datetime-edit {
7403
+ color: var(--site-text-secondary);
7404
+ opacity: 0.75;
7405
+ }
7406
+
7407
+ /* Replaced by .compose-publish-date-picker, which drives the same picker from
7408
+ a target the height of the field instead of a ~13px UA glyph. */
7409
+ .compose-publish-date-input::-webkit-calendar-picker-indicator {
7410
+ display: none;
7411
+ }
7412
+
7413
+ .compose-publish-date-picker {
7414
+ display: inline-grid;
7415
+ flex-shrink: 0;
7416
+ place-items: center;
7417
+ width: 1.7rem;
7418
+ height: 1.7rem;
7419
+ margin-right: -0.35rem;
7420
+ border: none;
7421
+ border-radius: 7px;
7422
+ background: transparent;
7423
+ color: var(--site-text-secondary);
7424
+ cursor: pointer;
7425
+ transition:
7426
+ background-color 0.16s ease,
7427
+ color 0.16s ease;
7428
+ }
7429
+
7430
+ .compose-publish-date-picker:hover {
7431
+ background-color: var(--compose-control-bg-strong);
7432
+ color: var(--site-text-primary);
7433
+ }
7434
+
7435
+ .compose-publish-date-picker:focus-visible {
7436
+ outline: 2px solid
7437
+ color-mix(in srgb, var(--site-text-primary) 22%, transparent);
7438
+ outline-offset: 1px;
7439
+ }
7440
+
7441
+ .compose-publish-date-picker-icon {
7442
+ width: 0.95rem;
7443
+ height: 0.95rem;
7444
+ }
7445
+
6938
7446
  .compose-publish-slug-input::placeholder,
6939
7447
  .compose-publish-date-input::placeholder {
6940
- color: color-mix(
6941
- in srgb,
6942
- var(--site-text-secondary) 82%,
6943
- var(--compose-paper-bg)
6944
- );
7448
+ color: var(--site-text-secondary);
7449
+ opacity: 0.75;
6945
7450
  }
6946
7451
 
6947
7452
  .compose-publish-slug-input:focus,
@@ -6962,21 +7467,13 @@
6962
7467
  color: var(--destructive);
6963
7468
  }
6964
7469
 
7470
+ /* The URL a slug resolves to is a confirmation, not a control — a bordered
7471
+ box for it made a third framed thing in a panel with two fields. */
6965
7472
  .compose-publish-slug-preview {
6966
- display: flex;
6967
- flex-wrap: wrap;
6968
- align-items: baseline;
6969
- gap: 0.04rem;
6970
- padding: 0.48rem 0.62rem;
6971
- border-radius: 9px;
6972
- border: 1px solid var(--compose-control-border);
6973
- background-color: color-mix(
6974
- in srgb,
6975
- var(--compose-control-bg) 58%,
6976
- var(--compose-paper-bg)
6977
- );
6978
- font-size: var(--type-ui-caption);
6979
- line-height: 1.35;
7473
+ margin: 0;
7474
+ color: var(--site-text-secondary);
7475
+ font-size: var(--type-ui-micro);
7476
+ line-height: 1.4;
6980
7477
  word-break: break-word;
6981
7478
  font-family:
6982
7479
  ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
@@ -6984,19 +7481,11 @@
6984
7481
  }
6985
7482
 
6986
7483
  .compose-publish-slug-preview-origin {
6987
- color: color-mix(
6988
- in srgb,
6989
- var(--site-text-secondary) 92%,
6990
- var(--compose-paper-bg)
6991
- );
6992
- font-size: var(--type-ui-micro);
6993
- font-weight: var(--fw-normal);
7484
+ opacity: 0.75;
6994
7485
  }
6995
7486
 
6996
7487
  .compose-publish-slug-preview-path {
6997
7488
  color: var(--site-text-primary);
6998
- font-size: var(--type-ui-caption);
6999
- font-weight: var(--fw-medium);
7000
7489
  overflow-wrap: anywhere;
7001
7490
  }
7002
7491
 
@@ -7095,10 +7584,13 @@
7095
7584
  transform: scale(1.02);
7096
7585
  }
7097
7586
 
7587
+ /* Sized to be looked at, not to leave room for a pinned toolbar. Nothing
7588
+ clips the dock any more, so a tall preview just makes `.compose-scroll`
7589
+ longer. The dvh half keeps it from filling a short window on its own. */
7098
7590
  .compose-attachment-img {
7099
7591
  display: block;
7100
7592
  border-radius: var(--media-radius, 0.5rem);
7101
- max-height: min(160px, 18dvh);
7593
+ max-height: min(420px, 46dvh);
7102
7594
  object-fit: contain;
7103
7595
  }
7104
7596
 
@@ -7155,7 +7647,7 @@
7155
7647
  cropped from the top (object-fit: cover) instead of shrinking to a sliver. */
7156
7648
  .compose-attachment:only-child .compose-attachment-img {
7157
7649
  max-width: 100%;
7158
- max-height: min(200px, 22dvh);
7650
+ max-height: min(460px, 52dvh);
7159
7651
  min-width: 48px;
7160
7652
  object-fit: cover;
7161
7653
  object-position: center top;
@@ -7166,9 +7658,9 @@
7166
7658
  aspect-ratio: 4 / 3;
7167
7659
  }
7168
7660
 
7169
- /* Multiple images: fixed height, cover */
7661
+ /* Multiple images: one shared height so the row reads as a strip, cover */
7170
7662
  .compose-attachment:not(:only-child) .compose-attachment-img {
7171
- height: 200px;
7663
+ height: min(340px, 40dvh);
7172
7664
  width: auto;
7173
7665
  min-width: 120px;
7174
7666
  object-fit: cover;
@@ -7507,18 +7999,21 @@
7507
7999
  --compose-thread-padding-left: 16px;
7508
8000
  --compose-thread-padding-right: 16px;
7509
8001
  --compose-thread-gap: 12px;
7510
- --compose-thread-rail-left: calc(var(--compose-thread-padding-left) + 5px);
7511
- --site-thread-marker-surface: var(--compose-paper-bg);
8002
+ /* Fixed track every rail child sits in and centres its circle inside, so the
8003
+ line's x depends on the track alone and no dot size can pull it off centre.
8004
+ The constant this replaced was half the *feed's* 10px marker and stayed
8005
+ behind when compose dropped to 7px, leaving the line down the dot's edge. */
8006
+ --compose-thread-rail-width: 10px;
8007
+ --compose-thread-rail-left: calc(
8008
+ var(--compose-thread-padding-left) + var(--compose-thread-rail-width) / 2
8009
+ );
7512
8010
  display: flex;
7513
8011
  flex-direction: column;
7514
- flex: 1;
7515
- min-height: 0;
7516
- overflow: hidden;
7517
8012
  position: relative;
7518
8013
  }
7519
8014
 
7520
8015
  .compose-reply-compose-layout {
7521
- --compose-title-input-size: var(--type-content-subtitle);
8016
+ --compose-title-input-size: calc(var(--type-content-body) * 1.2);
7522
8017
  --compose-title-input-leading: 1.35;
7523
8018
  --compose-quote-input-size: calc(var(--type-content-body) * 1.06);
7524
8019
  --compose-quote-input-leading: 1.42;
@@ -7541,8 +8036,9 @@
7541
8036
  width: 1.55rem;
7542
8037
  }
7543
8038
 
8039
+ /* Height comes from the shared four-line floor, which already reads this
8040
+ layout's own leading and type size — only the lead-in changes here. */
7544
8041
  .compose-reply-compose-layout .compose-quote-text {
7545
- min-height: 6.5rem;
7546
8042
  padding-top: 0.35rem;
7547
8043
  }
7548
8044
 
@@ -7557,187 +8053,147 @@
7557
8053
  left: 1rem;
7558
8054
  width: 1.6rem;
7559
8055
  }
7560
-
7561
- .compose-reply-compose-layout .compose-quote-text {
7562
- min-height: 6.5rem;
7563
- padding-top: 0.35rem;
7564
- }
7565
- }
7566
-
7567
- /* Continuous vertical line through the rail */
7568
- .compose-thread-layout::before {
7569
- content: "";
7570
- position: absolute;
7571
- left: var(--compose-thread-rail-left);
7572
- top: 0;
7573
- bottom: 0;
7574
- width: 1px;
7575
- background: linear-gradient(
7576
- 180deg,
7577
- transparent 0,
7578
- color-mix(in srgb, var(--site-threadline) 85%, transparent) 8%,
7579
- color-mix(in srgb, var(--site-threadline) 55%, transparent) 100%
7580
- );
7581
- z-index: 0;
7582
- }
7583
-
7584
- /* Row containing dot + reply context */
7585
- .compose-reply-row {
7586
- display: flex;
7587
- align-items: flex-start;
7588
- gap: var(--compose-thread-gap);
7589
- padding-right: var(--compose-thread-padding-right);
7590
- padding-left: var(--compose-thread-padding-left);
7591
- }
7592
-
7593
- /* Row containing dot + editor */
7594
- .compose-editor-row {
7595
- display: flex;
7596
- align-items: flex-start;
7597
- gap: var(--compose-thread-gap);
7598
- padding-right: var(--compose-thread-padding-right);
7599
- padding-left: var(--compose-thread-padding-left);
7600
- flex: 1;
7601
- min-height: 0;
7602
- overflow: hidden;
7603
8056
  }
7604
8057
 
7605
- .compose-editor-row > jant-compose-editor {
7606
- flex: 1;
7607
- min-height: 0;
7608
- display: flex;
7609
- flex-direction: column;
7610
- align-self: stretch;
7611
- overflow: hidden;
8058
+ /* The rail is drawn one row at a time rather than as a single strip down the
8059
+ layout, so the two ends can be trimmed: it starts at the first dot and
8060
+ stops at the last post's. Under the last post it used to keep going —
8061
+ that's the tallest row in the layout, so it was a long line pointing at
8062
+ nothing. */
8063
+ /* Distance from a row's top edge to the centre of its dot: where the rail
8064
+ has to start or stop, and where the dot itself has to land. The dot derives
8065
+ its own margin from this pair (see `.compose-thread-dot`) rather than
8066
+ restating it, so resizing the marker cannot drift the line off the dot. */
8067
+ .compose-thread-layout > * {
7612
8068
  position: relative;
8069
+ --compose-thread-row-padding-top: 0px;
8070
+ --compose-thread-dot-center: calc(0.65rem + 5px);
7613
8071
  }
7614
8072
 
7615
- /* Reduce editor body left padding — the thread rail already provides indent */
7616
- .compose-editor-row .compose-body {
7617
- padding-left: 0;
7618
- }
7619
-
7620
- .compose-editor-row .compose-attachments-dock {
7621
- flex: 0 1 auto;
7622
- min-height: 0;
7623
- max-height: min(240px, 34dvh);
7624
- overflow-y: auto;
7625
- padding-inline: 0;
7626
- background: transparent;
8073
+ .compose-thread-layout > .compose-editor-row {
8074
+ --compose-thread-row-padding-top: 14px;
8075
+ --compose-thread-dot-center: calc(14px + 1rem + 7px);
7627
8076
  }
7628
8077
 
7629
- .compose-reply-compose-layout .compose-editor-row .compose-attachments-dock {
7630
- --compose-reply-attachment-width: 96px;
7631
- --compose-reply-attachment-height: 72px;
7632
-
7633
- max-height: min(116px, 18dvh);
7634
- padding-block: 4px 2px;
8078
+ .compose-thread-compose-layout > .compose-editor-row {
8079
+ --compose-thread-row-padding-top: 4px;
8080
+ --compose-thread-dot-center: calc(4px + 1.05rem + 7px);
7635
8081
  }
7636
8082
 
7637
- .compose-reply-compose-layout .compose-editor-row .compose-attachments {
7638
- gap: 6px;
7639
- padding-block: 0;
8083
+ .compose-thread-layout > *::before {
8084
+ content: "";
8085
+ position: absolute;
8086
+ left: var(--compose-thread-rail-left);
8087
+ top: 0;
8088
+ bottom: 0;
8089
+ width: var(--site-thread-rail-line-width);
8090
+ margin-left: calc(var(--site-thread-rail-line-width) / -2);
8091
+ border-radius: 1px;
8092
+ /* Matched to the weight of the reading rail's gradient rather than its
8093
+ implementation. That one runs 85% → 55% down its length, so ~70% flat
8094
+ reads at the same strength here. It can't literally share the gradient:
8095
+ this rail is drawn per row so both ends can be trimmed, and a per-row
8096
+ gradient would fade in once per post instead of once per thread. */
8097
+ background-color: color-mix(
8098
+ in srgb,
8099
+ var(--site-threadline) 70%,
8100
+ transparent
8101
+ );
7640
8102
  }
7641
8103
 
7642
- .compose-reply-compose-layout .compose-editor-row .compose-attachment {
7643
- gap: 2px;
7644
- max-width: none;
8104
+ .compose-thread-layout > :first-child::before {
8105
+ top: var(--compose-thread-dot-center);
7645
8106
  }
7646
8107
 
7647
- .compose-reply-compose-layout
7648
- .compose-editor-row
7649
- .compose-attachment:has(.compose-attachment-preview-fallback) {
7650
- width: var(--compose-reply-attachment-width);
8108
+ /* The last post ends the rail. `.compose-thread-add-row` sits below it and is
8109
+ a placeholder, not a post, so its dashed dot is left unconnected. */
8110
+ .compose-thread-layout
8111
+ > .compose-editor-row:not(:has(~ .compose-editor-row))::before {
8112
+ bottom: auto;
8113
+ height: var(--compose-thread-dot-center);
7651
8114
  }
7652
8115
 
7653
- .compose-reply-compose-layout
7654
- .compose-editor-row
7655
- .compose-attachment:only-child
7656
- .compose-attachment-img,
7657
- .compose-reply-compose-layout
7658
- .compose-editor-row
7659
- .compose-attachment:not(:only-child)
7660
- .compose-attachment-img {
7661
- width: var(--compose-reply-attachment-width);
7662
- height: var(--compose-reply-attachment-height);
7663
- min-width: 0;
7664
- max-width: var(--compose-reply-attachment-width);
7665
- max-height: var(--compose-reply-attachment-height);
7666
- object-fit: cover;
7667
- object-position: center;
8116
+ .compose-thread-layout > .compose-thread-add-row::before {
8117
+ display: none;
7668
8118
  }
7669
8119
 
7670
- .compose-reply-compose-layout
7671
- .compose-editor-row
7672
- .compose-attachment-preview-fallback,
7673
- .compose-reply-compose-layout
7674
- .compose-editor-row
7675
- .compose-attachment-processing {
7676
- width: var(--compose-reply-attachment-width);
7677
- height: var(--compose-reply-attachment-height);
7678
- aspect-ratio: auto;
8120
+ /* Row containing dot + reply context */
8121
+ .compose-reply-row {
8122
+ display: flex;
8123
+ align-items: flex-start;
8124
+ gap: var(--compose-thread-gap);
8125
+ padding-right: var(--compose-thread-padding-right);
8126
+ padding-left: var(--compose-thread-padding-left);
7679
8127
  }
7680
8128
 
7681
- .compose-reply-compose-layout
7682
- .compose-editor-row
7683
- .compose-attachment-preview-fallback
7684
- svg {
7685
- width: 28px;
7686
- height: 28px;
7687
- }
8129
+ /* Row containing dot + editor */
8130
+ .compose-editor-row {
8131
+ /* The rail already indents this row from outside the editor, so nothing
8132
+ inside it adds a gutter of its own. */
8133
+ --compose-gutter: 0px;
7688
8134
 
7689
- .compose-reply-compose-layout
7690
- .compose-editor-row
7691
- .compose-attachment-file-card,
7692
- .compose-reply-compose-layout
7693
- .compose-editor-row
7694
- .compose-attachment:not(:only-child)
7695
- .compose-attachment-file-card {
7696
- width: 112px;
7697
- height: var(--compose-reply-attachment-height);
7698
- gap: 4px;
7699
- padding: 8px 10px;
8135
+ display: flex;
8136
+ align-items: flex-start;
8137
+ gap: var(--compose-thread-gap);
8138
+ padding-right: var(--compose-thread-padding-right);
8139
+ padding-left: var(--compose-thread-padding-left);
8140
+ padding-top: 4px;
8141
+ padding-bottom: 4px;
7700
8142
  }
7701
8143
 
7702
- .compose-reply-compose-layout .compose-editor-row .compose-attachment-alt {
7703
- padding: 1px 3px;
7704
- font-size: var(--type-ui-micro);
7705
- line-height: 1.2;
8144
+ /* `flex: 1` here is horizontal — it claims the width left by the rail dot.
8145
+ The row's height comes from the editor's own content; nothing in this
8146
+ chain clips or scrolls, that is `.compose-scroll`'s job. */
8147
+ .compose-editor-row > jant-compose-editor {
8148
+ flex: 1;
8149
+ min-width: 0;
8150
+ display: flex;
8151
+ flex-direction: column;
8152
+ position: relative;
7706
8153
  }
7707
8154
 
7708
- .compose-reply-compose-layout .compose-editor-row .compose-attachment-remove {
7709
- top: 4px;
7710
- right: 4px;
7711
- width: 20px;
7712
- height: 20px;
8155
+ .compose-editor-row .compose-attachments-dock {
8156
+ padding-inline-end: 0;
8157
+ background: transparent;
7713
8158
  }
7714
8159
 
7715
- /* Dot marker for thread positions */
8160
+ /* Dot marker for thread positions. Sits above the rail so it reads as a joint
8161
+ in the line, and takes its offset from the row's declared dot centre so the
8162
+ two ends of the rail always meet it. The element is the full rail track and
8163
+ only centres the circle inside it, which is what keeps the line through the
8164
+ dot's middle no matter how the marker is resized. */
7716
8165
  .compose-thread-dot {
7717
- width: var(--site-thread-marker-size);
8166
+ position: relative;
8167
+ z-index: 1;
8168
+ display: flex;
8169
+ justify-content: center;
8170
+ width: var(--compose-thread-rail-width);
7718
8171
  height: var(--site-thread-marker-size);
7719
- margin-top: 0.65rem;
7720
- border-radius: 50%;
7721
- border: var(--site-thread-marker-border-width) solid
7722
- var(--site-thread-marker-surface);
7723
- background-color: var(--site-threadline);
8172
+ margin-top: calc(
8173
+ var(--compose-thread-dot-center) - var(--compose-thread-row-padding-top) -
8174
+ var(--site-thread-marker-size) / 2
8175
+ );
7724
8176
  flex-shrink: 0;
7725
8177
  align-self: flex-start;
7726
- box-shadow: 0 0 0 var(--site-thread-marker-ring-width)
7727
- var(--site-thread-dot-ring);
7728
- z-index: 1;
7729
8178
  }
7730
8179
 
7731
- .compose-editor-row .compose-thread-dot {
7732
- margin-top: 1rem;
7733
- margin-left: calc(
7734
- (var(--site-thread-marker-size) - var(--site-thread-marker-hero-size)) / 2
7735
- );
8180
+ .compose-thread-dot::before {
8181
+ content: "";
8182
+ width: var(--site-thread-marker-size);
8183
+ height: var(--site-thread-marker-size);
8184
+ border-radius: 50%;
8185
+ background-color: var(--site-threadline);
8186
+ box-shadow: 0 0 0 var(--site-thread-marker-gap)
8187
+ var(--site-thread-marker-surface);
8188
+ transition: background-color 0.15s ease;
7736
8189
  }
7737
8190
 
7738
- /* In thread compose mode, dot aligns with the post header row */
7739
- .compose-thread-compose-layout .compose-editor-row .compose-thread-dot {
7740
- margin-top: 1.05rem;
8191
+ /* Every post in a thread is equally new, so accenting all of them says nothing
8192
+ — it just makes the rail loud. The accent marks where you are on the chain
8193
+ instead: the one post holding the cursor. The reply context above stays on
8194
+ the line's own colour. */
8195
+ .compose-editor-row.is-current .compose-thread-dot::before {
8196
+ background-color: var(--site-thread-marker-current);
7741
8197
  }
7742
8198
 
7743
8199
  .compose-reply-context {
@@ -7747,12 +8203,15 @@
7747
8203
  overflow: hidden;
7748
8204
  position: relative;
7749
8205
  padding: 0.08rem 0 0.18rem;
7750
- transition: max-height 0.3s ease;
7751
8206
  }
7752
8207
 
8208
+ /* "Show more" means show it: expanding hands the post its full height rather
8209
+ than a taller box with a second scrollbar in it. That inner scrollbar is
8210
+ what made the parent's pictures thumbnails — there was never room for one
8211
+ at full size. Length is the toggle's job now, not the preview's. */
7753
8212
  .compose-reply-context.expanded {
7754
- max-height: min(400px, 35dvh);
7755
- overflow-y: auto;
8213
+ max-height: none;
8214
+ overflow: visible;
7756
8215
  }
7757
8216
 
7758
8217
  .compose-reply-context-body {
@@ -7807,21 +8266,26 @@
7807
8266
  background-color: transparent;
7808
8267
  }
7809
8268
 
8269
+ /* The parent reads at the same scale as the reply being written — the same
8270
+ ceiling as `.compose-attachment-img`, the same radius token — so the two
8271
+ halves of the exchange look like the same material. This is the only size
8272
+ rule for pictures in the context: it covers both the ones inside the post's
8273
+ own HTML and the media strip below it, which sits in here too. */
7810
8274
  .compose-reply-context-body img,
7811
8275
  .compose-reply-context-body video {
7812
8276
  width: auto !important;
7813
8277
  height: auto !important;
7814
- max-height: 120px;
7815
- border-radius: 6px;
8278
+ max-height: min(420px, 46dvh);
8279
+ border-radius: var(--media-radius, 0.5rem);
7816
8280
  object-fit: contain;
7817
8281
  background-image: none !important;
7818
8282
  }
7819
8283
 
7820
8284
  .compose-reply-context-media {
7821
8285
  display: flex;
7822
- gap: 6px;
8286
+ gap: 8px;
7823
8287
  flex-wrap: wrap;
7824
- margin-top: 6px;
8288
+ margin-top: 8px;
7825
8289
  }
7826
8290
 
7827
8291
  .compose-reply-context-media-link {
@@ -7829,12 +8293,6 @@
7829
8293
  cursor: pointer;
7830
8294
  }
7831
8295
 
7832
- .compose-reply-context-media-img {
7833
- max-height: 80px;
7834
- border-radius: 6px;
7835
- object-fit: contain;
7836
- }
7837
-
7838
8296
  .compose-reply-fade {
7839
8297
  position: absolute;
7840
8298
  bottom: 0;
@@ -7852,7 +8310,8 @@
7852
8310
  padding-top: 4px;
7853
8311
  padding-right: var(--compose-thread-padding-right);
7854
8312
  padding-left: calc(
7855
- var(--compose-thread-padding-left) + var(--compose-thread-gap) + 10px
8313
+ var(--compose-thread-padding-left) + var(--compose-thread-gap) +
8314
+ var(--compose-thread-rail-width)
7856
8315
  );
7857
8316
  font-size: var(--type-thread-context-meta);
7858
8317
  color: var(--site-text-secondary);
@@ -7952,14 +8411,26 @@
7952
8411
  padding: 0.78rem 0.9rem;
7953
8412
  }
7954
8413
 
7955
- .compose-collection-trigger {
7956
- min-height: 2.65rem;
7957
- padding-right: 0.82rem;
8414
+ /* Touch keeps the roomier set: the reference's 36px box is tuned for a
8415
+ cursor, and a thumb needs the millimetres back. Width still comes from
8416
+ the word — only the box around it grows. */
8417
+ .compose-publish-main {
8418
+ min-height: 2.8em;
8419
+ padding-inline: 22px;
7958
8420
  }
7959
8421
 
7960
- .compose-collection-trigger-icon {
7961
- width: 1.75rem;
7962
- height: 1.75rem;
8422
+ .compose-options-trigger {
8423
+ width: 2.5rem;
8424
+ height: 2.5rem;
8425
+ }
8426
+
8427
+ .compose-collection-select {
8428
+ --compose-collection-pad-icon: 0.68rem;
8429
+ --compose-collection-pad-label: 0.85rem;
8430
+ }
8431
+
8432
+ .compose-collection-trigger {
8433
+ min-height: 2.5rem;
7963
8434
  }
7964
8435
 
7965
8436
  .compose-collection-select [data-popover] {
@@ -7967,13 +8438,24 @@
7967
8438
  max-width: min(20rem, calc(100vw - 1rem));
7968
8439
  }
7969
8440
 
8441
+ .compose-collection-search-icon {
8442
+ width: 1.15rem;
8443
+ height: 1.15rem;
8444
+ }
8445
+
8446
+ .compose-collection-options {
8447
+ max-height: 15rem;
8448
+ }
8449
+
7970
8450
  .compose-collection-popover {
7971
8451
  padding: 0.62rem 0.68rem 0.58rem;
7972
8452
  }
7973
8453
 
7974
8454
  .compose-collection-search-shell {
8455
+ gap: 0.52rem;
7975
8456
  min-height: 3rem;
7976
8457
  padding: 0 1rem;
8458
+ border-radius: 0.92rem;
7977
8459
  }
7978
8460
 
7979
8461
  .compose-collection-options {
@@ -7992,12 +8474,20 @@
7992
8474
  height: 1.5rem;
7993
8475
  }
7994
8476
 
7995
- .compose-thread-layout {
7996
- padding-top: 0.75rem;
8477
+ .compose-collection-add-action {
8478
+ gap: 0.82rem;
8479
+ min-height: 3rem;
8480
+ padding: 0 0.92rem;
8481
+ border-radius: 1rem;
8482
+ }
8483
+
8484
+ .compose-collection-footer {
8485
+ margin-top: 0.35rem;
8486
+ padding: 0.35rem 0.22rem 0.12rem;
7997
8487
  }
7998
8488
 
7999
- .compose-thread-layout::before {
8000
- top: 0.75rem;
8489
+ .compose-thread-layout {
8490
+ padding-top: 0.75rem;
8001
8491
  }
8002
8492
 
8003
8493
  .compose-reply-context {
@@ -8020,35 +8510,14 @@
8020
8510
  }
8021
8511
  }
8022
8512
 
8023
- /* --- Thread compose layout (multi-post, scrollable) --------------------- */
8024
-
8025
- /* Scrollable variant for composing a new thread (vs reply thread which is
8026
- fixed-height). Each editor item grows naturally; the whole layout scrolls. */
8027
- .compose-thread-compose-layout {
8028
- overflow-y: auto;
8029
- overflow-x: hidden;
8030
- }
8031
-
8032
- .compose-thread-compose-layout .compose-editor-row {
8033
- flex: none;
8034
- overflow: visible;
8035
- align-items: flex-start;
8036
- padding-top: 4px;
8037
- padding-bottom: 4px;
8038
- }
8039
-
8040
- .compose-thread-compose-layout .compose-editor-row > jant-compose-editor {
8041
- overflow: visible;
8042
- min-width: 0;
8043
- }
8513
+ /* --- Thread compose layout (multi-post) --------------------------------- */
8044
8514
 
8045
- .compose-thread-compose-layout .compose-body {
8046
- flex: none;
8047
- overflow-y: visible;
8048
- min-height: 72px;
8049
- }
8515
+ /* `.compose-thread-compose-layout` carries no layout rules of its own: the
8516
+ grow-naturally-and-scroll-as-one behaviour it used to opt into is now what
8517
+ every mode does, under `.compose-scroll`. It survives as a hook for the
8518
+ rail geometry above.
8050
8519
 
8051
- /* Focused thread post gets a subtle left accent. Use an inset box-shadow, not
8520
+ Focused thread post gets a subtle left accent. Use an inset box-shadow, not
8052
8521
  a border: a border adds 2px of layout width only while focused, so switching
8053
8522
  focus between thread posts shifted each one sideways ("jitter"). box-shadow
8054
8523
  paints in the same spot without affecting layout. */
@@ -8056,7 +8525,8 @@
8056
8525
  box-shadow: inset 2px 0 0 transparent;
8057
8526
  }
8058
8527
 
8059
- /* "Add to thread" row inside thread compose layout */
8528
+ /* "Add to thread" on the rail — thread compose, and a reply before it has
8529
+ grown a second post. */
8060
8530
  .compose-thread-add-row {
8061
8531
  display: flex;
8062
8532
  align-items: center;
@@ -8067,92 +8537,63 @@
8067
8537
  padding-bottom: 10px;
8068
8538
  }
8069
8539
 
8540
+ /* Fills the rail track exactly, so it lands on the same centre line as the
8541
+ post dots without any offset of its own. */
8070
8542
  .compose-thread-add-dot {
8071
- width: 10px;
8072
- height: 10px;
8543
+ width: var(--compose-thread-rail-width);
8544
+ height: var(--compose-thread-rail-width);
8073
8545
  border-radius: 50%;
8074
8546
  border: 1.5px dashed
8075
8547
  color-mix(in srgb, var(--site-threadline) 60%, transparent);
8076
8548
  flex-shrink: 0;
8077
8549
  margin-top: 1px;
8078
- margin-left: calc(
8079
- (var(--site-thread-marker-hero-size) - var(--site-thread-marker-size)) / 2
8080
- );
8081
- }
8082
-
8083
- .compose-thread-add-btn {
8084
- display: inline-flex;
8085
- align-items: center;
8086
- gap: 6px;
8087
- font-size: var(--type-ui-caption);
8088
- font-family: var(--font-ui);
8089
- color: var(--site-text-secondary);
8090
- padding: 4px 12px 4px 9px;
8091
- border-radius: 20px;
8092
- border: 1.5px dashed
8093
- color-mix(in srgb, var(--site-threadline) 70%, transparent);
8094
- background: transparent;
8095
- cursor: pointer;
8096
- transition:
8097
- color 0.15s ease,
8098
- background-color 0.15s ease,
8099
- border-color 0.15s ease;
8100
- }
8101
-
8102
- .compose-thread-compose-layout .compose-thread-add-btn {
8103
- margin-inline-start: -8px;
8104
- }
8105
-
8106
- .compose-thread-add-btn:hover:not(:disabled) {
8107
- color: var(--site-text-primary);
8108
- background-color: var(--compose-control-bg);
8109
- border-color: var(--site-threadline);
8110
- border-style: solid;
8111
- }
8112
-
8113
- .compose-thread-add-btn:disabled,
8114
- .compose-add-thread-btn:disabled {
8115
- opacity: 0.4;
8116
- cursor: default;
8117
8550
  }
8118
8551
 
8119
- /* "Add to thread" trigger below single-post editor */
8552
+ /* "Add to thread" trigger with no rail under it — a single post, which has
8553
+ no next slot to point at yet. */
8120
8554
  .compose-add-thread-trigger {
8121
- padding: 1px 10px 4px;
8555
+ padding: 1px 10px 4px var(--compose-gutter);
8122
8556
  display: flex;
8123
8557
  align-items: center;
8124
8558
  }
8125
8559
 
8126
8560
  @media (min-width: 700px) {
8127
8561
  .compose-add-thread-trigger {
8128
- padding-inline: 16px;
8562
+ padding-inline: var(--compose-gutter) 16px;
8129
8563
  }
8130
8564
  }
8131
8565
 
8132
- .compose-add-thread-btn {
8566
+ /* No glyph of its own: on the rail the dashed dot already says "next slot",
8567
+ and on a single post the row's position says it. The label is all that is
8568
+ left, so it sits at the row's own quiet weight — and hangs back by its own
8569
+ side padding so the label, not the hover chip, starts on the text column. */
8570
+ .compose-thread-add-btn {
8571
+ --compose-add-hang: 8px;
8572
+
8133
8573
  display: inline-flex;
8134
8574
  align-items: center;
8135
- gap: 6px;
8136
8575
  font-size: var(--type-ui-caption);
8137
8576
  font-family: var(--font-ui);
8138
8577
  color: var(--site-text-secondary);
8139
- padding: 4px 12px 4px 9px;
8140
- border-radius: 20px;
8141
- border: 1.5px dashed
8142
- color-mix(in srgb, var(--site-threadline) 70%, transparent);
8578
+ padding: 5px var(--compose-add-hang);
8579
+ margin-inline-start: calc(-1 * var(--compose-add-hang));
8580
+ border-radius: 9px;
8581
+ border: none;
8143
8582
  background: transparent;
8144
8583
  cursor: pointer;
8145
8584
  transition:
8146
8585
  color 0.15s ease,
8147
- background-color 0.15s ease,
8148
- border-color 0.15s ease;
8586
+ background-color 0.15s ease;
8149
8587
  }
8150
8588
 
8151
- .compose-add-thread-btn:hover:not(:disabled) {
8589
+ .compose-thread-add-btn:hover:not(:disabled) {
8152
8590
  color: var(--site-text-primary);
8153
8591
  background-color: var(--compose-control-bg);
8154
- border-color: var(--site-threadline);
8155
- border-style: solid;
8592
+ }
8593
+
8594
+ .compose-thread-add-btn:disabled {
8595
+ opacity: 0.4;
8596
+ cursor: default;
8156
8597
  }
8157
8598
 
8158
8599
  /* --- Thread post header: format tag switcher + × on right --------------- */
@@ -8160,8 +8601,45 @@
8160
8601
  .compose-thread-post-header {
8161
8602
  display: flex;
8162
8603
  align-items: center;
8163
- justify-content: space-between;
8164
- padding: 6px 0 4px;
8604
+ /* The format selector will not shrink, so on a narrow screen let the
8605
+ date/permalink pill drop to its own line rather than clip. */
8606
+ flex-wrap: wrap;
8607
+ row-gap: 4px;
8608
+ gap: 8px;
8609
+ /* Left is the shared text column: the switcher hangs back out of it by its
8610
+ own item padding, so it is the label that starts on the edge. Right keeps
8611
+ the tools row's 16px so × and the date pill stay over the tool buttons
8612
+ bracketing the editor from below. */
8613
+ padding: 10px 16px 2px var(--compose-gutter);
8614
+ }
8615
+
8616
+ .compose-editor-row .compose-thread-post-header {
8617
+ padding-block: 6px 4px;
8618
+ }
8619
+
8620
+ /* "1/3" position marker. Identity comes before attribute, so it leads the
8621
+ row; it pairs visually with the thread rail dot to its left. */
8622
+ .compose-post-position {
8623
+ flex-shrink: 0;
8624
+ padding: 1px 7px;
8625
+ border-radius: 999px;
8626
+ background: var(--compose-control-bg);
8627
+ color: var(--site-text-secondary);
8628
+ font-size: 0.68rem;
8629
+ font-variant-numeric: tabular-nums;
8630
+ letter-spacing: 0.01em;
8631
+ }
8632
+
8633
+ /* "Editing" — the one thing a compose title ever said that you could not read
8634
+ off the composer itself: this post is already live. New / reply / thread
8635
+ all announce themselves through the parent post, the 2/3 markers and the
8636
+ submit label, so only this case earns a word.
8637
+ It takes the position marker's weight rather than an accent tint: it sits
8638
+ in that marker's slot, and everything else in this row that is not the
8639
+ format selector is deliberately quiet. Only the numerals are reset. */
8640
+ .compose-post-badge {
8641
+ font-variant-numeric: normal;
8642
+ letter-spacing: 0.02em;
8165
8643
  }
8166
8644
 
8167
8645
  /* Reply / edit-reply compose (a .compose-editor-row directly under
@@ -8176,7 +8654,8 @@
8176
8654
  padding-top: 14px;
8177
8655
  }
8178
8656
 
8179
- .compose-thread-post-header + .compose-body {
8657
+ /* The header now sits above every post, so it owns the gap to the body. */
8658
+ .compose-editor-row .compose-thread-post-header + .compose-body {
8180
8659
  padding-top: 8px;
8181
8660
  }
8182
8661
 
@@ -8184,55 +8663,12 @@
8184
8663
  padding-top: 12px;
8185
8664
  }
8186
8665
 
8187
- /* Thread format selector: pill-tag style (lighter than the main segmented) */
8188
- .compose-thread-segmented {
8189
- background: transparent;
8190
- box-shadow: none;
8191
- padding: 0;
8192
- gap: 4px;
8193
- }
8194
-
8195
- .compose-thread-segmented .compose-format-pill {
8196
- display: none;
8197
- }
8198
-
8199
- .compose-thread-segmented .compose-segmented-item {
8200
- flex: none;
8201
- font-size: var(--type-ui-caption);
8202
- padding: 3px 10px;
8203
- border-radius: 20px;
8204
- border: 1.5px solid
8205
- color-mix(in srgb, var(--site-text-secondary) 22%, transparent);
8206
- letter-spacing: 0.01em;
8207
- transition:
8208
- border-color 0.15s ease,
8209
- color 0.15s ease,
8210
- background-color 0.15s ease;
8211
- }
8212
-
8213
- .compose-thread-segmented
8214
- .compose-segmented-item:hover:not(.compose-segmented-item-active) {
8215
- border-color: color-mix(
8216
- in srgb,
8217
- var(--site-text-secondary) 50%,
8218
- transparent
8219
- );
8220
- color: var(--site-text-primary);
8221
- background: transparent;
8222
- }
8223
-
8224
- .compose-thread-segmented .compose-segmented-item-active {
8225
- color: var(--site-text-primary);
8226
- font-weight: var(--fw-medium);
8227
- border-color: color-mix(in srgb, var(--site-text-primary) 55%, transparent);
8228
- background: color-mix(in srgb, var(--site-text-primary) 7%, transparent);
8229
- }
8230
-
8231
8666
  /* Remove button (×) — hidden by default, revealed on thread post hover/focus */
8232
8667
  .compose-thread-post-remove {
8233
8668
  display: inline-flex;
8234
8669
  align-items: center;
8235
8670
  justify-content: center;
8671
+ margin-left: auto;
8236
8672
  width: 28px;
8237
8673
  height: 28px;
8238
8674
  border-radius: 8px;
@@ -8262,11 +8698,6 @@
8262
8698
  color: var(--site-text-primary);
8263
8699
  }
8264
8700
 
8265
- /* In thread compose, toolbar padding matches thread indent */
8266
- .compose-thread-compose-layout .compose-tools-row {
8267
- padding-inline-start: 0;
8268
- }
8269
-
8270
8701
  /* --- Alt text overlay panel (Threads-style, covers entire dialog) ------- */
8271
8702
 
8272
8703
  .compose-alt-panel {
@@ -8438,11 +8869,18 @@
8438
8869
  }
8439
8870
 
8440
8871
  .compose-draft-meta {
8872
+ display: flex;
8873
+ align-items: center;
8874
+ gap: 0.35rem;
8441
8875
  font-size: var(--type-ui-caption);
8442
8876
  color: var(--site-text-secondary);
8443
8877
  margin-top: 4px;
8444
8878
  }
8445
8879
 
8880
+ .compose-draft-format {
8881
+ font-weight: 500;
8882
+ }
8883
+
8446
8884
  .compose-draft-more {
8447
8885
  width: 40px;
8448
8886
  height: 40px;
@@ -8933,9 +9371,17 @@
8933
9371
  min-height: 0;
8934
9372
  }
8935
9373
 
9374
+ /* The floor is two lines, written as two lines. It is a target to click into
9375
+ on an empty post, not a reserved shape, so it should follow the type
9376
+ instead of a pixel count that goes stale when the leading moves. Every
9377
+ compose text surface shares it: a note's body and the "thoughts" under a
9378
+ link or a quote all open the same way and grow from there. */
8936
9379
  .compose-tiptap-body .tiptap {
8937
9380
  outline: none;
8938
- min-height: 120px;
9381
+ min-height: calc(var(--type-body-leading) * 2em);
9382
+ min-width: 0;
9383
+ max-width: 100%;
9384
+ overflow-x: auto;
8939
9385
  font-size: var(--type-content-body);
8940
9386
  line-height: var(--type-body-leading);
8941
9387
  letter-spacing: var(--type-body-tracking);
@@ -8948,14 +9394,6 @@
8948
9394
  line-height: var(--type-body-leading);
8949
9395
  }
8950
9396
 
8951
- .compose-tiptap-thoughts .tiptap {
8952
- min-height: 72px;
8953
- }
8954
-
8955
- .compose-tiptap-thoughts-quote .tiptap {
8956
- min-height: 56px;
8957
- }
8958
-
8959
9397
  .compose-tiptap-link .tiptap {
8960
9398
  color: var(--site-text-primary);
8961
9399
  }
@@ -9016,37 +9454,77 @@
9016
9454
  margin-bottom: 1rem;
9017
9455
  }
9018
9456
 
9019
- /* In inline-format compose (reply / edit-reply / thread — the editors wrapped
9020
- in .compose-editor-row), the format header sits right above the body, so the
9021
- first block's 1.4rem top margin reads as a gap between the format pills and
9022
- the input. Drop it there so the text hugs the header. Standalone new/edit
9023
- editors keep their natural lead-in. */
9024
- .compose-editor-row .compose-tiptap-body .tiptap > :first-child {
9457
+ /* The first block's 1.4rem top margin is a lead-in that only made sense when
9458
+ the body was the first thing in the editor. Every editor now renders a
9459
+ format header above it — and notes a title field too — so it reads as a gap
9460
+ instead. (This used to be scoped to .compose-editor-row, back when
9461
+ standalone new/edit editors did start with the body.) */
9462
+ .compose-tiptap-body .tiptap > :first-child {
9025
9463
  margin-top: 0;
9026
9464
  }
9027
9465
 
9028
9466
  .compose-tiptap-body .tiptap ul {
9029
9467
  list-style-type: disc;
9468
+ padding-left: 1.4em;
9469
+ margin: 1.25em 0;
9470
+ }
9471
+
9472
+ .compose-tiptap-body .tiptap ol {
9473
+ list-style-type: decimal;
9474
+ /* Keep multi-digit markers inside the editor's scrollable bounds. */
9475
+ padding-left: 2.25em;
9476
+ margin: 1.25em 0;
9477
+ }
9478
+
9479
+ .compose-tiptap-body .tiptap :is(ul, ol) :is(ul, ol) {
9030
9480
  padding-left: 1.5em;
9031
- margin: 0.25em 0;
9481
+ margin: 0.4em 0;
9482
+ }
9483
+
9484
+ .compose-tiptap-body .tiptap ul ul {
9485
+ list-style-type: circle;
9486
+ }
9487
+
9488
+ .compose-tiptap-body .tiptap ul ul ul {
9489
+ list-style-type: square;
9490
+ }
9491
+
9492
+ .compose-tiptap-body .tiptap ol ol {
9493
+ list-style-type: lower-alpha;
9494
+ }
9495
+
9496
+ .compose-tiptap-body .tiptap ol ol ol {
9497
+ list-style-type: lower-roman;
9498
+ }
9499
+
9500
+ .compose-tiptap-body .tiptap li {
9501
+ margin: 0.5em 0;
9502
+ }
9503
+
9504
+ .compose-tiptap-body .tiptap li > p:first-child {
9505
+ margin-top: 0;
9032
9506
  }
9033
9507
 
9034
- .compose-tiptap-body .tiptap ol {
9035
- list-style-type: decimal;
9036
- padding-left: 1.5em;
9037
- margin: 0.25em 0;
9508
+ .compose-tiptap-body .tiptap li > p:last-child {
9509
+ margin-bottom: 0;
9038
9510
  }
9039
9511
 
9040
- .compose-tiptap-body .tiptap li {
9041
- margin: 0.15em 0;
9512
+ /* A nested list makes its leading paragraph no longer the last child, so
9513
+ clear the body-paragraph margin that would otherwise separate the levels. */
9514
+ .compose-tiptap-body .tiptap li > p:has(+ :is(ul, ol)),
9515
+ .compose-tiptap-body
9516
+ .tiptap
9517
+ li
9518
+ > p:has(+ .ProseMirror-gapcursor + :is(ul, ol)) {
9519
+ margin-bottom: 0;
9042
9520
  }
9043
9521
 
9044
9522
  .compose-tiptap-body .tiptap blockquote {
9045
9523
  --_blockquote-bg: var(--compose-blockquote-bg);
9046
9524
  --_blockquote-text: var(--compose-blockquote-text);
9047
9525
  position: relative;
9048
- margin: 0.72em 0;
9049
- padding: 1.3rem 0.9em 0.76em 1em;
9526
+ margin: 1.4rem 0;
9527
+ padding: 1.4rem 1rem 0.75rem;
9050
9528
  border: none;
9051
9529
  border-radius: 6px;
9052
9530
  background: var(--_blockquote-bg);
@@ -9061,6 +9539,35 @@
9061
9539
  color 0.18s ease;
9062
9540
  }
9063
9541
 
9542
+ /* Reply compose deliberately stays denser than ordinary authored prose. */
9543
+ .compose-reply-compose-layout .compose-tiptap-body .tiptap ul {
9544
+ padding-left: 1.5em;
9545
+ margin: 0.25em 0;
9546
+ }
9547
+
9548
+ .compose-reply-compose-layout .compose-tiptap-body .tiptap ol {
9549
+ padding-left: 2.25em;
9550
+ margin: 0.25em 0;
9551
+ }
9552
+
9553
+ .compose-reply-compose-layout
9554
+ .compose-tiptap-body
9555
+ .tiptap
9556
+ :is(ul, ol)
9557
+ :is(ul, ol) {
9558
+ padding-left: 1.5em;
9559
+ margin: 0.2em 0;
9560
+ }
9561
+
9562
+ .compose-reply-compose-layout .compose-tiptap-body .tiptap li {
9563
+ margin: 0.15em 0;
9564
+ }
9565
+
9566
+ .compose-reply-compose-layout .compose-tiptap-body .tiptap blockquote {
9567
+ margin: 0.72em 0;
9568
+ padding: 1.3rem 0.9em 0.76em 1em;
9569
+ }
9570
+
9064
9571
  /* CJK: no italic for blockquotes (no true italic glyph) */
9065
9572
  :lang(zh) .compose-tiptap-body .tiptap blockquote,
9066
9573
  :lang(ja) .compose-tiptap-body .tiptap blockquote,
@@ -9141,9 +9648,9 @@
9141
9648
 
9142
9649
  .compose-tiptap-body .tiptap table {
9143
9650
  border-collapse: collapse;
9144
- width: 100%;
9651
+ width: max-content;
9652
+ min-width: 100%;
9145
9653
  margin: 0.75em 0;
9146
- overflow: hidden;
9147
9654
  }
9148
9655
 
9149
9656
  .compose-tiptap-body .tiptap td,
@@ -9151,7 +9658,7 @@
9151
9658
  border: 1px solid var(--site-divider);
9152
9659
  padding: 0.4em 0.6em;
9153
9660
  vertical-align: top;
9154
- min-width: 1em;
9661
+ min-width: 6rem;
9155
9662
  position: relative;
9156
9663
  }
9157
9664
 
@@ -9164,6 +9671,211 @@
9164
9671
  background: oklch(from var(--site-accent) l c h / 0.12);
9165
9672
  }
9166
9673
 
9674
+ /* Contextual table toolbar — deliberately quieter than the text bubble menu. */
9675
+ .tiptap-table-controls {
9676
+ position: fixed;
9677
+ z-index: 10001;
9678
+ align-items: center;
9679
+ gap: 2px;
9680
+ padding: 3px;
9681
+ border: 1px solid color-mix(in srgb, var(--site-divider) 58%, transparent);
9682
+ border-radius: 11px;
9683
+ background: color-mix(
9684
+ in srgb,
9685
+ var(--site-elevated-bg) 96%,
9686
+ var(--site-nav-hover-bg) 4%
9687
+ );
9688
+ box-shadow:
9689
+ 0 10px 24px oklch(from var(--site-text-primary) l c h / 0.07),
9690
+ 0 2px 5px oklch(from var(--site-text-primary) l c h / 0.035);
9691
+ }
9692
+
9693
+ .tiptap-table-control-btn {
9694
+ display: inline-flex;
9695
+ align-items: center;
9696
+ justify-content: center;
9697
+ width: 2rem;
9698
+ height: 2rem;
9699
+ border-radius: 8px;
9700
+ color: var(--site-text-secondary);
9701
+ cursor: pointer;
9702
+ transition:
9703
+ color 0.15s ease,
9704
+ background-color 0.15s ease;
9705
+ }
9706
+
9707
+ .tiptap-table-control-btn:hover,
9708
+ .tiptap-table-control-btn:focus-visible,
9709
+ .tiptap-table-control-btn[aria-expanded="true"] {
9710
+ color: var(--site-text-primary);
9711
+ background: color-mix(
9712
+ in srgb,
9713
+ var(--site-nav-hover-bg) 78%,
9714
+ var(--site-elevated-bg) 22%
9715
+ );
9716
+ }
9717
+
9718
+ .tiptap-table-control-btn:focus-visible,
9719
+ .tiptap-table-menu-item:focus-visible,
9720
+ .tiptap-table-size-cell:focus-visible {
9721
+ outline: 2px solid color-mix(in srgb, var(--site-accent) 72%, transparent);
9722
+ outline-offset: 1px;
9723
+ }
9724
+
9725
+ .tiptap-table-control-btn:disabled,
9726
+ .tiptap-table-menu-item:disabled {
9727
+ opacity: 0.35;
9728
+ cursor: not-allowed;
9729
+ }
9730
+
9731
+ .tiptap-table-options {
9732
+ position: absolute;
9733
+ top: calc(100% + 6px);
9734
+ left: 50%;
9735
+ width: min(13.5rem, calc(100vw - 20px));
9736
+ padding: 5px;
9737
+ border: 1px solid color-mix(in srgb, var(--site-divider) 58%, transparent);
9738
+ border-radius: 12px;
9739
+ background: var(--site-elevated-bg);
9740
+ box-shadow:
9741
+ 0 14px 28px oklch(from var(--site-text-primary) l c h / 0.08),
9742
+ 0 2px 6px oklch(from var(--site-text-primary) l c h / 0.04);
9743
+ transform: translateX(-50%);
9744
+ }
9745
+
9746
+ .tiptap-table-options[hidden] {
9747
+ display: none;
9748
+ }
9749
+
9750
+ .tiptap-table-menu-item {
9751
+ display: flex;
9752
+ align-items: center;
9753
+ width: 100%;
9754
+ min-height: 2.25rem;
9755
+ padding: 0.45rem 0.65rem;
9756
+ border-radius: 8px;
9757
+ color: var(--site-text-primary);
9758
+ font-size: var(--type-sm);
9759
+ line-height: 1.25;
9760
+ text-align: left;
9761
+ cursor: pointer;
9762
+ }
9763
+
9764
+ .tiptap-table-menu-item:hover,
9765
+ .tiptap-table-menu-item:focus-visible {
9766
+ background: var(--site-nav-hover-bg);
9767
+ }
9768
+
9769
+ .tiptap-table-menu-item.is-destructive {
9770
+ color: var(--destructive);
9771
+ }
9772
+
9773
+ .tiptap-table-options-separator {
9774
+ height: 1px;
9775
+ margin: 4px 5px;
9776
+ background: color-mix(in srgb, var(--site-divider) 58%, transparent);
9777
+ }
9778
+
9779
+ /* Slash-command table size picker. */
9780
+ .tiptap-table-size-picker {
9781
+ position: fixed;
9782
+ z-index: 10002;
9783
+ width: min(17rem, calc(100vw - 20px));
9784
+ padding: 10px;
9785
+ border: 1px solid color-mix(in srgb, var(--site-divider) 58%, transparent);
9786
+ border-radius: 13px;
9787
+ background: linear-gradient(
9788
+ 180deg,
9789
+ color-mix(
9790
+ in srgb,
9791
+ var(--site-elevated-bg) 96%,
9792
+ var(--site-nav-hover-bg) 4%
9793
+ )
9794
+ 0%,
9795
+ var(--site-elevated-bg) 100%
9796
+ );
9797
+ box-shadow:
9798
+ 0 14px 30px oklch(from var(--site-text-primary) l c h / 0.08),
9799
+ 0 2px 6px oklch(from var(--site-text-primary) l c h / 0.035);
9800
+ }
9801
+
9802
+ .tiptap-table-size-picker-header {
9803
+ display: flex;
9804
+ align-items: baseline;
9805
+ justify-content: space-between;
9806
+ gap: 1rem;
9807
+ padding: 0 2px 8px;
9808
+ color: var(--site-text-secondary);
9809
+ font-size: var(--type-xs);
9810
+ font-weight: var(--fw-medium);
9811
+ }
9812
+
9813
+ .tiptap-table-size-picker-dimensions {
9814
+ color: var(--site-text-primary);
9815
+ font-variant-numeric: tabular-nums;
9816
+ }
9817
+
9818
+ .tiptap-table-size-grid {
9819
+ display: grid;
9820
+ gap: 3px;
9821
+ }
9822
+
9823
+ .tiptap-table-size-grid-row {
9824
+ display: grid;
9825
+ grid-template-columns: repeat(8, minmax(0, 1fr));
9826
+ gap: 3px;
9827
+ }
9828
+
9829
+ .tiptap-table-size-cell {
9830
+ aspect-ratio: 1;
9831
+ min-width: 0;
9832
+ border: 1px solid color-mix(in srgb, var(--site-divider) 68%, transparent);
9833
+ border-radius: 4px;
9834
+ background: color-mix(
9835
+ in srgb,
9836
+ var(--site-elevated-bg) 90%,
9837
+ var(--site-nav-hover-bg) 10%
9838
+ );
9839
+ cursor: pointer;
9840
+ transition:
9841
+ border-color 0.1s ease,
9842
+ background-color 0.1s ease,
9843
+ transform 0.1s ease;
9844
+ }
9845
+
9846
+ .tiptap-table-size-cell.is-selected {
9847
+ border-color: color-mix(
9848
+ in srgb,
9849
+ var(--site-accent) 38%,
9850
+ var(--site-divider)
9851
+ );
9852
+ background: color-mix(
9853
+ in srgb,
9854
+ var(--site-accent) 14%,
9855
+ var(--site-elevated-bg)
9856
+ );
9857
+ }
9858
+
9859
+ .tiptap-table-size-cell.is-current {
9860
+ border-color: color-mix(
9861
+ in srgb,
9862
+ var(--site-accent) 72%,
9863
+ var(--site-divider)
9864
+ );
9865
+ transform: scale(0.9);
9866
+ }
9867
+
9868
+ @media (pointer: coarse) {
9869
+ .tiptap-table-control-btn {
9870
+ width: 2.75rem;
9871
+ height: 2.75rem;
9872
+ }
9873
+
9874
+ .tiptap-table-menu-item {
9875
+ min-height: 2.75rem;
9876
+ }
9877
+ }
9878
+
9167
9879
  /* --- Image figure in editor ------------------------------------------- */
9168
9880
 
9169
9881
  .tiptap-image-figure {
@@ -9847,10 +10559,10 @@
9847
10559
  }
9848
10560
 
9849
10561
  .compose-tiptap-body .tiptap .tiptap-footnote-reference {
9850
- font-size: 0.75em;
10562
+ font-size: var(--type-footnote-ref);
9851
10563
  line-height: 1;
9852
10564
  vertical-align: super;
9853
- color: var(--site-accent);
10565
+ color: var(--site-footnote-marker);
9854
10566
  white-space: nowrap;
9855
10567
  cursor: pointer;
9856
10568
  }
@@ -9858,6 +10570,7 @@
9858
10570
  .compose-tiptap-body
9859
10571
  .tiptap
9860
10572
  .tiptap-footnote-reference.ProseMirror-selectednode {
10573
+ color: var(--site-accent);
9861
10574
  outline: 1px solid var(--site-accent);
9862
10575
  outline-offset: 2px;
9863
10576
  border-radius: 4px;
@@ -9872,7 +10585,7 @@
9872
10585
  }
9873
10586
 
9874
10587
  .compose-tiptap-body .tiptap .tiptap-footnote-definition::before {
9875
- content: attr(data-footnote-label);
10588
+ content: "[^" attr(data-footnote-label) "]:";
9876
10589
  grid-column: 1;
9877
10590
  align-self: start;
9878
10591
  color: var(--site-text-secondary);
@@ -10577,12 +11290,16 @@
10577
11290
  var(--site-page-bg) 88%,
10578
11291
  var(--site-elevated-bg) 12%
10579
11292
  );
11293
+ }
10580
11294
 
10581
- overflow: visible;
11295
+ /* Scoped to the collapsed state. Unscoped it also outranked
11296
+ `.compose-reply-context.expanded` — same specificity, declared later — so
11297
+ fullscreen's "Show more" expanded a box that was still pinned at 140px. */
11298
+ .compose-fullscreen-thread-layout .compose-reply-context:not(.expanded) {
11299
+ max-height: 140px;
10582
11300
  }
10583
11301
 
10584
11302
  .compose-fullscreen-thread-layout .compose-reply-context {
10585
- max-height: 140px;
10586
11303
  padding: 0.08rem 0 0.08rem;
10587
11304
  }
10588
11305
 
@@ -10596,12 +11313,7 @@
10596
11313
  color: var(--site-text-secondary);
10597
11314
  }
10598
11315
 
10599
- .compose-fullscreen-thread-layout .compose-thread-dot {
10600
- margin-top: 0.6rem;
10601
- }
10602
-
10603
11316
  .compose-fullscreen-editor-row {
10604
- overflow: visible;
10605
11317
  padding-top: 1.5rem;
10606
11318
  }
10607
11319
 
@@ -10617,10 +11329,9 @@
10617
11329
  gap: 0.7rem;
10618
11330
  }
10619
11331
 
10620
- .compose-fullscreen-editor-row .compose-thread-dot {
10621
- margin-top: 0.95rem;
10622
- }
10623
-
11332
+ /* Stand-in for a hidden title: reads as the field's own placeholder, and
11333
+ clicking it swaps in the real input. Fullscreen has no toolbar to put the
11334
+ `T` toggle on, so this is the way back to a title from here. */
10624
11335
  .compose-fullscreen-title-placeholder {
10625
11336
  display: block;
10626
11337
  width: 100%;
@@ -10852,6 +11563,13 @@
10852
11563
 
10853
11564
  /* --- View toggle -------------------------------------------------------- */
10854
11565
 
11566
+ .archive-toolbar-toggles {
11567
+ display: flex;
11568
+ align-items: center;
11569
+ gap: 6px;
11570
+ flex-shrink: 0;
11571
+ }
11572
+
10855
11573
  .archive-view-toggle {
10856
11574
  display: flex;
10857
11575
  align-items: center;
@@ -12157,6 +12875,168 @@
12157
12875
  }
12158
12876
  }
12159
12877
 
12878
+ /* ---------------------------------------------------------------------------
12879
+ * Wide public-post footnote rail.
12880
+ *
12881
+ * Kept OUTSIDE @layer components so the runtime layout can override the
12882
+ * unlayered `.prose` list rules. The source HTML remains one natural endnote
12883
+ * list; these selectors only apply after the client has measured a wide detail
12884
+ * or timeline container and confirmed that the rail fits safely.
12885
+ * ------------------------------------------------------------------------- */
12886
+
12887
+ :is(
12888
+ [data-post-view] [data-page="post"] [data-post-body].post-detail-body,
12889
+ [data-timeline-item] article[data-post] > [data-post-body].prose
12890
+ ) {
12891
+ --jant-footnote-rail-enabled: 0;
12892
+ }
12893
+
12894
+ @media screen {
12895
+ @container post-view (min-width: 56rem) {
12896
+ :is(
12897
+ [data-post-view] [data-page="post"] [data-post-body].post-detail-body,
12898
+ [data-timeline-item] article[data-post] > [data-post-body].prose
12899
+ ) {
12900
+ --jant-footnote-rail-enabled: 1;
12901
+ }
12902
+
12903
+ [data-post-body].prose:is(.footnote-rail-measuring, .footnote-rail-ready) {
12904
+ position: relative;
12905
+ }
12906
+
12907
+ [data-post-body].prose:is(.footnote-rail-measuring, .footnote-rail-ready)
12908
+ > .footnote-endnotes {
12909
+ position: absolute;
12910
+ inset-block-start: 0;
12911
+ inset-inline-start: calc(100% + var(--layout-sidenote-gap));
12912
+ width: var(--layout-sidenote-width);
12913
+ margin: 0;
12914
+ padding: 0;
12915
+ border: 0;
12916
+ }
12917
+
12918
+ [data-post-body].prose:is(.footnote-rail-measuring, .footnote-rail-ready)
12919
+ > .footnote-endnotes
12920
+ > .footnote-list {
12921
+ margin: 0;
12922
+ padding: 0;
12923
+ list-style: none;
12924
+ row-gap: 0.75rem;
12925
+ }
12926
+
12927
+ [data-post-body].prose:is(.footnote-rail-measuring, .footnote-rail-ready)
12928
+ > .footnote-endnotes
12929
+ > .footnote-list
12930
+ > .footnote {
12931
+ display: grid;
12932
+ grid-template-columns: max-content minmax(0, 1fr);
12933
+ column-gap: 1ch;
12934
+ align-items: baseline;
12935
+ min-width: 0;
12936
+ margin: 0;
12937
+ padding: 0;
12938
+ list-style: none;
12939
+ }
12940
+
12941
+ [data-post-body].prose:is(.footnote-rail-measuring, .footnote-rail-ready)
12942
+ > .footnote-endnotes
12943
+ > .footnote-list
12944
+ > .footnote::before {
12945
+ content: var(--footnote-rail-number);
12946
+ grid-column: 1;
12947
+ grid-row: 1;
12948
+ font-size: inherit;
12949
+ line-height: inherit;
12950
+ font-variant-numeric: tabular-nums;
12951
+ color: var(--site-footnote-marker);
12952
+ }
12953
+
12954
+ [data-post-body].prose:is(.footnote-rail-measuring, .footnote-rail-ready)
12955
+ > .footnote-endnotes
12956
+ > .footnote-list
12957
+ > .footnote
12958
+ > * {
12959
+ grid-column: 2;
12960
+ min-width: 0;
12961
+ }
12962
+
12963
+ [data-post-body].prose:is(.footnote-rail-measuring, .footnote-rail-ready)
12964
+ > .footnote-endnotes
12965
+ > .footnote-list
12966
+ > .footnote
12967
+ > :first-child {
12968
+ grid-row: 1;
12969
+ }
12970
+
12971
+ [data-post-body].prose.footnote-rail-measuring > .footnote-endnotes {
12972
+ visibility: hidden;
12973
+ }
12974
+
12975
+ [data-post-body].prose.footnote-rail-measuring
12976
+ > .footnote-endnotes
12977
+ > .footnote-list {
12978
+ display: grid;
12979
+ }
12980
+
12981
+ [data-post-body].prose.footnote-rail-ready
12982
+ > .footnote-endnotes
12983
+ > .footnote-list {
12984
+ position: relative;
12985
+ display: block;
12986
+ block-size: var(--footnote-rail-block-size);
12987
+ }
12988
+
12989
+ [data-post-body].prose.footnote-rail-ready
12990
+ > .footnote-endnotes
12991
+ > .footnote-list
12992
+ > .footnote {
12993
+ position: absolute;
12994
+ inset-block-start: var(--footnote-rail-y);
12995
+ inset-inline: 0;
12996
+ width: 100%;
12997
+ }
12998
+
12999
+ /* The note is already beside its first reference. Keep reciprocal links in
13000
+ the DOM for keyboard and assistive-technology users, revealing them when
13001
+ focus enters rather than adding a persistent arrow to every rail item. */
13002
+ [data-post-body].prose.footnote-rail-ready .footnote-backlinks {
13003
+ position: absolute;
13004
+ width: 1px;
13005
+ height: 1px;
13006
+ padding: 0;
13007
+ margin: -1px;
13008
+ overflow: hidden;
13009
+ clip: rect(0, 0, 0, 0);
13010
+ clip-path: inset(50%);
13011
+ white-space: nowrap;
13012
+ border: 0;
13013
+ }
13014
+
13015
+ [data-post-body].prose.footnote-rail-ready
13016
+ .footnote-backlinks:focus-within {
13017
+ position: static;
13018
+ grid-column: 2;
13019
+ width: auto;
13020
+ height: auto;
13021
+ margin: 0;
13022
+ overflow: visible;
13023
+ clip: auto;
13024
+ clip-path: none;
13025
+ white-space: normal;
13026
+ }
13027
+
13028
+ [data-post-body].prose.footnote-rail-ready .footnote:target {
13029
+ background: transparent;
13030
+ }
13031
+ }
13032
+ }
13033
+
13034
+ @media print {
13035
+ [data-post-body].prose:is(.footnote-rail-measuring, .footnote-rail-ready) {
13036
+ min-block-size: auto !important;
13037
+ }
13038
+ }
13039
+
12160
13040
  /* ---------------------------------------------------------------------------
12161
13041
  * Home description — editorial "lede" paragraph.
12162
13042
  *