@palate/cli 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1222) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +151 -2
  3. package/bin/palate.mjs +894 -0
  4. package/kb/anatomy/data-table.md +24 -0
  5. package/kb/anatomy/empty-state.md +16 -0
  6. package/kb/anatomy/filter-toolbar.md +19 -0
  7. package/kb/anatomy/meter.md +16 -0
  8. package/kb/anatomy/product-window.md +18 -0
  9. package/kb/anatomy/settings-page.md +18 -0
  10. package/kb/anatomy/status-indicator.md +17 -0
  11. package/kb/anatomy/toast-undo.md +16 -0
  12. package/kb/bench/briefs.json +618 -0
  13. package/kb/craft/SOURCES.md +48 -0
  14. package/kb/craft/creativity.md +55 -0
  15. package/kb/craft/details.md +75 -0
  16. package/kb/craft/landing-demos.md +46 -0
  17. package/kb/craft/mistakes.md +73 -0
  18. package/kb/craft/motion.md +53 -0
  19. package/kb/craft/principles.md +68 -0
  20. package/kb/index/discovery.json +1 -0
  21. package/kb/inspiration/index.json +79 -0
  22. package/kb/moves/animated-comparison-table.md +58 -0
  23. package/kb/moves/before-after-slider.md +58 -0
  24. package/kb/moves/bento-live-tiles.md +57 -0
  25. package/kb/moves/calm-motion-system.md +63 -0
  26. package/kb/moves/canvas-grain.md +58 -0
  27. package/kb/moves/changelog-as-magazine.md +56 -0
  28. package/kb/moves/code-drawn-diagram.md +56 -0
  29. package/kb/moves/colophon.md +54 -0
  30. package/kb/moves/command-palette-site-nav.md +58 -0
  31. package/kb/moves/confetti-free-celebration.md +58 -0
  32. package/kb/moves/console-easter-egg.md +57 -0
  33. package/kb/moves/copy-confirmation.md +57 -0
  34. package/kb/moves/cursor-reactive-light.md +57 -0
  35. package/kb/moves/dark-band-contrast-section.md +55 -0
  36. package/kb/moves/designed-dark-mode.md +58 -0
  37. package/kb/moves/duotone-photography.md +55 -0
  38. package/kb/moves/editorial-drop-cap.md +58 -0
  39. package/kb/moves/editorial-grid-pull-quotes.md +55 -0
  40. package/kb/moves/engraving-colour-lens.md +64 -0
  41. package/kb/moves/explorable-control.md +57 -0
  42. package/kb/moves/feature-films-on-painted-stages.md +58 -0
  43. package/kb/moves/first-win-onboarding.md +59 -0
  44. package/kb/moves/footer-live-status.md +56 -0
  45. package/kb/moves/footer-rewards-the-end.md +58 -0
  46. package/kb/moves/generative-brand-pattern.md +55 -0
  47. package/kb/moves/gentle-error-nudge.md +59 -0
  48. package/kb/moves/halftone-dot-imagery.md +55 -0
  49. package/kb/moves/hand-drawn-annotations.md +55 -0
  50. package/kb/moves/hero-paints-itself-in.md +65 -0
  51. package/kb/moves/honest-progress-narration.md +58 -0
  52. package/kb/moves/honest-testimonial-wall.md +57 -0
  53. package/kb/moves/inline-sparklines-in-copy.md +57 -0
  54. package/kb/moves/inspect-our-taste.md +58 -0
  55. package/kb/moves/interactive-pricing-calculator.md +59 -0
  56. package/kb/moves/isometric-illustration-system.md +56 -0
  57. package/kb/moves/kinetic-variable-type.md +57 -0
  58. package/kb/moves/konami-secret.md +59 -0
  59. package/kb/moves/layered-paper-depth.md +56 -0
  60. package/kb/moves/lightweight-3d-object-hero.md +59 -0
  61. package/kb/moves/line-engraving-from-image.md +63 -0
  62. package/kb/moves/link-preview-peek.md +57 -0
  63. package/kb/moves/live-data-ticker.md +61 -0
  64. package/kb/moves/made-with-proof.md +51 -0
  65. package/kb/moves/magnetic-buttons.md +55 -0
  66. package/kb/moves/marquee-done-right.md +58 -0
  67. package/kb/moves/moves.yaml +450 -0
  68. package/kb/moves/museum-painting-backdrop.md +59 -0
  69. package/kb/moves/one-colour-brand-world.md +56 -0
  70. package/kb/moves/playful-404.md +58 -0
  71. package/kb/moves/pointer-highlight-word.md +56 -0
  72. package/kb/moves/product-in-context.md +56 -0
  73. package/kb/moves/real-world-data.md +58 -0
  74. package/kb/moves/referral-reward-reveal.md +59 -0
  75. package/kb/moves/riso-print-texture.md +57 -0
  76. package/kb/moves/screenshot-detail-crops.md +56 -0
  77. package/kb/moves/scroll-progress-chapters.md +57 -0
  78. package/kb/moves/scroll-scrubbed-product-film.md +62 -0
  79. package/kb/moves/self-playing-product-demo.md +61 -0
  80. package/kb/moves/shape-continuity-morph.md +58 -0
  81. package/kb/moves/sliding-nav-indicator.md +58 -0
  82. package/kb/moves/sticky-scroll-story.md +56 -0
  83. package/kb/moves/story-led-long-scroll.md +56 -0
  84. package/kb/moves/tactile-button.md +63 -0
  85. package/kb/moves/technical-drawing-lines.md +57 -0
  86. package/kb/moves/time-of-day-theming.md +56 -0
  87. package/kb/moves/tiny-game-easter-egg.md +60 -0
  88. package/kb/moves/type-decode-reveal.md +57 -0
  89. package/kb/moves/typographic-numeral-hero.md +58 -0
  90. package/kb/moves/view-transitions-between-pages.md +57 -0
  91. package/kb/moves/waitlist-position-live.md +60 -0
  92. package/kb/patterns/app-team-members.md +27 -0
  93. package/kb/patterns/chat-agent-approvals.md +31 -0
  94. package/kb/patterns/commerce-product-page.md +34 -0
  95. package/kb/patterns/dashboard-entity-table.md +37 -0
  96. package/kb/patterns/dashboard-home-today.md +24 -0
  97. package/kb/patterns/docs-api-reference.md +31 -0
  98. package/kb/patterns/marketing-product-led-landing.md +30 -0
  99. package/kb/patterns/mobile-daily-checklist.md +26 -0
  100. package/kb/references/index.json +1 -0
  101. package/kb/references/thumbs/004c5e9072.jpg +0 -0
  102. package/kb/references/thumbs/008915eb21.jpg +0 -0
  103. package/kb/references/thumbs/0098cf66c2.jpg +0 -0
  104. package/kb/references/thumbs/012e61b31f.jpg +0 -0
  105. package/kb/references/thumbs/014d4e46fc.jpg +0 -0
  106. package/kb/references/thumbs/01e54f394a.jpg +0 -0
  107. package/kb/references/thumbs/01e99a579c.jpg +0 -0
  108. package/kb/references/thumbs/01fd014575.jpg +0 -0
  109. package/kb/references/thumbs/022778ca72.jpg +0 -0
  110. package/kb/references/thumbs/02717bf1e1.jpg +0 -0
  111. package/kb/references/thumbs/02a5cfdbe9.jpg +0 -0
  112. package/kb/references/thumbs/02c9823f8d.jpg +0 -0
  113. package/kb/references/thumbs/02f3974613.jpg +0 -0
  114. package/kb/references/thumbs/032427f1ce.jpg +0 -0
  115. package/kb/references/thumbs/03ba677d07.jpg +0 -0
  116. package/kb/references/thumbs/04f2476cfe.jpg +0 -0
  117. package/kb/references/thumbs/0563e7c52e.jpg +0 -0
  118. package/kb/references/thumbs/05fa6b8eef.jpg +0 -0
  119. package/kb/references/thumbs/065bce1517.jpg +0 -0
  120. package/kb/references/thumbs/06c017f113.jpg +0 -0
  121. package/kb/references/thumbs/0770e2d8d3.jpg +0 -0
  122. package/kb/references/thumbs/078a26c4cb.jpg +0 -0
  123. package/kb/references/thumbs/08679b6e84.jpg +0 -0
  124. package/kb/references/thumbs/088c45a34c.jpg +0 -0
  125. package/kb/references/thumbs/089e9fd4e4.jpg +0 -0
  126. package/kb/references/thumbs/08f773231e.jpg +0 -0
  127. package/kb/references/thumbs/093ca3e2e3.jpg +0 -0
  128. package/kb/references/thumbs/0963320f74.jpg +0 -0
  129. package/kb/references/thumbs/09fc1ded85.jpg +0 -0
  130. package/kb/references/thumbs/0a368f3963.jpg +0 -0
  131. package/kb/references/thumbs/0a9e0e5201.jpg +0 -0
  132. package/kb/references/thumbs/0aace67ee7.jpg +0 -0
  133. package/kb/references/thumbs/0b61ecf793.jpg +0 -0
  134. package/kb/references/thumbs/0bde18ae09.jpg +0 -0
  135. package/kb/references/thumbs/0bf7ee8143.jpg +0 -0
  136. package/kb/references/thumbs/0ca20a13e0.jpg +0 -0
  137. package/kb/references/thumbs/0cdc0198c4.jpg +0 -0
  138. package/kb/references/thumbs/0cf9790b28.jpg +0 -0
  139. package/kb/references/thumbs/0d0202904b.jpg +0 -0
  140. package/kb/references/thumbs/0d053aba18.jpg +0 -0
  141. package/kb/references/thumbs/0d0c2c13d2.jpg +0 -0
  142. package/kb/references/thumbs/0d30be99b0.jpg +0 -0
  143. package/kb/references/thumbs/0d503da1b7.jpg +0 -0
  144. package/kb/references/thumbs/0ddad7b740.jpg +0 -0
  145. package/kb/references/thumbs/0e0a148101.jpg +0 -0
  146. package/kb/references/thumbs/0e401b6b75.jpg +0 -0
  147. package/kb/references/thumbs/0e8f32ba0f.jpg +0 -0
  148. package/kb/references/thumbs/0ef1966a7d.jpg +0 -0
  149. package/kb/references/thumbs/0efd416d11.jpg +0 -0
  150. package/kb/references/thumbs/0f3af263ae.jpg +0 -0
  151. package/kb/references/thumbs/0f9644c3a5.jpg +0 -0
  152. package/kb/references/thumbs/1020552737.jpg +0 -0
  153. package/kb/references/thumbs/1037626587.jpg +0 -0
  154. package/kb/references/thumbs/10512b7e94.jpg +0 -0
  155. package/kb/references/thumbs/106dc4a7b5.jpg +0 -0
  156. package/kb/references/thumbs/108943f6f7.jpg +0 -0
  157. package/kb/references/thumbs/10a5551d51.jpg +0 -0
  158. package/kb/references/thumbs/10dd01076c.jpg +0 -0
  159. package/kb/references/thumbs/11b06e39da.jpg +0 -0
  160. package/kb/references/thumbs/11baa1ec83.jpg +0 -0
  161. package/kb/references/thumbs/11bbe12167.jpg +0 -0
  162. package/kb/references/thumbs/11ca37feb8.jpg +0 -0
  163. package/kb/references/thumbs/11f14d3f12.jpg +0 -0
  164. package/kb/references/thumbs/124530ceb5.jpg +0 -0
  165. package/kb/references/thumbs/1276402336.jpg +0 -0
  166. package/kb/references/thumbs/1280e5c9d6.jpg +0 -0
  167. package/kb/references/thumbs/12cac0d172.jpg +0 -0
  168. package/kb/references/thumbs/138be6623a.jpg +0 -0
  169. package/kb/references/thumbs/13a0c544d1.jpg +0 -0
  170. package/kb/references/thumbs/13a32cb94c.jpg +0 -0
  171. package/kb/references/thumbs/144035d641.jpg +0 -0
  172. package/kb/references/thumbs/1457e41924.jpg +0 -0
  173. package/kb/references/thumbs/14972e3a86.jpg +0 -0
  174. package/kb/references/thumbs/15a47f2e4c.jpg +0 -0
  175. package/kb/references/thumbs/15aa15d22e.jpg +0 -0
  176. package/kb/references/thumbs/160dcd24b7.jpg +0 -0
  177. package/kb/references/thumbs/1610978529.jpg +0 -0
  178. package/kb/references/thumbs/16db30ba37.jpg +0 -0
  179. package/kb/references/thumbs/16f19ae4f3.jpg +0 -0
  180. package/kb/references/thumbs/16f758b03f.jpg +0 -0
  181. package/kb/references/thumbs/1748e5b751.jpg +0 -0
  182. package/kb/references/thumbs/175d0b0e69.jpg +0 -0
  183. package/kb/references/thumbs/178f571a36.jpg +0 -0
  184. package/kb/references/thumbs/17c43dc025.jpg +0 -0
  185. package/kb/references/thumbs/17c7f05b50.jpg +0 -0
  186. package/kb/references/thumbs/17f97e61dd.jpg +0 -0
  187. package/kb/references/thumbs/1804e17365.jpg +0 -0
  188. package/kb/references/thumbs/195c162ed1.jpg +0 -0
  189. package/kb/references/thumbs/19e3699c29.jpg +0 -0
  190. package/kb/references/thumbs/1a48e0a4d5.jpg +0 -0
  191. package/kb/references/thumbs/1a6cda265e.jpg +0 -0
  192. package/kb/references/thumbs/1a7e012091.jpg +0 -0
  193. package/kb/references/thumbs/1a9173a172.jpg +0 -0
  194. package/kb/references/thumbs/1b34a6b4d2.jpg +0 -0
  195. package/kb/references/thumbs/1b5284f3b2.jpg +0 -0
  196. package/kb/references/thumbs/1b93fbd6d3.jpg +0 -0
  197. package/kb/references/thumbs/1c6728fef6.jpg +0 -0
  198. package/kb/references/thumbs/1c6f101618.jpg +0 -0
  199. package/kb/references/thumbs/1cf7011a94.jpg +0 -0
  200. package/kb/references/thumbs/1d1769e539.jpg +0 -0
  201. package/kb/references/thumbs/1d1fd23199.jpg +0 -0
  202. package/kb/references/thumbs/1d7b6602ac.jpg +0 -0
  203. package/kb/references/thumbs/1dbbc61b9e.jpg +0 -0
  204. package/kb/references/thumbs/1dbdd6d21d.jpg +0 -0
  205. package/kb/references/thumbs/1dd873744b.jpg +0 -0
  206. package/kb/references/thumbs/1df4dd7cf9.jpg +0 -0
  207. package/kb/references/thumbs/1ece8c35b8.jpg +0 -0
  208. package/kb/references/thumbs/1f0e5b9aa4.jpg +0 -0
  209. package/kb/references/thumbs/1f1419651b.jpg +0 -0
  210. package/kb/references/thumbs/1f2a6da92d.jpg +0 -0
  211. package/kb/references/thumbs/1f651f33c0.jpg +0 -0
  212. package/kb/references/thumbs/1f8b697d18.jpg +0 -0
  213. package/kb/references/thumbs/1fee3824cd.jpg +0 -0
  214. package/kb/references/thumbs/1ff7757853.jpg +0 -0
  215. package/kb/references/thumbs/2043182f01.jpg +0 -0
  216. package/kb/references/thumbs/2059dabcd5.jpg +0 -0
  217. package/kb/references/thumbs/209d9dbb95.jpg +0 -0
  218. package/kb/references/thumbs/20d8923554.jpg +0 -0
  219. package/kb/references/thumbs/210dd3b671.jpg +0 -0
  220. package/kb/references/thumbs/2120719116.jpg +0 -0
  221. package/kb/references/thumbs/21a7384429.jpg +0 -0
  222. package/kb/references/thumbs/225f187391.jpg +0 -0
  223. package/kb/references/thumbs/230caf7dd2.jpg +0 -0
  224. package/kb/references/thumbs/235afad355.jpg +0 -0
  225. package/kb/references/thumbs/2378066687.jpg +0 -0
  226. package/kb/references/thumbs/241faf68a1.jpg +0 -0
  227. package/kb/references/thumbs/24d9f1a9b9.jpg +0 -0
  228. package/kb/references/thumbs/2505c8b9a0.jpg +0 -0
  229. package/kb/references/thumbs/25593638ca.jpg +0 -0
  230. package/kb/references/thumbs/25dd454f18.jpg +0 -0
  231. package/kb/references/thumbs/2602e123e2.jpg +0 -0
  232. package/kb/references/thumbs/26095c0b11.jpg +0 -0
  233. package/kb/references/thumbs/26260d6238.jpg +0 -0
  234. package/kb/references/thumbs/26bba21836.jpg +0 -0
  235. package/kb/references/thumbs/26bde3520b.jpg +0 -0
  236. package/kb/references/thumbs/272e22067c.jpg +0 -0
  237. package/kb/references/thumbs/27421b3226.jpg +0 -0
  238. package/kb/references/thumbs/2769ebca41.jpg +0 -0
  239. package/kb/references/thumbs/27e2f6e8b0.jpg +0 -0
  240. package/kb/references/thumbs/28432ba373.jpg +0 -0
  241. package/kb/references/thumbs/2846626f19.jpg +0 -0
  242. package/kb/references/thumbs/28750e821e.jpg +0 -0
  243. package/kb/references/thumbs/289481230f.jpg +0 -0
  244. package/kb/references/thumbs/2897967408.jpg +0 -0
  245. package/kb/references/thumbs/28c0a5fa17.jpg +0 -0
  246. package/kb/references/thumbs/28c181fba5.jpg +0 -0
  247. package/kb/references/thumbs/29806f539f.jpg +0 -0
  248. package/kb/references/thumbs/2997f56ee7.jpg +0 -0
  249. package/kb/references/thumbs/29b34a10fb.jpg +0 -0
  250. package/kb/references/thumbs/29bd78cdb4.jpg +0 -0
  251. package/kb/references/thumbs/29c8c6c22a.jpg +0 -0
  252. package/kb/references/thumbs/2ac23f7fae.jpg +0 -0
  253. package/kb/references/thumbs/2b76e13f79.jpg +0 -0
  254. package/kb/references/thumbs/2b84226479.jpg +0 -0
  255. package/kb/references/thumbs/2bf2dbf737.jpg +0 -0
  256. package/kb/references/thumbs/2bf89abbf6.jpg +0 -0
  257. package/kb/references/thumbs/2c562cd534.jpg +0 -0
  258. package/kb/references/thumbs/2cfe9d48c4.jpg +0 -0
  259. package/kb/references/thumbs/2d49f19b27.jpg +0 -0
  260. package/kb/references/thumbs/2dc8670da7.jpg +0 -0
  261. package/kb/references/thumbs/2de947c469.jpg +0 -0
  262. package/kb/references/thumbs/2e075ca79f.jpg +0 -0
  263. package/kb/references/thumbs/2eb0820e6b.jpg +0 -0
  264. package/kb/references/thumbs/2ec56a9dc0.jpg +0 -0
  265. package/kb/references/thumbs/30bb4ad607.jpg +0 -0
  266. package/kb/references/thumbs/30d19b8444.jpg +0 -0
  267. package/kb/references/thumbs/3173c71d73.jpg +0 -0
  268. package/kb/references/thumbs/31e59e4518.jpg +0 -0
  269. package/kb/references/thumbs/322b8960e8.jpg +0 -0
  270. package/kb/references/thumbs/325c9be6f4.jpg +0 -0
  271. package/kb/references/thumbs/3273338982.jpg +0 -0
  272. package/kb/references/thumbs/32f8387f38.jpg +0 -0
  273. package/kb/references/thumbs/332f6b74c3.jpg +0 -0
  274. package/kb/references/thumbs/334b146b6a.jpg +0 -0
  275. package/kb/references/thumbs/339abd60e4.jpg +0 -0
  276. package/kb/references/thumbs/33e88ec295.jpg +0 -0
  277. package/kb/references/thumbs/340931b5d4.jpg +0 -0
  278. package/kb/references/thumbs/34165a93bd.jpg +0 -0
  279. package/kb/references/thumbs/34d0575dd7.jpg +0 -0
  280. package/kb/references/thumbs/3549e75f0a.jpg +0 -0
  281. package/kb/references/thumbs/35827f1561.jpg +0 -0
  282. package/kb/references/thumbs/37c50280ec.jpg +0 -0
  283. package/kb/references/thumbs/37e66ecf11.jpg +0 -0
  284. package/kb/references/thumbs/3841a04fdc.jpg +0 -0
  285. package/kb/references/thumbs/38490c088f.jpg +0 -0
  286. package/kb/references/thumbs/3850319e9f.jpg +0 -0
  287. package/kb/references/thumbs/3856a64166.jpg +0 -0
  288. package/kb/references/thumbs/3881f211c2.jpg +0 -0
  289. package/kb/references/thumbs/38c7dde2f3.jpg +0 -0
  290. package/kb/references/thumbs/38ce9e486c.jpg +0 -0
  291. package/kb/references/thumbs/390ea8e01b.jpg +0 -0
  292. package/kb/references/thumbs/391b7fc27c.jpg +0 -0
  293. package/kb/references/thumbs/395f7214dc.jpg +0 -0
  294. package/kb/references/thumbs/39e81a5db2.jpg +0 -0
  295. package/kb/references/thumbs/3a67c8d655.jpg +0 -0
  296. package/kb/references/thumbs/3a89c25e3b.jpg +0 -0
  297. package/kb/references/thumbs/3aa0837542.jpg +0 -0
  298. package/kb/references/thumbs/3aae18aa8a.jpg +0 -0
  299. package/kb/references/thumbs/3bdbe767db.jpg +0 -0
  300. package/kb/references/thumbs/3c0616fc7f.jpg +0 -0
  301. package/kb/references/thumbs/3c18e9db1d.jpg +0 -0
  302. package/kb/references/thumbs/3c5a14076c.jpg +0 -0
  303. package/kb/references/thumbs/3ca9ec15c6.jpg +0 -0
  304. package/kb/references/thumbs/3d7756fd35.jpg +0 -0
  305. package/kb/references/thumbs/3dd45bd33d.jpg +0 -0
  306. package/kb/references/thumbs/3de72d5414.jpg +0 -0
  307. package/kb/references/thumbs/3dede597af.jpg +0 -0
  308. package/kb/references/thumbs/3ea948619e.jpg +0 -0
  309. package/kb/references/thumbs/402c12054c.jpg +0 -0
  310. package/kb/references/thumbs/403ab4af90.jpg +0 -0
  311. package/kb/references/thumbs/40773a8c63.jpg +0 -0
  312. package/kb/references/thumbs/407d9583db.jpg +0 -0
  313. package/kb/references/thumbs/415dc1d84c.jpg +0 -0
  314. package/kb/references/thumbs/420b676a4f.jpg +0 -0
  315. package/kb/references/thumbs/42a1ee85eb.jpg +0 -0
  316. package/kb/references/thumbs/43059221aa.jpg +0 -0
  317. package/kb/references/thumbs/432fa0d22c.jpg +0 -0
  318. package/kb/references/thumbs/43826f20e3.jpg +0 -0
  319. package/kb/references/thumbs/43ac55c98a.jpg +0 -0
  320. package/kb/references/thumbs/43d098f3c3.jpg +0 -0
  321. package/kb/references/thumbs/4449d80c2c.jpg +0 -0
  322. package/kb/references/thumbs/44625727dc.jpg +0 -0
  323. package/kb/references/thumbs/4482bb87a1.jpg +0 -0
  324. package/kb/references/thumbs/44b078acdc.jpg +0 -0
  325. package/kb/references/thumbs/44d0305850.jpg +0 -0
  326. package/kb/references/thumbs/456bb6b59e.jpg +0 -0
  327. package/kb/references/thumbs/45df73ce48.jpg +0 -0
  328. package/kb/references/thumbs/45edb45d87.jpg +0 -0
  329. package/kb/references/thumbs/4671d57cd5.jpg +0 -0
  330. package/kb/references/thumbs/46cc108518.jpg +0 -0
  331. package/kb/references/thumbs/470022e614.jpg +0 -0
  332. package/kb/references/thumbs/4719a669ce.jpg +0 -0
  333. package/kb/references/thumbs/47404f5ad4.jpg +0 -0
  334. package/kb/references/thumbs/479d3eb977.jpg +0 -0
  335. package/kb/references/thumbs/479ed92ceb.jpg +0 -0
  336. package/kb/references/thumbs/47baed50c7.jpg +0 -0
  337. package/kb/references/thumbs/47fdcfd7b0.jpg +0 -0
  338. package/kb/references/thumbs/48fd5e94c5.jpg +0 -0
  339. package/kb/references/thumbs/493807917b.jpg +0 -0
  340. package/kb/references/thumbs/49a1c67293.jpg +0 -0
  341. package/kb/references/thumbs/49e2d7fafe.jpg +0 -0
  342. package/kb/references/thumbs/49f540fcb6.jpg +0 -0
  343. package/kb/references/thumbs/4a67ec4028.jpg +0 -0
  344. package/kb/references/thumbs/4a93275b04.jpg +0 -0
  345. package/kb/references/thumbs/4aae6f52da.jpg +0 -0
  346. package/kb/references/thumbs/4b1bae66bd.jpg +0 -0
  347. package/kb/references/thumbs/4b75da15a9.jpg +0 -0
  348. package/kb/references/thumbs/4b77789b88.jpg +0 -0
  349. package/kb/references/thumbs/4bc2f3f3a4.jpg +0 -0
  350. package/kb/references/thumbs/4c48f27b88.jpg +0 -0
  351. package/kb/references/thumbs/4c52bd8bfb.jpg +0 -0
  352. package/kb/references/thumbs/4c9b4acc60.jpg +0 -0
  353. package/kb/references/thumbs/4cbf0752d4.jpg +0 -0
  354. package/kb/references/thumbs/4cd2672517.jpg +0 -0
  355. package/kb/references/thumbs/4dd22a8181.jpg +0 -0
  356. package/kb/references/thumbs/4ee3447314.jpg +0 -0
  357. package/kb/references/thumbs/4ef29c8144.jpg +0 -0
  358. package/kb/references/thumbs/4f82fadfd6.jpg +0 -0
  359. package/kb/references/thumbs/4fc9b643d8.jpg +0 -0
  360. package/kb/references/thumbs/50549524b0.jpg +0 -0
  361. package/kb/references/thumbs/509a615883.jpg +0 -0
  362. package/kb/references/thumbs/50c03ef855.jpg +0 -0
  363. package/kb/references/thumbs/50cf9acad6.jpg +0 -0
  364. package/kb/references/thumbs/51131b025c.jpg +0 -0
  365. package/kb/references/thumbs/51da1124f0.jpg +0 -0
  366. package/kb/references/thumbs/524d5e7288.jpg +0 -0
  367. package/kb/references/thumbs/5251247c81.jpg +0 -0
  368. package/kb/references/thumbs/526b91b7ec.jpg +0 -0
  369. package/kb/references/thumbs/52f02f6c2e.jpg +0 -0
  370. package/kb/references/thumbs/530ab530dc.jpg +0 -0
  371. package/kb/references/thumbs/5335ee4404.jpg +0 -0
  372. package/kb/references/thumbs/5385b90939.jpg +0 -0
  373. package/kb/references/thumbs/53c54654cc.jpg +0 -0
  374. package/kb/references/thumbs/5420794c6c.jpg +0 -0
  375. package/kb/references/thumbs/542a9dce8b.jpg +0 -0
  376. package/kb/references/thumbs/5571530a5f.jpg +0 -0
  377. package/kb/references/thumbs/56cc68d804.jpg +0 -0
  378. package/kb/references/thumbs/577b7f7c44.jpg +0 -0
  379. package/kb/references/thumbs/57adf8f1bb.jpg +0 -0
  380. package/kb/references/thumbs/57c5f5e3a6.jpg +0 -0
  381. package/kb/references/thumbs/57ffe4e839.jpg +0 -0
  382. package/kb/references/thumbs/5817dfc7d6.jpg +0 -0
  383. package/kb/references/thumbs/581a182a7a.jpg +0 -0
  384. package/kb/references/thumbs/58807679c8.jpg +0 -0
  385. package/kb/references/thumbs/589a1d9111.jpg +0 -0
  386. package/kb/references/thumbs/59167391ab.jpg +0 -0
  387. package/kb/references/thumbs/5a70420166.jpg +0 -0
  388. package/kb/references/thumbs/5b10df1b8d.jpg +0 -0
  389. package/kb/references/thumbs/5b7a712fdb.jpg +0 -0
  390. package/kb/references/thumbs/5bc46b92ad.jpg +0 -0
  391. package/kb/references/thumbs/5c0546a120.jpg +0 -0
  392. package/kb/references/thumbs/5c2b6125a8.jpg +0 -0
  393. package/kb/references/thumbs/5c536b9e42.jpg +0 -0
  394. package/kb/references/thumbs/5c8e441d30.jpg +0 -0
  395. package/kb/references/thumbs/5cb6f1baa5.jpg +0 -0
  396. package/kb/references/thumbs/5ce6c1f21e.jpg +0 -0
  397. package/kb/references/thumbs/5cef052c0a.jpg +0 -0
  398. package/kb/references/thumbs/5d08438209.jpg +0 -0
  399. package/kb/references/thumbs/5d0f9af9b0.jpg +0 -0
  400. package/kb/references/thumbs/5d300ed332.jpg +0 -0
  401. package/kb/references/thumbs/5d708a4794.jpg +0 -0
  402. package/kb/references/thumbs/5de8826a98.jpg +0 -0
  403. package/kb/references/thumbs/5df7bff22c.jpg +0 -0
  404. package/kb/references/thumbs/5e087d0f00.jpg +0 -0
  405. package/kb/references/thumbs/5e44e70ca0.jpg +0 -0
  406. package/kb/references/thumbs/5eff5d80ce.jpg +0 -0
  407. package/kb/references/thumbs/5f015c96b1.jpg +0 -0
  408. package/kb/references/thumbs/5f3289b7c4.jpg +0 -0
  409. package/kb/references/thumbs/5f830c40eb.jpg +0 -0
  410. package/kb/references/thumbs/60c4c3527b.jpg +0 -0
  411. package/kb/references/thumbs/60e9e594dd.jpg +0 -0
  412. package/kb/references/thumbs/615421d7be.jpg +0 -0
  413. package/kb/references/thumbs/6176e4004f.jpg +0 -0
  414. package/kb/references/thumbs/61f0fe5e06.jpg +0 -0
  415. package/kb/references/thumbs/6213785db4.jpg +0 -0
  416. package/kb/references/thumbs/62a670ae09.jpg +0 -0
  417. package/kb/references/thumbs/63cc12981b.jpg +0 -0
  418. package/kb/references/thumbs/641f15cc71.jpg +0 -0
  419. package/kb/references/thumbs/642ab2081d.jpg +0 -0
  420. package/kb/references/thumbs/644a59bf6e.jpg +0 -0
  421. package/kb/references/thumbs/644da8a8e3.jpg +0 -0
  422. package/kb/references/thumbs/6458986897.jpg +0 -0
  423. package/kb/references/thumbs/64cfbd7b93.jpg +0 -0
  424. package/kb/references/thumbs/661a57686a.jpg +0 -0
  425. package/kb/references/thumbs/66355a813a.jpg +0 -0
  426. package/kb/references/thumbs/664396dd44.jpg +0 -0
  427. package/kb/references/thumbs/66d832d65f.jpg +0 -0
  428. package/kb/references/thumbs/66de9e9833.jpg +0 -0
  429. package/kb/references/thumbs/6750964fd9.jpg +0 -0
  430. package/kb/references/thumbs/67c7164832.jpg +0 -0
  431. package/kb/references/thumbs/681600b6cd.jpg +0 -0
  432. package/kb/references/thumbs/682e75afe4.jpg +0 -0
  433. package/kb/references/thumbs/69274bc952.jpg +0 -0
  434. package/kb/references/thumbs/696ec08c52.jpg +0 -0
  435. package/kb/references/thumbs/69760111cc.jpg +0 -0
  436. package/kb/references/thumbs/697e32664f.jpg +0 -0
  437. package/kb/references/thumbs/69df92544b.jpg +0 -0
  438. package/kb/references/thumbs/69e48c5229.jpg +0 -0
  439. package/kb/references/thumbs/6a5fb75de1.jpg +0 -0
  440. package/kb/references/thumbs/6a7389e472.jpg +0 -0
  441. package/kb/references/thumbs/6a9b041e02.jpg +0 -0
  442. package/kb/references/thumbs/6ab2fa93ee.jpg +0 -0
  443. package/kb/references/thumbs/6ae3400319.jpg +0 -0
  444. package/kb/references/thumbs/6b21344bf3.jpg +0 -0
  445. package/kb/references/thumbs/6bd1b67a8a.jpg +0 -0
  446. package/kb/references/thumbs/6c99156c4f.jpg +0 -0
  447. package/kb/references/thumbs/6cc9b65384.jpg +0 -0
  448. package/kb/references/thumbs/6d15830a73.jpg +0 -0
  449. package/kb/references/thumbs/6d4c33d180.jpg +0 -0
  450. package/kb/references/thumbs/6e36815799.jpg +0 -0
  451. package/kb/references/thumbs/6eddb9cbfc.jpg +0 -0
  452. package/kb/references/thumbs/6f97968a7c.jpg +0 -0
  453. package/kb/references/thumbs/6fa0716a7d.jpg +0 -0
  454. package/kb/references/thumbs/6facd57184.jpg +0 -0
  455. package/kb/references/thumbs/6fd57b785a.jpg +0 -0
  456. package/kb/references/thumbs/700b3cb185.jpg +0 -0
  457. package/kb/references/thumbs/71197443fa.jpg +0 -0
  458. package/kb/references/thumbs/72082bc9ee.jpg +0 -0
  459. package/kb/references/thumbs/72453db479.jpg +0 -0
  460. package/kb/references/thumbs/72cd64d500.jpg +0 -0
  461. package/kb/references/thumbs/72d1e75283.jpg +0 -0
  462. package/kb/references/thumbs/72e13f7f5b.jpg +0 -0
  463. package/kb/references/thumbs/7459f8e644.jpg +0 -0
  464. package/kb/references/thumbs/7470d55c79.jpg +0 -0
  465. package/kb/references/thumbs/74c468f02a.jpg +0 -0
  466. package/kb/references/thumbs/74fed6eb77.jpg +0 -0
  467. package/kb/references/thumbs/756e05080d.jpg +0 -0
  468. package/kb/references/thumbs/75df7e5f49.jpg +0 -0
  469. package/kb/references/thumbs/76226da6ef.jpg +0 -0
  470. package/kb/references/thumbs/76a34aa056.jpg +0 -0
  471. package/kb/references/thumbs/76c7076589.jpg +0 -0
  472. package/kb/references/thumbs/76cccc396c.jpg +0 -0
  473. package/kb/references/thumbs/774654789f.jpg +0 -0
  474. package/kb/references/thumbs/7746cc494d.jpg +0 -0
  475. package/kb/references/thumbs/7766e36aa3.jpg +0 -0
  476. package/kb/references/thumbs/78049331ef.jpg +0 -0
  477. package/kb/references/thumbs/78808a0ca5.jpg +0 -0
  478. package/kb/references/thumbs/7890b0a310.jpg +0 -0
  479. package/kb/references/thumbs/78911975e6.jpg +0 -0
  480. package/kb/references/thumbs/78d289d5a0.jpg +0 -0
  481. package/kb/references/thumbs/794126267e.jpg +0 -0
  482. package/kb/references/thumbs/7976eb9025.jpg +0 -0
  483. package/kb/references/thumbs/798519ec7c.jpg +0 -0
  484. package/kb/references/thumbs/7a6cc5eecc.jpg +0 -0
  485. package/kb/references/thumbs/7ac8a7cd4a.jpg +0 -0
  486. package/kb/references/thumbs/7b1341d61d.jpg +0 -0
  487. package/kb/references/thumbs/7b9c4031e3.jpg +0 -0
  488. package/kb/references/thumbs/7ba5ad3779.jpg +0 -0
  489. package/kb/references/thumbs/7be6eb2bd0.jpg +0 -0
  490. package/kb/references/thumbs/7c30864f5d.jpg +0 -0
  491. package/kb/references/thumbs/7c379b5e17.jpg +0 -0
  492. package/kb/references/thumbs/7c4050eb02.jpg +0 -0
  493. package/kb/references/thumbs/7ca47b3221.jpg +0 -0
  494. package/kb/references/thumbs/7d43516bd7.jpg +0 -0
  495. package/kb/references/thumbs/7da9dbb98a.jpg +0 -0
  496. package/kb/references/thumbs/7ddda8f57c.jpg +0 -0
  497. package/kb/references/thumbs/7ece6d215c.jpg +0 -0
  498. package/kb/references/thumbs/7eebf029de.jpg +0 -0
  499. package/kb/references/thumbs/7f45483c15.jpg +0 -0
  500. package/kb/references/thumbs/7fde06eaa5.jpg +0 -0
  501. package/kb/references/thumbs/80407b97e9.jpg +0 -0
  502. package/kb/references/thumbs/80f0a6cf4b.jpg +0 -0
  503. package/kb/references/thumbs/8118b451b8.jpg +0 -0
  504. package/kb/references/thumbs/816baed4a8.jpg +0 -0
  505. package/kb/references/thumbs/817d2d15d9.jpg +0 -0
  506. package/kb/references/thumbs/8260cb430d.jpg +0 -0
  507. package/kb/references/thumbs/827b562899.jpg +0 -0
  508. package/kb/references/thumbs/8293cf1e92.jpg +0 -0
  509. package/kb/references/thumbs/8294653169.jpg +0 -0
  510. package/kb/references/thumbs/83061f871f.jpg +0 -0
  511. package/kb/references/thumbs/833df778ca.jpg +0 -0
  512. package/kb/references/thumbs/83d3acaafc.jpg +0 -0
  513. package/kb/references/thumbs/844c6efeff.jpg +0 -0
  514. package/kb/references/thumbs/84563d482a.jpg +0 -0
  515. package/kb/references/thumbs/848082dd94.jpg +0 -0
  516. package/kb/references/thumbs/8494b1da94.jpg +0 -0
  517. package/kb/references/thumbs/84b71014be.jpg +0 -0
  518. package/kb/references/thumbs/84be4a5f3c.jpg +0 -0
  519. package/kb/references/thumbs/84fd0fd25a.jpg +0 -0
  520. package/kb/references/thumbs/853e488e22.jpg +0 -0
  521. package/kb/references/thumbs/858c91585d.jpg +0 -0
  522. package/kb/references/thumbs/85903e8fe9.jpg +0 -0
  523. package/kb/references/thumbs/86597d43fd.jpg +0 -0
  524. package/kb/references/thumbs/866eb99500.jpg +0 -0
  525. package/kb/references/thumbs/86ac050340.jpg +0 -0
  526. package/kb/references/thumbs/86afec7b64.jpg +0 -0
  527. package/kb/references/thumbs/87238127f5.jpg +0 -0
  528. package/kb/references/thumbs/87321df792.jpg +0 -0
  529. package/kb/references/thumbs/87460f70cd.jpg +0 -0
  530. package/kb/references/thumbs/8770dfeaec.jpg +0 -0
  531. package/kb/references/thumbs/8798451a8c.jpg +0 -0
  532. package/kb/references/thumbs/87da1f34a3.jpg +0 -0
  533. package/kb/references/thumbs/882cdf8c05.jpg +0 -0
  534. package/kb/references/thumbs/8839f4106f.jpg +0 -0
  535. package/kb/references/thumbs/8844455212.jpg +0 -0
  536. package/kb/references/thumbs/88abba0497.jpg +0 -0
  537. package/kb/references/thumbs/88b372435d.jpg +0 -0
  538. package/kb/references/thumbs/88bd4f35fa.jpg +0 -0
  539. package/kb/references/thumbs/88c71a075e.jpg +0 -0
  540. package/kb/references/thumbs/892e5f048e.jpg +0 -0
  541. package/kb/references/thumbs/893cc8e920.jpg +0 -0
  542. package/kb/references/thumbs/896c198814.jpg +0 -0
  543. package/kb/references/thumbs/8a8e22f756.jpg +0 -0
  544. package/kb/references/thumbs/8a9d8a35cb.jpg +0 -0
  545. package/kb/references/thumbs/8ab1d67fec.jpg +0 -0
  546. package/kb/references/thumbs/8ab201fe12.jpg +0 -0
  547. package/kb/references/thumbs/8ac372c102.jpg +0 -0
  548. package/kb/references/thumbs/8b15736d62.jpg +0 -0
  549. package/kb/references/thumbs/8b1d8d6a05.jpg +0 -0
  550. package/kb/references/thumbs/8bb6050e20.jpg +0 -0
  551. package/kb/references/thumbs/8c6c9d1577.jpg +0 -0
  552. package/kb/references/thumbs/8d337851e0.jpg +0 -0
  553. package/kb/references/thumbs/8d44b33cc5.jpg +0 -0
  554. package/kb/references/thumbs/8d4536ff3b.jpg +0 -0
  555. package/kb/references/thumbs/8d9b99454a.jpg +0 -0
  556. package/kb/references/thumbs/8dda2303ff.jpg +0 -0
  557. package/kb/references/thumbs/8dec722d62.jpg +0 -0
  558. package/kb/references/thumbs/8e0ea4ca74.jpg +0 -0
  559. package/kb/references/thumbs/8ebba84d69.jpg +0 -0
  560. package/kb/references/thumbs/8ef09140c0.jpg +0 -0
  561. package/kb/references/thumbs/8f2928a1b0.jpg +0 -0
  562. package/kb/references/thumbs/8f88f9ddba.jpg +0 -0
  563. package/kb/references/thumbs/8fa4451009.jpg +0 -0
  564. package/kb/references/thumbs/90109b0e02.jpg +0 -0
  565. package/kb/references/thumbs/90e854e5c9.jpg +0 -0
  566. package/kb/references/thumbs/914e79f893.jpg +0 -0
  567. package/kb/references/thumbs/9161369197.jpg +0 -0
  568. package/kb/references/thumbs/9165c87d6f.jpg +0 -0
  569. package/kb/references/thumbs/9193f6558a.jpg +0 -0
  570. package/kb/references/thumbs/91ac7df877.jpg +0 -0
  571. package/kb/references/thumbs/9216bd4931.jpg +0 -0
  572. package/kb/references/thumbs/922ae8a26a.jpg +0 -0
  573. package/kb/references/thumbs/92a70e6867.jpg +0 -0
  574. package/kb/references/thumbs/92c77ece22.jpg +0 -0
  575. package/kb/references/thumbs/92e5914fbf.jpg +0 -0
  576. package/kb/references/thumbs/92ee36419a.jpg +0 -0
  577. package/kb/references/thumbs/94153a60b0.jpg +0 -0
  578. package/kb/references/thumbs/9449666640.jpg +0 -0
  579. package/kb/references/thumbs/944be6a646.jpg +0 -0
  580. package/kb/references/thumbs/945831476e.jpg +0 -0
  581. package/kb/references/thumbs/94c1af82f4.jpg +0 -0
  582. package/kb/references/thumbs/9599d6d589.jpg +0 -0
  583. package/kb/references/thumbs/960710f4cf.jpg +0 -0
  584. package/kb/references/thumbs/9608f12d64.jpg +0 -0
  585. package/kb/references/thumbs/962dabfaa0.jpg +0 -0
  586. package/kb/references/thumbs/965a3a95c6.jpg +0 -0
  587. package/kb/references/thumbs/9672ed5ae4.jpg +0 -0
  588. package/kb/references/thumbs/96891ae9c1.jpg +0 -0
  589. package/kb/references/thumbs/96e1907c99.jpg +0 -0
  590. package/kb/references/thumbs/96f25b6578.jpg +0 -0
  591. package/kb/references/thumbs/97211df9ae.jpg +0 -0
  592. package/kb/references/thumbs/9769d763a5.jpg +0 -0
  593. package/kb/references/thumbs/9792bd6d05.jpg +0 -0
  594. package/kb/references/thumbs/97d2837e3e.jpg +0 -0
  595. package/kb/references/thumbs/97d90d1d85.jpg +0 -0
  596. package/kb/references/thumbs/97fcd5774a.jpg +0 -0
  597. package/kb/references/thumbs/980f68564e.jpg +0 -0
  598. package/kb/references/thumbs/98511fb393.jpg +0 -0
  599. package/kb/references/thumbs/98f8fa694d.jpg +0 -0
  600. package/kb/references/thumbs/9a42c6bb97.jpg +0 -0
  601. package/kb/references/thumbs/9ab3600a06.jpg +0 -0
  602. package/kb/references/thumbs/9ab48d9130.jpg +0 -0
  603. package/kb/references/thumbs/9ad519197d.jpg +0 -0
  604. package/kb/references/thumbs/9b1382c657.jpg +0 -0
  605. package/kb/references/thumbs/9b6cc1e077.jpg +0 -0
  606. package/kb/references/thumbs/9beede8480.jpg +0 -0
  607. package/kb/references/thumbs/9bf8976952.jpg +0 -0
  608. package/kb/references/thumbs/9cace7b75a.jpg +0 -0
  609. package/kb/references/thumbs/9cdcc5745e.jpg +0 -0
  610. package/kb/references/thumbs/9d0b269074.jpg +0 -0
  611. package/kb/references/thumbs/9d41788b48.jpg +0 -0
  612. package/kb/references/thumbs/9d475bd6af.jpg +0 -0
  613. package/kb/references/thumbs/9e6bc543d0.jpg +0 -0
  614. package/kb/references/thumbs/9ed0e0a206.jpg +0 -0
  615. package/kb/references/thumbs/9ef538de13.jpg +0 -0
  616. package/kb/references/thumbs/9efc373ab4.jpg +0 -0
  617. package/kb/references/thumbs/9f4a939fa5.jpg +0 -0
  618. package/kb/references/thumbs/9f8d12d7b9.jpg +0 -0
  619. package/kb/references/thumbs/9fd5e3efa7.jpg +0 -0
  620. package/kb/references/thumbs/a0a9b9c572.jpg +0 -0
  621. package/kb/references/thumbs/a29ca9b185.jpg +0 -0
  622. package/kb/references/thumbs/a2a42884af.jpg +0 -0
  623. package/kb/references/thumbs/a2f5064e34.jpg +0 -0
  624. package/kb/references/thumbs/a3112ce672.jpg +0 -0
  625. package/kb/references/thumbs/a3437736d1.jpg +0 -0
  626. package/kb/references/thumbs/a372667e76.jpg +0 -0
  627. package/kb/references/thumbs/a39bae94e9.jpg +0 -0
  628. package/kb/references/thumbs/a3fc75d94c.jpg +0 -0
  629. package/kb/references/thumbs/a440487631.jpg +0 -0
  630. package/kb/references/thumbs/a4993d5fb5.jpg +0 -0
  631. package/kb/references/thumbs/a500476a86.jpg +0 -0
  632. package/kb/references/thumbs/a55506bf46.jpg +0 -0
  633. package/kb/references/thumbs/a59a5f9843.jpg +0 -0
  634. package/kb/references/thumbs/a5bce0eede.jpg +0 -0
  635. package/kb/references/thumbs/a5d921e1f0.jpg +0 -0
  636. package/kb/references/thumbs/a5e19d4637.jpg +0 -0
  637. package/kb/references/thumbs/a5f92b97d5.jpg +0 -0
  638. package/kb/references/thumbs/a6562c210a.jpg +0 -0
  639. package/kb/references/thumbs/a6f61a8d09.jpg +0 -0
  640. package/kb/references/thumbs/a719fdf663.jpg +0 -0
  641. package/kb/references/thumbs/a724e5442f.jpg +0 -0
  642. package/kb/references/thumbs/a852b4319f.jpg +0 -0
  643. package/kb/references/thumbs/a896c08448.jpg +0 -0
  644. package/kb/references/thumbs/a8d190a4b8.jpg +0 -0
  645. package/kb/references/thumbs/a8dcdd57d7.jpg +0 -0
  646. package/kb/references/thumbs/a951347e18.jpg +0 -0
  647. package/kb/references/thumbs/a9a1a0bf7d.jpg +0 -0
  648. package/kb/references/thumbs/a9b72986a8.jpg +0 -0
  649. package/kb/references/thumbs/aa11555ba8.jpg +0 -0
  650. package/kb/references/thumbs/aa4aab9093.jpg +0 -0
  651. package/kb/references/thumbs/aa89b96658.jpg +0 -0
  652. package/kb/references/thumbs/aaeb3739b6.jpg +0 -0
  653. package/kb/references/thumbs/ab25a608ec.jpg +0 -0
  654. package/kb/references/thumbs/ab34b62268.jpg +0 -0
  655. package/kb/references/thumbs/ab3b0217bb.jpg +0 -0
  656. package/kb/references/thumbs/ac70c04afb.jpg +0 -0
  657. package/kb/references/thumbs/ace0879117.jpg +0 -0
  658. package/kb/references/thumbs/ad0515ae55.jpg +0 -0
  659. package/kb/references/thumbs/ad66b25b47.jpg +0 -0
  660. package/kb/references/thumbs/ad68229d2f.jpg +0 -0
  661. package/kb/references/thumbs/add6480693.jpg +0 -0
  662. package/kb/references/thumbs/ae5b8c3b8c.jpg +0 -0
  663. package/kb/references/thumbs/ae81247b47.jpg +0 -0
  664. package/kb/references/thumbs/af8aa54379.jpg +0 -0
  665. package/kb/references/thumbs/afa80dedd4.jpg +0 -0
  666. package/kb/references/thumbs/afe04d730f.jpg +0 -0
  667. package/kb/references/thumbs/b09a7dc315.jpg +0 -0
  668. package/kb/references/thumbs/b0c02629f6.jpg +0 -0
  669. package/kb/references/thumbs/b1cb4f91de.jpg +0 -0
  670. package/kb/references/thumbs/b1d4c13c92.jpg +0 -0
  671. package/kb/references/thumbs/b20eef86db.jpg +0 -0
  672. package/kb/references/thumbs/b3136ccdde.jpg +0 -0
  673. package/kb/references/thumbs/b3f5e8d7dd.jpg +0 -0
  674. package/kb/references/thumbs/b44adb81c9.jpg +0 -0
  675. package/kb/references/thumbs/b457a5894b.jpg +0 -0
  676. package/kb/references/thumbs/b461281e0f.jpg +0 -0
  677. package/kb/references/thumbs/b4942644f0.jpg +0 -0
  678. package/kb/references/thumbs/b49f8eccad.jpg +0 -0
  679. package/kb/references/thumbs/b56c69be23.jpg +0 -0
  680. package/kb/references/thumbs/b59786b33c.jpg +0 -0
  681. package/kb/references/thumbs/b5d368ce5a.jpg +0 -0
  682. package/kb/references/thumbs/b5eb442406.jpg +0 -0
  683. package/kb/references/thumbs/b7fdabf4ab.jpg +0 -0
  684. package/kb/references/thumbs/b88ee8d235.jpg +0 -0
  685. package/kb/references/thumbs/b8a4e5be8a.jpg +0 -0
  686. package/kb/references/thumbs/b8c85b661c.jpg +0 -0
  687. package/kb/references/thumbs/ba1b8e7435.jpg +0 -0
  688. package/kb/references/thumbs/ba9a593eed.jpg +0 -0
  689. package/kb/references/thumbs/baa7afc201.jpg +0 -0
  690. package/kb/references/thumbs/baf74398f7.jpg +0 -0
  691. package/kb/references/thumbs/bb3e947c10.jpg +0 -0
  692. package/kb/references/thumbs/bb4c1bb7c1.jpg +0 -0
  693. package/kb/references/thumbs/bbaa25cc8a.jpg +0 -0
  694. package/kb/references/thumbs/bc7e7f88cc.jpg +0 -0
  695. package/kb/references/thumbs/bc9d5b58f5.jpg +0 -0
  696. package/kb/references/thumbs/bcc99a51f7.jpg +0 -0
  697. package/kb/references/thumbs/bce13dd1c1.jpg +0 -0
  698. package/kb/references/thumbs/bd079a6130.jpg +0 -0
  699. package/kb/references/thumbs/bd2d70dda7.jpg +0 -0
  700. package/kb/references/thumbs/bd5895c5f2.jpg +0 -0
  701. package/kb/references/thumbs/bde8e31d62.jpg +0 -0
  702. package/kb/references/thumbs/be084e78f3.jpg +0 -0
  703. package/kb/references/thumbs/be94f170c8.jpg +0 -0
  704. package/kb/references/thumbs/bed33654f7.jpg +0 -0
  705. package/kb/references/thumbs/beeb9a0c18.jpg +0 -0
  706. package/kb/references/thumbs/bef42094dc.jpg +0 -0
  707. package/kb/references/thumbs/bf625496fa.jpg +0 -0
  708. package/kb/references/thumbs/bf771f1398.jpg +0 -0
  709. package/kb/references/thumbs/c0ddefa3d9.jpg +0 -0
  710. package/kb/references/thumbs/c0f01e536e.jpg +0 -0
  711. package/kb/references/thumbs/c1d592b678.jpg +0 -0
  712. package/kb/references/thumbs/c1e83e1b3e.jpg +0 -0
  713. package/kb/references/thumbs/c2168f8096.jpg +0 -0
  714. package/kb/references/thumbs/c249cd6b17.jpg +0 -0
  715. package/kb/references/thumbs/c2e3d4feed.jpg +0 -0
  716. package/kb/references/thumbs/c3cec01df1.jpg +0 -0
  717. package/kb/references/thumbs/c47bedb42d.jpg +0 -0
  718. package/kb/references/thumbs/c4b03b4680.jpg +0 -0
  719. package/kb/references/thumbs/c4e6d18ec3.jpg +0 -0
  720. package/kb/references/thumbs/c5477e2877.jpg +0 -0
  721. package/kb/references/thumbs/c566794674.jpg +0 -0
  722. package/kb/references/thumbs/c5b945afbb.jpg +0 -0
  723. package/kb/references/thumbs/c5e4659497.jpg +0 -0
  724. package/kb/references/thumbs/c622786702.jpg +0 -0
  725. package/kb/references/thumbs/c6250649ca.jpg +0 -0
  726. package/kb/references/thumbs/c6261a9526.jpg +0 -0
  727. package/kb/references/thumbs/c62e609076.jpg +0 -0
  728. package/kb/references/thumbs/c6602d3e85.jpg +0 -0
  729. package/kb/references/thumbs/c6c1a16e40.jpg +0 -0
  730. package/kb/references/thumbs/c77f932157.jpg +0 -0
  731. package/kb/references/thumbs/c79788f01e.jpg +0 -0
  732. package/kb/references/thumbs/c7a511ad1e.jpg +0 -0
  733. package/kb/references/thumbs/c7acfbc3c8.jpg +0 -0
  734. package/kb/references/thumbs/c7bb6d1bb8.jpg +0 -0
  735. package/kb/references/thumbs/c829eadcb9.jpg +0 -0
  736. package/kb/references/thumbs/c891fd9d85.jpg +0 -0
  737. package/kb/references/thumbs/c89f812ee3.jpg +0 -0
  738. package/kb/references/thumbs/c8a85b9f61.jpg +0 -0
  739. package/kb/references/thumbs/c8fdf85c9a.jpg +0 -0
  740. package/kb/references/thumbs/c961ac1031.jpg +0 -0
  741. package/kb/references/thumbs/c9fd3f88d8.jpg +0 -0
  742. package/kb/references/thumbs/ca0d6399c3.jpg +0 -0
  743. package/kb/references/thumbs/ca316c83e4.jpg +0 -0
  744. package/kb/references/thumbs/ca3b93aa0d.jpg +0 -0
  745. package/kb/references/thumbs/cb0596a85b.jpg +0 -0
  746. package/kb/references/thumbs/cb95a5c3ef.jpg +0 -0
  747. package/kb/references/thumbs/cbe238b656.jpg +0 -0
  748. package/kb/references/thumbs/cbf530e608.jpg +0 -0
  749. package/kb/references/thumbs/cc173494ec.jpg +0 -0
  750. package/kb/references/thumbs/ccb664bddf.jpg +0 -0
  751. package/kb/references/thumbs/ccf69d36cd.jpg +0 -0
  752. package/kb/references/thumbs/ccf759db42.jpg +0 -0
  753. package/kb/references/thumbs/cd2a285807.jpg +0 -0
  754. package/kb/references/thumbs/cd4a24a8ca.jpg +0 -0
  755. package/kb/references/thumbs/cdb3fe8938.jpg +0 -0
  756. package/kb/references/thumbs/cdce1b9bc6.jpg +0 -0
  757. package/kb/references/thumbs/cde3aa761a.jpg +0 -0
  758. package/kb/references/thumbs/ce9d5cd9c2.jpg +0 -0
  759. package/kb/references/thumbs/ceb01f8a34.jpg +0 -0
  760. package/kb/references/thumbs/cee15c98d0.jpg +0 -0
  761. package/kb/references/thumbs/cf6944ddae.jpg +0 -0
  762. package/kb/references/thumbs/d15dd21cd8.jpg +0 -0
  763. package/kb/references/thumbs/d1633bf2ed.jpg +0 -0
  764. package/kb/references/thumbs/d16a838000.jpg +0 -0
  765. package/kb/references/thumbs/d1cf87e9f0.jpg +0 -0
  766. package/kb/references/thumbs/d32511d913.jpg +0 -0
  767. package/kb/references/thumbs/d327e9dedd.jpg +0 -0
  768. package/kb/references/thumbs/d35bf6c85e.jpg +0 -0
  769. package/kb/references/thumbs/d37afc9d61.jpg +0 -0
  770. package/kb/references/thumbs/d3b66655c7.jpg +0 -0
  771. package/kb/references/thumbs/d40b4172b6.jpg +0 -0
  772. package/kb/references/thumbs/d4c40d1824.jpg +0 -0
  773. package/kb/references/thumbs/d4cb55a4b6.jpg +0 -0
  774. package/kb/references/thumbs/d4f0c30f7e.jpg +0 -0
  775. package/kb/references/thumbs/d51cce3ae5.jpg +0 -0
  776. package/kb/references/thumbs/d52948f9ba.jpg +0 -0
  777. package/kb/references/thumbs/d62bd3cba4.jpg +0 -0
  778. package/kb/references/thumbs/d668a21c92.jpg +0 -0
  779. package/kb/references/thumbs/d685072c52.jpg +0 -0
  780. package/kb/references/thumbs/d6d7b96093.jpg +0 -0
  781. package/kb/references/thumbs/d7cd874b21.jpg +0 -0
  782. package/kb/references/thumbs/d7e111736a.jpg +0 -0
  783. package/kb/references/thumbs/d88325f832.jpg +0 -0
  784. package/kb/references/thumbs/d9272ded76.jpg +0 -0
  785. package/kb/references/thumbs/d962e7c654.jpg +0 -0
  786. package/kb/references/thumbs/d97f000ee2.jpg +0 -0
  787. package/kb/references/thumbs/d9c1ae2543.jpg +0 -0
  788. package/kb/references/thumbs/d9ccb3cac8.jpg +0 -0
  789. package/kb/references/thumbs/da99019680.jpg +0 -0
  790. package/kb/references/thumbs/da9b8bc615.jpg +0 -0
  791. package/kb/references/thumbs/daafbee2c1.jpg +0 -0
  792. package/kb/references/thumbs/db0f11f143.jpg +0 -0
  793. package/kb/references/thumbs/db5dd367f5.jpg +0 -0
  794. package/kb/references/thumbs/db69677866.jpg +0 -0
  795. package/kb/references/thumbs/db87c1956e.jpg +0 -0
  796. package/kb/references/thumbs/dbb86d8bda.jpg +0 -0
  797. package/kb/references/thumbs/dbd3a8ff2a.jpg +0 -0
  798. package/kb/references/thumbs/dbdf4b2794.jpg +0 -0
  799. package/kb/references/thumbs/dd4c19b4b1.jpg +0 -0
  800. package/kb/references/thumbs/dd98d9f6fa.jpg +0 -0
  801. package/kb/references/thumbs/ddb00e3b6a.jpg +0 -0
  802. package/kb/references/thumbs/dde3541a5c.jpg +0 -0
  803. package/kb/references/thumbs/de2a4da502.jpg +0 -0
  804. package/kb/references/thumbs/de4c387a66.jpg +0 -0
  805. package/kb/references/thumbs/de5618fa82.jpg +0 -0
  806. package/kb/references/thumbs/de6bec2538.jpg +0 -0
  807. package/kb/references/thumbs/dea13cbb1f.jpg +0 -0
  808. package/kb/references/thumbs/deff9202aa.jpg +0 -0
  809. package/kb/references/thumbs/df12835f8d.jpg +0 -0
  810. package/kb/references/thumbs/df57acbb3d.jpg +0 -0
  811. package/kb/references/thumbs/df8265009e.jpg +0 -0
  812. package/kb/references/thumbs/dfc99a1e8e.jpg +0 -0
  813. package/kb/references/thumbs/dfc9fde460.jpg +0 -0
  814. package/kb/references/thumbs/e038348eee.jpg +0 -0
  815. package/kb/references/thumbs/e04c2ae1d5.jpg +0 -0
  816. package/kb/references/thumbs/e070345c43.jpg +0 -0
  817. package/kb/references/thumbs/e0a0c84d47.jpg +0 -0
  818. package/kb/references/thumbs/e0af1e6b9e.jpg +0 -0
  819. package/kb/references/thumbs/e1025942c4.jpg +0 -0
  820. package/kb/references/thumbs/e11b6f5b82.jpg +0 -0
  821. package/kb/references/thumbs/e1553de3fc.jpg +0 -0
  822. package/kb/references/thumbs/e162ebe7cd.jpg +0 -0
  823. package/kb/references/thumbs/e1f1e531d8.jpg +0 -0
  824. package/kb/references/thumbs/e20842e5c9.jpg +0 -0
  825. package/kb/references/thumbs/e24e7d949b.jpg +0 -0
  826. package/kb/references/thumbs/e2a3e5d10a.jpg +0 -0
  827. package/kb/references/thumbs/e2de02615a.jpg +0 -0
  828. package/kb/references/thumbs/e305dad7dd.jpg +0 -0
  829. package/kb/references/thumbs/e52e3ef97f.jpg +0 -0
  830. package/kb/references/thumbs/e570e92d06.jpg +0 -0
  831. package/kb/references/thumbs/e583932fb9.jpg +0 -0
  832. package/kb/references/thumbs/e5da3c038f.jpg +0 -0
  833. package/kb/references/thumbs/e657634af3.jpg +0 -0
  834. package/kb/references/thumbs/e67946e3e1.jpg +0 -0
  835. package/kb/references/thumbs/e69e0f3639.jpg +0 -0
  836. package/kb/references/thumbs/e6ebe66031.jpg +0 -0
  837. package/kb/references/thumbs/e7db9563c9.jpg +0 -0
  838. package/kb/references/thumbs/e84a4c3b7b.jpg +0 -0
  839. package/kb/references/thumbs/e84cf52c27.jpg +0 -0
  840. package/kb/references/thumbs/e8707cf6e8.jpg +0 -0
  841. package/kb/references/thumbs/e888890b40.jpg +0 -0
  842. package/kb/references/thumbs/e8a502b418.jpg +0 -0
  843. package/kb/references/thumbs/e8ba68371d.jpg +0 -0
  844. package/kb/references/thumbs/e9098f54cc.jpg +0 -0
  845. package/kb/references/thumbs/e9144deae9.jpg +0 -0
  846. package/kb/references/thumbs/e93e98d757.jpg +0 -0
  847. package/kb/references/thumbs/e9762dd435.jpg +0 -0
  848. package/kb/references/thumbs/e9db4544c8.jpg +0 -0
  849. package/kb/references/thumbs/e9ddffeacd.jpg +0 -0
  850. package/kb/references/thumbs/e9e2cf1d7e.jpg +0 -0
  851. package/kb/references/thumbs/ea1e23e623.jpg +0 -0
  852. package/kb/references/thumbs/ea41200f5e.jpg +0 -0
  853. package/kb/references/thumbs/eaf4916e1a.jpg +0 -0
  854. package/kb/references/thumbs/eb2c505fc8.jpg +0 -0
  855. package/kb/references/thumbs/eb47943693.jpg +0 -0
  856. package/kb/references/thumbs/eb4ffd6625.jpg +0 -0
  857. package/kb/references/thumbs/eb82ac0a4d.jpg +0 -0
  858. package/kb/references/thumbs/ebeb345ade.jpg +0 -0
  859. package/kb/references/thumbs/ec4e2aba0a.jpg +0 -0
  860. package/kb/references/thumbs/ed683bc9f7.jpg +0 -0
  861. package/kb/references/thumbs/ed9e236a84.jpg +0 -0
  862. package/kb/references/thumbs/ee45e1caea.jpg +0 -0
  863. package/kb/references/thumbs/ee50bbf2e7.jpg +0 -0
  864. package/kb/references/thumbs/eed6ad3b7b.jpg +0 -0
  865. package/kb/references/thumbs/eee247fd70.jpg +0 -0
  866. package/kb/references/thumbs/ef87b6cb99.jpg +0 -0
  867. package/kb/references/thumbs/ef87dccf04.jpg +0 -0
  868. package/kb/references/thumbs/ef941bba1e.jpg +0 -0
  869. package/kb/references/thumbs/effc14fb2c.jpg +0 -0
  870. package/kb/references/thumbs/f04aa5a191.jpg +0 -0
  871. package/kb/references/thumbs/f13f12485f.jpg +0 -0
  872. package/kb/references/thumbs/f16a5c0fd4.jpg +0 -0
  873. package/kb/references/thumbs/f1ba7a7a8f.jpg +0 -0
  874. package/kb/references/thumbs/f1bc4c9c19.jpg +0 -0
  875. package/kb/references/thumbs/f1ee827878.jpg +0 -0
  876. package/kb/references/thumbs/f21736aeff.jpg +0 -0
  877. package/kb/references/thumbs/f22bc41213.jpg +0 -0
  878. package/kb/references/thumbs/f266aaffcc.jpg +0 -0
  879. package/kb/references/thumbs/f2c0ed0d65.jpg +0 -0
  880. package/kb/references/thumbs/f32ac710fd.jpg +0 -0
  881. package/kb/references/thumbs/f32b697ff6.jpg +0 -0
  882. package/kb/references/thumbs/f39c883904.jpg +0 -0
  883. package/kb/references/thumbs/f3b9d55e6e.jpg +0 -0
  884. package/kb/references/thumbs/f3c1763e14.jpg +0 -0
  885. package/kb/references/thumbs/f40853fd43.jpg +0 -0
  886. package/kb/references/thumbs/f4fd07dc4d.jpg +0 -0
  887. package/kb/references/thumbs/f50b756ee5.jpg +0 -0
  888. package/kb/references/thumbs/f51272e9b3.jpg +0 -0
  889. package/kb/references/thumbs/f538895eee.jpg +0 -0
  890. package/kb/references/thumbs/f5d28081b5.jpg +0 -0
  891. package/kb/references/thumbs/f6046c9501.jpg +0 -0
  892. package/kb/references/thumbs/f62e4af316.jpg +0 -0
  893. package/kb/references/thumbs/f6f6bffeb8.jpg +0 -0
  894. package/kb/references/thumbs/f736e00af6.jpg +0 -0
  895. package/kb/references/thumbs/f7932da47d.jpg +0 -0
  896. package/kb/references/thumbs/f879e118ca.jpg +0 -0
  897. package/kb/references/thumbs/f88257215d.jpg +0 -0
  898. package/kb/references/thumbs/f88c45a6d7.jpg +0 -0
  899. package/kb/references/thumbs/f89a7b646e.jpg +0 -0
  900. package/kb/references/thumbs/f8a1e7a274.jpg +0 -0
  901. package/kb/references/thumbs/f9183cf2b6.jpg +0 -0
  902. package/kb/references/thumbs/f995de5ce9.jpg +0 -0
  903. package/kb/references/thumbs/f9ada2c3e1.jpg +0 -0
  904. package/kb/references/thumbs/f9d4616f89.jpg +0 -0
  905. package/kb/references/thumbs/f9f4307bb0.jpg +0 -0
  906. package/kb/references/thumbs/fa070122c7.jpg +0 -0
  907. package/kb/references/thumbs/fa29cc1369.jpg +0 -0
  908. package/kb/references/thumbs/fa791a4dab.jpg +0 -0
  909. package/kb/references/thumbs/fa8060509e.jpg +0 -0
  910. package/kb/references/thumbs/fafeed4003.jpg +0 -0
  911. package/kb/references/thumbs/fb1c024f38.jpg +0 -0
  912. package/kb/references/thumbs/fb77aa28b3.jpg +0 -0
  913. package/kb/references/thumbs/fb9f7f1966.jpg +0 -0
  914. package/kb/references/thumbs/fba8a8c2da.jpg +0 -0
  915. package/kb/references/thumbs/fce2754c36.jpg +0 -0
  916. package/kb/references/thumbs/fce2fb83df.jpg +0 -0
  917. package/kb/references/thumbs/fd4c51bb47.jpg +0 -0
  918. package/kb/references/thumbs/fd8c269817.jpg +0 -0
  919. package/kb/references/thumbs/fdb7b250ca.jpg +0 -0
  920. package/kb/references/thumbs/fe18088de8.jpg +0 -0
  921. package/kb/references/thumbs/fe4f742ba7.jpg +0 -0
  922. package/kb/references/thumbs/fe64742ce7.jpg +0 -0
  923. package/kb/references/thumbs/feb5d62161.jpg +0 -0
  924. package/kb/references/thumbs/fed21b3e98.jpg +0 -0
  925. package/kb/references/thumbs/fed2205951.jpg +0 -0
  926. package/kb/references/thumbs/ffdacf1715.jpg +0 -0
  927. package/kb/sections/_buttons.md +93 -0
  928. package/kb/sections/_finish.md +325 -0
  929. package/kb/sections/about-founder-note.md +85 -0
  930. package/kb/sections/about-team-roster.md +86 -0
  931. package/kb/sections/alternating-rows-live-tiles.md +110 -0
  932. package/kb/sections/annotated-product-shot.md +105 -0
  933. package/kb/sections/asymmetric-bento-dominant.md +114 -0
  934. package/kb/sections/before-after-features.md +104 -0
  935. package/kb/sections/before-after-repair.md +107 -0
  936. package/kb/sections/bento-lead-tile.md +112 -0
  937. package/kb/sections/book-a-slot-honest.md +116 -0
  938. package/kb/sections/capability-prompt-ledger.md +68 -0
  939. package/kb/sections/case-study-band.md +113 -0
  940. package/kb/sections/centred-stack-hero.md +107 -0
  941. package/kb/sections/closing-demo-final-state.md +81 -0
  942. package/kb/sections/closing-full-bleed-line-and-form.md +86 -0
  943. package/kb/sections/closing-result-previewed.md +88 -0
  944. package/kb/sections/closing-three-real-steps.md +87 -0
  945. package/kb/sections/command-reference-table.md +70 -0
  946. package/kb/sections/comparison-old-way-bars.md +88 -0
  947. package/kb/sections/comparison-same-task-two-ways.md +88 -0
  948. package/kb/sections/day-of-the-place.md +102 -0
  949. package/kb/sections/docs-reading-rail.md +88 -0
  950. package/kb/sections/editorial-columns-hero.md +107 -0
  951. package/kb/sections/editorial-essay-pull-quote.md +112 -0
  952. package/kb/sections/events-calendar-list.md +107 -0
  953. package/kb/sections/explorable-control-demo.md +102 -0
  954. package/kb/sections/faq-open-answers-editorial.md +85 -0
  955. package/kb/sections/faq-ruled-accordion-aside.md +87 -0
  956. package/kb/sections/footer-closing-scene.md +84 -0
  957. package/kb/sections/footer-editorial-sitemap.md +85 -0
  958. package/kb/sections/footer-minimal-credits.md +80 -0
  959. package/kb/sections/full-bleed-image-band.md +103 -0
  960. package/kb/sections/full-bleed-image-hero.md +105 -0
  961. package/kb/sections/gallery-first-hero.md +109 -0
  962. package/kb/sections/horizontal-scroll-strip.md +101 -0
  963. package/kb/sections/how-i-work-own-voice.md +111 -0
  964. package/kb/sections/how-it-works-in-place.md +109 -0
  965. package/kb/sections/impact-numbers-sourced.md +109 -0
  966. package/kb/sections/install-steps-per-agent.md +77 -0
  967. package/kb/sections/masonry-real-work.md +109 -0
  968. package/kb/sections/numbered-editorial-columns.md +105 -0
  969. package/kb/sections/object-first-hero.md +106 -0
  970. package/kb/sections/object-timeline.md +105 -0
  971. package/kb/sections/one-huge-number.md +100 -0
  972. package/kb/sections/one-person-story.md +103 -0
  973. package/kb/sections/order-ahead-ticket.md +119 -0
  974. package/kb/sections/press-strip-quoted.md +106 -0
  975. package/kb/sections/pricing-comparison-table-sticky.md +94 -0
  976. package/kb/sections/pricing-free-pro-upgrade-moment.md +85 -0
  977. package/kb/sections/pricing-plan-beside-product.md +88 -0
  978. package/kb/sections/pricing-plans-ledger.md +94 -0
  979. package/kb/sections/pricing-usage-calculator.md +93 -0
  980. package/kb/sections/printed-menu-card.md +110 -0
  981. package/kb/sections/process-happening.md +102 -0
  982. package/kb/sections/project-index-ruled.md +106 -0
  983. package/kb/sections/proof-logo-cloud-with-reasons.md +82 -0
  984. package/kb/sections/proof-measured-numbers.md +88 -0
  985. package/kb/sections/proof-output-wall.md +84 -0
  986. package/kb/sections/proof-single-honest-quote.md +81 -0
  987. package/kb/sections/resources-searchable-list.md +110 -0
  988. package/kb/sections/rooms-and-rates.md +107 -0
  989. package/kb/sections/ruled-facts-table.md +108 -0
  990. package/kb/sections/ruled-list-pinned-visual.md +107 -0
  991. package/kb/sections/scroll-scrubbed-product-demo.md +103 -0
  992. package/kb/sections/self-playing-run-demo.md +104 -0
  993. package/kb/sections/services-price-ledger.md +106 -0
  994. package/kb/sections/signup-form-with-next-steps.md +86 -0
  995. package/kb/sections/signup-waitlist-place-in-line.md +90 -0
  996. package/kb/sections/spec-sheet-features.md +108 -0
  997. package/kb/sections/split-hero-live-object.md +108 -0
  998. package/kb/sections/sticky-feature-narrative.md +104 -0
  999. package/kb/sections/todays-board-hero.md +112 -0
  1000. package/kb/sections/touchable-tool-hero.md +111 -0
  1001. package/kb/sections/typographic-statement-hero.md +99 -0
  1002. package/kb/sections/visit-hours-map.md +110 -0
  1003. package/kb/sections/ways-to-help-honest.md +115 -0
  1004. package/kb/sources/design-reference.md +991 -0
  1005. package/kb/sources/product-and-docs.md +37 -0
  1006. package/kb/systems/1win/DESIGN.md +463 -0
  1007. package/kb/systems/LICENSE-awesome-design-md +21 -0
  1008. package/kb/systems/README.md +7 -0
  1009. package/kb/systems/airbnb/DESIGN.md +545 -0
  1010. package/kb/systems/airtable/DESIGN.md +554 -0
  1011. package/kb/systems/aljazeera/DESIGN.md +594 -0
  1012. package/kb/systems/apple/DESIGN.md +562 -0
  1013. package/kb/systems/binance/DESIGN.md +634 -0
  1014. package/kb/systems/bmw/DESIGN.md +544 -0
  1015. package/kb/systems/bmw-m/DESIGN.md +503 -0
  1016. package/kb/systems/bugatti/DESIGN.md +454 -0
  1017. package/kb/systems/cal/DESIGN.md +542 -0
  1018. package/kb/systems/categories.json +87 -0
  1019. package/kb/systems/claude/DESIGN.md +589 -0
  1020. package/kb/systems/clay/DESIGN.md +541 -0
  1021. package/kb/systems/clickhouse/DESIGN.md +544 -0
  1022. package/kb/systems/cohere/DESIGN.md +451 -0
  1023. package/kb/systems/coinbase/DESIGN.md +570 -0
  1024. package/kb/systems/composio/DESIGN.md +506 -0
  1025. package/kb/systems/cursor/DESIGN.md +537 -0
  1026. package/kb/systems/dell-1996/DESIGN.md +632 -0
  1027. package/kb/systems/dji/DESIGN.md +882 -0
  1028. package/kb/systems/elevenlabs/DESIGN.md +504 -0
  1029. package/kb/systems/expo/DESIGN.md +526 -0
  1030. package/kb/systems/ferrari/DESIGN.md +531 -0
  1031. package/kb/systems/figma/DESIGN.md +578 -0
  1032. package/kb/systems/framer/DESIGN.md +544 -0
  1033. package/kb/systems/hashicorp/DESIGN.md +575 -0
  1034. package/kb/systems/hp/DESIGN.md +670 -0
  1035. package/kb/systems/ibm/DESIGN.md +550 -0
  1036. package/kb/systems/intercom/DESIGN.md +546 -0
  1037. package/kb/systems/jobright/DESIGN.md +525 -0
  1038. package/kb/systems/kraken/DESIGN.md +125 -0
  1039. package/kb/systems/lamborghini/DESIGN.md +288 -0
  1040. package/kb/systems/linear.app/DESIGN.md +548 -0
  1041. package/kb/systems/lovable/DESIGN.md +298 -0
  1042. package/kb/systems/mastercard/DESIGN.md +365 -0
  1043. package/kb/systems/mercury/DESIGN.md +568 -0
  1044. package/kb/systems/meta/DESIGN.md +683 -0
  1045. package/kb/systems/minimax/DESIGN.md +746 -0
  1046. package/kb/systems/mintlify/DESIGN.md +852 -0
  1047. package/kb/systems/miro/DESIGN.md +825 -0
  1048. package/kb/systems/mistral.ai/DESIGN.md +773 -0
  1049. package/kb/systems/mongodb/DESIGN.md +767 -0
  1050. package/kb/systems/mydealz/DESIGN.md +551 -0
  1051. package/kb/systems/nerdwallet/DESIGN.md +722 -0
  1052. package/kb/systems/nike/DESIGN.md +575 -0
  1053. package/kb/systems/nintendo-2001/DESIGN.md +649 -0
  1054. package/kb/systems/notion/DESIGN.md +821 -0
  1055. package/kb/systems/nvidia/DESIGN.md +640 -0
  1056. package/kb/systems/ollama/DESIGN.md +539 -0
  1057. package/kb/systems/opencode.ai/DESIGN.md +521 -0
  1058. package/kb/systems/oracle/DESIGN.md +778 -0
  1059. package/kb/systems/oura/DESIGN.md +616 -0
  1060. package/kb/systems/pinterest/DESIGN.md +597 -0
  1061. package/kb/systems/playstation/DESIGN.md +661 -0
  1062. package/kb/systems/posthog/DESIGN.md +690 -0
  1063. package/kb/systems/raycast/DESIGN.md +669 -0
  1064. package/kb/systems/renault/DESIGN.md +589 -0
  1065. package/kb/systems/replicate/DESIGN.md +616 -0
  1066. package/kb/systems/resend/DESIGN.md +585 -0
  1067. package/kb/systems/revolut/DESIGN.md +636 -0
  1068. package/kb/systems/runwayml/DESIGN.md +244 -0
  1069. package/kb/systems/sanity/DESIGN.md +357 -0
  1070. package/kb/systems/sap/DESIGN.md +639 -0
  1071. package/kb/systems/sentry/DESIGN.md +551 -0
  1072. package/kb/systems/shopify/DESIGN.md +516 -0
  1073. package/kb/systems/slack/DESIGN.md +482 -0
  1074. package/kb/systems/spacex/DESIGN.md +363 -0
  1075. package/kb/systems/spotify/DESIGN.md +246 -0
  1076. package/kb/systems/starbucks/DESIGN.md +580 -0
  1077. package/kb/systems/stripe/DESIGN.md +487 -0
  1078. package/kb/systems/supabase/DESIGN.md +462 -0
  1079. package/kb/systems/superhuman/DESIGN.md +448 -0
  1080. package/kb/systems/tesla/DESIGN.md +286 -0
  1081. package/kb/systems/theverge/DESIGN.md +339 -0
  1082. package/kb/systems/together.ai/DESIGN.md +633 -0
  1083. package/kb/systems/uber/DESIGN.md +636 -0
  1084. package/kb/systems/vercel/DESIGN.md +736 -0
  1085. package/kb/systems/vodafone/DESIGN.md +538 -0
  1086. package/kb/systems/voltagent/DESIGN.md +521 -0
  1087. package/kb/systems/warp/DESIGN.md +526 -0
  1088. package/kb/systems/webflow/DESIGN.md +588 -0
  1089. package/kb/systems/wired/DESIGN.md +497 -0
  1090. package/kb/systems/wise/DESIGN.md +544 -0
  1091. package/kb/systems/x.ai/DESIGN.md +465 -0
  1092. package/kb/systems/zapier/DESIGN.md +537 -0
  1093. package/kb/taste/garvit-by-job.md +46 -0
  1094. package/kb/taste/garvit-portfolio.md +94 -0
  1095. package/kb/universal/color-roles.md +26 -0
  1096. package/kb/universal/copy.md +16 -0
  1097. package/kb/universal/details.md +31 -0
  1098. package/kb/universal/first-screen.md +30 -0
  1099. package/kb/universal/fit-the-codebase.md +29 -0
  1100. package/kb/universal/layout-width.md +28 -0
  1101. package/kb/universal/motion-feedback.md +23 -0
  1102. package/kb/universal/signature.md +29 -0
  1103. package/kb/universal/states.md +17 -0
  1104. package/kb/universal/type-app.md +33 -0
  1105. package/kb/universal/type-marketing.md +28 -0
  1106. package/package.json +36 -4
  1107. package/scripts/pack-assets.mjs +48 -0
  1108. package/skill/palate/SKILL.md +261 -0
  1109. package/src/auth-recheck.mjs +4 -0
  1110. package/src/auth.mjs +416 -0
  1111. package/src/capture.mjs +182 -0
  1112. package/src/catalog/classify.mjs +96 -0
  1113. package/src/catalog/curated.mjs +128 -0
  1114. package/src/catalog/index-build.mjs +67 -0
  1115. package/src/catalog/literal.mjs +146 -0
  1116. package/src/catalog/store.mjs +40 -0
  1117. package/src/catalog/vibrant.mjs +126 -0
  1118. package/src/check.mjs +93 -0
  1119. package/src/concept.mjs +157 -0
  1120. package/src/context.mjs +226 -0
  1121. package/src/copy.mjs +188 -0
  1122. package/src/devserver.mjs +135 -0
  1123. package/src/fontgenre.mjs +44 -0
  1124. package/src/guard-agents.mjs +213 -0
  1125. package/src/guard.mjs +377 -0
  1126. package/src/kb.mjs +91 -0
  1127. package/src/look.mjs +312 -0
  1128. package/src/loop/ambition.mjs +620 -0
  1129. package/src/loop/animate.mjs +70 -0
  1130. package/src/loop/artdirection.mjs +264 -0
  1131. package/src/loop/bench.mjs +184 -0
  1132. package/src/loop/benchpick.mjs +155 -0
  1133. package/src/loop/blueprint.mjs +620 -0
  1134. package/src/loop/board.html +180 -0
  1135. package/src/loop/build.mjs +393 -0
  1136. package/src/loop/buttons.mjs +413 -0
  1137. package/src/loop/common.mjs +206 -0
  1138. package/src/loop/cost.mjs +63 -0
  1139. package/src/loop/craft.mjs +152 -0
  1140. package/src/loop/craftchecks.mjs +148 -0
  1141. package/src/loop/critique.mjs +393 -0
  1142. package/src/loop/details.mjs +225 -0
  1143. package/src/loop/direction.html +168 -0
  1144. package/src/loop/direction.mjs +256 -0
  1145. package/src/loop/directions.json +8 -0
  1146. package/src/loop/explore.mjs +219 -0
  1147. package/src/loop/fail.mjs +110 -0
  1148. package/src/loop/fix.mjs +194 -0
  1149. package/src/loop/floor.mjs +677 -0
  1150. package/src/loop/forget.mjs +52 -0
  1151. package/src/loop/gate.mjs +1053 -0
  1152. package/src/loop/grid.html +123 -0
  1153. package/src/loop/growth.mjs +118 -0
  1154. package/src/loop/icons.mjs +109 -0
  1155. package/src/loop/imagery.mjs +1280 -0
  1156. package/src/loop/judge-api.mjs +245 -0
  1157. package/src/loop/judge.mjs +167 -0
  1158. package/src/loop/layouts.mjs +492 -0
  1159. package/src/loop/library.mjs +82 -0
  1160. package/src/loop/lookplan.mjs +182 -0
  1161. package/src/loop/lookvec.mjs +645 -0
  1162. package/src/loop/match.mjs +923 -0
  1163. package/src/loop/matched.mjs +26 -0
  1164. package/src/loop/myrefs.mjs +151 -0
  1165. package/src/loop/pick.mjs +213 -0
  1166. package/src/loop/picker.html +118 -0
  1167. package/src/loop/plan.mjs +289 -0
  1168. package/src/loop/polish.mjs +39 -0
  1169. package/src/loop/research-learn.mjs +41 -0
  1170. package/src/loop/research.mjs +1130 -0
  1171. package/src/loop/rulereview.mjs +146 -0
  1172. package/src/loop/rules-board.html +199 -0
  1173. package/src/loop/rules.mjs +939 -0
  1174. package/src/loop/similar.mjs +276 -0
  1175. package/src/loop/states-source.mjs +258 -0
  1176. package/src/loop/states.mjs +1547 -0
  1177. package/src/loop/stats.mjs +78 -0
  1178. package/src/loop/taste.mjs +81 -0
  1179. package/src/loop/timeline.mjs +149 -0
  1180. package/src/loop/variants.mjs +228 -0
  1181. package/src/loop/vary.mjs +249 -0
  1182. package/src/mcp/server.mjs +1246 -0
  1183. package/src/motion.mjs +207 -0
  1184. package/src/moves.mjs +242 -0
  1185. package/src/novelty.mjs +116 -0
  1186. package/src/page-ambition.js +181 -0
  1187. package/src/page-craft.js +724 -0
  1188. package/src/page-details.js +415 -0
  1189. package/src/page-devices.js +365 -0
  1190. package/src/page-fingerprint.js +281 -0
  1191. package/src/page-gate.js +1307 -0
  1192. package/src/page-look.js +698 -0
  1193. package/src/page-metrics.js +78 -0
  1194. package/src/page-principles.js +1167 -0
  1195. package/src/page-rules.js +308 -0
  1196. package/src/page-sections.js +211 -0
  1197. package/src/page-states.js +703 -0
  1198. package/src/paths.mjs +13 -0
  1199. package/src/png.mjs +107 -0
  1200. package/src/point/auto.mjs +224 -0
  1201. package/src/point/locate.mjs +114 -0
  1202. package/src/point/overlay.js +1516 -0
  1203. package/src/point/server.mjs +1122 -0
  1204. package/src/point/trail.mjs +219 -0
  1205. package/src/pool.mjs +80 -0
  1206. package/src/principles.mjs +279 -0
  1207. package/src/refs.mjs +299 -0
  1208. package/src/robots.mjs +145 -0
  1209. package/src/sections.mjs +210 -0
  1210. package/src/shoot.mjs +235 -0
  1211. package/src/systems/apply.mjs +355 -0
  1212. package/src/systems/load.mjs +251 -0
  1213. package/src/systems/pick.mjs +213 -0
  1214. package/templates/vite-react-tw/index.html +12 -0
  1215. package/templates/vite-react-tw/package.json +25 -0
  1216. package/templates/vite-react-tw/src/App.tsx +3 -0
  1217. package/templates/vite-react-tw/src/index.css +1 -0
  1218. package/templates/vite-react-tw/src/main.tsx +10 -0
  1219. package/templates/vite-react-tw/tsconfig.app.json +26 -0
  1220. package/templates/vite-react-tw/tsconfig.json +7 -0
  1221. package/templates/vite-react-tw/tsconfig.node.json +23 -0
  1222. package/templates/vite-react-tw/vite.config.ts +8 -0
