@meith/web 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (511) hide show
  1. package/LICENSE.md +165 -0
  2. package/app/(auth)/layout.tsx +6 -0
  3. package/app/(auth)/login/page.tsx +106 -0
  4. package/app/(auth)/login/verify/page.tsx +55 -0
  5. package/app/(auth)/register/page.tsx +83 -0
  6. package/app/(auth)/reset/confirm/page.tsx +45 -0
  7. package/app/(auth)/reset/page.tsx +22 -0
  8. package/app/(auth)/verify/page.tsx +44 -0
  9. package/app/(auth)/verify/resend/page.tsx +51 -0
  10. package/app/(board)/[slug]/feed.xml/route.ts +48 -0
  11. package/app/(board)/[slug]/new/page.tsx +112 -0
  12. package/app/(board)/[slug]/page.tsx +403 -0
  13. package/app/(board)/discover/[view]/page.tsx +115 -0
  14. package/app/(board)/discover/page.tsx +5 -0
  15. package/app/(board)/layout.tsx +22 -0
  16. package/app/(board)/member/[id]/page.tsx +187 -0
  17. package/app/(board)/member/[id]/reputation/page.tsx +195 -0
  18. package/app/(board)/member/by-name/[name]/page.tsx +32 -0
  19. package/app/(board)/messages/[id]/page.tsx +103 -0
  20. package/app/(board)/messages/compose/page.tsx +60 -0
  21. package/app/(board)/messages/layout.tsx +5 -0
  22. package/app/(board)/messages/page.tsx +179 -0
  23. package/app/(board)/modcp/forums/page.tsx +59 -0
  24. package/app/(board)/modcp/ip/page.tsx +158 -0
  25. package/app/(board)/modcp/layout.tsx +5 -0
  26. package/app/(board)/modcp/log/page.tsx +98 -0
  27. package/app/(board)/modcp/page.tsx +112 -0
  28. package/app/(board)/moderation/layout.tsx +5 -0
  29. package/app/(board)/moderation/page.tsx +111 -0
  30. package/app/(board)/moderation/reports/page.tsx +193 -0
  31. package/app/(board)/moderation/warn/page.tsx +158 -0
  32. package/app/(board)/notifications/layout.tsx +5 -0
  33. package/app/(board)/notifications/page.tsx +144 -0
  34. package/app/(board)/notifications/preferences/page.tsx +65 -0
  35. package/app/(board)/online/page.tsx +119 -0
  36. package/app/(board)/page.tsx +195 -0
  37. package/app/(board)/plugins/[key]/[[...path]]/page.tsx +72 -0
  38. package/app/(board)/privacy/page.tsx +12 -0
  39. package/app/(board)/report/page.tsx +71 -0
  40. package/app/(board)/rules/page.tsx +12 -0
  41. package/app/(board)/search/[token]/page.tsx +130 -0
  42. package/app/(board)/search/page.tsx +289 -0
  43. package/app/(board)/stats/page.tsx +202 -0
  44. package/app/(board)/subscriptions/layout.tsx +5 -0
  45. package/app/(board)/subscriptions/page.tsx +142 -0
  46. package/app/(board)/terms/page.tsx +12 -0
  47. package/app/(board)/thread/[slug]/edit/page.tsx +107 -0
  48. package/app/(board)/thread/[slug]/feed.xml/route.ts +42 -0
  49. package/app/(board)/thread/[slug]/page.tsx +707 -0
  50. package/app/(board)/thread/[slug]/poll/page.tsx +41 -0
  51. package/app/(board)/thread/[slug]/post/[postId]/history/page.tsx +160 -0
  52. package/app/(board)/thread/[slug]/reply/page.tsx +136 -0
  53. package/app/(board)/unsubscribe/page.tsx +73 -0
  54. package/app/(board)/usercp/avatar/page.tsx +56 -0
  55. package/app/(board)/usercp/contacts/page.tsx +141 -0
  56. package/app/(board)/usercp/email/confirm/page.tsx +52 -0
  57. package/app/(board)/usercp/layout.tsx +23 -0
  58. package/app/(board)/usercp/options/page.tsx +56 -0
  59. package/app/(board)/usercp/page.tsx +71 -0
  60. package/app/(board)/usercp/profile/page.tsx +81 -0
  61. package/app/(board)/usercp/security/page.tsx +238 -0
  62. package/app/(board)/usercp/security/verify/page.tsx +92 -0
  63. package/app/(board)/usercp/signature/page.tsx +67 -0
  64. package/app/admin/antispam/page.tsx +155 -0
  65. package/app/admin/api/marketplace/screenshot/route.ts +68 -0
  66. package/app/admin/api/plugins/[key]/[...path]/route.ts +27 -0
  67. package/app/admin/api-tokens/page.tsx +129 -0
  68. package/app/admin/content/announcements/page.tsx +110 -0
  69. package/app/admin/content/attachments/page.tsx +199 -0
  70. package/app/admin/content/navigation/page.tsx +123 -0
  71. package/app/admin/content/page.tsx +170 -0
  72. package/app/admin/forums/[id]/page.tsx +96 -0
  73. package/app/admin/forums/[id]/permissions/page.tsx +108 -0
  74. package/app/admin/forums/page.tsx +45 -0
  75. package/app/admin/groups/[id]/page.tsx +133 -0
  76. package/app/admin/groups/memberships/page.tsx +56 -0
  77. package/app/admin/groups/page.tsx +104 -0
  78. package/app/admin/groups/promotions/page.tsx +143 -0
  79. package/app/admin/layout.tsx +86 -0
  80. package/app/admin/log/page.tsx +116 -0
  81. package/app/admin/page.tsx +206 -0
  82. package/app/admin/plugins/[key]/[[...path]]/page.tsx +276 -0
  83. package/app/admin/plugins/browse/page.tsx +70 -0
  84. package/app/admin/plugins/page.tsx +165 -0
  85. package/app/admin/security/page.tsx +124 -0
  86. package/app/admin/settings/page.tsx +190 -0
  87. package/app/admin/system/page.tsx +303 -0
  88. package/app/admin/themes/[key]/page.tsx +110 -0
  89. package/app/admin/themes/browse/page.tsx +70 -0
  90. package/app/admin/themes/page.tsx +163 -0
  91. package/app/admin/users/[id]/merge/page.tsx +136 -0
  92. package/app/admin/users/[id]/page.tsx +208 -0
  93. package/app/admin/users/mail/page.tsx +68 -0
  94. package/app/admin/users/page.tsx +260 -0
  95. package/app/admin/users/prune/page.tsx +186 -0
  96. package/app/api/health/route.ts +11 -0
  97. package/app/api/metrics/route.ts +66 -0
  98. package/app/api/plugins/[key]/[...path]/route.ts +27 -0
  99. package/app/api/read/all/route.ts +15 -0
  100. package/app/api/read/forum/[id]/route.ts +29 -0
  101. package/app/api/read/thread/[id]/route.ts +40 -0
  102. package/app/api/ready/route.ts +63 -0
  103. package/app/api/system/tick/route.ts +77 -0
  104. package/app/api/v1/[...path]/route.ts +208 -0
  105. package/app/api/v1/openapi.json/route.ts +12 -0
  106. package/app/apple-icon.ts +11 -0
  107. package/app/atom.xml/route.ts +7 -0
  108. package/app/attachment/[id]/route.ts +10 -0
  109. package/app/attachment/[id]/thumb/route.ts +10 -0
  110. package/app/auth/passkey/options/route.ts +145 -0
  111. package/app/auth/passkey/verify/route.ts +241 -0
  112. package/app/auth/resume/route.ts +55 -0
  113. package/app/auth/sso/[provider]/callback/route.ts +173 -0
  114. package/app/auth/sso/[provider]/go/route.ts +57 -0
  115. package/app/auth/sso/[provider]/route.ts +105 -0
  116. package/app/avatar/[id]/route.ts +31 -0
  117. package/app/brand/icon-192.png/route.ts +7 -0
  118. package/app/brand/icon-512.png/route.ts +7 -0
  119. package/app/brand/icon-maskable.png/route.ts +7 -0
  120. package/app/demo/stripe/[[...path]]/route.ts +27 -0
  121. package/app/error.tsx +15 -0
  122. package/app/feed.xml/route.ts +7 -0
  123. package/app/forumdisplay.php/page.tsx +11 -0
  124. package/app/group/[id]/badge/[scheme]/route.ts +26 -0
  125. package/app/icon.ts +13 -0
  126. package/app/install/page.tsx +221 -0
  127. package/app/jump/page.tsx +28 -0
  128. package/app/layout.tsx +124 -0
  129. package/app/logo/[scheme]/route.ts +24 -0
  130. package/app/manifest.ts +43 -0
  131. package/app/member.php/page.tsx +11 -0
  132. package/app/memberlist.php/page.tsx +11 -0
  133. package/app/not-found.tsx +14 -0
  134. package/app/opengraph-image.ts +11 -0
  135. package/app/redirect/page.tsx +44 -0
  136. package/app/robots.txt/route.ts +35 -0
  137. package/app/showthread.php/page.tsx +11 -0
  138. package/app/sitemap/[page]/route.ts +64 -0
  139. package/app/sitemap.xml/route.ts +31 -0
  140. package/app/twitter-image.ts +11 -0
  141. package/app/viewforum.php/page.tsx +11 -0
  142. package/app/viewtopic.php/page.tsx +11 -0
  143. package/bin/forum-web.mjs +241 -0
  144. package/components.json +21 -0
  145. package/instrumentation.ts +18 -0
  146. package/next.config.mjs +182 -0
  147. package/package.json +99 -0
  148. package/postcss.config.mjs +7 -0
  149. package/proxy.ts +160 -0
  150. package/src/components/account/active-sessions.tsx +87 -0
  151. package/src/components/account/avatar-form.tsx +119 -0
  152. package/src/components/account/credential-proof-form.tsx +76 -0
  153. package/src/components/account/logout-form.tsx +14 -0
  154. package/src/components/account/notification-forms.tsx +157 -0
  155. package/src/components/account/passkey-enrol.tsx +81 -0
  156. package/src/components/account/passkey-proof-button.tsx +47 -0
  157. package/src/components/account/push-device-form.tsx +193 -0
  158. package/src/components/account/relation-forms.tsx +69 -0
  159. package/src/components/account/reputation-forms.tsx +124 -0
  160. package/src/components/account/security-activity.tsx +46 -0
  161. package/src/components/account/sign-in-methods.tsx +166 -0
  162. package/src/components/account/subscription-forms.tsx +151 -0
  163. package/src/components/account/two-factor-forms.tsx +226 -0
  164. package/src/components/account/usercp-forms.tsx +387 -0
  165. package/src/components/account/usercp-nav.tsx +14 -0
  166. package/src/components/account/usercp-shell.tsx +34 -0
  167. package/src/components/admin/admin-forms.tsx +92 -0
  168. package/src/components/admin/admin-nav.tsx +52 -0
  169. package/src/components/admin/admin-undo.tsx +32 -0
  170. package/src/components/admin/api-token-forms.tsx +89 -0
  171. package/src/components/admin/badge-forms.tsx +97 -0
  172. package/src/components/admin/branding-forms.tsx +102 -0
  173. package/src/components/admin/content-forms.tsx +712 -0
  174. package/src/components/admin/form-bits.tsx +11 -0
  175. package/src/components/admin/forum-forms.tsx +524 -0
  176. package/src/components/admin/forum-tree.tsx +454 -0
  177. package/src/components/admin/group-forms.tsx +646 -0
  178. package/src/components/admin/mail-test-card.tsx +90 -0
  179. package/src/components/admin/marketplace-forms.tsx +30 -0
  180. package/src/components/admin/marketplace-listing.tsx +123 -0
  181. package/src/components/admin/navigation-forms.tsx +229 -0
  182. package/src/components/admin/navigation-tree.tsx +442 -0
  183. package/src/components/admin/oklch-picker.tsx +180 -0
  184. package/src/components/admin/plugin-forms.tsx +225 -0
  185. package/src/components/admin/settings-form.tsx +148 -0
  186. package/src/components/admin/system-forms.tsx +169 -0
  187. package/src/components/admin/theme-changes.tsx +354 -0
  188. package/src/components/admin/theme-forms.tsx +636 -0
  189. package/src/components/admin/theme-palette.tsx +119 -0
  190. package/src/components/admin/theme-preview.tsx +461 -0
  191. package/src/components/admin/user-bulk-toolbar.tsx +99 -0
  192. package/src/components/admin/user-forms.tsx +488 -0
  193. package/src/components/auth/auth-page.tsx +45 -0
  194. package/src/components/auth/form-controls.tsx +117 -0
  195. package/src/components/auth/login-form.tsx +47 -0
  196. package/src/components/auth/passkey-client.ts +171 -0
  197. package/src/components/auth/passkey-second-factor.tsx +69 -0
  198. package/src/components/auth/passkey-sign-in.tsx +61 -0
  199. package/src/components/auth/register-form.tsx +136 -0
  200. package/src/components/auth/resend-verification-form.tsx +34 -0
  201. package/src/components/auth/reset-confirm-form.tsx +43 -0
  202. package/src/components/auth/reset-request-form.tsx +35 -0
  203. package/src/components/auth/second-factor-form.tsx +51 -0
  204. package/src/components/auth/sso-buttons.tsx +47 -0
  205. package/src/components/board/live-region.tsx +74 -0
  206. package/src/components/board/search-off-notice.tsx +18 -0
  207. package/src/components/board/section-page.tsx +114 -0
  208. package/src/components/content/attachment-field.tsx +51 -0
  209. package/src/components/content/composer-field.ts +31 -0
  210. package/src/components/content/composer-ids.ts +6 -0
  211. package/src/components/content/composer-intents.tsx +27 -0
  212. package/src/components/content/composer-recovery.tsx +215 -0
  213. package/src/components/content/edit-post-form.tsx +121 -0
  214. package/src/components/content/markdown-editor.tsx +590 -0
  215. package/src/components/content/markdown-syntax.ts +53 -0
  216. package/src/components/content/mention-token.ts +23 -0
  217. package/src/components/content/multiquote-button.tsx +32 -0
  218. package/src/components/content/multiquote-selection.tsx +105 -0
  219. package/src/components/content/multiquote.ts +125 -0
  220. package/src/components/content/new-thread-form.tsx +169 -0
  221. package/src/components/content/poll-edit-form.tsx +96 -0
  222. package/src/components/content/poll.tsx +89 -0
  223. package/src/components/content/quote-in-place.tsx +51 -0
  224. package/src/components/content/reply-form.tsx +113 -0
  225. package/src/components/content/thanks-button.tsx +65 -0
  226. package/src/components/content/thread-rating.tsx +143 -0
  227. package/src/components/install/install-form.tsx +497 -0
  228. package/src/components/legal/legal-document.tsx +22 -0
  229. package/src/components/messages/message-forms.tsx +152 -0
  230. package/src/components/moderation/inline-moderation-form.tsx +158 -0
  231. package/src/components/moderation/modcp-nav.tsx +15 -0
  232. package/src/components/moderation/modcp-shell.tsx +41 -0
  233. package/src/components/moderation/queue-form.tsx +96 -0
  234. package/src/components/moderation/report-forms.tsx +120 -0
  235. package/src/components/moderation/signature-lock-form.tsx +83 -0
  236. package/src/components/moderation/thread-surgery-form.tsx +96 -0
  237. package/src/components/moderation/thread-tools-form.tsx +137 -0
  238. package/src/components/moderation/warning-forms.tsx +125 -0
  239. package/src/components/profile/custom-field.tsx +79 -0
  240. package/src/components/shell/appearance-icons.tsx +35 -0
  241. package/src/components/shell/board-notice.tsx +21 -0
  242. package/src/components/shell/copy-record.ts +20 -0
  243. package/src/components/shell/copy.tsx +17 -0
  244. package/src/components/shell/crash-notice.tsx +86 -0
  245. package/src/components/shell/demo-banner.tsx +49 -0
  246. package/src/components/shell/group-name-style.tsx +6 -0
  247. package/src/components/shell/measure.ts +1 -0
  248. package/src/components/shell/notification-menu.tsx +361 -0
  249. package/src/components/shell/offline-notice.tsx +29 -0
  250. package/src/components/shell/onboarding-banner.tsx +39 -0
  251. package/src/components/shell/page-shell.tsx +167 -0
  252. package/src/components/shell/panel-list.tsx +95 -0
  253. package/src/components/shell/panel-nav.tsx +40 -0
  254. package/src/components/shell/panel-overview.tsx +81 -0
  255. package/src/components/shell/panel-page.tsx +96 -0
  256. package/src/components/shell/panel-pagination.tsx +28 -0
  257. package/src/components/shell/panel-shell.tsx +31 -0
  258. package/src/components/shell/theme-runtime-style.tsx +6 -0
  259. package/src/components/shell/theme-switcher.tsx +97 -0
  260. package/src/components/shell/timezone-probe.tsx +45 -0
  261. package/src/components/shell/view-tabs.tsx +60 -0
  262. package/src/config.ts +13 -0
  263. package/src/server/absent-services.ts +29 -0
  264. package/src/server/admin-actions.ts +171 -0
  265. package/src/server/admin-settings-actions.ts +83 -0
  266. package/src/server/admin-undo-actions.ts +70 -0
  267. package/src/server/admin-undo.ts +75 -0
  268. package/src/server/admin.ts +112 -0
  269. package/src/server/announcements.ts +82 -0
  270. package/src/server/antispam.ts +310 -0
  271. package/src/server/api/content.ts +486 -0
  272. package/src/server/api/http.ts +119 -0
  273. package/src/server/api/members.ts +109 -0
  274. package/src/server/api/messages.ts +113 -0
  275. package/src/server/api/registry.ts +30 -0
  276. package/src/server/api/search.ts +98 -0
  277. package/src/server/api/subscriptions.ts +106 -0
  278. package/src/server/api-auth.ts +66 -0
  279. package/src/server/api-token-actions.ts +85 -0
  280. package/src/server/api-tokens-admin.ts +95 -0
  281. package/src/server/appearance-actions.ts +39 -0
  282. package/src/server/attachment-download.ts +43 -0
  283. package/src/server/attachment-embed.ts +63 -0
  284. package/src/server/attachment-upload-actions.ts +92 -0
  285. package/src/server/attachments.ts +257 -0
  286. package/src/server/auth-actions.ts +479 -0
  287. package/src/server/auth-config.ts +61 -0
  288. package/src/server/auth-events.ts +92 -0
  289. package/src/server/auth-form-state.ts +14 -0
  290. package/src/server/auth-mail.ts +104 -0
  291. package/src/server/avatars.ts +83 -0
  292. package/src/server/board-latest-actions.ts +7 -0
  293. package/src/server/board-latest.tsx +103 -0
  294. package/src/server/board-offline.ts +23 -0
  295. package/src/server/board-url.ts +14 -0
  296. package/src/server/brand-assets.ts +151 -0
  297. package/src/server/brand-mark.tsx +110 -0
  298. package/src/server/branding-actions.ts +53 -0
  299. package/src/server/branding.ts +101 -0
  300. package/src/server/cache-targets.ts +13 -0
  301. package/src/server/code-highlighting.ts +33 -0
  302. package/src/server/container.ts +402 -0
  303. package/src/server/content-actions.ts +459 -0
  304. package/src/server/content-admin-actions.ts +492 -0
  305. package/src/server/content-admin.ts +95 -0
  306. package/src/server/content-security-policy.ts +35 -0
  307. package/src/server/context.ts +32 -0
  308. package/src/server/cookies.ts +139 -0
  309. package/src/server/credential-proof-actions.ts +42 -0
  310. package/src/server/credential-proof.ts +35 -0
  311. package/src/server/current-location.ts +19 -0
  312. package/src/server/demo-stripe.ts +104 -0
  313. package/src/server/demo-uploads.ts +28 -0
  314. package/src/server/demo.ts +80 -0
  315. package/src/server/discovery.ts +104 -0
  316. package/src/server/embeds.ts +113 -0
  317. package/src/server/error-notice.tsx +41 -0
  318. package/src/server/federation-actions.ts +113 -0
  319. package/src/server/federation.ts +138 -0
  320. package/src/server/feed-builder.ts +72 -0
  321. package/src/server/feed-routes.ts +122 -0
  322. package/src/server/fixture-actor-source.ts +62 -0
  323. package/src/server/fixture-forum-repo.ts +59 -0
  324. package/src/server/fixture-member-profile-repo.ts +30 -0
  325. package/src/server/fixture-post-repo.ts +108 -0
  326. package/src/server/fixture-thread-repo.ts +98 -0
  327. package/src/server/form-state-reporter.ts +34 -0
  328. package/src/server/form-values.ts +23 -0
  329. package/src/server/forum-admin-actions.ts +353 -0
  330. package/src/server/forum-admin.ts +98 -0
  331. package/src/server/group-admin-actions.ts +341 -0
  332. package/src/server/group-admin.ts +98 -0
  333. package/src/server/group-badge-actions.ts +61 -0
  334. package/src/server/group-badge.ts +62 -0
  335. package/src/server/group-identity.ts +104 -0
  336. package/src/server/i18n-catalogs.ts +42 -0
  337. package/src/server/i18n.ts +69 -0
  338. package/src/server/image-upload.ts +109 -0
  339. package/src/server/inline-moderation-actions.ts +129 -0
  340. package/src/server/install-actions.ts +124 -0
  341. package/src/server/install.ts +281 -0
  342. package/src/server/legacy-redirect.ts +83 -0
  343. package/src/server/legal.ts +43 -0
  344. package/src/server/location-header.ts +37 -0
  345. package/src/server/mail-brand.ts +43 -0
  346. package/src/server/mail-health.ts +41 -0
  347. package/src/server/mail-send.ts +44 -0
  348. package/src/server/mail-test-actions.ts +67 -0
  349. package/src/server/mail-test.ts +56 -0
  350. package/src/server/markdown-pipeline.ts +31 -0
  351. package/src/server/marketplace-actions.ts +47 -0
  352. package/src/server/marketplace-admin.ts +260 -0
  353. package/src/server/mention-actions.ts +21 -0
  354. package/src/server/mention-search.ts +35 -0
  355. package/src/server/message-actions.ts +110 -0
  356. package/src/server/messages.ts +141 -0
  357. package/src/server/modcp.ts +125 -0
  358. package/src/server/moderation-actions.ts +165 -0
  359. package/src/server/navigation-admin-actions.ts +217 -0
  360. package/src/server/navigation.ts +112 -0
  361. package/src/server/nonce.ts +13 -0
  362. package/src/server/notification-actions.ts +84 -0
  363. package/src/server/notification-audience.ts +10 -0
  364. package/src/server/notification-menu-actions.ts +82 -0
  365. package/src/server/notification-menu.ts +96 -0
  366. package/src/server/notifications.ts +93 -0
  367. package/src/server/onboarding-actions.ts +27 -0
  368. package/src/server/onboarding.ts +89 -0
  369. package/src/server/page-metadata.ts +14 -0
  370. package/src/server/passkey-challenge.ts +42 -0
  371. package/src/server/plugin-admin-actions.ts +232 -0
  372. package/src/server/plugin-admin.ts +276 -0
  373. package/src/server/plugin-host.ts +143 -0
  374. package/src/server/plugin-pages.ts +192 -0
  375. package/src/server/plugin-panel.ts +49 -0
  376. package/src/server/plugin-routes.ts +298 -0
  377. package/src/server/plugin-view.tsx +58 -0
  378. package/src/server/poll-actions.ts +104 -0
  379. package/src/server/poll-scope.ts +52 -0
  380. package/src/server/post-notifications.ts +71 -0
  381. package/src/server/post-scope.ts +86 -0
  382. package/src/server/presence.ts +177 -0
  383. package/src/server/profile-fields.ts +112 -0
  384. package/src/server/push-actions.ts +113 -0
  385. package/src/server/push.ts +40 -0
  386. package/src/server/redirect-back.ts +15 -0
  387. package/src/server/registration.ts +7 -0
  388. package/src/server/relation-actions.ts +89 -0
  389. package/src/server/relations.ts +45 -0
  390. package/src/server/reply-core.ts +173 -0
  391. package/src/server/report-actions.ts +165 -0
  392. package/src/server/report-scope.ts +21 -0
  393. package/src/server/reputation-actions.ts +146 -0
  394. package/src/server/reputation-target.ts +49 -0
  395. package/src/server/reputation.ts +70 -0
  396. package/src/server/request-fingerprint.ts +65 -0
  397. package/src/server/safe-path.ts +16 -0
  398. package/src/server/same-origin.ts +45 -0
  399. package/src/server/search-page.ts +313 -0
  400. package/src/server/search.ts +45 -0
  401. package/src/server/secret-auth.ts +20 -0
  402. package/src/server/see-other.ts +11 -0
  403. package/src/server/seed-board.ts +417 -0
  404. package/src/server/session-actions.ts +78 -0
  405. package/src/server/session-cookies.ts +124 -0
  406. package/src/server/session-key.ts +17 -0
  407. package/src/server/settings.ts +47 -0
  408. package/src/server/signatures.ts +62 -0
  409. package/src/server/sso-handshake.ts +79 -0
  410. package/src/server/stats.ts +63 -0
  411. package/src/server/subscription-actions.ts +161 -0
  412. package/src/server/surgery-actions.ts +180 -0
  413. package/src/server/syndication.ts +77 -0
  414. package/src/server/system-admin-actions.ts +134 -0
  415. package/src/server/system-admin.ts +90 -0
  416. package/src/server/test-container.ts +114 -0
  417. package/src/server/theme-admin-actions.ts +242 -0
  418. package/src/server/theme-admin.ts +203 -0
  419. package/src/server/theme-runtime.ts +89 -0
  420. package/src/server/theme-style.ts +279 -0
  421. package/src/server/theme.ts +52 -0
  422. package/src/server/thread-core.ts +207 -0
  423. package/src/server/thread-rating-actions.ts +55 -0
  424. package/src/server/thread-tool-actions.ts +154 -0
  425. package/src/server/two-factor-actions.ts +178 -0
  426. package/src/server/two-factor.ts +86 -0
  427. package/src/server/upgrade-notice.ts +41 -0
  428. package/src/server/user-admin-actions.ts +572 -0
  429. package/src/server/user-admin.ts +165 -0
  430. package/src/server/usercp-actions.ts +263 -0
  431. package/src/server/usercp-mail.ts +120 -0
  432. package/src/server/viewer-preferences.ts +64 -0
  433. package/src/server/warning-actions.ts +122 -0
  434. package/src/styles/globals.css +969 -0
  435. package/src/theme/contract.fixture.ts +793 -0
  436. package/src/view/account-copy.ts +351 -0
  437. package/src/view/admin-content-copy.ts +66 -0
  438. package/src/view/admin-copy.ts +106 -0
  439. package/src/view/admin-forum-tree-copy.ts +40 -0
  440. package/src/view/admin-group-copy.ts +80 -0
  441. package/src/view/admin-log.ts +58 -0
  442. package/src/view/admin-nav.ts +164 -0
  443. package/src/view/admin-navigation-copy.ts +58 -0
  444. package/src/view/admin-panel-copy.ts +194 -0
  445. package/src/view/admin-settings.ts +173 -0
  446. package/src/view/admin-theme-copy.ts +229 -0
  447. package/src/view/admin-user-copy.ts +96 -0
  448. package/src/view/attachments.ts +51 -0
  449. package/src/view/auth-copy.ts +147 -0
  450. package/src/view/board-index.ts +167 -0
  451. package/src/view/board-latest.ts +101 -0
  452. package/src/view/breadcrumb.ts +50 -0
  453. package/src/view/contacts.ts +85 -0
  454. package/src/view/content-copy.ts +118 -0
  455. package/src/view/contrast.ts +286 -0
  456. package/src/view/copy.ts +101 -0
  457. package/src/view/count.ts +8 -0
  458. package/src/view/discovery-view.ts +74 -0
  459. package/src/view/editor-toolbar.ts +84 -0
  460. package/src/view/error-notice.ts +52 -0
  461. package/src/view/feed.ts +130 -0
  462. package/src/view/forum-cursor.ts +54 -0
  463. package/src/view/forum-display.ts +152 -0
  464. package/src/view/forum-jump.ts +60 -0
  465. package/src/view/forum-notice.ts +24 -0
  466. package/src/view/inline-moderation.ts +87 -0
  467. package/src/view/install-copy.ts +95 -0
  468. package/src/view/legal.ts +58 -0
  469. package/src/view/locale.ts +40 -0
  470. package/src/view/marketplace-panel.ts +51 -0
  471. package/src/view/member-identity.ts +23 -0
  472. package/src/view/member-profile.ts +65 -0
  473. package/src/view/messages.ts +222 -0
  474. package/src/view/metadata.ts +75 -0
  475. package/src/view/modcp-nav.ts +76 -0
  476. package/src/view/moderation-copy.ts +84 -0
  477. package/src/view/moderation-queue.ts +67 -0
  478. package/src/view/navigation-arrange.ts +188 -0
  479. package/src/view/navigation.ts +173 -0
  480. package/src/view/notification-menu.ts +202 -0
  481. package/src/view/notifications.ts +84 -0
  482. package/src/view/onboarding.ts +26 -0
  483. package/src/view/pager.ts +191 -0
  484. package/src/view/panel-nav.ts +203 -0
  485. package/src/view/plugin-panel.ts +42 -0
  486. package/src/view/post-form.ts +61 -0
  487. package/src/view/post-link.ts +29 -0
  488. package/src/view/presence.ts +99 -0
  489. package/src/view/promotion-rules.ts +52 -0
  490. package/src/view/redirect-notice.ts +21 -0
  491. package/src/view/reputation.ts +107 -0
  492. package/src/view/search-controls.ts +103 -0
  493. package/src/view/search-results.ts +291 -0
  494. package/src/view/security-activity.ts +73 -0
  495. package/src/view/setting-groups.ts +43 -0
  496. package/src/view/shell.ts +164 -0
  497. package/src/view/slug-id.ts +12 -0
  498. package/src/view/sso-hand-off.ts +44 -0
  499. package/src/view/sso-notices.ts +48 -0
  500. package/src/view/subscriptions.ts +76 -0
  501. package/src/view/theme-draft.ts +167 -0
  502. package/src/view/theme-preference.ts +30 -0
  503. package/src/view/theme-tokens.ts +333 -0
  504. package/src/view/thread-view.ts +280 -0
  505. package/src/view/time.ts +31 -0
  506. package/src/view/timezone.ts +35 -0
  507. package/src/view/two-factor.ts +6 -0
  508. package/src/view/usercp-nav.ts +103 -0
  509. package/src/view/usercp.ts +196 -0
  510. package/src/view/warnings.ts +128 -0
  511. package/src/view/word-filter.ts +5 -0
