pandorga 1.2.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 (376) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +77 -0
  3. data/DESIGN.md +1434 -0
  4. data/LICENSE +21 -0
  5. data/README.md +81 -0
  6. data/_data/themes/dark.yml +97 -0
  7. data/_data/themes/light.yml +120 -0
  8. data/_data/themes/shared.yml +37 -0
  9. data/_data/translations.yml +85 -0
  10. data/_includes/chord-tab.html +38 -0
  11. data/_includes/content-runtime/00-config.html +22 -0
  12. data/_includes/content-runtime/10-utils.html +176 -0
  13. data/_includes/content-runtime/20-markdown.html +264 -0
  14. data/_includes/content-runtime/25-code-blocks.html +315 -0
  15. data/_includes/content-runtime/30-includes-bib.html +190 -0
  16. data/_includes/content-runtime/30-includes-xcite.html +122 -0
  17. data/_includes/content-runtime/30-includes.html +365 -0
  18. data/_includes/content-runtime/40-listing.html +225 -0
  19. data/_includes/content-runtime/41-listing-helpers.html +71 -0
  20. data/_includes/content-runtime/42-listing-filter.html +208 -0
  21. data/_includes/content-runtime/43-blocks.html +379 -0
  22. data/_includes/content-runtime/50-detail.html +861 -0
  23. data/_includes/content-runtime/55-toc.html +176 -0
  24. data/_includes/content-runtime/60-fragments.html +567 -0
  25. data/_includes/content-runtime/90-bootstrap.html +76 -0
  26. data/_includes/cut-in.html +10 -0
  27. data/_includes/cv/cv-box.html +18 -0
  28. data/_includes/dict-card.html +24 -0
  29. data/_includes/figure.html +36 -0
  30. data/_includes/home/band-index.html +43 -0
  31. data/_includes/home/band-listing.html +49 -0
  32. data/_includes/mathjax.html +10 -0
  33. data/_includes/ongoing-product.html +1 -0
  34. data/_includes/page/content-runtime.html +20 -0
  35. data/_includes/page/date_evolution.html +19 -0
  36. data/_includes/page/figure-lightbox.html +102 -0
  37. data/_includes/page/formatted_date.html +15 -0
  38. data/_includes/page/hero-default.html +18 -0
  39. data/_includes/page/listing-runtime-attrs.html +8 -0
  40. data/_includes/page/mermaid.html +101 -0
  41. data/_includes/page/network-graph.html +1253 -0
  42. data/_includes/page/page-header.html +41 -0
  43. data/_includes/page/professional-connections.html +2 -0
  44. data/_includes/page/site-footer.html +101 -0
  45. data/_includes/page/timeline-section.html +868 -0
  46. data/_includes/plotly.html +22 -0
  47. data/_includes/theme/dev-reexport.html +100 -0
  48. data/_includes/theme/favicons.html +4 -0
  49. data/_includes/theme/font-loader.html +55 -0
  50. data/_includes/theme/shell-theme-boot.html +115 -0
  51. data/_includes/theme/site-logo.html +6 -0
  52. data/_includes/theme/tailwind-theme-colors.json.liquid +65 -0
  53. data/_includes/theme/tailwind-theme-fonts.json.liquid +6 -0
  54. data/_includes/theme/theme-context.liquid +20 -0
  55. data/_includes/theme/theme-toggle.html +94 -0
  56. data/_includes/theme/theme-vars-block.liquid +161 -0
  57. data/_includes/theme/theme-vars.html +16 -0
  58. data/_includes/transcription.html +80 -0
  59. data/_includes/tweet.html +22 -0
  60. data/_includes/video.html +11 -0
  61. data/_includes/xcite.html +3 -0
  62. data/_includes/youtube-short.html +26 -0
  63. data/_includes/youtube.html +12 -0
  64. data/_layouts/article.html +27 -0
  65. data/_layouts/articles.html +534 -0
  66. data/_layouts/bibliography.html +417 -0
  67. data/_layouts/blog.html +544 -0
  68. data/_layouts/cv.html +393 -0
  69. data/_layouts/home.html +319 -0
  70. data/_layouts/network.html +9 -0
  71. data/_layouts/pandorga/articles.html +4 -0
  72. data/_layouts/pandorga/bibliography.html +4 -0
  73. data/_layouts/pandorga/blog.html +4 -0
  74. data/_layouts/pandorga/cv.html +4 -0
  75. data/_layouts/pandorga/media.html +4 -0
  76. data/_layouts/pandorga/network.html +4 -0
  77. data/_layouts/pandorga/portfolio.html +4 -0
  78. data/_layouts/post.html +7 -0
  79. data/_layouts/product.html +5 -0
  80. data/_layouts/project.html +27 -0
  81. data/_layouts/projects.html +534 -0
  82. data/_layouts/resources.html +387 -0
  83. data/_layouts/shell.html +203 -0
  84. data/_layouts/text.html +246 -0
  85. data/_plugins/bibliography_artifact.rb +57 -0
  86. data/_plugins/content_backend.rb +37 -0
  87. data/_plugins/content_collections.rb +51 -0
  88. data/_plugins/content_detail_paths.rb +58 -0
  89. data/_plugins/content_headers.rb +73 -0
  90. data/_plugins/content_keys.rb +310 -0
  91. data/_plugins/content_local_serve.rb +168 -0
  92. data/_plugins/content_site_adapter.rb +159 -0
  93. data/_plugins/content_validators_hook.rb +20 -0
  94. data/_plugins/content_writing_index.rb +52 -0
  95. data/_plugins/content_writing_paths.rb +55 -0
  96. data/_plugins/force_utf8_encoding.rb +13 -0
  97. data/_plugins/git_chronology.rb +211 -0
  98. data/_plugins/git_metadata.rb +43 -0
  99. data/_plugins/job_sections.rb +25 -0
  100. data/_plugins/lib/content_validators.rb +27 -0
  101. data/_plugins/lib/resource_validator.rb +56 -0
  102. data/_plugins/lib/tag_validator.rb +153 -0
  103. data/_plugins/lib/text_excerpt.rb +56 -0
  104. data/_plugins/lib/writing_entry.rb +107 -0
  105. data/_plugins/local_detail_rewrites.rb +194 -0
  106. data/_plugins/timeline.rb +507 -0
  107. data/_plugins/writing_slug_limits.rb +70 -0
  108. data/assets/css/article.css +1330 -0
  109. data/assets/css/base.css +1432 -0
  110. data/assets/css/cv.css +13 -0
  111. data/assets/css/global-link.css +94 -0
  112. data/assets/css/ledger/bibliography.css +307 -0
  113. data/assets/css/ledger/chrome.css +118 -0
  114. data/assets/css/ledger/cv.css +836 -0
  115. data/assets/css/ledger/detail.css +1270 -0
  116. data/assets/css/ledger/footer.css +298 -0
  117. data/assets/css/ledger/home.css +1490 -0
  118. data/assets/css/ledger/network.css +106 -0
  119. data/assets/css/ledger/projects.css +716 -0
  120. data/assets/css/ledger/rascunhos.css +379 -0
  121. data/assets/css/ledger/sources.css +327 -0
  122. data/assets/css/ledger/writing.css +754 -0
  123. data/assets/tailwind-play.js +15 -0
  124. data/examples/full/Gemfile +7 -0
  125. data/examples/full/_config.yml +92 -0
  126. data/examples/full/_redirects +3 -0
  127. data/examples/full/content/collections/articles/2026-01-01-welcome.md +9 -0
  128. data/examples/full/content/collections/data/contacts.yml +3 -0
  129. data/examples/full/content/collections/data/education.yml +4 -0
  130. data/examples/full/content/collections/jobs/research-fellow.md +12 -0
  131. data/examples/full/content/collections/posts/2026-01-02-note.md +9 -0
  132. data/examples/full/content/collections/products/ledger-tool.md +6 -0
  133. data/examples/full/content/collections/projects/demo-kit.md +10 -0
  134. data/examples/full/content/collections/resources/style-guide.md +7 -0
  135. data/examples/full/content/pages/cv/summary.md +1 -0
  136. data/examples/full/content/pages/headers.yml +20 -0
  137. data/examples/full/index.html +4 -0
  138. data/examples/minimal/Gemfile +7 -0
  139. data/examples/minimal/_config.yml +47 -0
  140. data/examples/minimal/_redirects +3 -0
  141. data/examples/minimal/content/collections/articles/2026-01-01-hello.md +10 -0
  142. data/examples/minimal/content/collections/articles/2026-01-02-second.md +9 -0
  143. data/examples/minimal/content/pages/headers.yml +6 -0
  144. data/examples/minimal/index.html +4 -0
  145. data/exe/pandorga +7 -0
  146. data/functions/api/studio/[[path]].js +562 -0
  147. data/functions/api/studio/_lib/auth.js +131 -0
  148. data/functions/api/studio/_lib/blocks.js +156 -0
  149. data/functions/api/studio/_lib/gemini.js +62 -0
  150. data/functions/api/studio/_lib/github.js +278 -0
  151. data/functions/api/studio/_lib/guidelines.js +143 -0
  152. data/functions/api/studio/_lib/paths.js +76 -0
  153. data/functions/api/studio/_lib/prompts.js +165 -0
  154. data/functions/api/studio/_lib/rate-limit.js +40 -0
  155. data/functions/api/studio/_lib/rewrite.js +136 -0
  156. data/functions/api/studio/_lib/sanitize.js +143 -0
  157. data/lib/pandorga/cli.rb +66 -0
  158. data/lib/pandorga/commands/doctor.rb +66 -0
  159. data/lib/pandorga/commands/export.rb +30 -0
  160. data/lib/pandorga/commands/install_functions.rb +41 -0
  161. data/lib/pandorga/commands/new.rb +113 -0
  162. data/lib/pandorga/commands/publish.rb +53 -0
  163. data/lib/pandorga/commands/serve.rb +34 -0
  164. data/lib/pandorga/commands/studio_schema.rb +309 -0
  165. data/lib/pandorga/jekyll/generator.rb +74 -0
  166. data/lib/pandorga/jekyll/plugins/bibliography_artifact.rb +57 -0
  167. data/lib/pandorga/jekyll/plugins/content_backend.rb +37 -0
  168. data/lib/pandorga/jekyll/plugins/content_collections.rb +51 -0
  169. data/lib/pandorga/jekyll/plugins/content_detail_paths.rb +58 -0
  170. data/lib/pandorga/jekyll/plugins/content_headers.rb +73 -0
  171. data/lib/pandorga/jekyll/plugins/content_keys.rb +310 -0
  172. data/lib/pandorga/jekyll/plugins/content_local_serve.rb +168 -0
  173. data/lib/pandorga/jekyll/plugins/content_site_adapter.rb +159 -0
  174. data/lib/pandorga/jekyll/plugins/content_validators_hook.rb +20 -0
  175. data/lib/pandorga/jekyll/plugins/content_writing_index.rb +52 -0
  176. data/lib/pandorga/jekyll/plugins/content_writing_paths.rb +55 -0
  177. data/lib/pandorga/jekyll/plugins/force_utf8_encoding.rb +13 -0
  178. data/lib/pandorga/jekyll/plugins/git_chronology.rb +211 -0
  179. data/lib/pandorga/jekyll/plugins/git_metadata.rb +43 -0
  180. data/lib/pandorga/jekyll/plugins/job_sections.rb +25 -0
  181. data/lib/pandorga/jekyll/plugins/lib/content_validators.rb +27 -0
  182. data/lib/pandorga/jekyll/plugins/lib/resource_validator.rb +56 -0
  183. data/lib/pandorga/jekyll/plugins/lib/tag_validator.rb +153 -0
  184. data/lib/pandorga/jekyll/plugins/lib/text_excerpt.rb +56 -0
  185. data/lib/pandorga/jekyll/plugins/lib/writing_entry.rb +107 -0
  186. data/lib/pandorga/jekyll/plugins/local_detail_rewrites.rb +194 -0
  187. data/lib/pandorga/jekyll/plugins/timeline.rb +507 -0
  188. data/lib/pandorga/jekyll/plugins/writing_slug_limits.rb +70 -0
  189. data/lib/pandorga/jekyll.rb +15 -0
  190. data/lib/pandorga/object_store_env.rb +51 -0
  191. data/lib/pandorga/registry/schema.yml +115 -0
  192. data/lib/pandorga/registry.rb +279 -0
  193. data/lib/pandorga/version.rb +5 -0
  194. data/lib/pandorga.rb +19 -0
  195. data/scripts/cdn-allowlist.json +19 -0
  196. data/scripts/content/backfill-content-keys.rb +99 -0
  197. data/scripts/content/band_art.rb +362 -0
  198. data/scripts/content/export-content-json.rb +741 -0
  199. data/scripts/content/flatten-ansi-capture.mjs +153 -0
  200. data/scripts/content/hero_portraits.rb +28 -0
  201. data/scripts/content/publish-content-to-object-store.sh +78 -0
  202. data/scripts/content/publish-content-to-r2.sh +5 -0
  203. data/scripts/content/sync-content-json-to-r2.rb +167 -0
  204. data/scripts/content/sync-content-redirects.rb +12 -0
  205. data/scripts/generate-configuration-doc.rb +127 -0
  206. data/scripts/lib/content_license.rb +50 -0
  207. data/scripts/lib/content_storage_config.rb +70 -0
  208. data/scripts/lib/normalize-display-math.mjs +52 -0
  209. data/scripts/lib/r2_json_sync.rb +76 -0
  210. data/scripts/lib/withdrawn_public_objects.rb +23 -0
  211. data/scripts/lib/writing_index_contract.rb +61 -0
  212. data/scripts/pdf/md-to-pdf.mjs +1400 -0
  213. data/scripts/pdf/md-to-pdf.sh +27 -0
  214. data/scripts/test/fixtures/dual-template/_config.yml +32 -0
  215. data/scripts/test/fixtures/dual-template/index.html +4 -0
  216. data/scripts/test/render-parity/article-note.json +5 -0
  217. data/scripts/test/render-parity/blockquote.json +5 -0
  218. data/scripts/test/render-parity/blocks/ansi-basic.json +7 -0
  219. data/scripts/test/render-parity/blocks/ansi-bold-is-bright.json +7 -0
  220. data/scripts/test/render-parity/blocks/ansi-drops-cursor-moves.json +7 -0
  221. data/scripts/test/render-parity/blocks/ansi-escapes-markup.json +7 -0
  222. data/scripts/test/render-parity/blocks/ansi-extended-colour.json +7 -0
  223. data/scripts/test/render-parity/blocks/ansi-real-control-byte.json +7 -0
  224. data/scripts/test/render-parity/blocks/archive-card-date-once.json +19 -0
  225. data/scripts/test/render-parity/blocks/archive-card-empty-foot.json +7 -0
  226. data/scripts/test/render-parity/blocks/archive-card-footer-dates.json +22 -0
  227. data/scripts/test/render-parity/blocks/archive-card-meta-parts.json +26 -0
  228. data/scripts/test/render-parity/blocks/archive-card.json +32 -0
  229. data/scripts/test/render-parity/blocks/cta-link.json +13 -0
  230. data/scripts/test/render-parity/blocks/lang-pill-pt.json +8 -0
  231. data/scripts/test/render-parity/blocks/lang-pill.json +8 -0
  232. data/scripts/test/render-parity/blocks/ledger-row-meta-parts.json +29 -0
  233. data/scripts/test/render-parity/blocks/ledger-row.json +35 -0
  234. data/scripts/test/render-parity/blocks/meta-strip.json +27 -0
  235. data/scripts/test/render-parity/blocks/poster-media-image.json +8 -0
  236. data/scripts/test/render-parity/blocks/poster-media-video.json +9 -0
  237. data/scripts/test/render-parity/blocks/section-banner.json +13 -0
  238. data/scripts/test/render-parity/blocks/system-card-custom-meta.json +22 -0
  239. data/scripts/test/render-parity/blocks/system-card.json +48 -0
  240. data/scripts/test/render-parity/blocks/tag-chips-links.json +12 -0
  241. data/scripts/test/render-parity/blocks/tag-chips.json +9 -0
  242. data/scripts/test/render-parity/cut-in-blank-line.json +9 -0
  243. data/scripts/test/render-parity/cut-in-right.json +9 -0
  244. data/scripts/test/render-parity/cut-in.json +8 -0
  245. data/scripts/test/render-parity/figure.json +5 -0
  246. data/scripts/test/render-parity/load-runtime.mjs +337 -0
  247. data/scripts/test/render-parity/ongoing-product.json +5 -0
  248. data/scripts/test/render-parity/plotly.json +5 -0
  249. data/scripts/test/render-parity/poem.json +5 -0
  250. data/scripts/test/render-parity/run-absent-field-checks.mjs +232 -0
  251. data/scripts/test/render-parity/run-block-fixture.mjs +86 -0
  252. data/scripts/test/render-parity/run-detail-parity-checks.mjs +123 -0
  253. data/scripts/test/render-parity/run-field-flow-checks.mjs +227 -0
  254. data/scripts/test/render-parity/run-fixture.mjs +159 -0
  255. data/scripts/test/render-parity/selectors.mjs +43 -0
  256. data/scripts/test/render-parity/video.json +5 -0
  257. data/scripts/test/render-parity/youtube-short.json +10 -0
  258. data/scripts/test/render-parity/youtube.json +8 -0
  259. data/scripts/test/test-absent-field-absent-element.rb +43 -0
  260. data/scripts/test/test-cdn-pins.rb +44 -0
  261. data/scripts/test/test-cloudflare-watch-paths-doc.rb +31 -0
  262. data/scripts/test/test-configuration-doc.rb +36 -0
  263. data/scripts/test/test-content-detail-urls.rb +346 -0
  264. data/scripts/test/test-content-keys.rb +110 -0
  265. data/scripts/test/test-content-license.rb +45 -0
  266. data/scripts/test/test-content-render-parity.rb +38 -0
  267. data/scripts/test/test-content-validators.rb +12 -0
  268. data/scripts/test/test-css-structure.rb +72 -0
  269. data/scripts/test/test-cv-export-contract.rb +158 -0
  270. data/scripts/test/test-detail-render-parity.rb +40 -0
  271. data/scripts/test/test-dev-reexport.rb +122 -0
  272. data/scripts/test/test-doctor.rb +39 -0
  273. data/scripts/test/test-dual-template-instances.rb +87 -0
  274. data/scripts/test/test-dual-template-runtime.rb +167 -0
  275. data/scripts/test/test-examples-build.rb +60 -0
  276. data/scripts/test/test-export-content-json-incremental.rb +138 -0
  277. data/scripts/test/test-export.rb +53 -0
  278. data/scripts/test/test-exported-fields-reach-the-view.rb +60 -0
  279. data/scripts/test/test-form-control-theming.rb +143 -0
  280. data/scripts/test/test-git-chronology.rb +414 -0
  281. data/scripts/test/test-hero-portraits.mjs +97 -0
  282. data/scripts/test/test-hero-portraits.rb +162 -0
  283. data/scripts/test/test-home-band-art.rb +135 -0
  284. data/scripts/test/test-install-functions.rb +39 -0
  285. data/scripts/test/test-language-export.rb +79 -0
  286. data/scripts/test/test-listing-filter-contract.rb +119 -0
  287. data/scripts/test/test-math-row-breaks.mjs +79 -0
  288. data/scripts/test/test-math-row-breaks.rb +18 -0
  289. data/scripts/test/test-new.rb +113 -0
  290. data/scripts/test/test-no-personal-data.rb +85 -0
  291. data/scripts/test/test-public-cv-export.rb +164 -0
  292. data/scripts/test/test-r2-config-consistency.rb +57 -0
  293. data/scripts/test/test-r2-json-sync.rb +193 -0
  294. data/scripts/test/test-registry-home.rb +34 -0
  295. data/scripts/test/test-runtime-js-syntax.rb +64 -0
  296. data/scripts/test/test-slim-writing-index.rb +126 -0
  297. data/scripts/test/test-studio-api-contract.rb +99 -0
  298. data/scripts/test/test-studio-content-files.rb +79 -0
  299. data/scripts/test/test-studio-liquid-roundtrip.mjs +179 -0
  300. data/scripts/test/test-studio-rate-limit.mjs +70 -0
  301. data/scripts/test/test-studio-rewrite-api-contract.rb +59 -0
  302. data/scripts/test/test-studio-rewrite-sanitize.mjs +210 -0
  303. data/scripts/test/test-studio-schema-composition.rb +126 -0
  304. data/scripts/test/test-studio-slug-config.rb +167 -0
  305. data/scripts/test/test-template-packages.rb +51 -0
  306. data/scripts/test/test-text-excerpt.rb +51 -0
  307. data/scripts/test/test-unwrap-media-src.mjs +39 -0
  308. data/scripts/test/test-unwrap-media-src.rb +19 -0
  309. data/scripts/test/test-withdrawn-content-export.rb +69 -0
  310. data/scripts/test/test-writing-index-export-slugs.rb +84 -0
  311. data/scripts/test/test-writing-slug-limits.rb +61 -0
  312. data/scripts/test/test-xcite-chronology.rb +106 -0
  313. data/scripts/validate.sh +74 -0
  314. data/scripts/visual/build-cv-pages-json.js +33 -0
  315. data/scripts/visual/build-site-pages-json.js +94 -0
  316. data/scripts/visual/publish-visual-inspection-to-r2.sh +42 -0
  317. data/scripts/visual/take-screenshots.js +88 -0
  318. data/scripts/visual/visual-inspection-pages.json +113 -0
  319. data/studio/assets/index-B_j0pMD5.css +1 -0
  320. data/studio/assets/index-Ch4YEv9g.js +285 -0
  321. data/studio/index.html +27 -0
  322. data/studio/schema.yml +869 -0
  323. data/studio-app/README.md +27 -0
  324. data/studio-app/index.html +26 -0
  325. data/studio-app/package-lock.json +2499 -0
  326. data/studio-app/package.json +26 -0
  327. data/studio-app/skills-lock.json +47 -0
  328. data/studio-app/src/api/client.js +71 -0
  329. data/studio-app/src/editor/body-editor.js +70 -0
  330. data/studio-app/src/editor/liquid-core.js +382 -0
  331. data/studio-app/src/editor/liquid.js +77 -0
  332. data/studio-app/src/lib/document.js +207 -0
  333. data/studio-app/src/lib/list-controls.js +317 -0
  334. data/studio-app/src/lib/schema.js +66 -0
  335. data/studio-app/src/lib/yaml-doc.js +100 -0
  336. data/studio-app/src/main.js +3693 -0
  337. data/studio-app/src/media/browser.js +578 -0
  338. data/studio-app/src/preview/render.js +430 -0
  339. data/studio-app/src/styles/studio.css +2826 -0
  340. data/studio-app/vite.config.js +25 -0
  341. data/templates/articles/README.md +40 -0
  342. data/templates/articles/fixtures/article.md +9 -0
  343. data/templates/articles/screenshots/README.md +6 -0
  344. data/templates/articles/studio.yml +73 -0
  345. data/templates/articles/template.yml +21 -0
  346. data/templates/bibliography/README.md +41 -0
  347. data/templates/bibliography/fixtures/references.json +9 -0
  348. data/templates/bibliography/screenshots/README.md +6 -0
  349. data/templates/bibliography/studio.yml +66 -0
  350. data/templates/bibliography/template.yml +18 -0
  351. data/templates/blog/README.md +38 -0
  352. data/templates/blog/fixtures/post.md +9 -0
  353. data/templates/blog/screenshots/README.md +6 -0
  354. data/templates/blog/studio.yml +69 -0
  355. data/templates/blog/template.yml +20 -0
  356. data/templates/cv/README.md +34 -0
  357. data/templates/cv/fixtures/job.md +12 -0
  358. data/templates/cv/screenshots/README.md +6 -0
  359. data/templates/cv/studio.yml +200 -0
  360. data/templates/cv/template.yml +25 -0
  361. data/templates/media/README.md +38 -0
  362. data/templates/media/fixtures/resource.md +7 -0
  363. data/templates/media/screenshots/README.md +6 -0
  364. data/templates/media/studio.yml +58 -0
  365. data/templates/media/template.yml +17 -0
  366. data/templates/network/README.md +34 -0
  367. data/templates/network/fixtures/sources.json +3 -0
  368. data/templates/network/screenshots/README.md +6 -0
  369. data/templates/network/studio.yml +28 -0
  370. data/templates/network/template.yml +10 -0
  371. data/templates/portfolio/README.md +39 -0
  372. data/templates/portfolio/fixtures/project.md +10 -0
  373. data/templates/portfolio/screenshots/README.md +6 -0
  374. data/templates/portfolio/studio.yml +89 -0
  375. data/templates/portfolio/template.yml +18 -0
  376. metadata +505 -0