@@ -0,0 +1,1280 @@
1
+ // `palate imagery "<query>"` and `palate texture <name>`: real imagery and material for a page, never a
2
+ // placeholder and never a flat colour where the design wants something to look at.
3
+ // 1. Photos come from museum open-access collections that publish a licence flag per object: The Met
4
+ // (collectionapi.metmuseum.org, /public/collection/v1.1/search then /v1/objects/{id}; kept only when
5
+ // isPublicDomain === true) and the Cleveland Museum of Art (openaccess-api.clevelandart.org; kept only when
6
+ // share_license_status === "CC0"). The flag is read from the API for every single item and anything not
7
+ // explicitly CC0 / public domain is dropped; palate never infers a licence. No key, no login.
8
+ // 2. Politely: a few requests at a time per host, a shared pause and a retry with backoff when a museum says
9
+ // 403 / 429 / 5xx, timeouts on every call, a User-Agent that names palate, no scraping, no bot-wall or
10
+ // Referer tricks (that is why the Art Institute of Chicago is not a source). Answers are cached for two hours
11
+ // in the project's .palate folder, so a repeated or overlapping search costs nothing. Unsplash and Pexels
12
+ // need the user's own key and their attribution / hotlinking rules, so they are not built in yet.
13
+ // 3. The agent sees contact sheets of the candidates (thumbnails with ids and pixel sizes, 12 to a sheet),
14
+ // picks by eye, and `pickImages` downloads the chosen files into the project's own public folder
15
+ // (self-hosted, never hotlinked) as web-sized JPEGs, with a museum mount trimmed when one is found and a
16
+ // credits.json beside them. CC0 needs no credit; palate credits anyway (title, artist, date, museum).
17
+ // 4. One treatment for every photo on the page (duotone, tone or grain, in CSS with no dependencies) so museum
18
+ // objects read as the site's material, not as a gallery of borrowed pictures.
19
+ // 5. Material drawn in code (grain, paper, speckle, halftone, linen): deterministic SVG from a seeded PRNG,
20
+ // written to public/textures/ — no licence question at all, because palate drew it.
21
+ // Everything palate keeps for itself lives in <project>/.palate/imagery/ (gitignored): per search
22
+ // <slug>/candidates.json, the thumbnails and contact sheets; .cache/ (API answers); originals/ (full files).
23
+ import crypto from "node:crypto";
24
+ import fs from "node:fs";
25
+ import os from "node:os";
26
+ import path from "node:path";
27
+
28
+ export const UA = "palate-imagery/0.2 (open-licence image search a person asked for, a few requests at a time; https://github.com/Garvit1000/palate)";
29
+ export const SOURCES = {
30
+ met: { name: "The Met", search: "https://collectionapi.metmuseum.org/public/collection/v1.1/search", object: "https://collectionapi.metmuseum.org/public/collection/v1/objects/" },
31
+ cleveland: { name: "Cleveland Museum of Art", search: "https://openaccess-api.clevelandart.org/api/artworks/" },
32
+ commons: { name: "Wikimedia Commons", search: "https://commons.wikimedia.org/w/api.php" },
33
+ };
34
+ const ALIASES = { met: "met", metmuseum: "met", cleveland: "cleveland", cma: "cleveland", commons: "commons", wikimedia: "commons", wiki: "commons" };
35
+ const FUTURE = { unsplash: "Unsplash (needs the user's own key; hotlinked URLs, photographer credit and a download ping)", pexels: "Pexels (needs the user's own key and a visible Pexels link)" };
36
+ // The Met's medium filter for a Cleveland-style type, where the two agree.
37
+ const MET_MEDIUM = { Ceramic: "Ceramics", Painting: "Paintings", Textile: "Textiles", Photograph: "Photographs", Glass: "Glass", Sculpture: "Sculpture", Drawing: "Drawings", Print: "Prints" };
38
+ export const TYPES = Object.keys(MET_MEDIUM);
39
+ // The command names the CLI exposes (kept here so every line palate prints agrees with the CLI wiring).
40
+ export const CMD = { search: "palate imagery", pick: "palate imagery pick", texture: "palate texture" };
41
+ // Per sheet: 4 across, 3 down, so a sheet stays readable at any count.
42
+ export const SHEET_SIZE = 12;
43
+
44
+ const sleep = (ms) => (ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve());
45
+ export const slugify = (s) => String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "images";
46
+ const clean = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
47
+ const num = (v) => (v == null || v === "" || !Number.isFinite(Number(v)) ? undefined : Number(v));
48
+ const plural = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
49
+ // Lower case without accents, for comparing names ("Käsebier" and "Kasebier").
50
+ const fold = (s) => String(s ?? "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
51
+
52
+ // A person's name rather than a subject: two to four words, every one capitalised or an initial
53
+ // ("Clarence H. White", "Julia Margaret Cameron"). Only decides whether the Met is asked for a maker first.
54
+ export const looksLikeName = (q) => /^(\p{Lu}[\p{L}'’-]*\.?|\p{Lu}\.)(\s+(\p{Lu}[\p{L}'’-]*\.?|\p{Lu}\.|van|von|de|der|du|la|le|da|di)){1,3}$/u.test(clean(q));
55
+
56
+ function normaliseSources(sources) {
57
+ const list = (Array.isArray(sources) ? sources : String(sources).split(",")).map((s) => String(s).trim().toLowerCase()).filter(Boolean);
58
+ const known = [];
59
+ const notes = [];
60
+ for (const s of list) {
61
+ if (ALIASES[s]) known.includes(ALIASES[s]) || known.push(ALIASES[s]);
62
+ else if (FUTURE[s]) notes.push(`${FUTURE[s]} is not built in yet; skipped.`);
63
+ else notes.push(`Unknown image source "${s}"; skipped (sources: met, cleveland, commons).`);
64
+ }
65
+ return { known, notes };
66
+ }
67
+
68
+ // ---- requests: a gate per host, retries with backoff, timeouts, and the reason in plain words.
69
+ export class FetchError extends Error {
70
+ constructor({ status, kind, url, tries, wall, until }) {
71
+ const host = (() => {
72
+ try {
73
+ return new URL(url).host;
74
+ } catch {
75
+ return url;
76
+ }
77
+ })();
78
+ const time = (t) => new Date(t).toTimeString().slice(0, 8);
79
+ const why = until
80
+ ? `skipped: its bot wall turned palate away a moment ago, so it is left alone until ${time(until)}`
81
+ : wall
82
+ ? `403 Forbidden from ${host}: its bot wall turned palate away (too many requests from this machine lately); left alone for ${WALL_MS / 60_000} minutes`
83
+ : `${status ? statusReason(status) : kind} from ${host}${tries > 1 ? ` after ${tries} tries` : ""}`;
84
+ super(why);
85
+ this.status = status;
86
+ this.kind = status ? String(status) : kind;
87
+ this.wall = Boolean(wall || until);
88
+ this.url = url;
89
+ }
90
+ }
91
+ export function statusReason(status) {
92
+ if (status === 403) return "403 Forbidden (a rate limit or bot block)";
93
+ if (status === 429) return "429 Too Many Requests (a rate limit)";
94
+ if (status === 404) return "404 Not Found";
95
+ if (status >= 500) return `${status} (the museum's server is down or busy)`;
96
+ return String(status);
97
+ }
98
+ const RETRYABLE = (s) => s === 403 || s === 408 || s === 425 || s === 429 || s >= 500;
99
+ const BACKOFF = [1000, 3000];
100
+
101
+ // The least time between two request starts per host. The Met sits behind a bot wall (Imperva) that turned
102
+ // palate away for about a minute after ~33 requests in 3 s, and again after ~100 in 20 s at 5 a second; once
103
+ // it has, it turns a client away again within a few requests for minutes after (measured Nov 2026). The old
104
+ // one-at-a-time search ran at ~2 a second and never met it, so palate stays near 2.5 a second there.
105
+ // Cleveland has no such wall.
106
+ export const PACE = { "collectionapi.metmuseum.org": 400, "images.metmuseum.org": 250, "commons.wikimedia.org": 300, "upload.wikimedia.org": 200 };
107
+
108
+ // ---- licences palate accepts. Museums: CC0 only. Commons: CC0, public domain and CC BY (any version), read from
109
+ // the file's own licence field. Never share-alike (palate tones and crops photos, and a changed share-alike photo
110
+ // would have to carry that licence), never non-commercial, no-derivatives, GFDL-only or fair use, and never a file
111
+ // marked with restrictions (personality rights, trademarks, insignia).
112
+ const BY = /^CC BY \d(\.\d)?$/;
113
+ export const openLicence = (licence) => licence === "CC0" || licence === "Public domain" || BY.test(licence ?? "");
114
+ export const needsCredit = (licence) => BY.test(licence ?? "");
115
+ export function commonsLicence(meta = {}) {
116
+ const v = (k) => clean(String(meta[k]?.value ?? "").replace(/<[^>]+>/g, " "));
117
+ if (v("Restrictions") || /^true$/i.test(v("NonFree"))) return null;
118
+ const code = v("License").toLowerCase();
119
+ if (code === "cc0" || code === "cc-zero") return "CC0";
120
+ if (code === "pd" || /^pd[-_]/.test(code)) return "Public domain";
121
+ const by = /^cc-by-(\d(?:\.\d)?)$/.exec(code);
122
+ if (by) return `CC BY ${by[1]}`;
123
+ return null; // cc-by-sa-*, cc-by-nc-*, gfdl, fair use, anything unknown
124
+ }
125
+ // How long a bot wall is left alone once it has answered (asking again only keeps it up).
126
+ export const WALL_MS = 2 * 60_000;
127
+
128
+ // At most n requests in flight per host, starts spaced by the host's pace, and one shared pause after a
129
+ // 429 or a plain 403 so every worker backs off together.
130
+ function hostGate(n, pace = 0) {
131
+ let active = 0;
132
+ let pauseUntil = 0;
133
+ let nextStart = 0;
134
+ const waiting = [];
135
+ return {
136
+ async run(fn) {
137
+ if (active < n) active++;
138
+ else await new Promise((r) => waiting.push(r)); // the slot is handed over, active stays
139
+ try {
140
+ const at = Math.max(Date.now(), nextStart, pauseUntil);
141
+ nextStart = at + pace;
142
+ await sleep(at - Date.now());
143
+ return await fn();
144
+ } finally {
145
+ const next = waiting.shift();
146
+ if (next) next();
147
+ else active--;
148
+ }
149
+ },
150
+ pause(ms) {
151
+ pauseUntil = Math.max(pauseUntil, Date.now() + ms);
152
+ },
153
+ };
154
+ }
155
+
156
+ // The shared state of one search or pick: fetch, the host gates, the backoff, the cache, and the bot walls met
157
+ // (kept in the cache too, so the next search in the session leaves that host alone as well).
158
+ export function context({ fetchImpl = fetch, concurrency = 4, backoff = BACKOFF, cache = null, pace = PACE } = {}) {
159
+ const gates = new Map();
160
+ const walls = new Map();
161
+ return {
162
+ fetchImpl,
163
+ backoff,
164
+ cache,
165
+ gate(url) {
166
+ const host = new URL(url).host;
167
+ if (!gates.has(host)) gates.set(host, hostGate(concurrency, pace[host] ?? 0));
168
+ return gates.get(host);
169
+ },
170
+ wallUntil(host) {
171
+ if (!walls.has(host)) walls.set(host, cache?.get(`wall:${host}`)?.until ?? 0);
172
+ const t = walls.get(host);
173
+ return t > Date.now() ? t : 0;
174
+ },
175
+ markWall(host) {
176
+ const until = Date.now() + WALL_MS;
177
+ walls.set(host, until);
178
+ cache?.set(`wall:${host}`, { until });
179
+ },
180
+ };
181
+ }
182
+
183
+ // One request, retried after a 429, a 5xx, a plain 403, a timeout or a dropped connection: 1 s then 3 s (or the
184
+ // server's Retry-After, at most 10 s). A bot wall's 403 (an HTML page, not the API's JSON) is not retried: it is
185
+ // remembered for WALL_MS and that host is skipped meanwhile. Resolves { body, headers, status }; body is parsed
186
+ // JSON or a Buffer.
187
+ async function request(ctx, url, { as = "json", timeout = 12_000, range } = {}) {
188
+ const host = new URL(url).host;
189
+ const until = ctx.wallUntil?.(host);
190
+ if (until) throw new FetchError({ status: 403, url, until });
191
+ const gate = ctx.gate(url);
192
+ const headers = { "user-agent": UA, accept: as === "json" ? "application/json" : "image/*" };
193
+ if (range) headers.range = range;
194
+ for (let i = 0; ; i++) {
195
+ let out;
196
+ try {
197
+ out = await gate.run(async () => {
198
+ if (ctx.wallUntil?.(host)) return { wall: true };
199
+ const res = await ctx.fetchImpl(url, { headers, signal: AbortSignal.timeout(timeout) });
200
+ if (!res.ok) return { status: res.status, headers: res.headers };
201
+ const body = as === "json" ? await res.json() : Buffer.from(await res.arrayBuffer());
202
+ return { ok: true, status: res.status, headers: res.headers, body };
203
+ });
204
+ } catch (e) {
205
+ const m = String(e?.name ?? "") + " " + String(e?.message ?? e);
206
+ const kind = /timeout|abort/i.test(m) ? `timed out after ${Math.round(timeout / 1000)} s` : `network error (${clean(e?.cause?.code ?? e?.message ?? e).slice(0, 60)})`;
207
+ out = { kind };
208
+ }
209
+ if (out.ok) return out;
210
+ if (out.wall) throw new FetchError({ status: 403, url, until: ctx.wallUntil(host) });
211
+ if (out.status === 403 && /html/i.test(out.headers?.get?.("content-type") ?? "")) {
212
+ ctx.markWall?.(host);
213
+ throw new FetchError({ status: 403, url, wall: true });
214
+ }
215
+ const retry = out.kind || RETRYABLE(out.status);
216
+ if (!retry || i >= ctx.backoff.length) throw new FetchError({ status: out.status, kind: out.kind, url, tries: i + 1 });
217
+ const after = num(out.headers?.get?.("retry-after"));
218
+ const wait = after ? Math.min(10_000, after * 1000) : ctx.backoff[i];
219
+ if (out.status === 403 || out.status === 429) gate.pause(wait);
220
+ await sleep(wait);
221
+ }
222
+ }
223
+
224
+ // A short on-disk cache of API answers in the project (two hours): the same search, or an overlapping one,
225
+ // costs nothing the second time. Only successful answers are kept.
226
+ export function diskCache(dir, ttl = 2 * 3600_000) {
227
+ if (!dir) return null;
228
+ const file = (key) => path.join(dir, crypto.createHash("sha1").update(key).digest("hex").slice(0, 24) + ".json");
229
+ return {
230
+ get(key) {
231
+ try {
232
+ const f = file(key);
233
+ if (Date.now() - fs.statSync(f).mtimeMs > ttl) return undefined;
234
+ return JSON.parse(fs.readFileSync(f, "utf8")).body;
235
+ } catch {
236
+ return undefined;
237
+ }
238
+ },
239
+ set(key, body) {
240
+ try {
241
+ fs.mkdirSync(dir, { recursive: true });
242
+ fs.writeFileSync(file(key), JSON.stringify({ key, body }));
243
+ } catch {}
244
+ },
245
+ // Drops answers past their time, so the folder never grows for long.
246
+ prune() {
247
+ try {
248
+ for (const f of fs.readdirSync(dir)) {
249
+ const p = path.join(dir, f);
250
+ if (Date.now() - fs.statSync(p).mtimeMs > ttl) fs.rmSync(p, { force: true });
251
+ }
252
+ } catch {}
253
+ },
254
+ };
255
+ }
256
+
257
+ async function getJson(ctx, url) {
258
+ const hit = ctx.cache?.get(url);
259
+ if (hit !== undefined) return hit;
260
+ const { body } = await request(ctx, url);
261
+ ctx.cache?.set(url, body);
262
+ return body;
263
+ }
264
+
265
+ // Pixel size (and file size) of a remote image from its first bytes: a Range request, since JPEG keeps its
266
+ // size in the header. A second, longer read when a large EXIF / colour profile pushes the size further in.
267
+ export async function remoteSize(ctx, url) {
268
+ const key = `size:${url}`;
269
+ const hit = ctx.cache?.get(key);
270
+ if (hit !== undefined) return hit;
271
+ for (const end of [32_767, 262_143]) {
272
+ const res = await request(ctx, url, { as: "buffer", timeout: 10_000, range: `bytes=0-${end}` });
273
+ const s = imageSize(res.body);
274
+ const total = num(/\/(\d+)\s*$/.exec(res.headers?.get?.("content-range") ?? "")?.[1]) ?? (res.status === 200 ? res.body.length : undefined);
275
+ if (s) {
276
+ const out = { width: s.width, height: s.height, bytes: total ?? null };
277
+ ctx.cache?.set(key, out);
278
+ return out;
279
+ }
280
+ if (res.status === 200) break; // the server ignored the range: the whole file was read and holds no size
281
+ }
282
+ return null;
283
+ }
284
+
285
+ // Runs fn over items with at most n in flight; fn gets (item, index, stop) and stop() ends the dispatch.
286
+ async function eachLimit(items, n, fn) {
287
+ let next = 0;
288
+ let stopped = false;
289
+ const stop = () => {
290
+ stopped = true;
291
+ };
292
+ await Promise.all(
293
+ Array.from({ length: Math.max(1, Math.min(n, items.length)) }, async () => {
294
+ while (!stopped && next < items.length) {
295
+ const i = next++;
296
+ await fn(items[i], i, stop);
297
+ }
298
+ }),
299
+ );
300
+ }
301
+
302
+ const tally = (stats, e) => {
303
+ const k = e?.status ? statusReason(e.status) : e?.kind ?? "error";
304
+ stats.failed[k] = (stats.failed[k] ?? 0) + 1;
305
+ };
306
+
307
+ // One credit line: "Title, Artist, Date. The Met, CC0" or "Title, Author. Wikimedia Commons, CC BY 4.0".
308
+ export function creditLine({ title, artist, culture, date, source, licence = "CC0" }) {
309
+ const who = clean(artist) || clean(culture);
310
+ const parts = [clean(title) || "Untitled", who, clean(date)].filter(Boolean);
311
+ return `${parts.join(", ")}. ${SOURCES[source]?.name ?? source}, ${licence}`;
312
+ }
313
+
314
+ // ---- Wikimedia Commons: one search call returns files with their licence metadata. Photos only (JPEG), the
315
+ // licence read per file (commonsLicence), a CC BY file kept only when it names its author (the credit needs one).
316
+ export async function searchCommons({ query = "", artist, count = 12, minWidth = 0, fetchImpl = fetch, ctx, stats = {} } = {}) {
317
+ ctx ??= context({ fetchImpl });
318
+ Object.assign(stats, { matched: 0, looked: 0, publicDomain: 0, small: 0, failed: {}, by: clean(artist) || null, restricted: 0 });
319
+ const words = [clean(query), clean(artist) && `"${clean(artist)}"`].filter(Boolean).join(" ");
320
+ const params = new URLSearchParams({
321
+ action: "query",
322
+ format: "json",
323
+ generator: "search",
324
+ gsrsearch: `${words} filetype:bitmap${minWidth ? ` filew:>${minWidth - 1}` : ""}`,
325
+ gsrnamespace: "6",
326
+ gsrlimit: String(Math.min(50, Math.max(20, count * 4))),
327
+ prop: "imageinfo",
328
+ iiprop: "url|size|mime|extmetadata",
329
+ iiurlwidth: "600",
330
+ iiextmetadatafilter: "License|LicenseShortName|LicenseUrl|Artist|ObjectName|DateTimeOriginal|Restrictions|NonFree",
331
+ });
332
+ const data = await getJson(ctx, `${SOURCES.commons.search}?${params}`);
333
+ const pages = Object.values(data?.query?.pages ?? {}).sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
334
+ stats.matched = num(data?.query?.searchinfo?.totalhits) ?? pages.length;
335
+ const out = [];
336
+ const text = (v) => clean(String(v ?? "").replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ");
337
+ for (const p of pages) {
338
+ if (out.length >= count) break;
339
+ const i = p.imageinfo?.[0];
340
+ if (!i?.url) continue;
341
+ stats.looked++;
342
+ if (!/^image\/jpeg$/i.test(i.mime ?? "")) continue;
343
+ const licence = commonsLicence(i.extmetadata);
344
+ if (!licence) {
345
+ if (text(i.extmetadata?.Restrictions?.value)) stats.restricted++;
346
+ continue;
347
+ }
348
+ const author = text(i.extmetadata?.Artist?.value).slice(0, 80);
349
+ if (needsCredit(licence) && (!author || /^unknown/i.test(author))) continue;
350
+ stats.publicDomain++;
351
+ if (minWidth && num(i.width) && num(i.width) < minWidth) {
352
+ stats.small++;
353
+ continue;
354
+ }
355
+ const name = text(i.extmetadata?.ObjectName?.value) || clean(String(p.title).replace(/^File:/, "").replace(/\.[a-z]+$/i, "").replace(/_/g, " "));
356
+ // The year is enough for a credit ("2023-03-01 09:13:55", "Taken on 4 May 2019" → 2023, 2019).
357
+ const when = text(i.extmetadata?.DateTimeOriginal?.value);
358
+ const date = /\b(1[6-9]\d\d|20\d\d)\b/.exec(when)?.[1] ?? (when.slice(0, 20) || undefined);
359
+ // The 2400 px rendition when the original is bigger (a Commons thumb URL differs only in its width).
360
+ const big = num(i.width) > 2400 && /\/\d+px-[^/]+$/.test(i.thumburl ?? "") ? i.thumburl.replace(/\/\d+px-([^/]+)$/, "/2400px-$1") : i.url;
361
+ const c = {
362
+ id: `wc-${p.pageid}`,
363
+ source: "commons",
364
+ title: name.slice(0, 90),
365
+ artist: author || undefined,
366
+ date,
367
+ page: i.descriptionurl || `https://commons.wikimedia.org/wiki/${encodeURIComponent(p.title)}`,
368
+ full: big,
369
+ thumb: i.thumburl || i.url,
370
+ width: num(i.width),
371
+ height: num(i.height),
372
+ ...(num(i.size) ? { bytes: num(i.size) } : {}),
373
+ licence,
374
+ ...(text(i.extmetadata?.LicenseUrl?.value) ? { licenceUrl: text(i.extmetadata.LicenseUrl.value) } : {}),
375
+ };
376
+ c.credit = creditLine(c);
377
+ out.push(c);
378
+ }
379
+ return out;
380
+ }
381
+
382
+
383
+ // ---- the Met: search (v1.1; v1 search was retired 2026-10-01), then the objects a few at a time. A maker's name
384
+ // is asked as a maker first (artistOrCulture), since a keyword search for a name mostly finds other people's work.
385
+ // Pixel sizes come from the image header (the API has none), so small images can be dropped like Cleveland's.
386
+ export async function searchMet({ query = "", artist, count = 12, type, minWidth = 0, fetchImpl = fetch, gap = 0, ctx, concurrency = 4, maxLookups, sizes = true, budget = 30_000, stats = {} } = {}) {
387
+ ctx ??= context({ fetchImpl, concurrency });
388
+ Object.assign(stats, { matched: 0, looked: 0, publicDomain: 0, small: 0, failed: {}, by: null });
389
+ const params = (q, extra = {}) => {
390
+ const p = new URLSearchParams({ q, hasImages: "true", limit: String(Math.min(200, Math.max(20, count * 5))), ...extra });
391
+ if (type && MET_MEDIUM[type]) p.set("medium", MET_MEDIUM[type]);
392
+ return p;
393
+ };
394
+ let found = null;
395
+ const maker = clean(artist) || (looksLikeName(query) ? clean(query) : "");
396
+ if (maker) {
397
+ found = await getJson(ctx, `${SOURCES.met.search}?${params(maker, { artistOrCulture: "true" })}`);
398
+ if (found?.objectIDs?.length) stats.by = maker;
399
+ }
400
+ if (!found?.objectIDs?.length && clean(query)) found = await getJson(ctx, `${SOURCES.met.search}?${params(clean(query))}`);
401
+ const all = found?.objectIDs ?? [];
402
+ stats.matched = num(found?.total) ?? all.length;
403
+ const ids = all.slice(0, maxLookups ?? Math.min(120, Math.max(40, count * 5)));
404
+ const kept = [];
405
+ const t0 = Date.now();
406
+ let refused = 0;
407
+ await eachLimit(ids, concurrency, async (id, i, stop) => {
408
+ if (kept.length >= count || stats.blocked) return stop();
409
+ if (Date.now() - t0 > budget) {
410
+ stats.outOfTime = true;
411
+ return stop();
412
+ }
413
+ await sleep(gap);
414
+ let o;
415
+ try {
416
+ o = await getJson(ctx, SOURCES.met.object + id);
417
+ refused = 0;
418
+ } catch (e) {
419
+ if (e.status === 404) return; // an id the search still lists but the collection has withdrawn
420
+ tally(stats, e);
421
+ // Its bot wall, or three refusals in a row: the Met is blocking for now; stop asking.
422
+ if (e.wall || ((e.status === 403 || e.status === 429) && ++refused >= 3)) {
423
+ stats.blocked = e.message;
424
+ stop();
425
+ }
426
+ return;
427
+ }
428
+ // A maker search also matches cultures and other fields: keep only that maker's own work (every word of the
429
+ // name but initials appears among the object's makers).
430
+ if (stats.by) {
431
+ const makers = fold([o.artistDisplayName, ...(o.constituents ?? []).map((k) => k?.name)].join(" "));
432
+ if (!fold(stats.by).split(/[\s.]+/).filter((w) => w.length > 2).every((w) => makers.includes(w))) return;
433
+ }
434
+ stats.looked++;
435
+ // The licence flag, read from the API for this object. Non-public-domain objects also come without images.
436
+ if (o?.isPublicDomain !== true || !o.primaryImage) return;
437
+ stats.publicDomain++;
438
+ const c = {
439
+ id: `met-${o.objectID ?? id}`,
440
+ source: "met",
441
+ title: clean(o.title) || clean(o.objectName),
442
+ artist: clean(o.artistDisplayName) || undefined,
443
+ culture: clean(o.culture) || undefined,
444
+ date: clean(o.objectDate) || undefined,
445
+ medium: clean(o.medium) || undefined,
446
+ page: o.objectURL || `https://www.metmuseum.org/art/collection/search/${id}`,
447
+ full: o.primaryImage,
448
+ thumb: o.primaryImageSmall || o.primaryImage,
449
+ licence: "CC0",
450
+ };
451
+ if (sizes) {
452
+ try {
453
+ const s = await remoteSize(ctx, o.primaryImage);
454
+ if (s) Object.assign(c, { width: s.width, height: s.height, ...(s.bytes ? { bytes: s.bytes } : {}) });
455
+ } catch {} // size unknown: kept, shown as such
456
+ }
457
+ if (minWidth && c.width && c.width < minWidth) {
458
+ stats.small++;
459
+ return;
460
+ }
461
+ c.credit = creditLine(c);
462
+ kept.push([i, c]);
463
+ });
464
+ return kept.sort((a, b) => a[0] - b[0]).slice(0, count).map(([, c]) => c);
465
+ }
466
+
467
+ // ---- Cleveland: one search call; images.print (~3400 px) is the full image, images.web (~900 px) the thumb.
468
+ // A maker goes in artists=, the query words in q= (a name in q= works too: Cleveland searches its creators).
469
+ export async function searchCleveland({ query = "", artist, count = 12, type, minWidth = 0, fetchImpl = fetch, ctx, stats = {} } = {}) {
470
+ ctx ??= context({ fetchImpl });
471
+ Object.assign(stats, { matched: 0, looked: 0, publicDomain: 0, small: 0, failed: {}, by: clean(artist) || null });
472
+ const params = new URLSearchParams({ has_image: "1", limit: String(Math.min(100, Math.max(20, count * 3))) });
473
+ if (clean(query)) params.set("q", clean(query));
474
+ if (clean(artist)) params.set("artists", clean(artist));
475
+ if (type) params.set("type", type);
476
+ const data = await getJson(ctx, `${SOURCES.cleveland.search}?cc0&${params}`);
477
+ stats.matched = num(data?.info?.total) ?? data?.data?.length ?? 0;
478
+ const out = [];
479
+ for (const a of data?.data ?? []) {
480
+ if (out.length >= count) break;
481
+ stats.looked++;
482
+ // The literal "CC0" (the docs say "Open Access"; the API says CC0). Anything else is dropped.
483
+ if (a?.share_license_status !== "CC0") continue;
484
+ stats.publicDomain++;
485
+ const big = a.images?.print?.url ? a.images.print : a.images?.web;
486
+ if (!big?.url) continue;
487
+ const width = num(big.width);
488
+ if (minWidth && width && width < minWidth) {
489
+ stats.small++;
490
+ continue;
491
+ }
492
+ const creator = clean(a.creators?.[0]?.description).replace(/\s*\(.*\)\s*$/, "");
493
+ const c = {
494
+ id: `cma-${a.id}`,
495
+ source: "cleveland",
496
+ title: clean(a.title),
497
+ artist: creator || undefined,
498
+ culture: clean(a.culture?.[0]).split(",")[0] || undefined,
499
+ date: clean(a.creation_date) || undefined,
500
+ medium: clean(a.technique) || undefined,
501
+ page: a.url || `https://clevelandart.org/art/${a.accession_number}`,
502
+ full: big.url,
503
+ thumb: a.images?.web?.url || big.url,
504
+ width,
505
+ height: num(big.height),
506
+ ...(num(big.filesize) ? { bytes: num(big.filesize) } : {}),
507
+ licence: "CC0",
508
+ };
509
+ c.credit = creditLine(c);
510
+ out.push(c);
511
+ }
512
+ return out;
513
+ }
514
+
515
+ const SEARCH = { met: searchMet, cleveland: searchCleveland, commons: searchCommons };
516
+
517
+ // What a source did, in one line, when it needs saying: it failed (and why), was blocked part-way, matched
518
+ // nothing, or matched only work that isn't public domain.
519
+ export function sourceNote(name, st, { query, artist, error } = {}) {
520
+ const what = artist ? `works by "${artist}"` : `"${query}"`;
521
+ if (error) return `${name} failed: ${error}. ${name === "The Met" ? "Showing the other source's results; it lets palate in again after a minute or two, or use --sources cleveland." : "Showing the other source's results."}`;
522
+ const failed = Object.entries(st.failed ?? {});
523
+ const fails = failed.reduce((n, [, k]) => n + k, 0);
524
+ const parts = [];
525
+ if (st.blocked) parts.push(`${name} stopped answering part-way (${st.blocked}); ${plural(st.kept ?? 0, "candidate")} from it. It lets palate in again after a minute or two.`);
526
+ else if (fails) parts.push(`${name}: ${fails} of ${fails + st.looked} lookups failed (${failed.map(([k, n]) => `${n}× ${k}`).join(", ")}), even after retrying.`);
527
+ if (!st.matched) parts.push(`${name}: nothing matches ${what}.${!artist && clean(query).split(" ").length > 1 ? " Fewer words find more (each word must match)." : ""}`);
528
+ else if (!st.kept && st.looked && !st.publicDomain) parts.push(`${name}: ${plural(st.matched, "work")} match${st.matched === 1 ? "es" : ""} ${what}${st.by && !artist ? " (by that maker)" : ""}, but none of the ${st.looked} looked at is public domain.`);
529
+ if (st.small) parts.push(`${name}: ${st.small} too small (narrower than the minimum width) left out.`);
530
+ if (st.outOfTime) parts.push(`${name}: stopped looking after ${st.looked} objects to stay quick.`);
531
+ return parts.join(" ");
532
+ }
533
+
534
+ // Candidates from every source at once, interleaved (one of each in turn) so no single museum fills the sheet.
535
+ // The returned array carries .notes (sources that failed, were blocked or found nothing) and .stats.
536
+ export async function searchImages({ query = "", artist, sources = ["met", "cleveland"], count = 12, minWidth = 1600, type, fetchImpl = fetch, gap = 0, ctx, cache = null, concurrency = 4, backoff, pace } = {}) {
537
+ if (!clean(query) && !clean(artist)) throw new Error('searchImages needs a query, e.g. "stoneware bowl" (or an artist).');
538
+ ctx ??= context({ fetchImpl, cache, concurrency, ...(backoff ? { backoff } : {}), ...(pace ? { pace } : {}) });
539
+ const { known, notes } = normaliseSources(sources);
540
+ const each = known.length ? Math.ceil(count / known.length) + 2 : 0;
541
+ const stats = {};
542
+ const lists = await Promise.all(
543
+ known.map(async (s) => {
544
+ stats[s] = {};
545
+ try {
546
+ const l = await SEARCH[s]({ query, artist, count: each, type, minWidth, fetchImpl, gap, ctx, concurrency, stats: stats[s] });
547
+ stats[s].kept = l.length;
548
+ return l;
549
+ } catch (e) {
550
+ stats[s].error = e.message;
551
+ return [];
552
+ }
553
+ }),
554
+ );
555
+ known.forEach((s) => {
556
+ const n = sourceNote(SOURCES[s].name, stats[s], { query, artist, error: stats[s].error });
557
+ if (n) notes.push(n);
558
+ });
559
+ const out = [];
560
+ for (let i = 0; out.length < count && lists.some((l) => i < l.length); i++) for (const l of lists) if (i < l.length && out.length < count) out.push(l[i]);
561
+ // Belt and braces: nothing leaves this function without a licence palate accepts (openLicence).
562
+ const kept = out.filter((c) => openLicence(c.licence));
563
+ kept.notes = notes;
564
+ kept.stats = stats;
565
+ return kept;
566
+ }
567
+
568
+ // ---- image sizes from the file header (JPEG, PNG, GIF, WebP); no image library. Works on the first bytes alone.
569
+ export function imageSize(buf) {
570
+ const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
571
+ if (b.length >= 24 && b.readUInt32BE(0) === 0x89504e47) return { width: b.readUInt32BE(16), height: b.readUInt32BE(20), type: "png" };
572
+ if (b.length >= 10 && b.toString("ascii", 0, 3) === "GIF") return { width: b.readUInt16LE(6), height: b.readUInt16LE(8), type: "gif" };
573
+ if (b.length >= 30 && b.toString("ascii", 0, 4) === "RIFF" && b.toString("ascii", 8, 12) === "WEBP") {
574
+ const kind = b.toString("ascii", 12, 16);
575
+ if (kind === "VP8X") return { width: 1 + b.readUIntLE(24, 3), height: 1 + b.readUIntLE(27, 3), type: "webp" };
576
+ if (kind === "VP8 ") return { width: b.readUInt16LE(26) & 0x3fff, height: b.readUInt16LE(28) & 0x3fff, type: "webp" };
577
+ if (kind === "VP8L") {
578
+ const bits = b.readUInt32LE(21);
579
+ return { width: 1 + (bits & 0x3fff), height: 1 + ((bits >> 14) & 0x3fff), type: "webp" };
580
+ }
581
+ }
582
+ if (b.length >= 4 && b[0] === 0xff && b[1] === 0xd8) {
583
+ let i = 2;
584
+ while (i + 9 < b.length) {
585
+ if (b[i] !== 0xff) return null;
586
+ const m = b[i + 1];
587
+ if (m === 0xff) {
588
+ i++;
589
+ continue;
590
+ }
591
+ if (m === 0xd8 || m === 0x01 || (m >= 0xd0 && m <= 0xd7)) {
592
+ i += 2;
593
+ continue;
594
+ }
595
+ if ((m >= 0xc0 && m <= 0xcf) && m !== 0xc4 && m !== 0xc8 && m !== 0xcc) return { width: b.readUInt16BE(i + 7), height: b.readUInt16BE(i + 5), type: "jpeg" };
596
+ i += 2 + b.readUInt16BE(i + 2);
597
+ }
598
+ }
599
+ return null;
600
+ }
601
+
602
+ const EXT = { jpeg: "jpg", png: "png", gif: "gif", webp: "webp" };
603
+ function extOf(url, contentType, buf) {
604
+ const t = imageSize(buf)?.type;
605
+ if (t) return EXT[t];
606
+ if (/png/.test(contentType ?? "")) return "png";
607
+ if (/webp/.test(contentType ?? "")) return "webp";
608
+ const m = /\.(jpe?g|png|webp|gif)(?:$|\?)/i.exec(url);
609
+ return m ? m[1].toLowerCase().replace("jpeg", "jpg") : "jpg";
610
+ }
611
+
612
+ async function download(ctx, url, timeout = 90_000) {
613
+ const res = await request(ctx, url, { as: "buffer", timeout });
614
+ return { buf: res.body, type: res.headers?.get?.("content-type") ?? "" };
615
+ }
616
+
617
+ // ---- museum mounts: the card an old photograph is pasted on, scanned with it (often on a backdrop as well).
618
+ // Pure, deterministic, and run on a small copy of the image (RGBA pixels, w × h). It must stay self-contained:
619
+ // the browser runs its source. A border line is one that is mostly a single colour (its own median: a card's
620
+ // tone drifts with age and light, and a backdrop and a card are two colours), scanned inward from each side to
621
+ // the print's edge. It is a mount only when all four sides have a band and every edge is sharp and straight
622
+ // (just inside it, the print is mostly not border); an object on a plain backdrop (a bowl) has no straight
623
+ // edge on its sides, so it is never cropped.
624
+ // Returns null (no border), { trim: false, sides, colour } (a border on some sides only: flagged, not cut) or
625
+ // { trim: true, top, right, bottom, left, colour } (pixels to cut from each side, a small inset included).
626
+ export function findMount(px, w, h) {
627
+ const TOL = 34; // per channel: card texture, foxing and scan noise
628
+ const MOUNT = 0.9; // a line is border when this share of it is within TOL of its own colour
629
+ const CONTENT = 0.5; // just inside the edge, at most this share may still be the border's colour
630
+ if (w < 40 || h < 40) return null;
631
+ const median = (a) => a.slice().sort((x, y) => x - y)[a.length >> 1];
632
+ const near = (p, c) => Math.abs(px[p] - c[0]) <= TOL && Math.abs(px[p + 1] - c[1]) <= TOL && Math.abs(px[p + 2] - c[2]) <= TOL;
633
+ const at = (horiz, i, j) => (horiz ? i * w + j : j * w + i) * 4;
634
+ // Row i (horiz) or column i, over [a, b): the share of it within TOL of colour c.
635
+ const share = (horiz, i, a, b, c) => {
636
+ let n = 0;
637
+ for (let j = a; j < b; j++) if (near(at(horiz, i, j), c)) n++;
638
+ return b > a ? n / (b - a) : 1;
639
+ };
640
+ const colourOf = (horiz, i, a, b) => {
641
+ const ch = [[], [], []];
642
+ const step = Math.max(1, Math.floor((b - a) / 200));
643
+ for (let j = a; j < b; j += step) for (let k = 0; k < 3; k++) ch[k].push(px[at(horiz, i, j) + k]);
644
+ return ch.map(median);
645
+ };
646
+ const isBorder = (horiz, i, span) => {
647
+ const c = colourOf(horiz, i, span[0], span[1]);
648
+ return share(horiz, i, span[0], span[1], c) >= MOUNT ? c : null;
649
+ };
650
+ // From one side inward while the line is border; a thin interruption (a pencil rule, a blind stamp) is
651
+ // stepped over. Returns the band's depth and the colour of its innermost line.
652
+ const scan = (horiz, from, dir, len, span) => {
653
+ const n = Math.floor(len / 2);
654
+ const skip = Math.max(2, Math.round(len * 0.006));
655
+ let d = 0;
656
+ let colour = null;
657
+ while (d < n) {
658
+ const c = isBorder(horiz, from + dir * d, span);
659
+ if (c) {
660
+ colour = c;
661
+ d++;
662
+ continue;
663
+ }
664
+ let again = 0;
665
+ for (let k = 1; k <= skip && d + k < n && !again; k++) if (isBorder(horiz, from + dir * (d + k), span)) again = k;
666
+ if (!again) break;
667
+ d += again;
668
+ }
669
+ return { d: Math.min(d, n), colour };
670
+ };
671
+ // Twice: first across whole lines, then across the span left between the other two sides' bands.
672
+ let top, right, bottom, left;
673
+ let T = 0, R = 0, B = 0, L = 0;
674
+ for (let pass = 0; pass < 2; pass++) {
675
+ const hx = [L, w - R];
676
+ const vy = [T, h - B];
677
+ top = scan(true, 0, 1, h, hx);
678
+ bottom = scan(true, h - 1, -1, h, hx);
679
+ left = scan(false, 0, 1, w, vy);
680
+ right = scan(false, w - 1, -1, w, vy);
681
+ [T, R, B, L] = [top.d, right.d, bottom.d, left.d];
682
+ }
683
+ const bands = [top, right, bottom, left];
684
+ const depth = [T / h, R / w, B / h, L / w];
685
+ const hexOf = (c) => "#" + c.map((v) => v.toString(16).padStart(2, "0")).join("");
686
+ const sides = depth.filter((x) => x >= 0.012).length;
687
+ if (!sides) return null;
688
+ // The edge must be sharp: a few lines inside it the print is mostly not the border's colour.
689
+ const k = (len) => Math.max(2, Math.round(len * 0.015));
690
+ const inside = [
691
+ top.colour && share(true, Math.min(h - 1, T + k(h)), L, w - R, top.colour),
692
+ right.colour && share(false, Math.max(0, w - 1 - R - k(w)), T, h - B, right.colour),
693
+ bottom.colour && share(true, Math.max(0, h - 1 - B - k(h)), L, w - R, bottom.colour),
694
+ left.colour && share(false, Math.min(w - 1, L + k(w)), T, h - B, left.colour),
695
+ ];
696
+ const sharp = inside.every((s) => s !== null && s <= CONTENT);
697
+ const area = ((w - L - R) * (h - T - B)) / (w * h);
698
+ if (sides < 4 || !sharp || area < 0.3) {
699
+ // Flagged, not cut: deep bands of one colour on two or more sides (a card cut unevenly, a caption below).
700
+ const deep = bands.filter((b, i) => depth[i] >= 0.03 && b.colour);
701
+ const agree = deep.length >= 2 && deep.every((b) => b.colour.every((v, j) => Math.abs(v - deep[0].colour[j]) <= TOL));
702
+ return agree ? { trim: false, sides: deep.length, colour: hexOf(deep[0].colour) } : null;
703
+ }
704
+ // A little past the edge, so no sliver of card or the print's soft border is left.
705
+ const ix = Math.round(w * 0.006);
706
+ const iy = Math.round(h * 0.006);
707
+ return { trim: true, top: T + iy, right: R + ix, bottom: B + iy, left: L + ix, colour: hexOf(top.colour) };
708
+ }
709
+
710
+ // One canvas pass in the browser palate already uses (no image library): finds a mount (and trims it unless
711
+ // trim is false), scales the longest side down to maxSide and encodes a JPEG at quality. Returns
712
+ // { buf, width, height, mount } or null when it can't (no browser, or a file the browser can't decode).
713
+ export async function processImage(buf, type, { maxSide = 2400, quality = 0.82, trim = true, browser } = {}) {
714
+ let release = async () => {};
715
+ try {
716
+ if (!browser) {
717
+ const { acquireBrowser } = await import("../pool.mjs");
718
+ ({ browser, release } = await acquireBrowser());
719
+ }
720
+ const page = await browser.newPage();
721
+ try {
722
+ const r = await page.evaluate(
723
+ async ([src, maxSide, quality, trim, finder]) => {
724
+ const img = new Image();
725
+ img.src = src;
726
+ await img.decode();
727
+ const W = img.naturalWidth;
728
+ const H = img.naturalHeight;
729
+ let crop = { x: 0, y: 0, w: W, h: H };
730
+ // The mount is looked for on a copy at most 800 px long: quick, and it evens out the card's grain.
731
+ const a = Math.min(1, 800 / Math.max(W, H));
732
+ const aw = Math.max(1, Math.round(W * a));
733
+ const ah = Math.max(1, Math.round(H * a));
734
+ const sc = document.createElement("canvas");
735
+ sc.width = aw;
736
+ sc.height = ah;
737
+ const sx = sc.getContext("2d", { willReadFrequently: true });
738
+ sx.drawImage(img, 0, 0, aw, ah);
739
+ const find = new Function(`return (${finder})`)();
740
+ const mount = find(sx.getImageData(0, 0, aw, ah).data, aw, ah);
741
+ if (trim && mount?.trim) {
742
+ const x = Math.round(mount.left / a);
743
+ const y = Math.round(mount.top / a);
744
+ crop = { x, y, w: Math.max(1, Math.round((aw - mount.right) / a) - x), h: Math.max(1, Math.round((ah - mount.bottom) / a) - y) };
745
+ }
746
+ const s = Math.min(1, maxSide / Math.max(crop.w, crop.h));
747
+ const c = document.createElement("canvas");
748
+ c.width = Math.round(crop.w * s);
749
+ c.height = Math.round(crop.h * s);
750
+ const ctx = c.getContext("2d");
751
+ ctx.imageSmoothingQuality = "high";
752
+ ctx.drawImage(img, crop.x, crop.y, crop.w, crop.h, 0, 0, c.width, c.height);
753
+ return { data: c.toDataURL("image/jpeg", quality), width: c.width, height: c.height, mount, crop: trim && mount?.trim ? crop : null };
754
+ },
755
+ [`data:${type || "image/jpeg"};base64,${buf.toString("base64")}`, maxSide, quality, trim, findMount.toString()],
756
+ );
757
+ return { buf: Buffer.from(r.data.split(",")[1], "base64"), width: r.width, height: r.height, mount: r.mount, crop: r.crop };
758
+ } finally {
759
+ await page.close();
760
+ }
761
+ } catch {
762
+ return null;
763
+ } finally {
764
+ await release();
765
+ }
766
+ }
767
+
768
+ // Kept for callers of the old name: scales to maxWidth, no trim.
769
+ export async function resizeImage(buf, type, maxWidth) {
770
+ return (await processImage(buf, type, { maxSide: maxWidth, trim: false, quality: 0.86 }))?.buf ?? null;
771
+ }
772
+
773
+ export const imageryRoot = (dir) => path.join(dir, ".palate", "imagery");
774
+
775
+ const sizeLabel = (c) => (c.width && c.height ? `${c.width}×${c.height}` : "size ?");
776
+
777
+ // ---- `palate imagery "<query>"`: candidates.json + contact sheets, and text telling the agent what's next.
778
+ // Museum and Commons answers barely change: one cache for every project on this machine, kept a week, so the same
779
+ // search in the next project (or the next round) costs nothing. Thumbnails stay in each project's own search folder.
780
+ export const IMAGERY_CACHE_TTL = 7 * 24 * 3600_000;
781
+ export const imageryCacheDir = () => process.env.PALATE_IMAGERY_CACHE || path.join(process.env.PALATE_HOME ?? path.join(os.homedir(), ".palate"), "cache", "imagery");
782
+
783
+ export async function imagery(dir, query, { count = 12, sources = ["met", "cleveland"], name, type, artist, minWidth = 1600, fetchImpl = fetch, gap = 0, sheet = true, refresh = false, backoff, pace, concurrency = 4, ctx: shared } = {}) {
784
+ query = clean(query);
785
+ artist = clean(artist) || undefined;
786
+ if (type && !TYPES.includes(type)) {
787
+ const t = TYPES.find((x) => x.toLowerCase() === String(type).toLowerCase().replace(/s$/, ""));
788
+ if (!t) throw new Error(`Unknown --type "${type}" (${TYPES.join(", ")}).`);
789
+ type = t;
790
+ }
791
+ const slug = slugify(name || [query, artist].filter(Boolean).join(" ") || "images");
792
+ const root = path.join(imageryRoot(dir), slug);
793
+ fs.mkdirSync(path.join(root, "thumbs"), { recursive: true });
794
+ fs.writeFileSync(path.join(dir, ".palate", ".gitignore"), "*\n");
795
+ const cache = shared ? null : refresh ? null : diskCache(imageryCacheDir(), IMAGERY_CACHE_TTL);
796
+ cache?.prune();
797
+ const ctx = shared ?? context({ fetchImpl, cache, concurrency, ...(backoff ? { backoff } : {}), ...(pace ? { pace } : {}) });
798
+ // The browser starts while the museums answer.
799
+ const pool = sheet ? import("../pool.mjs").then((p) => p.acquireBrowser()).catch((e) => ({ error: e })) : null;
800
+ let candidates;
801
+ try {
802
+ candidates = await searchImages({ query, artist, sources, count, minWidth, type, fetchImpl, gap, ctx });
803
+ } catch (e) {
804
+ pool?.then((p) => p.release?.());
805
+ throw e;
806
+ }
807
+ const notes = [...(candidates.notes ?? [])];
808
+ fs.writeFileSync(path.join(root, "candidates.json"), JSON.stringify({ query, artist, sources, type, at: new Date().toISOString(), candidates: [...candidates] }, null, 2));
809
+
810
+ const sheets = [];
811
+ if (pool) {
812
+ const got = await pool;
813
+ try {
814
+ if (candidates.length) {
815
+ // Thumbnails a few at a time per host, each with a timeout; a failed one leaves a labelled gap.
816
+ const thumbs = new Array(candidates.length);
817
+ await eachLimit(candidates, concurrency * 2, async (c, i) => {
818
+ const f = path.join(root, "thumbs", `${c.id}.jpg`);
819
+ const caption = `${c.id} · ${sizeLabel(c)} · ${c.title.slice(0, 26)}`;
820
+ try {
821
+ if (!fs.existsSync(f)) fs.writeFileSync(f, (await download(ctx, c.thumb, 20_000)).buf);
822
+ thumbs[i] = { caption, file: f };
823
+ } catch (e) {
824
+ thumbs[i] = { caption, note: `thumbnail failed: ${e.message}` };
825
+ }
826
+ });
827
+ for (const f of fs.readdirSync(root)) if (/^contact(-\d+)?\.png$/.test(f)) fs.rmSync(path.join(root, f), { force: true });
828
+ if (got.error) throw got.error;
829
+ const { contactSheet } = await import("../look.mjs");
830
+ for (let p = 0; p < thumbs.length; p += SHEET_SIZE) {
831
+ const page = thumbs.slice(p, p + SHEET_SIZE);
832
+ const rows = [];
833
+ for (let i = 0; i < page.length; i += 4) rows.push({ cells: page.slice(i, i + 4) });
834
+ const file = path.join(root, p ? `contact-${p / SHEET_SIZE + 1}.png` : "contact.png");
835
+ sheets.push(await contactSheet(got.browser, rows, file, { cell: 260, maxCell: 260 }));
836
+ }
837
+ }
838
+ } catch (e) {
839
+ notes.push(`Contact sheet skipped (${String(e.message).split("\n")[0]}); the thumbnails are in ${path.join(root, "thumbs")}.`);
840
+ } finally {
841
+ await got.release?.();
842
+ }
843
+ }
844
+
845
+ const label = [query && `"${query}"`, artist && `--artist "${artist}"`, type && `--type ${type}`].filter(Boolean).join(" ");
846
+ const bySource = Object.keys(SOURCES).map((s) => [SOURCES[s].name, candidates.filter((c) => c.source === s).length]).filter(([, n]) => n);
847
+ const out = [`${CMD.search} ${label}: ${candidates.length} open-licence candidate${candidates.length === 1 ? "" : "s"}${bySource.length ? ` (${bySource.map(([n, k]) => `${k} ${n}`).join(", ")})` : ""}.`];
848
+ // What went wrong with a source comes first, where it can't be missed.
849
+ for (const n of notes) out.push(`Note: ${n}`);
850
+ if (!candidates.length) {
851
+ out.push(`Nothing CC0 matched. Try a plainer object word ("bowl", "vase", "textile", "landscape"), --type (${TYPES.join(", ")}), --artist "<name>" for one maker, or the other source.`);
852
+ } else {
853
+ if (sheets.length === 1) out.push(`Contact sheet: ${sheets[0]} — open it and choose by eye: the subject, room to crop, the light, one family of colour across every pick.`);
854
+ else if (sheets.length) out.push(`Contact sheets (${SHEET_SIZE} to a sheet): ${sheets.join(" , ")} — open them and choose by eye: the subject, room to crop, the light, one family of colour across every pick.`);
855
+ out.push("");
856
+ for (const c of candidates) out.push(` ${c.id.padEnd(12)} ${c.credit} [${sizeLabel(c)}${c.bytes ? `, ${(c.bytes / 1048576).toFixed(1)} MB` : ""}]`);
857
+ out.push("");
858
+ out.push(`Next: ${CMD.pick} <id> [<id> …] --from ${slug} (downloads into the project's public folder as web-sized JPEGs, longest side 2400 px, a museum mount trimmed; records credits.json)`);
859
+ out.push("Every candidate carries its source's own licence flag (museums: CC0; Commons: CC0, public domain or CC BY). Use one treatment for every photo on the page (duotone, tone or grain).");
860
+ if (candidates.some((c) => needsCredit(c.licence))) out.push("CC BY photos need their credit on the page (title, author, licence, and that it was changed): palate imagery pick writes the line, and palate guard checks it is there.");
861
+ }
862
+ out.push(`Candidates: ${path.join(root, "candidates.json")}`);
863
+ return out.join("\n");
864
+ }
865
+
866
+ // ---- `palate imagery "a | b | c"`: every search a page needs, in one call. The searches run side by side on one
867
+ // shared context (so the museums' pacing and bot walls are respected across all of them, not per search) and one
868
+ // cache; each keeps its own folder, candidates and contact sheet. On a benchmark page the planner ran 60 searches
869
+ // one at a time (about 37 minutes); one or two batches of 6 to 12 cover a page.
870
+ export const splitQueries = (q) => (Array.isArray(q) ? q : String(q).split(/\s*\|\s*|\n+/)).map((x) => clean(x)).filter(Boolean);
871
+ export async function imageryMany(dir, queries, { count = 8, sources, type, minWidth, fetchImpl = fetch, refresh = false, backoff, pace, concurrency = 4, sheet = true, parallel = 3 } = {}) {
872
+ queries = [...new Set(splitQueries(queries))].slice(0, 16);
873
+ if (!queries.length) throw new Error('Give the searches, e.g. palate imagery "wedding party | street at dusk | hands"');
874
+ if (queries.length === 1) return imagery(dir, queries[0], { count, sources, type, minWidth, fetchImpl, refresh, backoff, pace, concurrency, sheet });
875
+ const cache = refresh ? null : diskCache(imageryCacheDir(), IMAGERY_CACHE_TTL);
876
+ cache?.prune();
877
+ const ctx = context({ fetchImpl, cache, concurrency, ...(backoff ? { backoff } : {}), ...(pace ? { pace } : {}) });
878
+ const t0 = Date.now();
879
+ const results = new Array(queries.length);
880
+ await eachLimit(queries, parallel, async (q, i) => {
881
+ try {
882
+ results[i] = await imagery(dir, q, { count, sources, type, minWidth, fetchImpl, ctx, sheet });
883
+ } catch (e) {
884
+ results[i] = `${CMD.search} "${q}": failed (${e.message})`;
885
+ }
886
+ });
887
+ const found = results.map((r) => Number(/: (\d+) open-licence candidate/.exec(r)?.[1] ?? 0));
888
+ const head = [
889
+ `${queries.length} searches in one go, ${Math.round((Date.now() - t0) / 1000)} s: ${found.reduce((a, b) => a + b, 0)} candidates (${queries.map((q, i) => `"${q}" ${found[i]}`).join(", ")}).`,
890
+ "Open each search's contact sheet and pick across all of them at once (one family of colour and light for the whole page); a second batch only for a slot that is still empty. Then one pick for everything:",
891
+ ` ${CMD.pick} <id> [<id> …] (ids from any of these searches)`,
892
+ "",
893
+ ];
894
+ return [...head, ...results.map((r) => r.replace(/\nNext: [^\n]*/, ""))].join("\n\n");
895
+ }
896
+
897
+ // The project's static folder: public/ (Next, Vite, Astro, Nuxt …) or static/ (SvelteKit); public/ by default.
898
+ export function publicDir(dir) {
899
+ for (const d of ["public", "static"]) if (fs.existsSync(path.join(dir, d))) return d;
900
+ return "public";
901
+ }
902
+
903
+ function findCandidates(dir, ids, slug) {
904
+ const root = imageryRoot(dir);
905
+ const slugs = slug ? [slug] : fs.existsSync(root) ? fs.readdirSync(root).filter((s) => fs.existsSync(path.join(root, s, "candidates.json"))).sort((a, b) => fs.statSync(path.join(root, b, "candidates.json")).mtimeMs - fs.statSync(path.join(root, a, "candidates.json")).mtimeMs) : [];
906
+ const found = new Map();
907
+ for (const s of slugs) {
908
+ const f = path.join(root, s, "candidates.json");
909
+ if (!fs.existsSync(f)) continue;
910
+ for (const c of JSON.parse(fs.readFileSync(f, "utf8")).candidates ?? []) if (ids.includes(c.id) && !found.has(c.id)) found.set(c.id, c);
911
+ }
912
+ return found;
913
+ }
914
+
915
+ // A one-line credits paragraph for the whole page: "Images: Bowl (China, 14th century), The Met; … Public domain (CC0)."
916
+ // A CC BY photo's item names its author, the licence and that it was changed (resized, toned), as CC BY asks.
917
+ export function creditsParagraph(entries) {
918
+ if (!entries.length) return "";
919
+ const items = entries.map((e) => {
920
+ const who = [e.artist || e.culture, e.date].filter(Boolean).join(", ");
921
+ const by = needsCredit(e.licence) ? `, ${e.licence}${e.changed ? `, ${e.changed}` : ""}` : "";
922
+ return `${e.title}${who ? ` (${who})` : ""}, ${SOURCES[e.source]?.name ?? e.source}${by}`;
923
+ });
924
+ const credited = entries.filter((e) => needsCredit(e.licence)).length;
925
+ return `Images: ${items.join("; ")}.${!credited ? " Public domain (CC0)." : credited < entries.length ? " The rest public domain." : ""}`;
926
+ }
927
+
928
+ // A file ships as it came only when it is already a modest JPEG with nothing to trim.
929
+ const SHIP_AS_IS = 1.2 * 1048576;
930
+
931
+ // ---- `palate imagery pick <ids>`: download, trim a mount, web-size, self-host, record credits.
932
+ export async function pickImages(dir, ids, { out, maxWidth = 2400, quality = 0.82, trim = true, slug, fetchImpl = fetch, gap = 0, backoff, pace, processImage: proc = processImage } = {}) {
933
+ ids = [...new Set((Array.isArray(ids) ? ids : String(ids).split(/[\s,]+/)).map((s) => s.trim()).filter(Boolean))];
934
+ if (!ids.length) throw new Error(`Name the candidates to keep, e.g. ${CMD.pick} met-49209 cma-125978.`);
935
+ const found = findCandidates(dir, ids, slug);
936
+ const missing = ids.filter((id) => !found.has(id));
937
+ if (missing.length) throw new Error(`Not in ${slug ? `.palate/imagery/${slug}` : "any .palate/imagery search"}: ${missing.join(", ")}. Run ${CMD.search} "<query>" first and use the ids it prints.`);
938
+
939
+ const pub = publicDir(dir);
940
+ const outRel = out ?? `${pub}/images/palate`;
941
+ const outDir = path.resolve(dir, outRel);
942
+ fs.mkdirSync(outDir, { recursive: true });
943
+ const pubAbs = path.resolve(dir, pub);
944
+ const webPath = (f) => {
945
+ const rel = path.relative(pubAbs, f);
946
+ return rel.startsWith("..") || path.isAbsolute(rel) ? path.relative(dir, f).split(path.sep).join("/") : "/" + rel.split(path.sep).join("/");
947
+ };
948
+ const ctx = context({ fetchImpl, concurrency: 3, ...(backoff ? { backoff } : {}), ...(pace ? { pace } : {}) });
949
+ const keep = path.join(imageryRoot(dir), "originals");
950
+
951
+ // Downloads in parallel (a few per host); then one canvas pass each, in order.
952
+ const jobs = ids.map((id) => {
953
+ const c = found.get(id);
954
+ if (!openLicence(c.licence)) return { id, c, skip: true };
955
+ const existing = fs.readdirSync(outDir).find((f) => f.startsWith(`${id}.`) && /\.(jpe?g|png|webp|gif)$/i.test(f));
956
+ return { id, c, existing };
957
+ });
958
+ await eachLimit(jobs.filter((j) => !j.skip && !j.existing), 6, async (j) => {
959
+ await sleep(gap);
960
+ try {
961
+ j.got = await download(ctx, j.c.full);
962
+ } catch (e) {
963
+ j.error = e.message;
964
+ }
965
+ });
966
+
967
+ const lines = [];
968
+ const picked = [];
969
+ for (const j of jobs) {
970
+ const { id, c } = j;
971
+ if (j.skip) {
972
+ lines.push(` ${id}: skipped, its licence (${c.licence ?? "none"}) isn't one palate uses.`);
973
+ continue;
974
+ }
975
+ if (j.error) {
976
+ lines.push(` ${id}: download failed (${j.error}); try again in a minute.`);
977
+ continue;
978
+ }
979
+ let file, buf, mount = null, original = null;
980
+ if (j.existing) {
981
+ file = path.join(outDir, j.existing);
982
+ buf = fs.readFileSync(file);
983
+ } else {
984
+ buf = j.got.buf;
985
+ const ext = extOf(c.full, j.got.type, buf);
986
+ const orig = imageSize(buf);
987
+ original = { width: orig?.width ?? null, height: orig?.height ?? null, bytes: buf.length };
988
+ const done = process.env.PALATE_NO_RESIZE ? null : await proc(buf, j.got.type, { maxSide: maxWidth, quality, trim });
989
+ const trimmed = done?.mount?.trim && trim;
990
+ mount = done?.mount ?? null;
991
+ const changed = done && (trimmed || Math.max(orig?.width ?? 0, orig?.height ?? 0) > maxWidth || buf.length > SHIP_AS_IS || ext !== "jpg");
992
+ if (changed) {
993
+ // The original stays in .palate (gitignored); the project gets the web-sized JPEG.
994
+ fs.mkdirSync(keep, { recursive: true });
995
+ fs.writeFileSync(path.join(keep, `${id}.${ext}`), buf);
996
+ buf = done.buf;
997
+ file = path.join(outDir, `${id}.jpg`);
998
+ } else file = path.join(outDir, `${id}.${ext}`);
999
+ fs.writeFileSync(file, buf);
1000
+ }
1001
+ const size = imageSize(buf) ?? { width: c.width, height: c.height };
1002
+ const mb = (buf.length / 1048576).toFixed(1);
1003
+ const long = Math.max(size.width ?? 0, size.height ?? 0) > maxWidth ? ` — longer than ${maxWidth}px and couldn't be resized here: resize it before shipping` : "";
1004
+ const was = original && (original.width !== size.width || original.bytes !== buf.length) ? ` (from ${original.width ?? "?"}×${original.height ?? "?"}, ${(original.bytes / 1048576).toFixed(1)} MB)` : "";
1005
+ lines.push(` ${webPath(file)} ${size.width ?? "?"}×${size.height ?? "?"}, ${mb} MB${was}${j.existing ? " (already here)" : ""}${long}`);
1006
+ let mountNote = null;
1007
+ if (mount?.trim && trim) mountNote = `museum mount (${mount.colour}) trimmed`;
1008
+ else if (mount?.trim) mountNote = `museum mount (${mount.colour}) found, kept (--no-trim)`;
1009
+ else if (mount) mountNote = `a border on ${mount.sides} sides looks like a museum mount but has no straight edge all round; not trimmed (crop it with object-fit / object-position)`;
1010
+ if (mountNote) lines.push(` ${mountNote}`);
1011
+ lines.push(` ${c.credit} ${c.page}`);
1012
+ const prev = (() => {
1013
+ try {
1014
+ return JSON.parse(fs.readFileSync(path.join(outDir, "credits.json"), "utf8")).find((e) => e.file === webPath(file));
1015
+ } catch {
1016
+ return undefined;
1017
+ }
1018
+ })();
1019
+ picked.push({
1020
+ file: webPath(file),
1021
+ title: c.title,
1022
+ artist: c.artist ?? null,
1023
+ culture: c.culture ?? null,
1024
+ date: c.date ?? null,
1025
+ source: c.source,
1026
+ page: c.page,
1027
+ licence: c.licence,
1028
+ ...(c.licenceUrl ? { licenceUrl: c.licenceUrl } : {}),
1029
+ ...(needsCredit(c.licence) ? { attribution: true, changed: "resized" } : {}),
1030
+ credit: c.credit,
1031
+ width: size.width ?? null,
1032
+ height: size.height ?? null,
1033
+ ...(j.existing ? { original: prev?.original, mount: prev?.mount } : { original: original ?? undefined, mount: mountNote ?? undefined }),
1034
+ });
1035
+ }
1036
+
1037
+ // Merge into credits.json by file; earlier picks stay.
1038
+ const creditsFile = path.join(outDir, "credits.json");
1039
+ let credits = [];
1040
+ try {
1041
+ credits = JSON.parse(fs.readFileSync(creditsFile, "utf8"));
1042
+ if (!Array.isArray(credits)) credits = [];
1043
+ } catch {}
1044
+ for (const p of picked) {
1045
+ const i = credits.findIndex((e) => e.file === p.file);
1046
+ if (i >= 0) credits[i] = p;
1047
+ else credits.push(p);
1048
+ }
1049
+ fs.writeFileSync(creditsFile, JSON.stringify(credits, null, 2) + "\n");
1050
+
1051
+ return [
1052
+ `Kept ${picked.length} image${picked.length === 1 ? "" : "s"} in ${path.relative(dir, outDir).split(path.sep).join("/")}/ (self-hosted; never hotlink the museum; originals in .palate/imagery/originals):`,
1053
+ ...lines,
1054
+ "",
1055
+ `Credits (${path.relative(dir, creditsFile).split(path.sep).join("/")}), ready to paste in the footer or a colophon:`,
1056
+ ` ${creditsParagraph(credits)}`,
1057
+ "Give every <img> real alt text (what the object is, not \"image\"), width and height, and one shared treatment.",
1058
+ ].join("\n");
1059
+ }
1060
+
1061
+ // ---- treatments: one for every photo on the page. Pure CSS (an SVG filter as a data URI for duotone).
1062
+ function hex(c) {
1063
+ const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(c).trim());
1064
+ if (!m) throw new Error(`Expected a hex colour like #2b1d14, got "${c}".`);
1065
+ const h = m[1].length === 3 ? [...m[1]].map((x) => x + x).join("") : m[1];
1066
+ return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
1067
+ }
1068
+ const r3 = (n) => Math.round(n * 1000) / 1000;
1069
+ const svgUrl = (svg) => `url("data:image/svg+xml,${encodeURIComponent(svg).replace(/'/g, "%27")}")`;
1070
+
1071
+ export function duotoneFilter(shadow, highlight) {
1072
+ const s = hex(shadow);
1073
+ const h = hex(highlight);
1074
+ const L = [0.2126, 0.7152, 0.0722];
1075
+ const rows = [0, 1, 2].map((i) => [...L.map((w) => r3((h[i] - s[i]) * w)), 0, r3(s[i])].join(" "));
1076
+ return `<svg xmlns='http://www.w3.org/2000/svg'><filter id='palate-duotone' color-interpolation-filters='sRGB'><feColorMatrix type='matrix' values='${rows.join(" ")} 0 0 0 1 0'/></filter></svg>`;
1077
+ }
1078
+
1079
+ // treatmentCss("duotone" | "tone" | "natural" | "grain" | "duotone+grain", { shadow, highlight, colour, opacity, selector })
1080
+ // One treatment for every photo on the page: wrap each photo in <figure class="palate-photo"><img …></figure>.
1081
+ export function treatmentCss(name, { shadow = "#1f1a17", highlight = "#f3ece2", colour, opacity = 0.35, selector = ".palate-photo" } = {}) {
1082
+ const parts = String(name).split("+").map((s) => s.trim()).filter(Boolean);
1083
+ const css = [`/* palate treatment: ${parts.join(" + ")}. One treatment for every photo on the page: wrap each photo as <figure class="${selector.replace(/^\./, "")}"><img …></figure>. */`];
1084
+ css.push(`${selector} { position: relative; overflow: hidden; margin: 0; isolation: isolate; }`, `${selector} img { display: block; width: 100%; height: 100%; object-fit: cover; }`);
1085
+ for (const p of parts) {
1086
+ // Blend modes, not an SVG filter: Chrome drew no photo at all for a filter referenced by a data URI. The duotone:
1087
+ // a grey photo multiplied onto the highlight (its darks go black), then the shadow colour lightened over it
1088
+ // (black becomes the shadow). Its own layers, so a darker figure background set elsewhere can't turn it black.
1089
+ if (p === "duotone") {
1090
+ hex(shadow);
1091
+ hex(highlight);
1092
+ css.push(`${selector} { background: ${highlight} !important; }`, `${selector} img { filter: grayscale(1) contrast(1.08); mix-blend-mode: multiply; }`, `${selector}::before { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none; background: ${shadow}; mix-blend-mode: lighten; }`);
1093
+ } else if (p === "tone") {
1094
+ // Grey, then one colour laid over as hue only: the photo keeps its light and dark whatever is behind it.
1095
+ hex(colour ?? highlight);
1096
+ css.push(`${selector} img { filter: grayscale(1) contrast(1.06); }`, `${selector}::before { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none; background: ${colour ?? highlight}; mix-blend-mode: color; opacity: 0.6; }`);
1097
+ } else if (p === "natural") {
1098
+ // The photo's own colours, evened out: for objects whose colour is the point (a glaze, a dish, a garment).
1099
+ css.push(`${selector} img { filter: saturate(0.92) contrast(1.03); }`);
1100
+ } else if (p === "grain") {
1101
+ css.push(`${selector}::after { content: ""; position: absolute; inset: 0; pointer-events: none; background-image: ${svgUrl(texture("grain", { colour: "#000000", opacity: 1, seed: 7 }))}; background-size: 200px; opacity: ${opacity}; mix-blend-mode: overlay; }`);
1102
+ } else throw new Error(`Unknown treatment "${p}" (duotone, tone, natural, grain; combine with +, e.g. duotone+grain).`);
1103
+ }
1104
+ return css.join("\n") + "\n";
1105
+ }
1106
+
1107
+ // ---- textures drawn in code: seeded, deterministic, tileable SVG.
1108
+ function seedOf(seed) {
1109
+ if (typeof seed === "number") return seed >>> 0;
1110
+ let h = 2166136261;
1111
+ for (const ch of String(seed)) h = Math.imul(h ^ ch.charCodeAt(0), 16777619);
1112
+ return h >>> 0;
1113
+ }
1114
+ // mulberry32: a tiny seeded PRNG; the same seed always draws the same texture.
1115
+ export function prng(seed = 1) {
1116
+ let a = seedOf(seed) || 1;
1117
+ return () => {
1118
+ a = (a + 0x6d2b79f5) | 0;
1119
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
1120
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
1121
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
1122
+ };
1123
+ }
1124
+ const f2 = (n) => String(Math.round(n * 100) / 100);
1125
+
1126
+ export const TEXTURES = {
1127
+ grain: "fine film grain (feTurbulence); over photos and flat fills",
1128
+ paper: "paper fibre: soft mottling and short fibres; for warm, printed surfaces",
1129
+ speckle: "seeded dots of varied size, like a glaze speckle or terrazzo; for crafts and ceramics",
1130
+ halftone: "a halftone dot screen with a slow tonal swell; for print and editorial looks",
1131
+ linen: "woven threads crossing; for textile, bookbinding, quiet luxury",
1132
+ };
1133
+
1134
+ // texture(name, { colour, opacity, seed, size }) → SVG markup (a tile that repeats seamlessly).
1135
+ export function texture(name, { colour = "#000000", opacity = 0.12, seed = 1, size } = {}) {
1136
+ hex(colour);
1137
+ const rand = prng(`${name}:${seedOf(seed)}`);
1138
+ const S = size ?? (name === "grain" ? 200 : 240);
1139
+ const [r, g, b] = hex(colour).map(r3);
1140
+ const head = `<svg xmlns='http://www.w3.org/2000/svg' width='${S}' height='${S}' viewBox='0 0 ${S} ${S}'>`;
1141
+ const tSeed = Math.floor(rand() * 1000);
1142
+ // Copies of a shape across the tile edges, so the pattern repeats with no seam.
1143
+ const wrap = (x, y, pad, draw) => {
1144
+ let s = "";
1145
+ for (const dx of [0, -S, S]) for (const dy of [0, -S, S]) {
1146
+ const X = x + dx;
1147
+ const Y = y + dy;
1148
+ if (X > -pad && X < S + pad && Y > -pad && Y < S + pad) s += draw(X, Y);
1149
+ }
1150
+ return s;
1151
+ };
1152
+ if (name === "grain") {
1153
+ return `${head}<filter id='g' x='0' y='0' width='100%' height='100%'><feTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' seed='${tSeed}' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} 0 0 0 2.2 -0.9'/></filter><rect width='100%' height='100%' filter='url(#g)' opacity='${f2(opacity)}'/></svg>`;
1154
+ }
1155
+ if (name === "paper") {
1156
+ let fibres = "";
1157
+ for (let i = 0; i < 70; i++) {
1158
+ const x = rand() * S;
1159
+ const y = rand() * S;
1160
+ const a = rand() * Math.PI * 2;
1161
+ const len = 6 + rand() * 18;
1162
+ const bend = (rand() - 0.5) * 6;
1163
+ const w = 0.3 + rand() * 0.5;
1164
+ const o = 0.25 + rand() * 0.5;
1165
+ const dx = Math.cos(a) * len;
1166
+ const dy = Math.sin(a) * len;
1167
+ fibres += wrap(x, y, len + 6, (X, Y) => `<path d='M${f2(X)} ${f2(Y)}q${f2(dx / 2 - dy * bend * 0.05)} ${f2(dy / 2 + dx * bend * 0.05)} ${f2(dx)} ${f2(dy)}' stroke-width='${f2(w)}' stroke-opacity='${f2(o)}'/>`);
1168
+ }
1169
+ return `${head}<filter id='m' x='0' y='0' width='100%' height='100%'><feTurbulence type='fractalNoise' baseFrequency='0.035' numOctaves='4' seed='${tSeed}' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} 0 0 0 1.4 -0.55'/></filter><g opacity='${f2(opacity)}'><rect width='100%' height='100%' filter='url(#m)'/><g fill='none' stroke='${colour}' stroke-linecap='round'>${fibres}</g></g></svg>`;
1170
+ }
1171
+ if (name === "speckle") {
1172
+ let dots = "";
1173
+ for (let i = 0; i < 140; i++) {
1174
+ const x = rand() * S;
1175
+ const y = rand() * S;
1176
+ const rad = 0.35 + Math.pow(rand(), 3) * 2.6; // mostly fine, a few large flecks
1177
+ const o = 0.35 + rand() * 0.65;
1178
+ dots += wrap(x, y, rad + 1, (X, Y) => `<circle cx='${f2(X)}' cy='${f2(Y)}' r='${f2(rad)}' fill-opacity='${f2(o)}'/>`);
1179
+ }
1180
+ return `${head}<g fill='${colour}' opacity='${f2(opacity)}'>${dots}</g></svg>`;
1181
+ }
1182
+ if (name === "halftone") {
1183
+ const step = S / 24;
1184
+ const p1 = rand() * Math.PI * 2;
1185
+ const p2 = rand() * Math.PI * 2;
1186
+ let dots = "";
1187
+ for (let j = 0; j < 24; j++) {
1188
+ for (let i = 0; i < 24; i++) {
1189
+ const x = (i + 0.5 + (j % 2) * 0.5) * step;
1190
+ const y = (j + 0.5) * step;
1191
+ // A periodic tone field (whole sine periods across the tile) so the swell tiles too.
1192
+ const tone = 0.5 + 0.25 * Math.sin((2 * Math.PI * x) / S + p1) + 0.25 * Math.sin((4 * Math.PI * y) / S + p2);
1193
+ const rad = step * 0.5 * Math.sqrt(Math.max(0.04, tone));
1194
+ dots += wrap(x, y, rad + 1, (X, Y) => `<circle cx='${f2(X)}' cy='${f2(Y)}' r='${f2(rad)}'/>`);
1195
+ }
1196
+ }
1197
+ return `${head}<g fill='${colour}' opacity='${f2(opacity)}'>${dots}</g></svg>`;
1198
+ }
1199
+ if (name === "linen") {
1200
+ const n = 60;
1201
+ const step = S / n;
1202
+ let lines = "";
1203
+ for (let i = 0; i < n; i++) {
1204
+ const p = (i + 0.5) * step;
1205
+ const wv = f2(step * (0.35 + rand() * 0.45));
1206
+ const wh = f2(step * (0.35 + rand() * 0.45));
1207
+ lines += `<line x1='${f2(p)}' y1='0' x2='${f2(p)}' y2='${S}' stroke-width='${wv}' stroke-opacity='${f2(0.3 + rand() * 0.6)}'/>`;
1208
+ lines += `<line x1='0' y1='${f2(p)}' x2='${S}' y2='${f2(p)}' stroke-width='${wh}' stroke-opacity='${f2(0.3 + rand() * 0.6)}'/>`;
1209
+ }
1210
+ // Slubs: a few thicker runs of thread, as in real linen.
1211
+ for (let i = 0; i < 18; i++) {
1212
+ const horiz = rand() < 0.5;
1213
+ const at = (Math.floor(rand() * n) + 0.5) * step;
1214
+ const from = rand() * S;
1215
+ const len = 8 + rand() * 30;
1216
+ const w = f2(step * 0.9);
1217
+ lines += wrap(from, at, len + 2, (X, Y) => (horiz ? `<line x1='${f2(X)}' y1='${f2(Y)}' x2='${f2(X + len)}' y2='${f2(Y)}' stroke-width='${w}' stroke-opacity='0.45'/>` : `<line x1='${f2(Y)}' y1='${f2(X)}' x2='${f2(Y)}' y2='${f2(X + len)}' stroke-width='${w}' stroke-opacity='0.45'/>`));
1218
+ }
1219
+ return `${head}<g stroke='${colour}' opacity='${f2(opacity)}'>${lines}</g></svg>`;
1220
+ }
1221
+ throw new Error(`Unknown texture "${name}" (${Object.keys(TEXTURES).join(", ")}).`);
1222
+ }
1223
+
1224
+ // Writes <public>/textures/<file>.svg and returns the CSS (as a background, or as an overlay on anything).
1225
+ export function writeTexture(dir, name, opts = {}) {
1226
+ const svg = texture(name, opts);
1227
+ const pub = publicDir(dir);
1228
+ const file = opts.file ?? name;
1229
+ const abs = path.join(dir, pub, "textures", `${file}.svg`);
1230
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
1231
+ fs.writeFileSync(abs, svg + "\n");
1232
+ const url = `/textures/${file}.svg`;
1233
+ const tile = (svg.match(/width='(\d+)'/) ?? [])[1];
1234
+ return [
1235
+ `Wrote ${pub}/textures/${file}.svg (${TEXTURES[name]}; seed ${opts.seed ?? 1}, drawn by palate, no licence needed).`,
1236
+ "",
1237
+ `/* as a surface */`,
1238
+ `.texture-${file} { background-image: url("${url}"); background-size: ${tile}px; background-repeat: repeat; }`,
1239
+ `/* as an overlay on a coloured section or a photo */`,
1240
+ `.texture-${file}-over { position: relative; isolation: isolate; }`,
1241
+ `.texture-${file}-over::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: url("${url}") repeat; background-size: ${tile}px; mix-blend-mode: multiply; }`,
1242
+ ].join("\n");
1243
+ }
1244
+
1245
+ // ---- the build plan's imagery slots → the exact palate command for each. Pure.
1246
+ // plan: [{ section, imagery: "photo:met ceramics bowl" | "texture:speckle" | "product-ui" | "illustration:<what>" | "type" }]
1247
+ export function imageryPlanLines(plan = []) {
1248
+ const lines = [];
1249
+ let photos = 0;
1250
+ for (const { section, imagery: spec } of plan) {
1251
+ const s = clean(spec);
1252
+ const [kind, ...restParts] = s.split(":");
1253
+ const rest = clean(restParts.join(":"));
1254
+ const k = kind.toLowerCase();
1255
+ let line;
1256
+ if (k === "photo") {
1257
+ photos++;
1258
+ const words = rest.split(" ");
1259
+ const src = ALIASES[words[0]?.toLowerCase()];
1260
+ const query = src ? words.slice(1).join(" ") : rest;
1261
+ line = query
1262
+ ? `photo — ${CMD.search} "${query}"${src ? ` --sources ${src}` : ""} (--type Photograph for real photographs, or ${TYPES.filter((t) => t !== "Photograph").join("/")}; --artist "<name>" for one maker), open the contact sheet, then ${CMD.pick} <id>; wrap it in .palate-photo; write the page's one treatment once with palate imagery treatment duotone|tone|natural --shadow <ink> --highlight <paper> (natural when the object's own colour matters)`
1263
+ : `photo — needs a subject: ${CMD.search} "<object or scene>" [--type Photograph] [--artist "<name>"]`;
1264
+ } else if (k === "texture") {
1265
+ const name = rest.split(" ")[0]?.toLowerCase();
1266
+ line = TEXTURES[name] ? `texture — ${CMD.texture} ${name} --colour <the section's ink> (writes public/textures/${name}.svg; use the CSS it prints)` : `texture — unknown "${rest}"; one of ${Object.keys(TEXTURES).join(", ")}`;
1267
+ } else if (k === "product-ui") {
1268
+ line = "product UI — build the real interface in code (a live component, or a framed screenshot of the running app); no stock photo";
1269
+ } else if (k === "illustration") {
1270
+ line = `illustration — draw ${rest || "it"} yourself as inline SVG in the page's palette; never copy an existing illustration`;
1271
+ } else if (k === "type" || k === "none") {
1272
+ line = "type only — no image: scale, weight and measure carry this section";
1273
+ } else {
1274
+ line = `unknown imagery "${s}" — use photo:<query>, texture:<${Object.keys(TEXTURES).join("|")}>, product-ui, illustration:<what> or type`;
1275
+ }
1276
+ lines.push(`- ${section}: ${line}`);
1277
+ }
1278
+ if (photos) lines.push(`- every photo: one treatment for the whole page (treatmentCss duotone | tone | natural | grain, from the page's ink and paper colours; natural keeps an object's own colour), real alt text, credits from credits.json in the footer`);
1279
+ return lines;
1280
+ }