@@ -0,0 +1,969 @@
1
+ @import "tailwindcss";
2
+
3
+ /*
4
+ * Theme and UI packages must be scanned for class names (F25).
5
+ *
6
+ * Tailwind v4 detects sources automatically, but only under the directory it is
7
+ * invoked from — here `apps/community`. `themes/` and `packages/ui/` are separate
8
+ * workspace packages outside that tree, so every class a theme slot uses was
9
+ * being dropped from the generated stylesheet. Nothing fails when that happens:
10
+ * the build is green, the classes are simply absent, and the page renders
11
+ * unstyled with no error anywhere to explain why.
12
+ *
13
+ * Paths are relative to this file, and deliberately not the `node_modules`
14
+ * symlinks: scanning through a symlinked workspace package is inconsistent
15
+ * between pnpm layouts.
16
+ */
17
+ @source "../../../../themes";
18
+ @source "../../../../plugins";
19
+ @source "../../../../examples";
20
+ @source "../../../../packages/ui/src";
21
+
22
+ /*
23
+ * R7 design tokens.
24
+ *
25
+ * Every token in the R7 list is defined here as the theme *default*. At runtime
26
+ * `themes.token_overrides` is resolved on top of these and injected as a single
27
+ * <style> block in <head> (F26), so a board can be restyled without a deploy.
28
+ *
29
+ * Components must never hardcode a colour. A CI grep bans `#`, `rgb(` and
30
+ * `hsl(` literals in component files precisely so this file stays the only
31
+ * place colour is decided.
32
+ *
33
+ * ## The ground is neutral; the accent is one colour
34
+ *
35
+ * Every greyscale token here has **zero chroma**. Not "nearly grey" — zero. An
36
+ * earlier palette was the Meith identity applied to the whole page (hedgerow
37
+ * green ground, gorse accent), and it was a good palette for Meith and a bad
38
+ * default for everybody else: a board with a blue mark had a green wall behind
39
+ * it and no way to fix that short of overriding a dozen tokens by hand. The
40
+ * ground still holds no opinion about hue, and that part has not changed.
41
+ *
42
+ * What changed is `primary`. It was **ink** — near-black in light, near-white in
43
+ * dark — on the reasoning that a board's one loud control should be strongest by
44
+ * contrast rather than by hue. That is a real argument and it had a real cost: a
45
+ * board that never opened the theme editor had no accent at all, so "post reply"
46
+ * looked exactly like every other dark rectangle on the page, and the
47
+ * marketing site and a live board shared no visual language whatsoever.
48
+ *
49
+ * So `primary` is now the green the marketing site uses, and the rule it follows
50
+ * is the one that site states: *the ground is greyscale, the accent is a single
51
+ * colour, and it is spent deliberately.* It marks the one primary action, the
52
+ * current item in a list, a link's underline and a focus ring — and nothing
53
+ * else. A page where six things are green is a page where the button does not
54
+ * stand out.
55
+ *
56
+ * The two schemes carry different greens, and that is not a slip. The light
57
+ * scheme's pine (`oklch(0.508 …)`) reads at 5.2:1 against white; the emerald
58
+ * that works on near-black would read at about 3.5:1 there, under the floor for
59
+ * the small text it marks. Each scheme carries the step of the ramp that clears
60
+ * AA against its own ground.
61
+ *
62
+ * **None of this costs an operator their brand.** Branding is still the same
63
+ * one-group operation it was when the default was colourless: set `--primary`,
64
+ * `--primary-hover`, `--primary-foreground` and `--ring` in the control panel —
65
+ * or press one of the brand presets, which write all four — and nothing else in
66
+ * the palette argues with the result, because nothing else in the palette has a
67
+ * hue to argue with. The default now has an opinion; it is still one click to
68
+ * replace it.
69
+ *
70
+ * ## The semantic tokens still carry hue, because they carry meaning
71
+ *
72
+ * `thread-locked` is not decoration; it is the difference between a thread you
73
+ * can answer and one you cannot. Those keep conventional hues — amber for "needs
74
+ * an eye", rust for "stopped", green for "approved", blue and violet for the
75
+ * staff badges — held to chroma 0.11–0.17 so they read as information beside a
76
+ * brand rather than competing with it. Colour is never the only carrier: every
77
+ * state that a hue marks is also a word in the markup.
78
+ *
79
+ * ## Contrast is measured, not eyeballed
80
+ *
81
+ * Every foreground/surface pair below meets WCAG AA (4.5:1) for body text, and
82
+ * every semantic colour meets it against `card` — including the ambers, which is
83
+ * why they sit near L 0.55 in light rather than at the brighter value that looks
84
+ * nicer in isolation and fails on white. `input` is deliberately darker than
85
+ * `border`: a field's boundary is essential information (1.4.11, 3:1) and a
86
+ * rule between two rows is not.
87
+ */
88
+
89
+ @custom-variant dark (&:is(.dark *));
90
+
91
+ @theme inline {
92
+ --color-background: var(--background);
93
+ --color-foreground: var(--foreground);
94
+ /*
95
+ * The middle surface. `background` is the page, `card` is a panel raised off
96
+ * it, and `surface` is the band cut *into* it — a header, a table head, a
97
+ * panel's own toolbar. Those were `muted` before, which also means "a control
98
+ * that is disabled" and "the tint behind a hovered row"; one token doing three
99
+ * jobs is a token an operator cannot change without changing all three.
100
+ */
101
+ --color-surface: var(--surface);
102
+ --color-card: var(--card);
103
+ --color-card-foreground: var(--card-foreground);
104
+ --color-primary: var(--primary);
105
+ --color-primary-foreground: var(--primary-foreground);
106
+ /*
107
+ * The filled control under the cursor. This was `hover:opacity-90`, which
108
+ * fades the button *and its label* toward whatever is behind it — on a card
109
+ * that is white, so the one control the page is for got lighter and lower
110
+ * contrast at exactly the moment the reader aimed at it.
111
+ */
112
+ --color-primary-hover: var(--primary-hover);
113
+ --color-secondary: var(--secondary);
114
+ --color-secondary-foreground: var(--secondary-foreground);
115
+ --color-muted: var(--muted);
116
+ --color-muted-foreground: var(--muted-foreground);
117
+ --color-accent: var(--accent);
118
+ --color-accent-foreground: var(--accent-foreground);
119
+ --color-destructive: var(--destructive);
120
+ --color-destructive-foreground: var(--destructive-foreground);
121
+ --color-border: var(--border);
122
+ --color-input: var(--input);
123
+ --color-ring: var(--ring);
124
+
125
+ /* Domain-semantic tokens. These carry meaning, not just appearance. */
126
+ --color-forum-unread: var(--forum-unread);
127
+ --color-forum-read: var(--forum-read);
128
+ --color-forum-locked: var(--forum-locked);
129
+ --color-thread-pinned: var(--thread-pinned);
130
+ --color-thread-locked: var(--thread-locked);
131
+ --color-thread-moved: var(--thread-moved);
132
+ --color-thread-unapproved: var(--thread-unapproved);
133
+ --color-thread-deleted: var(--thread-deleted);
134
+ --color-post-highlight: var(--post-highlight);
135
+ --color-post-own: var(--post-own);
136
+ --color-post-unapproved: var(--post-unapproved);
137
+ --color-moderation-pending: var(--moderation-pending);
138
+ --color-moderation-approved: var(--moderation-approved);
139
+ --color-moderation-rejected: var(--moderation-rejected);
140
+ --color-group-admin: var(--group-admin);
141
+ --color-group-supermod: var(--group-supermod);
142
+ --color-group-mod: var(--group-mod);
143
+ --color-group-banned: var(--group-banned);
144
+
145
+ /*
146
+ * A token like the mono stack is, rather than a literal, so a board can change
147
+ * the face it reads in from the control panel. That was already true of code
148
+ * and false of everything else, which is backwards — the face a board is *read*
149
+ * in is the one an operator is most likely to have an opinion about.
150
+ */
151
+ --font-sans: var(--font-sans-stack);
152
+ --font-mono: var(--font-mono-stack);
153
+ /*
154
+ * The face headings are set in, and the reason `font-serif` no longer exists
155
+ * here.
156
+ *
157
+ * It used to be a literal — Newsreader, then a five-deep serif fallback — so
158
+ * "what face are this board's headings" was the one typographic question an
159
+ * operator could not answer from the control panel, while the *code* face next
160
+ * to it had been a token all along. That is backwards twice over: it is the
161
+ * more visible decision, and it is the one a board with its own identity is
162
+ * likeliest to have an opinion about.
163
+ *
164
+ * Dropping the Tailwind mapping rather than repointing it is deliberate.
165
+ * `font-serif` still existing would leave forty call sites compiling to a face
166
+ * the board no longer ships, and the next heading somebody wrote would reach
167
+ * for it out of habit. There is no `font-serif` utility now; there is
168
+ * `font-heading`, and it resolves to whatever the board says.
169
+ */
170
+ --font-heading: var(--font-heading-stack);
171
+
172
+ /*
173
+ * Panel lift. The board used to separate a panel from its page with a border
174
+ * and nothing else, which works and is why it survived this long — but a
175
+ * 1px rule is the only depth cue on the page, so a long listing reads as one
176
+ * flat sheet with lines ruled across it.
177
+ *
178
+ * Both stops are tinted with `--shadow-tint` rather than a literal black, so
179
+ * the dark scheme can carry a deeper shadow (a 5%-black shadow is invisible on
180
+ * a near-black page) and an operator can turn the whole thing off by setting
181
+ * `--elevation: none`.
182
+ */
183
+ --shadow-elevation: var(--elevation);
184
+
185
+ --radius-sm: calc(var(--radius) * 0.5);
186
+ --radius-md: calc(var(--radius) * 0.75);
187
+ --radius-lg: var(--radius);
188
+ --radius-xl: calc(var(--radius) * 1.5);
189
+
190
+ /*
191
+ * Density unit. Classic forums are information-dense; a compact theme sets
192
+ * this lower and every row inherits the change.
193
+ */
194
+ --spacing-density: var(--density-unit);
195
+ }
196
+
197
+ :root {
198
+ color-scheme: light;
199
+
200
+ /*
201
+ * Paper on a desk. The page is a light grey so that `card` — plain white — is
202
+ * a surface that sits *on* it: a board is a stack of panels, and a panel that
203
+ * is the same colour as its page needs a border to exist at all.
204
+ */
205
+ --background: oklch(0.968 0 0);
206
+ --foreground: oklch(0.205 0 0);
207
+ /* The band between page and panel: header, table head, a panel's toolbar. */
208
+ --surface: oklch(0.941 0 0);
209
+ --card: oklch(1 0 0);
210
+ --card-foreground: oklch(0.205 0 0);
211
+
212
+ /*
213
+ * Pine, not emerald. See this file's header: the dark scheme's green reads at
214
+ * about 3.5:1 on white, and the small text this marks needs 4.5. This is the
215
+ * step of the same ramp that clears it — 5.2:1 on the page, 5.5:1 on a card.
216
+ */
217
+ --primary: oklch(0.508 0.105 165.6);
218
+ --primary-foreground: oklch(0.985 0 0);
219
+ --primary-hover: oklch(0.434 0.089 165.6);
220
+ --secondary: oklch(0.94 0 0);
221
+ --secondary-foreground: oklch(0.269 0 0);
222
+ --muted: oklch(0.958 0 0);
223
+ --muted-foreground: oklch(0.494 0 0);
224
+ /*
225
+ * A hover surface, which is what `accent` means in this vocabulary — the
226
+ * previous palette used it as the one loud colour, and a token that is a
227
+ * highlight in one theme and a wash in another is a token nobody can style
228
+ * against. Anything that needs to shout uses a semantic token, which has a
229
+ * meaning to justify the volume.
230
+ */
231
+ --accent: oklch(0.951 0 0);
232
+ --accent-foreground: oklch(0.205 0 0);
233
+ --destructive: oklch(0.514 0.183 28.5);
234
+ --destructive-foreground: oklch(0.985 0 0);
235
+
236
+ --border: oklch(0.905 0 0);
237
+ /* Darker than `border` on purpose: a field's edge is information. */
238
+ --input: oklch(0.64 0 0);
239
+ /*
240
+ * The ring follows the accent rather than staying grey. A focus outline is one
241
+ * of the four things the accent is spent on, and a neutral ring on a branded
242
+ * board looks like the board forgot to brand one thing.
243
+ */
244
+ --ring: oklch(0.508 0.105 165.6);
245
+
246
+ /*
247
+ * The shadow's colour, apart from its geometry, because only this half changes
248
+ * between the schemes. Tinted with the foreground rather than pure black: a
249
+ * neutral-black shadow over a warm surface reads as dirt.
250
+ */
251
+ --shadow-tint: oklch(0.205 0 0 / 8%);
252
+
253
+ /*
254
+ * 8px, up from 6. The rounding that matters is on the *outer* panels; rows
255
+ * inside them are ruled, not boxed, so density survives the softer corner.
256
+ * `radius-sm` derives 4px from this for the things that must stay tight —
257
+ * badges, a code span — so the increase does not propagate to every chip.
258
+ */
259
+ --radius: 0.5rem;
260
+ --density-unit: 0.25rem;
261
+ --font-mono-stack: ui-monospace, "SFMono-Regular", "Menlo", monospace;
262
+ --font-sans-stack: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
263
+ /*
264
+ * Headings, and by default the same face as everything else.
265
+ *
266
+ * `var(--font-sans-stack)` rather than a repeat of its value, so a board that
267
+ * changes the face it is *read* in gets headings that follow — which is what
268
+ * an operator means nine times in ten by "change the font". Setting this token
269
+ * is the tenth: headings alone, to anything, including a serif
270
+ * (`Georgia, ui-serif, serif` needs no download) or a face the board already
271
+ * loads.
272
+ */
273
+ --font-heading-stack: var(--font-sans-stack);
274
+ --elevation: 0 1px 2px var(--shadow-tint), 0 12px 32px -16px var(--shadow-tint);
275
+
276
+ /*
277
+ * Read state: unread is emphatic, read recedes. Both are neutral, so a board
278
+ * that brands `--primary` does not accidentally turn every unread marker into
279
+ * its logo colour — and an operator who *wants* that overrides one token.
280
+ */
281
+ --forum-unread: oklch(0.205 0 0);
282
+ --forum-read: oklch(0.556 0 0);
283
+ --forum-locked: oklch(0.554 0.135 32);
284
+
285
+ --thread-pinned: oklch(0.55 0.12 68);
286
+ --thread-locked: oklch(0.554 0.135 32);
287
+ --thread-moved: oklch(0.556 0 0);
288
+ --thread-unapproved: oklch(0.545 0.11 88);
289
+ --thread-deleted: oklch(0.487 0.164 26);
290
+
291
+ /* Row washes. Held near the card so text on them keeps its full contrast. */
292
+ --post-highlight: oklch(0.964 0.036 92);
293
+ --post-own: oklch(0.972 0 0);
294
+ --post-unapproved: oklch(0.956 0.035 84);
295
+
296
+ --moderation-pending: oklch(0.55 0.12 68);
297
+ --moderation-approved: oklch(0.518 0.113 150);
298
+ --moderation-rejected: oklch(0.554 0.135 32);
299
+
300
+ --group-admin: oklch(0.505 0.169 27);
301
+ --group-supermod: oklch(0.496 0.153 305);
302
+ --group-mod: oklch(0.489 0.127 250);
303
+ --group-banned: oklch(0.556 0 0);
304
+ }
305
+
306
+ .dark {
307
+ color-scheme: dark;
308
+
309
+ /*
310
+ * The same neutral, inverted, and not to pure black: `background` at L 0.15
311
+ * rather than 0 leaves room for `card` to sit *above* it, which is the same
312
+ * page-and-panel relationship the light scheme has. Pure black would flatten
313
+ * the two and force every panel to be found by its border.
314
+ *
315
+ * `foreground` stops at 0.967 rather than 1 for the opposite reason —
316
+ * full-white body text on a near-black ground haloes, and a board is read for
317
+ * an hour at a time.
318
+ */
319
+ --background: oklch(0.15 0 0);
320
+ --foreground: oklch(0.967 0 0);
321
+ --surface: oklch(0.17 0 0);
322
+ --card: oklch(0.196 0 0);
323
+ --card-foreground: oklch(0.967 0 0);
324
+
325
+ /*
326
+ * Emerald, and a near-black label on it. A mid-tone accent cannot carry white
327
+ * text on one scheme and dark text on the other from a single value, which is
328
+ * why `primary-foreground` is a token and not `white`.
329
+ */
330
+ --primary: oklch(0.773 0.153 163.2);
331
+ --primary-foreground: oklch(0.181 0.029 166.6);
332
+ --primary-hover: oklch(0.84 0.139 166.8);
333
+ --secondary: oklch(0.256 0 0);
334
+ --secondary-foreground: oklch(0.94 0 0);
335
+ --muted: oklch(0.228 0 0);
336
+ --muted-foreground: oklch(0.702 0 0);
337
+ --accent: oklch(0.276 0 0);
338
+ --accent-foreground: oklch(0.967 0 0);
339
+ --destructive: oklch(0.674 0.174 26);
340
+ --destructive-foreground: oklch(0.18 0 0);
341
+
342
+ --border: oklch(0.31 0 0);
343
+ --input: oklch(0.52 0 0);
344
+ --ring: oklch(0.773 0.153 163.2);
345
+
346
+ /*
347
+ * Far heavier than the light scheme's 8%. A shadow is the absence of light,
348
+ * and on a near-black page there is almost none to take away — the light
349
+ * value is literally invisible here, so a panel would lose its lift entirely
350
+ * in dark mode while keeping it in light.
351
+ */
352
+ --shadow-tint: oklch(0 0 0 / 45%);
353
+
354
+ --forum-unread: oklch(0.967 0 0);
355
+ --forum-read: oklch(0.6 0 0);
356
+ --forum-locked: oklch(0.708 0.13 34);
357
+
358
+ --thread-pinned: oklch(0.769 0.13 74);
359
+ --thread-locked: oklch(0.708 0.13 34);
360
+ --thread-moved: oklch(0.6 0 0);
361
+ --thread-unapproved: oklch(0.777 0.12 85);
362
+ --thread-deleted: oklch(0.664 0.157 27);
363
+
364
+ --post-highlight: oklch(0.276 0.043 92);
365
+ --post-own: oklch(0.221 0 0);
366
+ --post-unapproved: oklch(0.263 0.04 85);
367
+
368
+ --moderation-pending: oklch(0.769 0.13 74);
369
+ --moderation-approved: oklch(0.716 0.126 151);
370
+ --moderation-rejected: oklch(0.708 0.13 34);
371
+
372
+ --group-admin: oklch(0.687 0.166 27);
373
+ --group-supermod: oklch(0.703 0.144 305);
374
+ --group-mod: oklch(0.694 0.122 250);
375
+ --group-banned: oklch(0.6 0 0);
376
+ }
377
+
378
+ @media (prefers-color-scheme: dark) {
379
+ :root:not(.light) {
380
+ color-scheme: dark;
381
+
382
+ --background: oklch(0.15 0 0);
383
+ --foreground: oklch(0.967 0 0);
384
+ --surface: oklch(0.17 0 0);
385
+ --card: oklch(0.196 0 0);
386
+ --card-foreground: oklch(0.967 0 0);
387
+
388
+ --primary: oklch(0.773 0.153 163.2);
389
+ --primary-foreground: oklch(0.181 0.029 166.6);
390
+ --primary-hover: oklch(0.84 0.139 166.8);
391
+ --secondary: oklch(0.256 0 0);
392
+ --secondary-foreground: oklch(0.94 0 0);
393
+ --muted: oklch(0.228 0 0);
394
+ --muted-foreground: oklch(0.702 0 0);
395
+ --accent: oklch(0.276 0 0);
396
+ --accent-foreground: oklch(0.967 0 0);
397
+ --destructive: oklch(0.674 0.174 26);
398
+ --destructive-foreground: oklch(0.18 0 0);
399
+
400
+ --border: oklch(0.31 0 0);
401
+ --input: oklch(0.52 0 0);
402
+ --ring: oklch(0.773 0.153 163.2);
403
+
404
+ --shadow-tint: oklch(0 0 0 / 45%);
405
+
406
+ --forum-unread: oklch(0.967 0 0);
407
+ --forum-read: oklch(0.6 0 0);
408
+ --forum-locked: oklch(0.708 0.13 34);
409
+
410
+ --thread-pinned: oklch(0.769 0.13 74);
411
+ --thread-locked: oklch(0.708 0.13 34);
412
+ --thread-moved: oklch(0.6 0 0);
413
+ --thread-unapproved: oklch(0.777 0.12 85);
414
+ --thread-deleted: oklch(0.664 0.157 27);
415
+
416
+ --post-highlight: oklch(0.276 0.043 92);
417
+ --post-own: oklch(0.221 0 0);
418
+ --post-unapproved: oklch(0.263 0.04 85);
419
+
420
+ --moderation-pending: oklch(0.769 0.13 74);
421
+ --moderation-approved: oklch(0.716 0.126 151);
422
+ --moderation-rejected: oklch(0.708 0.13 34);
423
+
424
+ --group-admin: oklch(0.687 0.166 27);
425
+ --group-supermod: oklch(0.703 0.144 305);
426
+ --group-mod: oklch(0.694 0.122 250);
427
+ --group-banned: oklch(0.6 0 0);
428
+ }
429
+ }
430
+
431
+ @layer base {
432
+ * {
433
+ border-color: var(--border);
434
+ }
435
+
436
+ body {
437
+ background-color: var(--background);
438
+ color: var(--foreground);
439
+ }
440
+
441
+ /*
442
+ * Balance only — deliberately **not** a font-family.
443
+ *
444
+ * An earlier version of this rule set every heading in the serif, as a
445
+ * backstop for markup that had not opted in. That was wrong, and rendering
446
+ * the midnight theme is what showed it: midnight is a terminal, its headings
447
+ * are IBM Plex Mono, and its slots only escaped the rule because every one of
448
+ * them happens to say `font-mono` explicitly. Any heading that did not — a
449
+ * plugin's, a future slot's — would have been silently overruled into the
450
+ * *default theme's* voice. Which face a heading wears is the theme's decision,
451
+ * and this file is the app.
452
+ *
453
+ * The default theme's slots and the app's own pages already carry
454
+ * `font-serif`, so nothing was gained by it either.
455
+ *
456
+ * Balance stays, because it is not a voice decision. Thread subjects are
457
+ * user-supplied and unbounded, and a two-line title with one orphaned word is
458
+ * the commonest way a dense listing looks broken — in any theme.
459
+ */
460
+ h1,
461
+ h2,
462
+ h3,
463
+ h4 {
464
+ text-wrap: balance;
465
+ }
466
+
467
+ /*
468
+ * The paragraph counterpart. `balance` is quadratic and browsers cap it at a
469
+ * handful of lines, so it is wrong for a post body; `pretty` only fixes the
470
+ * last line, which is the orphan that makes a paragraph look broken.
471
+ */
472
+ p {
473
+ text-wrap: pretty;
474
+ }
475
+
476
+ /* Keyboard focus must always be visible — R5 pages are used without a mouse. */
477
+ :focus-visible {
478
+ outline: 2px solid var(--ring);
479
+ outline-offset: 2px;
480
+ }
481
+
482
+ /*
483
+ * A permalink to `#post-12` otherwise lands the post flush against the top
484
+ * of the viewport, with its own header cropped and the previous post's footer
485
+ * nowhere — the reader cannot tell they arrived at the right one.
486
+ */
487
+ :target {
488
+ scroll-margin-block-start: 1.5rem;
489
+ }
490
+
491
+ ::selection {
492
+ background-color: var(--secondary);
493
+ color: var(--secondary-foreground);
494
+ }
495
+
496
+ /*
497
+ * Transitions on this board are hover and focus feedback of 100–150ms, and
498
+ * every one of them is an affordance rather than an animation. Honour the
499
+ * preference anyway: the setting is often on for reasons that have nothing to
500
+ * do with how large the movement is.
501
+ */
502
+ @media (prefers-reduced-motion: reduce) {
503
+ *,
504
+ *::before,
505
+ *::after {
506
+ animation-duration: 0.01ms !important;
507
+ animation-iteration-count: 1 !important;
508
+ transition-duration: 0.01ms !important;
509
+ scroll-behavior: auto !important;
510
+ }
511
+ }
512
+ }
513
+
514
+ /* Skip link: visually hidden until focused. */
515
+ .skip-link {
516
+ position: absolute;
517
+ left: -9999px;
518
+ top: 0;
519
+ z-index: 100;
520
+ }
521
+
522
+ .skip-link:focus {
523
+ left: 0.5rem;
524
+ top: 0.5rem;
525
+ padding: 0.5rem 0.75rem;
526
+ background-color: var(--card);
527
+ color: var(--card-foreground);
528
+ border: 1px solid var(--border);
529
+ border-radius: var(--radius);
530
+ }
531
+
532
+ /*
533
+ * `.prose-md` — the typography of a rendered post body.
534
+ *
535
+ * **This block is a bug fix as much as a design.** Both shipped themes have
536
+ * carried a prose class on the elements that receive `bodyHtml` since the slots
537
+ * were written, and it was defined nowhere in the workspace. Tailwind's
538
+ * preflight zeroes the margin on `p`, `ul` and `ol` and removes the underline
539
+ * and colour from `a`, so a four-paragraph post rendered as one unbroken block
540
+ * of text with invisible links in it. Nothing failed — there is no such thing
541
+ * as a stylesheet error — and the contract suite could not see it, because the
542
+ * markup was correct and only its appearance was not.
543
+ *
544
+ * It lives here rather than in a theme for the same reason the `.md-*` rules
545
+ * below do: the class names are emitted by `@meith/markdown`, which is the
546
+ * app's renderer, and a theme that had to restate all of this to show a
547
+ * paragraph break would be inheriting a bug rather than a baseline. Everything
548
+ * is in tokens and `em`, so a theme changes it by changing its tokens and its
549
+ * base size — and a board's operator reaches it through F26 like any other
550
+ * surface.
551
+ *
552
+ * Markdown widened what a post can contain — headings, tables, task lists, a
553
+ * horizontal rule — so this block is longer than the BBCode one it replaces.
554
+ * Every addition is a construct the parser can now produce; nothing here styles
555
+ * anything a post cannot emit.
556
+ */
557
+ .prose-md {
558
+ line-height: 1.65;
559
+ overflow-wrap: break-word;
560
+ }
561
+
562
+ .prose-md > :first-child {
563
+ margin-top: 0;
564
+ }
565
+
566
+ .prose-md > :last-child {
567
+ margin-bottom: 0;
568
+ }
569
+
570
+ .prose-md p {
571
+ margin-block: 0.75em;
572
+ }
573
+
574
+ /*
575
+ * A link keeps the body's colour and is marked by its *underline*, which is the
576
+ * accent's third job (see this file's header).
577
+ *
578
+ * Colouring the text itself is the obvious alternative and it is the one that
579
+ * breaks under branding: a board whose operator has set a pale yellow accent has
580
+ * just made every link in every post unreadable, and the post body is the one
581
+ * region where that is not survivable. An underline carries the affordance at
582
+ * any accent lightness, because the text under it never stopped being body text.
583
+ *
584
+ * `color-mix` rather than a second token: the rule is "this accent, quieter",
585
+ * and an operator who changes the accent should not have to remember to change
586
+ * its underline to match.
587
+ */
588
+ .prose-md a {
589
+ color: inherit;
590
+ text-decoration-line: underline;
591
+ text-decoration-color: color-mix(in srgb, var(--primary) 45%, transparent);
592
+ text-decoration-thickness: 1px;
593
+ text-underline-offset: 0.2em;
594
+ transition: text-decoration-color 120ms ease;
595
+ }
596
+
597
+ .prose-md a:hover {
598
+ text-decoration-color: var(--primary);
599
+ }
600
+
601
+ .prose-md strong,
602
+ .prose-md b {
603
+ font-weight: 600;
604
+ }
605
+
606
+ /*
607
+ * A URL a member pasted is one unbroken token, and a long one widens the post,
608
+ * then the thread, then the page. Breaking it is uglier than the alternative
609
+ * only until you meet the alternative.
610
+ */
611
+ .prose-md a,
612
+ .prose-md code {
613
+ overflow-wrap: anywhere;
614
+ }
615
+
616
+ /*
617
+ * A post's headings start at `h2` — the renderer offsets them, because the page
618
+ * already has an `h1` — and they must not out-shout the thread's subject. Sized
619
+ * in `em` off the post's own text rather than the browser's defaults, which
620
+ * would make an `h2` in a post twice the size of the title above it.
621
+ */
622
+ .prose-md h2,
623
+ .prose-md h3,
624
+ .prose-md h4,
625
+ .prose-md h5,
626
+ .prose-md h6 {
627
+ margin-block: 1.2em 0.5em;
628
+ font-weight: 600;
629
+ line-height: 1.3;
630
+ }
631
+
632
+ .prose-md h2 {
633
+ font-size: 1.35em;
634
+ }
635
+ .prose-md h3 {
636
+ font-size: 1.2em;
637
+ }
638
+ .prose-md h4 {
639
+ font-size: 1.05em;
640
+ }
641
+ .prose-md h5,
642
+ .prose-md h6 {
643
+ font-size: 1em;
644
+ }
645
+
646
+ /* Inline images and smilies sit on the text baseline, not below it. */
647
+ .md-smiley {
648
+ display: inline-block;
649
+ height: 1.25em;
650
+ width: auto;
651
+ vertical-align: text-bottom;
652
+ }
653
+
654
+ /*
655
+ * F36 — the Markdown renderer's output.
656
+ *
657
+ * `@meith/markdown` emits a fixed, closed set of class names and no inline
658
+ * styling at all. Everything below is therefore the *theme's* say in what a
659
+ * quote or a code block looks like, expressed in tokens so a runtime override
660
+ * (F26) reaches it like any other surface.
661
+ */
662
+ .md-quote {
663
+ /* `em`, so a quote inside a heading scales with the text it is in. */
664
+ margin-block: 0.85em;
665
+ padding: 0.55em 0.85em;
666
+ border-inline-start: 3px solid var(--border);
667
+ background-color: var(--muted);
668
+ border-start-end-radius: var(--radius);
669
+ border-end-end-radius: var(--radius);
670
+ }
671
+
672
+ /*
673
+ * A quote of a quote of a quote is how a forum argument is shaped, and three
674
+ * nested backgrounds of the same colour are indistinguishable. The inner ones
675
+ * drop the fill and keep the rule, so depth reads as a stack of margins.
676
+ */
677
+ .md-quote .md-quote {
678
+ background-color: transparent;
679
+ }
680
+
681
+ /* Who was quoted on the left, the link back to their post in the corner. */
682
+ .md-quote-attribution {
683
+ display: flex;
684
+ flex-wrap: wrap;
685
+ align-items: baseline;
686
+ justify-content: space-between;
687
+ gap: 0.25em 1em;
688
+ margin-block: 0 0.5em;
689
+ }
690
+
691
+ .md-quote-source {
692
+ margin-inline-start: auto;
693
+ font-size: 0.9em;
694
+ white-space: nowrap;
695
+ opacity: 0.85;
696
+ }
697
+
698
+ .md-code {
699
+ margin-block: 0.85em;
700
+ padding: 0.75em 0.85em;
701
+ overflow-x: auto;
702
+ background-color: var(--muted);
703
+ border: 1px solid var(--border);
704
+ border-radius: var(--radius);
705
+ font-size: 0.875em;
706
+ line-height: 1.6;
707
+ /* `pre` already preserves newlines; wrapping long lines would hide overflow. */
708
+ white-space: pre;
709
+ /*
710
+ * Tabs in pasted code default to eight columns, which throws away a third of
711
+ * the width of a post before the code has said anything.
712
+ */
713
+ tab-size: 2;
714
+ }
715
+
716
+ /*
717
+ * A code *span* is a word inside a sentence, so it gets a tint and no border —
718
+ * a boxed word breaks the line's rhythm, and at this size a border is thicker
719
+ * than the letterforms it surrounds.
720
+ */
721
+ .md-code-span {
722
+ padding: 0.15em 0.35em;
723
+ background-color: var(--muted);
724
+ border-radius: calc(var(--radius) - 2px);
725
+ font-size: 0.9em;
726
+ }
727
+
728
+ /*
729
+ * Shiki (code-highlighting.ts) tags every token with both a light and a dark
730
+ * colour as CSS custom properties (`defaultColor: false`) and leaves picking
731
+ * one to the page. This is the same three-state switch as the rest of the
732
+ * theme — system preference by default, `.dark`/`.light` to override it.
733
+ */
734
+ .md-code code span {
735
+ color: var(--shiki-light);
736
+ }
737
+
738
+ @media (prefers-color-scheme: dark) {
739
+ :root:not(.light) .md-code code span {
740
+ color: var(--shiki-dark);
741
+ }
742
+ }
743
+
744
+ .dark .md-code code span {
745
+ color: var(--shiki-dark);
746
+ }
747
+
748
+ .md-list {
749
+ margin: 0.5em 0 0.5em 1.5em;
750
+ list-style-position: outside;
751
+ }
752
+
753
+ .md-list li {
754
+ margin-block: 0.2em;
755
+ }
756
+
757
+ ul.md-list {
758
+ list-style-type: disc;
759
+ }
760
+
761
+ ol.md-list {
762
+ list-style-type: decimal;
763
+ }
764
+
765
+ /*
766
+ * A task list is a list of statements, not of bullets: the checkbox is the
767
+ * marker, and leaving the disc there gives every line two.
768
+ */
769
+ .md-list li.md-task {
770
+ list-style: none;
771
+ margin-inline-start: -1.2em;
772
+ }
773
+
774
+ .md-task input {
775
+ margin-inline-end: 0.35em;
776
+ vertical-align: baseline;
777
+ }
778
+
779
+ /* An oversized image must not widen the thread and force a horizontal scroll. */
780
+ .md-image {
781
+ max-width: 100%;
782
+ height: auto;
783
+ }
784
+
785
+ /*
786
+ * An [attachment=id] placed inline (attachment-embed.ts). An image sits with
787
+ * the text around it, the way a post's own ![](…) image does; a non-image
788
+ * file is a plain link — the download route is what actually enforces who
789
+ * may have the bytes, so this is styling, not a second permission surface.
790
+ */
791
+ .md-attachment-image img {
792
+ max-width: 100%;
793
+ height: auto;
794
+ vertical-align: middle;
795
+ border-radius: var(--radius);
796
+ }
797
+
798
+ .md-attachment-file {
799
+ display: inline-flex;
800
+ align-items: baseline;
801
+ gap: 0.35em;
802
+ }
803
+
804
+ .md-attachment-size {
805
+ color: var(--muted-foreground);
806
+ font-size: 0.85em;
807
+ }
808
+
809
+ /*
810
+ * A link unfurl card (embeds.ts) — a static figure, never an iframe, so it
811
+ * carries no third-party script and needs nothing from the CSP beyond the
812
+ * `img-src` widening its own thumbnail already required.
813
+ */
814
+ .md-embed {
815
+ display: flex;
816
+ gap: 0.75em;
817
+ align-items: center;
818
+ margin-block: 0.5em 0.85em;
819
+ padding: 0.6em;
820
+ max-width: 28rem;
821
+ border: 1px solid var(--border);
822
+ border-radius: var(--radius);
823
+ background-color: var(--muted);
824
+ }
825
+
826
+ .md-embed-thumb {
827
+ flex: none;
828
+ display: block;
829
+ width: 6rem;
830
+ height: 6rem;
831
+ overflow: hidden;
832
+ border-radius: calc(var(--radius) - 2px);
833
+ }
834
+
835
+ .md-embed-thumb img {
836
+ width: 100%;
837
+ height: 100%;
838
+ object-fit: cover;
839
+ }
840
+
841
+ .md-embed figcaption {
842
+ display: flex;
843
+ flex-direction: column;
844
+ gap: 0.15em;
845
+ min-width: 0;
846
+ font-size: 0.9em;
847
+ }
848
+
849
+ .md-embed-title {
850
+ font-weight: 600;
851
+ overflow: hidden;
852
+ text-overflow: ellipsis;
853
+ white-space: nowrap;
854
+ }
855
+
856
+ .md-embed-author,
857
+ .md-embed-provider {
858
+ color: var(--muted-foreground);
859
+ }
860
+
861
+ .md-rule {
862
+ margin-block: 1.2em;
863
+ border: 0;
864
+ border-top: 1px solid var(--border);
865
+ }
866
+
867
+ /*
868
+ * The scroller is the renderer's, not the theme's: a table wider than the post
869
+ * would otherwise make the *thread* scroll sideways, which on a phone is every
870
+ * table with four columns in it.
871
+ */
872
+ .md-table-scroll {
873
+ margin-block: 0.85em;
874
+ overflow-x: auto;
875
+ }
876
+
877
+ .md-table {
878
+ border-collapse: collapse;
879
+ font-size: 0.95em;
880
+ }
881
+
882
+ .md-table th,
883
+ .md-table td {
884
+ padding: 0.35em 0.6em;
885
+ border: 1px solid var(--border);
886
+ text-align: start;
887
+ }
888
+
889
+ .md-table th {
890
+ background-color: var(--muted);
891
+ font-weight: 600;
892
+ }
893
+
894
+ .md-align-left {
895
+ text-align: start;
896
+ }
897
+ .md-align-center {
898
+ text-align: center;
899
+ }
900
+ .md-align-right {
901
+ text-align: end;
902
+ }
903
+
904
+ /*
905
+ * A board's own directives (F71). The renderer gives each one a class and
906
+ * nothing else — no colour, no box — because what a directive looks like is a
907
+ * decision for the board that defined it, made in its theme's own stylesheet.
908
+ * The only thing stated here is that a block one is a block. `spoiler` is not
909
+ * one of these: it is built in, rendered as `<details>` below, and styled here.
910
+ */
911
+ div.md-directive {
912
+ margin-block: 0.85em;
913
+ }
914
+
915
+ /*
916
+ * The spoiler reveal works from `<details>`/`<summary>` alone, so it is
917
+ * already correct with no JS and no click handler. The styling only has to
918
+ * make the closed state read as "tap to reveal" rather than as a stray box.
919
+ */
920
+ .md-spoiler {
921
+ margin-block: 0.85em;
922
+ padding: 0.6em 0.85em;
923
+ background-color: var(--muted);
924
+ border: 1px solid var(--border);
925
+ border-radius: var(--radius);
926
+ }
927
+
928
+ .md-spoiler > summary {
929
+ cursor: pointer;
930
+ font-weight: 600;
931
+ list-style: none;
932
+ }
933
+
934
+ .md-spoiler > summary::-webkit-details-marker {
935
+ display: none;
936
+ }
937
+
938
+ .md-spoiler > summary::before {
939
+ content: "▸ ";
940
+ }
941
+
942
+ .md-spoiler[open] > summary::before {
943
+ content: "▾ ";
944
+ }
945
+
946
+ .md-spoiler[open] > summary {
947
+ margin-block-end: 0.5em;
948
+ }
949
+
950
+ .md-spoiler > :last-child {
951
+ margin-block-end: 0;
952
+ }
953
+
954
+ @media (pointer: coarse) {
955
+ input:not(
956
+ [type="checkbox"],
957
+ [type="radio"],
958
+ [type="range"],
959
+ [type="color"],
960
+ [type="file"],
961
+ [type="submit"],
962
+ [type="button"],
963
+ [type="reset"]
964
+ ),
965
+ select,
966
+ textarea {
967
+ font-size: 1rem;
968
+ }
969
+ }