@@ -0,0 +1,716 @@
1
+ /* Ledger page styles: projects. Owned by the projects page; shared component
2
+ rules belong in assets/css/base.css under "Architectural Ledger blocks".
3
+
4
+ Scope note. Every ledger stylesheet is loaded on every page, so each rule is
5
+ scoped under `.projects-page` — the class on the <main> of _layouts/projects.html,
6
+ which wraps both the index and the client-rendered detail view.
7
+
8
+ Specificity note. The Tailwind Play CDN injects its utilities *after* this
9
+ file, so a one-class rule loses to any colliding utility already on the
10
+ element. The `.projects-page` prefix is what keeps these rules winning.
11
+
12
+ Colour note. No literal colour appears below. A hex would be right in one
13
+ palette and wrong in the other; every value comes from a theme custom
14
+ property, and both themes were rendered to confirm it.
15
+
16
+ This page is allowed to be denser and less generic than the rest of the
17
+ site: seven projects that change rarely, each with its own figures, videos,
18
+ plots, diagrams and tables. Depth beats uniformity here. What it is *not*
19
+ allowed to do is draw something no field backs — the mockup's metrics
20
+ strip, benchmark ledger and proof transcript are absent on purpose. */
21
+
22
+ .projects-page .projects-masthead {
23
+ padding-bottom: var(--space-lg);
24
+ margin-bottom: var(--space-2xl);
25
+ border-bottom: 1px solid var(--border-gentle-divider);
26
+ box-shadow: 0 3px 0 -2px var(--border-gentle-faint);
27
+ position: relative;
28
+ }
29
+
30
+ .projects-page .projects-masthead::after {
31
+ content: "";
32
+ position: absolute;
33
+ left: 0;
34
+ bottom: -1.5px;
35
+ width: 3rem;
36
+ height: 2px;
37
+ background: var(--color-primary);
38
+ }
39
+
40
+ /* A project's own illustration is the loudest thing on the card, so the text
41
+ block below it gets a little more room to breathe.
42
+
43
+ Note the selector carries no page scope, and every ledger sheet is linked on
44
+ every page: this also lands on the home page's engineering band, which draws
45
+ the same system card. That is the right result in both places, but it means a
46
+ page sheet is styling another page's cards — the rule belongs in base.css
47
+ next to `.ledger-poster`. Left here, and recorded, rather than scoped to
48
+ `#projects-grid`, which would silently take the spacing off the home band. */
49
+ .ledger-poster+.ledger-meta {
50
+ margin-top: var(--space-md);
51
+ }
52
+
53
+ .projects-page .projects-masthead-tags {
54
+ margin-top: var(--space-sm);
55
+ /* The page accent, not a fixed triad role. This line read green on a blue
56
+ page because it named --color-secondary directly; --color-primary is what
57
+ `.page-accent--*` rebinds, so the keywords follow whichever hue the page
58
+ is set in. */
59
+ color: var(--color-primary);
60
+ letter-spacing: 0.06em;
61
+ }
62
+
63
+ .projects-page .projects-state {
64
+ margin: var(--space-2xl) 0;
65
+ padding: var(--space-xl);
66
+ background: var(--color-surface-card);
67
+ border: 1.5px solid var(--border-ink);
68
+ border-radius: var(--radius-card);
69
+ }
70
+
71
+ /* ══ Media plates ══
72
+ One frame for every picture on the page. A project with a `thumbnail`
73
+ shows it; one without shows its own `icon` glyph on the panel step. The
74
+ glyph variant is not a stand-in for a missing image — `icon` is set on all
75
+ seven projects and is already how the site marks a project — so a project
76
+ that publishes no imagery still reads as finished rather than as broken. */
77
+ .projects-page .projects-plate {
78
+ position: relative;
79
+ overflow: hidden;
80
+ }
81
+
82
+ /* The shared poster removes itself when a hotlinked thumbnail 404s. Without
83
+ this the wrapper would survive as an empty band. */
84
+ .projects-page .projects-plate:empty {
85
+ display: none;
86
+ }
87
+
88
+ .projects-page .projects-plate--mark {
89
+ aspect-ratio: 16 / 9;
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ background: var(--color-panel-contrast);
94
+ }
95
+
96
+ .projects-page .projects-plate--mark::after {
97
+ content: "";
98
+ position: absolute;
99
+ right: -2.5rem;
100
+ bottom: -2.5rem;
101
+ width: 9rem;
102
+ height: 9rem;
103
+ clip-path: var(--shard-beta);
104
+ background: var(--color-tertiary);
105
+ opacity: var(--cubist-shard-opacity);
106
+ pointer-events: none;
107
+ }
108
+
109
+ .projects-page .material-symbols-outlined.projects-plate-glyph {
110
+ position: relative;
111
+ z-index: 1;
112
+ font-size: var(--type-display);
113
+ line-height: 1;
114
+ color: var(--color-primary);
115
+ }
116
+
117
+ .projects-page .projects-plate--lead .material-symbols-outlined.projects-plate-glyph,
118
+ .projects-page .projects-plate--hero .material-symbols-outlined.projects-plate-glyph {
119
+ font-size: var(--type-display-lg);
120
+ }
121
+
122
+ /* The detail masthead is itself on the panel step, so a glyph plate there
123
+ needs the next surface up or it dissolves into its own background. */
124
+ .projects-page .projects-hero-media .projects-plate--mark {
125
+ background: var(--color-surface-container);
126
+ }
127
+
128
+ /* ══ Filter note ══
129
+ The tag chips on every card link to `?tag=`, so the page answers that
130
+ query instead of leaving the affordance dead. Counts come from the index,
131
+ never from an authored number. */
132
+ .projects-page .projects-filter-note {
133
+ display: flex;
134
+ flex-wrap: wrap;
135
+ align-items: center;
136
+ gap: var(--space-sm);
137
+ margin-bottom: var(--space-xl);
138
+ padding: var(--space-sm) var(--space-md);
139
+ background: var(--color-panel-contrast);
140
+ color: var(--color-on-surface-variant);
141
+ }
142
+
143
+ .projects-page .projects-filter-note-label {
144
+ font-family: var(--font-label);
145
+ letter-spacing: 0.12em;
146
+ font-size: var(--type-label);
147
+ }
148
+
149
+ .projects-page .projects-filter-note-count {
150
+ margin-left: auto;
151
+ font-size: var(--type-label);
152
+ }
153
+
154
+ /* ══ Lead plate ══
155
+ The first entry by the author's own `sort_order` opens the page at full
156
+ width. A portfolio leads with one piece of work; seven equal thumbnails
157
+ state that nothing here matters more than anything else. */
158
+ .projects-page .projects-lead {
159
+ position: relative;
160
+ overflow: hidden;
161
+ display: grid;
162
+ background: var(--color-surface-card);
163
+ border: 1px solid var(--border-gentle-subtle);
164
+ border-left: 3px solid var(--color-primary);
165
+ margin-bottom: var(--space-3xl);
166
+ transition: border-color var(--motion-duration-fast) var(--motion-ease-default);
167
+ }
168
+
169
+ .projects-page .projects-lead:hover {
170
+ border-color: var(--border-gentle-hover);
171
+ border-left-color: var(--color-primary);
172
+ }
173
+
174
+ .projects-page .projects-lead-shard {
175
+ right: -5rem;
176
+ top: -5rem;
177
+ width: 18rem;
178
+ height: 18rem;
179
+ background: var(--color-primary);
180
+ }
181
+
182
+ .projects-page .projects-lead-media {
183
+ position: relative;
184
+ display: block;
185
+ text-decoration: none;
186
+ /* Empty blueprint as texture beNewsreader the plate — not a second view mode. */
187
+ background-color: var(--color-panel-contrast);
188
+ background-image:
189
+ linear-gradient(color-mix(in srgb, var(--color-outline) 10%, transparent) 1px, transparent 1px),
190
+ linear-gradient(90deg, color-mix(in srgb, var(--color-outline) 10%, transparent) 1px, transparent 1px);
191
+ background-size: 1rem 1rem;
192
+ }
193
+
194
+ .projects-page .projects-lead-media .projects-plate,
195
+ .projects-page .projects-lead-media .ledger-poster {
196
+ position: relative;
197
+ z-index: 1;
198
+ }
199
+
200
+ .projects-page .projects-lead-body {
201
+ position: relative;
202
+ z-index: 1;
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: var(--space-sm);
206
+ padding: var(--space-lg);
207
+ }
208
+
209
+ .projects-page .projects-lead-title {
210
+ font-family: var(--font-content-title);
211
+ font-size: var(--type-card-lg);
212
+ font-weight: var(--weight-title);
213
+ line-height: 1.25;
214
+ margin: var(--space-2xs) 0 0;
215
+ }
216
+
217
+ .projects-page .projects-lead-title a {
218
+ color: var(--color-on-surface);
219
+ text-decoration: none;
220
+ transition: color var(--motion-duration-fast) var(--motion-ease-default);
221
+ }
222
+
223
+ .projects-page .projects-lead:hover .projects-lead-title a {
224
+ color: var(--color-primary);
225
+ }
226
+
227
+ /* The long official `title`, under the short `project` name. Two real fields,
228
+ not a name and a slogan. */
229
+ .projects-page .projects-lead-subtitle,
230
+ .projects-page .projects-hero-subtitle {
231
+ margin: 0;
232
+ color: var(--color-on-surface-variant);
233
+ letter-spacing: 0.02em;
234
+ }
235
+
236
+ .projects-page .projects-lead-copy {
237
+ margin: var(--space-xs) 0 0;
238
+ font-size: var(--type-body);
239
+ line-height: var(--leading-body);
240
+ color: var(--color-legend);
241
+ max-width: var(--reading-measure);
242
+ }
243
+
244
+ .projects-page .projects-lead .ledger-chip-row {
245
+ margin-top: var(--space-xs);
246
+ }
247
+
248
+ .projects-page .projects-lead-links {
249
+ display: flex;
250
+ flex-wrap: wrap;
251
+ gap: var(--space-md);
252
+ margin-top: var(--space-md);
253
+ padding-top: var(--space-sm);
254
+ border-top: 1px solid var(--border-gentle-divider);
255
+ }
256
+
257
+ @media (min-width: 900px) {
258
+ .projects-page .projects-lead {
259
+ grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
260
+ align-items: stretch;
261
+ }
262
+
263
+ .projects-page .projects-lead-media,
264
+ .projects-page .projects-lead-media .projects-plate,
265
+ .projects-page .projects-lead-media .ledger-poster {
266
+ height: 100%;
267
+ }
268
+
269
+ /* Filling the column means the crop, not the ratio, decides the height. */
270
+ .projects-page .projects-lead-media .ledger-poster {
271
+ aspect-ratio: auto;
272
+ min-height: 13rem;
273
+ }
274
+
275
+ .projects-page .projects-lead-body {
276
+ justify-content: center;
277
+ padding: var(--space-xl) var(--space-2xl);
278
+ }
279
+
280
+ .projects-page .projects-lead-title {
281
+ font-size: var(--type-section);
282
+ }
283
+ }
284
+
285
+ /* ══ Index grid ══ */
286
+ /* Explicit column counts rather than auto-fill: the page uses `px-8 md:px-16`,
287
+ so the content box jumps from 44rem to 40rem at exactly the tablet
288
+ breakpoint, and an auto-fill track that fits two columns at 767px drops to
289
+ one at 768px — the widest phone layout on the site. */
290
+ .projects-page .projects-grid {
291
+ display: grid;
292
+ grid-template-columns: minmax(0, 1fr);
293
+ gap: var(--space-lg);
294
+ /* Cards stretch to the row. This used to be `align-items: start`, because a
295
+ project with no poster left a hole between its summary and its links; now
296
+ every project shows either its thumbnail or its glyph plate, so the row
297
+ aligns on both edges and the link row pins to the bottom of each card. */
298
+ align-items: stretch;
299
+ }
300
+
301
+ @media (min-width: 700px) {
302
+ .projects-page .projects-grid {
303
+ grid-template-columns: repeat(2, minmax(0, 1fr));
304
+ }
305
+ }
306
+
307
+ @media (min-width: 1024px) {
308
+ .projects-page .projects-grid {
309
+ grid-template-columns: repeat(3, minmax(0, 1fr));
310
+ }
311
+ }
312
+
313
+ /* The shared system card pads its own body, which insets the poster from the
314
+ card edge and makes the picture look like an afterthought. Pulling it back
315
+ out by the same padding is one rule and leaves every other spacing in the
316
+ shared block alone. */
317
+ .projects-page .ledger-system-card>.ledger-poster,
318
+ .projects-page .ledger-system-card>.projects-plate {
319
+ width: auto;
320
+ margin: calc(var(--space-lg) * -1) calc(var(--space-lg) * -1) 0;
321
+ }
322
+
323
+ /* The glyph plate is this page's own shape, so it takes the same breathing
324
+ room the unscoped poster rule above gives every system card. */
325
+ .projects-page .projects-plate+.ledger-meta {
326
+ margin-top: var(--space-md);
327
+ }
328
+
329
+ .projects-page .ledger-system-card .ledger-card-title {
330
+ font-size: var(--type-card-lg);
331
+ line-height: 1.15;
332
+ }
333
+
334
+ .projects-page .ledger-system-card .ledger-card-copy {
335
+ font-size: var(--type-subtitle);
336
+ line-height: var(--leading-body);
337
+ color: var(--color-legend);
338
+ }
339
+
340
+ /* ══ Repositories ══
341
+ The mockup closed on a source-code ledger. Its licence stamp, spec
342
+ download and commit hashes have nothing beNewsreader them; the repository rows
343
+ do — `github` is set on five of the seven projects. */
344
+ .projects-page .projects-repos {
345
+ margin-top: var(--space-3xl);
346
+ padding-top: var(--space-2xl);
347
+ border-top: 1px solid var(--border-gentle-subtle);
348
+ }
349
+
350
+ /* One row per repository rather than a grid of cells. A grid leaves the last
351
+ row half empty whenever the count is not a multiple of the column count,
352
+ and five repositories never will be; a stack of rules always closes. */
353
+ .projects-page .projects-repo-list {
354
+ display: flex;
355
+ flex-direction: column;
356
+ border-top: 1px solid var(--border-gentle-subtle);
357
+ }
358
+
359
+ .projects-page .projects-repo {
360
+ display: flex;
361
+ align-items: baseline;
362
+ gap: var(--space-sm);
363
+ padding: var(--space-md);
364
+ border-bottom: 1px solid var(--border-gentle-subtle);
365
+ color: var(--color-on-surface);
366
+ text-decoration: none;
367
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-default), color var(--motion-duration-fast) var(--motion-ease-default);
368
+ }
369
+
370
+ .projects-page .projects-repo:hover {
371
+ background: var(--color-surface-high);
372
+ color: var(--color-primary);
373
+ text-decoration: none;
374
+ }
375
+
376
+ .projects-page .projects-repo-name {
377
+ font-family: var(--font-content-title);
378
+ font-size: var(--type-card);
379
+ font-weight: var(--weight-title);
380
+ }
381
+
382
+ .projects-page .projects-repo-path {
383
+ margin-left: auto;
384
+ font-size: var(--type-label);
385
+ color: var(--color-on-surface-variant);
386
+ }
387
+
388
+ .projects-page .projects-repo .ledger-cta-arrow {
389
+ color: var(--color-primary);
390
+ }
391
+
392
+ .projects-page .projects-repo:hover .ledger-cta-arrow {
393
+ transform: translateX(4px);
394
+ }
395
+
396
+ /* ══ Detail masthead ══
397
+ The generic detail view puts the title in the narrow third column, where
398
+ "Base Operating System for Sensors and Actuators" breaks over five lines
399
+ and the repository link never appears above the fold. A project leads with
400
+ its short `project` name, carries its long `title` underneath, and states
401
+ what it is before the reader scrolls. */
402
+ .projects-page .projects-hero {
403
+ position: relative;
404
+ overflow: hidden;
405
+ display: grid;
406
+ background: var(--color-panel-contrast);
407
+ margin-bottom: var(--space-2xl);
408
+ }
409
+
410
+ .projects-page .projects-hero-shard {
411
+ right: -4rem;
412
+ bottom: -6rem;
413
+ width: 16rem;
414
+ height: 16rem;
415
+ background: var(--color-secondary);
416
+ }
417
+
418
+ .projects-page .projects-hero-body {
419
+ position: relative;
420
+ z-index: 1;
421
+ display: flex;
422
+ flex-direction: column;
423
+ gap: var(--space-xs);
424
+ padding: var(--space-xl);
425
+ }
426
+
427
+ .projects-page .material-symbols-outlined.projects-hero-icon {
428
+ font-size: var(--type-card);
429
+ line-height: 1;
430
+ color: var(--color-primary);
431
+ }
432
+
433
+ .projects-page .projects-hero-title {
434
+ font-family: var(--font-content-title);
435
+ font-size: var(--type-title-lg);
436
+ font-weight: var(--weight-title);
437
+ line-height: 1.12;
438
+ margin: var(--space-sm) 0 0;
439
+ color: var(--color-on-surface);
440
+ text-wrap: balance;
441
+ }
442
+
443
+ .projects-page .projects-hero-subtitle {
444
+ font-size: var(--type-body);
445
+ line-height: 1.3;
446
+ }
447
+
448
+ .projects-page .projects-hero-standfirst {
449
+ margin: var(--space-md) 0 0;
450
+ font-size: var(--type-subtitle);
451
+ line-height: var(--leading-body);
452
+ color: var(--color-legend);
453
+ max-width: var(--reading-measure);
454
+ }
455
+
456
+ .projects-page .projects-hero-actions {
457
+ display: flex;
458
+ flex-wrap: wrap;
459
+ gap: var(--space-sm);
460
+ margin-top: var(--space-lg);
461
+ }
462
+
463
+ .projects-page .projects-hero-actions .ledger-cta {
464
+ padding: var(--space-sm) var(--space-md);
465
+ background: var(--color-surface-card);
466
+ color: var(--color-on-surface);
467
+ }
468
+
469
+ .projects-page .projects-hero-actions .ledger-cta:hover {
470
+ background: var(--color-surface-high);
471
+ color: var(--color-primary);
472
+ }
473
+
474
+ /* The first action is whatever the project itself puts first — the source for
475
+ the libraries, the live site for Freshy and Fond de Terrasse — so the filled
476
+ button always points at the thing rather than at a page about the thing. */
477
+ .projects-page .projects-hero-actions .ledger-cta:first-child {
478
+ background: var(--color-primary);
479
+ color: var(--color-on-primary);
480
+ }
481
+
482
+ .projects-page .projects-hero-actions .ledger-cta:first-child:hover {
483
+ background: var(--color-on-primary-fixed-variant);
484
+ color: var(--color-on-primary);
485
+ }
486
+
487
+ .projects-page .projects-hero-media {
488
+ position: relative;
489
+ z-index: 1;
490
+ display: flex;
491
+ }
492
+
493
+ /* A flex child sizes to its content, which on a phone shrank the glyph plate
494
+ to the width of the glyph. The plate always takes the column. */
495
+ .projects-page .projects-hero-media .projects-plate {
496
+ flex: 1;
497
+ min-width: 0;
498
+ }
499
+
500
+ @media (min-width: 900px) {
501
+ .projects-page .projects-hero {
502
+ grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
503
+ align-items: stretch;
504
+ min-height: 19rem;
505
+ }
506
+
507
+ .projects-page .projects-hero-body {
508
+ justify-content: center;
509
+ padding: var(--space-2xl);
510
+ }
511
+
512
+ .projects-page .projects-hero-title {
513
+ font-size: var(--type-display);
514
+ }
515
+
516
+ .projects-page .projects-hero-media .ledger-poster {
517
+ height: 100%;
518
+ aspect-ratio: auto;
519
+ }
520
+ }
521
+
522
+ /* ══ Dossier ══
523
+ The mockup's right-hand spec column — ARCH, STACK, SPEC — with the
524
+ fabricated rows replaced by what a project actually declares. */
525
+
526
+ /* The dossier and the body take the shared detail tracks (ledger/detail.css),
527
+ which start at 768px. A project holds the split back to 1024px for the
528
+ reason recorded in the layout: at tablet width the dossier is narrow enough
529
+ to break a repository slug across three lines mid-word. */
530
+ @media (min-width: 768px) and (max-width: 1023px) {
531
+ .projects-page .grid.text-detail-grid {
532
+ grid-template-columns: minmax(0, 1fr);
533
+ }
534
+ }
535
+
536
+ .projects-page aside.projects-dossier-column {
537
+ /* The grid sets items-start, which shrinks the column to its own content
538
+ and leaves the sticky child no distance to travel. */
539
+ align-self: stretch;
540
+ }
541
+
542
+ .projects-page .projects-dossier-title {
543
+ margin: 0;
544
+ padding: var(--space-md) var(--space-md) var(--space-sm);
545
+ background: var(--color-surface-container-low);
546
+ font-family: var(--font-label);
547
+ font-size: var(--type-label-lg);
548
+ letter-spacing: 0.1em;
549
+ text-transform: uppercase;
550
+ font-weight: 600;
551
+ color: var(--color-on-surface);
552
+ }
553
+
554
+ .projects-page .projects-dossier-rows {
555
+ margin: 0;
556
+ padding: 0 var(--space-md) var(--space-sm);
557
+ background: var(--color-surface-container-low);
558
+ font-family: var(--font-mono);
559
+ font-size: var(--type-meta);
560
+ font-variant-numeric: tabular-nums;
561
+ }
562
+
563
+ .projects-page .projects-dossier-row {
564
+ display: flex;
565
+ flex-direction: column;
566
+ gap: 0.2rem;
567
+ padding: var(--space-sm) 0;
568
+ border-top: 1px solid var(--border-gentle-faint);
569
+ }
570
+
571
+ .projects-page .projects-dossier-row dt {
572
+ font-family: var(--font-label);
573
+ font-size: var(--type-label);
574
+ letter-spacing: 0.08em;
575
+ text-transform: uppercase;
576
+ color: var(--color-outline);
577
+ }
578
+
579
+ .projects-page .projects-dossier-row dd {
580
+ margin: 0;
581
+ color: var(--color-on-surface);
582
+ overflow-wrap: anywhere;
583
+ }
584
+
585
+ .projects-page .projects-dossier-row dd a {
586
+ color: var(--color-primary);
587
+ text-decoration: none;
588
+ }
589
+
590
+ .projects-page .projects-dossier-row dd a:hover {
591
+ text-decoration: underline;
592
+ }
593
+
594
+ .projects-page .projects-dossier-row .ledger-chip-row {
595
+ margin-top: 0.15rem;
596
+ }
597
+
598
+ /* Below the desktop split the dossier is a full-width band above the prose,
599
+ so its rows run across instead of stacking into one tall column. */
600
+ @media (max-width: 1023px) {
601
+ .projects-page .projects-dossier-rows {
602
+ display: grid;
603
+ grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
604
+ gap: 0 var(--space-lg);
605
+ padding-bottom: var(--space-md);
606
+ }
607
+ }
608
+
609
+ /* The markup pins the column with Tailwind's `lg:top-32`, which is 8rem. The
610
+ CV register rail, the rascunhos marginalia and the detail dossier all park
611
+ at 7rem below the 78px sticky nav; one sticky offset for the site. The
612
+ compound selector supplies the specificity the utility would win on. */
613
+ @media (min-width: 1024px) {
614
+ .projects-page aside>.projects-dossier {
615
+ top: 7rem;
616
+ max-height: calc(100vh - 9rem);
617
+ overflow-y: auto;
618
+ scrollbar-width: thin;
619
+ }
620
+ }
621
+
622
+ /* ══ The body ══
623
+ Where the portfolio actually lives. Project bodies carry figures, release
624
+ videos, Plotly embeds, Mermaid architecture diagrams and GFM tables.
625
+
626
+ This page used to release the whole column from the 45rem reading measure
627
+ and then hand the measure back to a whitelist of text blocks, on the theory
628
+ that anything you look at rather than read should have the full column. In
629
+ practice that put every plate 18% wider than the prose at 1440px and 6%
630
+ wider at 1280px — wide enough to read as a broken edge, never wide enough
631
+ to read as a deliberate break out of the column, and different from how the
632
+ same figure, video and Plotly includes sit on every article. A project body
633
+ now keeps the site's measure like everything else, and a Mermaid diagram
634
+ too wide for it scrolls inside its own plate, which `.article-content
635
+ .mermaid` in article.css has always been set up to do.
636
+
637
+ The figure, video, Plotly and caption plates this page proved live in
638
+ assets/css/article.css and apply to every `.article-content` on the site,
639
+ so they are deliberately not repeated here. */
640
+
641
+ /* Tables are authored GFM. The ledger treatment lives in detail.css; what is
642
+ added here is the full column and figures that line up. */
643
+ .projects-page .article-content table,
644
+ .article-content .projects-studio table {
645
+ width: 100%;
646
+ font-variant-numeric: tabular-nums;
647
+ }
648
+
649
+ /* ══ Breakpoints ══ */
650
+ @media (max-width: 899px) {
651
+ .projects-page .projects-hero-title {
652
+ font-size: var(--type-title);
653
+ }
654
+
655
+ .projects-page .projects-lead-title {
656
+ font-size: var(--type-title);
657
+ }
658
+
659
+ /* The hero stacks title-first: a reader who has just clicked into a project
660
+ needs to confirm where they are before being shown the picture. The index
661
+ lead is the opposite case, and already has its media first in the DOM. */
662
+ .projects-page .projects-hero {
663
+ gap: var(--space-lg);
664
+ }
665
+ }
666
+
667
+ @media (max-width: 640px) {
668
+
669
+ .projects-page .projects-lead-body,
670
+ .projects-page .projects-hero-body {
671
+ padding: var(--space-lg) var(--space-md);
672
+ }
673
+
674
+ .projects-page .projects-repo {
675
+ flex-wrap: wrap;
676
+ }
677
+
678
+ .projects-page .projects-repo-path {
679
+ margin-left: 0;
680
+ width: 100%;
681
+ }
682
+
683
+ /* A wide GFM table cannot reflow below a phone's width, so it scrolls
684
+ inside its own column instead of pushing the page sideways. */
685
+ .projects-page .article-content table {
686
+ display: block;
687
+ overflow-x: auto;
688
+ width: 100%;
689
+ }
690
+ }
691
+
692
+ /* ── Project header line ──
693
+ The period, stack and status above a project name are marginalia — the
694
+ small-caps label voice the rest of the site uses for chrome — rather than
695
+ running body text. */
696
+ .projects-page .ledger-meta,
697
+ .projects-detail .ledger-meta {
698
+ font-family: var(--font-label);
699
+ font-size: var(--type-meta);
700
+ letter-spacing: 0.03em;
701
+ }
702
+
703
+ /* The glyph belongs to the name, so it sits on the name's own line instead of
704
+ floating in the line above, set a quarter larger than the name so it reads
705
+ as a mark rather than a stray letterform. A Material Symbol draws its ink
706
+ from just above the baseline to near the top of its em box, so aligning it
707
+ on the baseline pinned the glyph's bottom to the text's bottom and left the
708
+ whole mark riding above the name. The site-wide default in base.css
709
+ (`vertical-align: middle`) centers it on the name instead, which is why
710
+ this rule no longer sets one. */
711
+ .material-symbols-outlined.projects-title-icon {
712
+ font-size: 1.25em;
713
+ line-height: 1;
714
+ margin-right: 0.4rem;
715
+ color: var(--color-primary);
716
+ }