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,1270 @@
1
+ /* Ledger page styles: detail. Owned by the detail page; shared component rules
2
+ belong in assets/css/base.css under "Architectural Ledger blocks".
3
+
4
+ Scope note. Public detail views are assembled in the browser by
5
+ `_includes/content-runtime/50-detail.html` (`buildTextSidebar`,
6
+ `renderRuntimeDetailSection`) and mounted by the listing layouts, so this
7
+ file styles the DOM that runtime emits. `_layouts/text.html` emits the same
8
+ class contract for the private studio build; every rule below therefore
9
+ targets classes, never a container that exists on only one of the two paths.
10
+
11
+ Specificity note. The Tailwind Play CDN injects its utilities *after* this
12
+ stylesheet, so a single-class rule loses to any colliding utility already on
13
+ the element (`text-tertiary`, `text-xs`, `flex`, `mb-8`, …). Rules that have
14
+ to beat one are written with two compound parts on purpose. */
15
+
16
+ /* ══ Reading progress ══
17
+ Pure UI state, no field beNewsreader it, and no DOM hook to hang it on — the
18
+ runtime owns the detail markup. A scroll-driven animation on a pseudo of
19
+ the detail section gives the mockup's indicator with zero markup, and the
20
+ @supports guard means older engines simply get no bar.
21
+
22
+ Sit under the sticky site nav (z-50), not over it — a top-of-viewport bar
23
+ was invisible against the nav surface. */
24
+ @supports (animation-timeline: scroll(root block)) {
25
+
26
+ :is([data-dynamic-article-detail],
27
+ [data-dynamic-post-detail],
28
+ [data-dynamic-project-detail],
29
+ [data-dynamic-product-detail],
30
+ [data-detail-view])::before {
31
+ content: "";
32
+ position: fixed;
33
+ left: 0;
34
+ right: 0;
35
+ /* Approx sticky nav stack: wordmark row + padding (shell sticky nav). */
36
+ top: 4.25rem;
37
+ z-index: 40;
38
+ height: 2px;
39
+ background: var(--color-primary);
40
+ transform: scaleX(0);
41
+ transform-origin: 0 50%;
42
+ pointer-events: none;
43
+ animation-name: detail-read-progress;
44
+ animation-duration: 1ms;
45
+ animation-timing-function: linear;
46
+ animation-fill-mode: both;
47
+ animation-timeline: scroll(root block);
48
+ }
49
+ }
50
+
51
+ @keyframes detail-read-progress {
52
+ from {
53
+ transform: scaleX(0);
54
+ }
55
+
56
+ to {
57
+ transform: scaleX(1);
58
+ }
59
+ }
60
+
61
+ /* The bar is decoration driven by the scroll position: nothing on the page
62
+ depends on it, so a reader who has asked for less motion simply does not get
63
+ a strip that tracks their scrolling. */
64
+ @media (prefers-reduced-motion: reduce) {
65
+
66
+ :is([data-dynamic-article-detail],
67
+ [data-dynamic-post-detail],
68
+ [data-dynamic-project-detail],
69
+ [data-dynamic-product-detail],
70
+ [data-detail-view])::before {
71
+ display: none;
72
+ }
73
+ }
74
+
75
+ /* ══ Return link ══
76
+ The ledger's first line: mono, uppercase, tracked out. */
77
+ a.back-link {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: var(--space-xs);
81
+ padding: 0.35rem 0.85rem;
82
+ background: var(--color-surface-card);
83
+ border: 1.5px solid var(--border-ink);
84
+ border-radius: var(--radius-card);
85
+ font-family: var(--font-mono);
86
+ font-size: var(--type-label-lg);
87
+ line-height: 1.4;
88
+ text-transform: uppercase;
89
+ letter-spacing: 0.12em;
90
+ color: var(--color-on-surface-variant);
91
+ text-decoration: none !important;
92
+ transition: color var(--motion-duration-fast) var(--motion-ease-default),
93
+ border-color var(--motion-duration-fast) var(--motion-ease-default),
94
+ background-color var(--motion-duration-fast) var(--motion-ease-default);
95
+ }
96
+
97
+ a.back-link:hover {
98
+ color: var(--color-primary);
99
+ border-color: var(--color-primary);
100
+ background: var(--color-surface-high);
101
+ }
102
+
103
+ a.back-link .material-symbols-outlined {
104
+ font-size: var(--type-body);
105
+ }
106
+
107
+ /* ══ Meta action row ══
108
+ Share · Cite · PDF as pill buttons with Google icons. */
109
+ .detail-actions {
110
+ display: flex;
111
+ flex-wrap: wrap;
112
+ gap: var(--space-xs) var(--space-sm);
113
+ margin-top: var(--space-sm);
114
+ padding-top: var(--space-sm);
115
+ border-top: 1px solid var(--border-gentle-faint);
116
+ }
117
+
118
+ .detail-action {
119
+ display: inline-flex;
120
+ align-items: center;
121
+ gap: 0.35rem;
122
+ padding: 0.35rem 0.85rem;
123
+ background: var(--color-surface-card);
124
+ border: 1px solid var(--border-ink);
125
+ border-radius: var(--radius-pill);
126
+ font-family: var(--font-label);
127
+ font-size: var(--type-label-lg);
128
+ font-weight: var(--weight-label);
129
+ letter-spacing: 0.04em;
130
+ color: var(--color-on-surface);
131
+ text-decoration: none !important;
132
+ cursor: pointer;
133
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-default),
134
+ border-color var(--motion-duration-fast) var(--motion-ease-default),
135
+ color var(--motion-duration-fast) var(--motion-ease-default);
136
+ }
137
+
138
+ .detail-action:hover {
139
+ background: var(--color-surface-high);
140
+ border-color: var(--color-primary);
141
+ color: var(--color-primary);
142
+ }
143
+
144
+ .detail-action .material-symbols-outlined {
145
+ font-size: 1rem;
146
+ line-height: 1;
147
+ color: var(--color-primary);
148
+ transition: color var(--motion-duration-fast) var(--motion-ease-default);
149
+ }
150
+
151
+ .detail-action:hover .material-symbols-outlined {
152
+ color: var(--color-primary);
153
+ }
154
+
155
+ /* Reading-size step. Compact pill beside Share and Cite; opens a −4…+4 menu. */
156
+ .detail-type-size {
157
+ position: relative;
158
+ display: inline-flex;
159
+ align-items: center;
160
+ color: var(--color-on-surface);
161
+ }
162
+
163
+ .detail-type-size-trigger {
164
+ appearance: none;
165
+ display: inline-flex;
166
+ align-items: center;
167
+ gap: 0.2rem;
168
+ padding: 0.35rem 0.55rem 0.35rem 0.7rem;
169
+ background: var(--color-surface-card);
170
+ border: 1px solid var(--border-ink);
171
+ border-radius: var(--radius-pill);
172
+ font-family: var(--font-label);
173
+ font-size: var(--type-label-lg);
174
+ font-weight: var(--weight-label);
175
+ letter-spacing: 0.04em;
176
+ line-height: 1;
177
+ color: var(--color-on-surface);
178
+ cursor: pointer;
179
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-default),
180
+ border-color var(--motion-duration-fast) var(--motion-ease-default),
181
+ color var(--motion-duration-fast) var(--motion-ease-default);
182
+ }
183
+
184
+ .detail-type-size-trigger:hover,
185
+ .detail-type-size-trigger[aria-expanded="true"] {
186
+ background: var(--color-surface-high);
187
+ border-color: var(--color-primary);
188
+ color: var(--color-primary);
189
+ }
190
+
191
+ .detail-type-size-trigger:focus-visible {
192
+ outline: 1px solid var(--color-primary);
193
+ outline-offset: 1px;
194
+ }
195
+
196
+ .detail-type-size-trigger .material-symbols-outlined {
197
+ font-size: 1rem;
198
+ line-height: 1;
199
+ color: var(--color-primary);
200
+ }
201
+
202
+ .detail-type-size-value {
203
+ min-width: 1.6em;
204
+ text-align: center;
205
+ font-variant-numeric: tabular-nums;
206
+ }
207
+
208
+ .detail-type-size-caret {
209
+ font-size: 1rem;
210
+ line-height: 1;
211
+ transition: transform var(--motion-duration-fast) var(--motion-ease-default);
212
+ }
213
+
214
+ .detail-type-size-trigger[aria-expanded="true"] .detail-type-size-caret {
215
+ transform: rotate(180deg);
216
+ }
217
+
218
+ .detail-type-size:has(.detail-type-size-trigger[aria-expanded="true"]) {
219
+ z-index: 30;
220
+ }
221
+
222
+ .detail-type-size-menu {
223
+ position: absolute;
224
+ top: calc(100% + 0.35rem);
225
+ left: 0;
226
+ z-index: 20;
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: 0.1rem;
230
+ min-width: 100%;
231
+ max-height: min(14.5rem, 70vh);
232
+ overflow-y: auto;
233
+ overscroll-behavior: contain;
234
+ padding: 0.35rem;
235
+ background: var(--color-surface-card);
236
+ border: 1px solid var(--border-ink);
237
+ border-radius: var(--radius-card);
238
+ box-shadow: 0 0.45rem 1.1rem color-mix(in srgb, var(--color-on-surface) 12%, transparent);
239
+ scrollbar-width: thin;
240
+ }
241
+
242
+ /* On the stacked mobile dossier the actions sit near the plate floor, so the
243
+ menu opens upward into the panel instead of vanishing under the next block. */
244
+ @media (max-width: 767px) {
245
+ .detail-type-size-menu {
246
+ top: auto;
247
+ bottom: calc(100% + 0.35rem);
248
+ }
249
+ }
250
+
251
+ .detail-type-size-menu[hidden] {
252
+ display: none;
253
+ }
254
+
255
+ .detail-type-size-step {
256
+ appearance: none;
257
+ display: flex;
258
+ align-items: center;
259
+ justify-content: space-between;
260
+ gap: 0.75rem;
261
+ width: 100%;
262
+ border: 0;
263
+ background: transparent;
264
+ border-radius: var(--radius-pill);
265
+ padding: 0.35rem 0.55rem;
266
+ font-family: var(--font-label);
267
+ font-size: var(--type-label);
268
+ font-weight: var(--weight-label);
269
+ letter-spacing: 0.02em;
270
+ line-height: 1;
271
+ color: var(--color-on-surface);
272
+ cursor: pointer;
273
+ font-variant-numeric: tabular-nums;
274
+ }
275
+
276
+ .detail-type-size-step::after {
277
+ content: "";
278
+ width: 0.35rem;
279
+ height: 0.35rem;
280
+ border-radius: 999px;
281
+ background: var(--border-ink);
282
+ opacity: 0.55;
283
+ }
284
+
285
+ .detail-type-size-step[aria-selected="true"] {
286
+ background: var(--color-surface-high);
287
+ color: var(--color-primary);
288
+ }
289
+
290
+ .detail-type-size-step[aria-selected="true"]::after {
291
+ background: var(--color-primary);
292
+ opacity: 1;
293
+ }
294
+
295
+ .detail-type-size-step:hover,
296
+ .detail-type-size-step:focus-visible {
297
+ color: var(--color-primary);
298
+ }
299
+
300
+ .detail-type-size-step:focus-visible {
301
+ outline: 1px solid var(--color-primary);
302
+ outline-offset: 1px;
303
+ }
304
+
305
+ /* ══ Detail column grid ══
306
+ Two tracks, dossier then body. This used to be Tailwind's twelve-column
307
+ grid with the dossier on `md:col-span-3`, which left the panel around 15rem
308
+ on a laptop: narrow enough that a fourteen-letter word in a title broke
309
+ mid-word rather than wrapping. The dossier now takes a quarter more width.
310
+
311
+ Three of twelve columns plus the two gaps between them is `25% - 0.75gap`,
312
+ so a quarter more is `31.25% - 0.9375gap`. The body track keeps whatever is
313
+ left, and since the reading column inside it is capped at
314
+ `--reading-measure` and sets flush left, the widening moves the prose right
315
+ by exactly what the dossier gained instead of narrowing it.
316
+
317
+ The gap values mirror the `gap-8 lg:gap-12` utilities the markup still
318
+ carries; the track formula needs the number, and Tailwind's own gap cannot
319
+ be read back out of the cascade. Change one and change the other. The
320
+ selector compounds `.grid` for the reason given at the top of this file:
321
+ the Play CDN injects `grid-cols-1` after this sheet, so a single class
322
+ loses the tie and the page stays stacked.
323
+
324
+ Every detail page uses this one rule, including the projects page, which
325
+ only differs in holding the split back to 1024px (ledger/projects.css). */
326
+ @media (min-width: 768px) {
327
+ .grid.text-detail-grid {
328
+ --detail-column-gap: 2rem;
329
+
330
+ grid-template-columns:
331
+ calc(31.25% - 0.9375 * var(--detail-column-gap)) minmax(0, 1fr);
332
+ }
333
+ }
334
+
335
+ @media (min-width: 1024px) {
336
+ .grid.text-detail-grid {
337
+ --detail-column-gap: 3rem;
338
+ }
339
+ }
340
+
341
+ /* ══ Dossier ══
342
+ Title, description, classification and dates read as one panel. The runtime
343
+ emits the heading block and the meta block as siblings with no wrapper, so
344
+ the panel is made by giving both the same surface and closing the gap
345
+ between them rather than by boxing them. */
346
+
347
+ /* The column already asks to be sticky (`md:sticky md:top-32`), but the grid
348
+ sets `items-start`, which shrinks the <aside> to its own content and leaves
349
+ the sticky child no distance to travel — so the dossier scrolled away with
350
+ the first screen. Stretching the column to the row restores it. */
351
+ aside:has(> .text-detail-sidebar) {
352
+ align-self: stretch;
353
+ }
354
+
355
+ /* The markup pins the column with Tailwind's `md:top-32`, which is 8rem. The
356
+ CV register rail and the rascunhos marginalia column both park at 7rem below
357
+ the 78px sticky nav; one sticky offset for the site. The child combinator
358
+ supplies the extra specificity the utility would otherwise win on. */
359
+ .text-detail-sidebar {
360
+ background: var(--color-panel-contrast);
361
+ border: 1.5px solid var(--border-ink);
362
+ border-radius: var(--radius-card);
363
+ overflow: hidden;
364
+ }
365
+
366
+ /* Reading-size menu is absolute and must escape the plate; the heading
367
+ already clips its own shard, so opening the menu can drop the plate clip. */
368
+ .text-detail-sidebar:has(.detail-type-size-trigger[aria-expanded="true"]) {
369
+ overflow: visible;
370
+ position: relative;
371
+ z-index: 4;
372
+ }
373
+
374
+ @media (min-width: 768px) {
375
+
376
+ /* Default park under the nav. After mount, syncDetailSidebarSticky()
377
+ rewrites top from the dossier's own height so a tall plate (Related Texts)
378
+ scrolls with the page until its bottom meets the viewport, then sticks. */
379
+ aside>.text-detail-sidebar {
380
+ top: 7rem;
381
+ }
382
+ }
383
+
384
+ /* Article / rascunho dossier uses the site type scale as-is — no local remaps. */
385
+
386
+ .text-detail-sidebar .article-sidebar-heading {
387
+ position: relative;
388
+ overflow: hidden;
389
+ margin-bottom: 0;
390
+ padding: var(--space-lg);
391
+ background: var(--color-panel-contrast);
392
+ border-bottom: 0;
393
+ }
394
+
395
+ /* Canonical shard, bottom-right, as in the mockup. Decoration only: it is
396
+ suppressed below the tablet breakpoint like every other shard. */
397
+ .text-detail-sidebar .article-sidebar-heading::after {
398
+ content: "";
399
+ position: absolute;
400
+ right: -1.75rem;
401
+ bottom: -1.75rem;
402
+ width: 5rem;
403
+ height: 5rem;
404
+ clip-path: var(--shard-alpha);
405
+ background: var(--color-secondary);
406
+ opacity: var(--cubist-shard-opacity);
407
+ pointer-events: none;
408
+ }
409
+
410
+ @media (max-width: 639px) {
411
+ .text-detail-sidebar .article-sidebar-heading::after {
412
+ display: none;
413
+ }
414
+ }
415
+
416
+ .text-detail-sidebar .article-sidebar-heading>* {
417
+ position: relative;
418
+ z-index: 1;
419
+ }
420
+
421
+ /* The detail title is the page h1 and stays in the left column, but it sits
422
+ inside a ~18rem panel now, so it comes down one step from the full
423
+ .text-title scale. */
424
+ .text-detail-sidebar .text-title {
425
+ /* Moves with the h2 above it. At 1.7rem against the new 1.87rem h2 the page
426
+ h1 would have been the smaller of the two — at the old 1.6em h2 the pair
427
+ were already dead level between 768px and 1024px. */
428
+ font-size: var(--type-title);
429
+ font-weight: var(--weight-title);
430
+ font-synthesis: none;
431
+ line-height: 1.15;
432
+ margin: 0;
433
+ text-wrap: balance;
434
+ /* A word longer than the panel breaks at a syllable with a real hyphen
435
+ rather than mid-letter: "Hiperespaçolândia" is seventeen characters and
436
+ still overruns the widened dossier, and it used to read as
437
+ "Hiperespaçolândi / a". The dictionary follows the document language the
438
+ shell sets (pt-BR on the rascunhos shell, en on the others), and a title
439
+ whose words already fit is left alone. `overflow-wrap` stays as the last
440
+ resort for a string no dictionary can break, such as a bare URL. */
441
+ hyphens: auto;
442
+ overflow-wrap: break-word;
443
+ }
444
+
445
+ @media (min-width: 1024px) {
446
+ .text-detail-sidebar .text-title {
447
+ font-size: var(--type-title-lg);
448
+ }
449
+ }
450
+
451
+ .text-detail-sidebar .article-sidebar-heading .material-symbols-outlined {
452
+ color: var(--color-secondary);
453
+ }
454
+
455
+ /* Lead thesis. `description` is the article's own one-sentence claim — body
456
+ face and body size, in legend ink so it sits under the H1 without matching
457
+ the reading column. */
458
+ .text-detail-sidebar .article-sidebar-description {
459
+ font-family: var(--font-body);
460
+ font-style: normal;
461
+ font-size: var(--type-body);
462
+ font-weight: var(--weight-body);
463
+ line-height: var(--leading-body);
464
+ color: var(--color-legend);
465
+ margin: var(--space-md) 0 0;
466
+ }
467
+
468
+ .text-detail-sidebar .article-sidebar-thumbnail {
469
+ margin-top: var(--space-md);
470
+ width: 100%;
471
+ max-width: none;
472
+ }
473
+
474
+ /* ── Dossier ledger rows ── */
475
+ .text-detail-sidebar .article-meta {
476
+ margin-bottom: 0;
477
+ padding: 0 var(--space-lg) var(--space-lg);
478
+ background: var(--color-panel-contrast);
479
+ color: var(--color-on-surface-variant);
480
+ font-family: var(--font-mono);
481
+ font-size: var(--type-meta);
482
+ letter-spacing: 0.04em;
483
+ text-transform: none;
484
+ gap: var(--space-md);
485
+ font-variant-numeric: tabular-nums;
486
+ }
487
+
488
+ /* Projects and products can carry no meta at all, and on articles and posts
489
+ the byline below is the block's only child. Either way an all-empty meta
490
+ block must not hang a stub of padding off the bottom of the panel. */
491
+ .text-detail-sidebar .article-meta:has(> div:empty),
492
+ .text-detail-sidebar .article-meta:has(> div > span.text-secondary:not(.font-bold):only-child) {
493
+ display: none;
494
+ }
495
+
496
+ .text-detail-sidebar .article-meta>div {
497
+ gap: var(--space-sm);
498
+ }
499
+
500
+ /* Author byline. Dropped: single-author site. The runtime still emits it, so
501
+ it is suppressed here until `buildTextSidebar` stops rendering it — see the
502
+ runtime-change note in this branch's commit message. The product/company
503
+ line is the same colour but carries `font-bold`, hence the :not(). */
504
+ .text-detail-sidebar .article-meta>div>span.text-secondary:not(.font-bold) {
505
+ display: none;
506
+ }
507
+
508
+ .text-detail-sidebar .article-meta .text-tertiary,
509
+ .text-detail-sidebar .article-meta span.font-bold {
510
+ font-family: var(--font-label);
511
+ font-size: var(--type-label-lg);
512
+ letter-spacing: 0.04em;
513
+ color: var(--color-tertiary);
514
+ }
515
+
516
+ /* Dates. The runtime already localises "Published" / "Updated" into each
517
+ span's title attribute, so the label can be lifted from the markup instead
518
+ of being invented in CSS — and it stays in the reader's language. */
519
+ .text-detail-sidebar .article-meta .date-evolution {
520
+ gap: var(--space-xs);
521
+ width: 100%;
522
+ }
523
+
524
+ .text-detail-sidebar .article-meta .date-evolution>span {
525
+ display: flex;
526
+ align-items: baseline;
527
+ justify-content: space-between;
528
+ gap: var(--space-sm);
529
+ opacity: 1;
530
+ color: var(--color-on-surface);
531
+ white-space: nowrap;
532
+ border-top: 1px solid var(--border-gentle-faint);
533
+ padding-top: var(--space-sm);
534
+ }
535
+
536
+ .text-detail-sidebar .article-meta .date-evolution>span[title]::before {
537
+ content: attr(title);
538
+ color: var(--color-outline);
539
+ text-transform: uppercase;
540
+ letter-spacing: 0.1em;
541
+ font-size: var(--type-label);
542
+ }
543
+
544
+ /* Classification chips and the language pill, once the dossier carries them. */
545
+ .text-detail-sidebar .article-meta .ledger-chip-row {
546
+ margin-bottom: var(--space-sm);
547
+ }
548
+
549
+
550
+ /* ══ Table of contents ══
551
+ Restyled from `55-toc.html`, which builds it from the body's own h2/h3. The
552
+ numbering is part of that same structural derivation, not a field. */
553
+ .article-toc--sidebar {
554
+ margin-top: var(--space-sm);
555
+ padding: var(--space-md);
556
+ border: 1.5px solid var(--border-ink);
557
+ border-radius: var(--radius-card);
558
+ background: var(--color-surface-container-low);
559
+ }
560
+
561
+ .article-toc-title,
562
+ .article-toc-summary {
563
+ font-family: var(--font-label);
564
+ font-size: var(--type-label-lg);
565
+ letter-spacing: 0.08em;
566
+ text-transform: uppercase;
567
+ }
568
+
569
+ .article-toc-title {
570
+ color: var(--color-on-surface);
571
+ font-weight: 600;
572
+ margin-bottom: var(--space-md);
573
+ }
574
+
575
+ .article-toc--sidebar .article-toc-list,
576
+ .article-toc-panel .article-toc-list {
577
+ padding-left: 0;
578
+ }
579
+
580
+ .article-toc-item+.article-toc-item {
581
+ margin-top: 0.35rem;
582
+ }
583
+
584
+ .article-toc-link {
585
+ display: flex;
586
+ align-items: baseline;
587
+ gap: var(--space-sm);
588
+ padding: 0.25rem 0;
589
+ font-family: var(--font-body);
590
+ font-size: var(--type-body);
591
+ line-height: 1.45;
592
+ letter-spacing: normal;
593
+ text-transform: none;
594
+ color: var(--color-on-surface-variant);
595
+ transition: color var(--motion-duration-fast) var(--motion-ease-default);
596
+ }
597
+
598
+ .article-toc-link:hover {
599
+ color: var(--color-primary);
600
+ text-decoration: none;
601
+ }
602
+
603
+ .article-toc-num {
604
+ flex: 0 0 auto;
605
+ min-width: 1.75rem;
606
+ font-family: var(--font-mono);
607
+ font-size: 0.9em;
608
+ letter-spacing: 0.04em;
609
+ color: var(--color-outline);
610
+ }
611
+
612
+ .article-toc-item.is-active>.article-toc-link,
613
+ .article-toc-item.is-active .article-toc-num {
614
+ color: var(--color-primary);
615
+ }
616
+
617
+ .article-toc-item.is-active>.article-toc-link {
618
+ text-decoration: underline;
619
+ text-decoration-thickness: 2px;
620
+ text-underline-offset: 0.22em;
621
+ text-decoration-color: color-mix(in srgb, var(--color-primary) 55%, transparent);
622
+ }
623
+
624
+ .article-toc-item--h3>.article-toc-link {
625
+ padding-left: 1.75rem;
626
+ font-size: var(--type-subtitle);
627
+ color: var(--color-outline);
628
+ }
629
+
630
+ .article-toc-item--h3>.article-toc-link:hover {
631
+ color: var(--color-primary);
632
+ }
633
+
634
+ /* Mobile disclosure: the same panel, folded. */
635
+ .article-toc--disclosure {
636
+ margin-bottom: var(--space-xl);
637
+ border: 0;
638
+ background: var(--color-surface-container-low);
639
+ }
640
+
641
+ .article-toc-summary {
642
+ padding: var(--space-sm) var(--space-md);
643
+ font-weight: 400;
644
+ color: var(--color-on-surface-variant);
645
+ }
646
+
647
+ .article-toc-panel {
648
+ padding: 0 var(--space-md) var(--space-md);
649
+ border-top: 1px solid var(--border-gentle-faint);
650
+ }
651
+
652
+ /* ══ Prose column ══
653
+ The page's reason to exist. Measure is already var(--reading-measure);
654
+ what changes here is the vertical rhythm and the heading voice. */
655
+ .article-content {
656
+ /* Body face; slightly larger than UI body. */
657
+ font-family: var(--font-body);
658
+ font-size: var(--type-prose);
659
+ font-weight: var(--weight-body);
660
+ line-height: var(--leading-prose);
661
+ }
662
+
663
+ /* Opening initial. Front matter `drop_cap: true` adds `.has-drop-cap` when the
664
+ body does not open with a heading. The letter spans `--drop-cap-lines`
665
+ (site config, default 2) in the reading face. `initial-letter` sinks it
666
+ inside the paragraph; the float is only for engines that still lack that
667
+ property. */
668
+ .article-content.has-drop-cap > p:first-of-type::first-letter {
669
+ font-family: var(--font-body);
670
+ font-weight: var(--weight-body);
671
+ font-style: normal;
672
+ color: var(--color-on-surface);
673
+ initial-letter: var(--drop-cap-lines);
674
+ margin-right: 0.2em;
675
+ }
676
+
677
+ @supports not (initial-letter: 3) {
678
+ .article-content.has-drop-cap > p:first-of-type::first-letter {
679
+ float: left;
680
+ font-size: calc(var(--drop-cap-lines) * var(--leading-prose) / 0.66 * 1em);
681
+ line-height: 0.75;
682
+ margin: 0.04em 0.2em 0 0;
683
+ }
684
+ }
685
+
686
+ /* The display serif for section heads, the same face as the title, with a
687
+ hairline above so a long article reads as a sequence of plates rather than a
688
+ scroll. The body face at semibold — what this replaces — belonged to no type
689
+ role.
690
+
691
+ Raised from 1.6em: at that size a section head opening a plate sat too close
692
+ to the paragraph under it to stop the eye on its own, and the hairline was
693
+ doing most of the work. h3 and the sidebar h1 move with it below. */
694
+ .article-content h2 {
695
+ font-family: var(--font-content-title);
696
+ font-size: var(--type-title);
697
+ font-weight: var(--weight-title);
698
+ font-synthesis: none;
699
+ line-height: 1.25;
700
+ color: var(--color-on-surface);
701
+ margin-top: var(--space-2xl);
702
+ margin-bottom: var(--space-md);
703
+ padding-top: var(--space-lg);
704
+ border-top: 1px solid var(--border-gentle-divider);
705
+ }
706
+
707
+ .article-content> :first-child {
708
+ margin-top: 0;
709
+ padding-top: 0;
710
+ border-top: 0;
711
+ }
712
+
713
+ .article-content> :last-child {
714
+ margin-bottom: 0;
715
+ }
716
+
717
+ /* Sub-heads are titles, so they take the display face a step below h2 rather
718
+ than the small-caps label voice. Small caps belong to tags and chrome; run
719
+ over a sentence-shaped heading they shout. */
720
+ .article-content h3 {
721
+ font-family: var(--font-content-title);
722
+ /* Holds a step below h2. */
723
+ font-size: var(--type-card-lg);
724
+ font-weight: var(--weight-title);
725
+ font-synthesis: none;
726
+ letter-spacing: normal;
727
+ text-transform: none;
728
+ line-height: 1.3;
729
+ color: var(--color-on-surface);
730
+ margin-top: var(--space-xl);
731
+ margin-bottom: var(--space-sm);
732
+ }
733
+
734
+ .article-content h4 {
735
+ font-family: var(--font-body);
736
+ font-size: var(--type-body);
737
+ font-weight: var(--weight-title);
738
+ font-synthesis: none;
739
+ color: var(--color-on-surface);
740
+ margin-top: var(--space-lg);
741
+ margin-bottom: var(--space-xs);
742
+ }
743
+
744
+ .article-content p {
745
+ margin-bottom: 1.15em;
746
+ }
747
+
748
+ /* Architectural rectilinearity: the prose furniture still had soft corners. */
749
+ .article-content code,
750
+ .article-content pre,
751
+ .article-content blockquote,
752
+ .article-content .article-note {
753
+ border-radius: 0;
754
+ }
755
+
756
+ .article-content code {
757
+ background: var(--color-surface-container);
758
+ color: var(--color-on-surface);
759
+ padding: 0.05em 0.35em;
760
+ font-size: var(--type-meta);
761
+ font-family: var(--font-code);
762
+ }
763
+
764
+ /* `--color-surface-container-lowest` is the mockup's well, but it resolves to
765
+ the page white in the light palette, so the deepest surface that reads in
766
+ both is the container step. */
767
+ .article-content pre {
768
+ background: var(--color-surface-container);
769
+ border-left: 2px solid var(--color-outline-variant);
770
+ padding: var(--space-md);
771
+ margin: var(--space-lg) 0;
772
+ font-size: var(--type-meta);
773
+ line-height: 1.6;
774
+ font-family: var(--font-code);
775
+ }
776
+
777
+ .article-content pre code {
778
+ color: var(--color-on-surface);
779
+ }
780
+
781
+ /* Pull quotes are running text and are read, not glanced at, so they keep the
782
+ body face at prose size. The tonal band and the rule mark the quote. */
783
+ .article-content blockquote {
784
+ background: var(--color-surface-container-low);
785
+ /* Borders Detail dialect: accent spine (secondary / forest), square plate. */
786
+ border: 1.5px solid var(--border-ink);
787
+ border-left: 3px solid var(--color-secondary);
788
+ border-radius: var(--radius-card);
789
+ padding: var(--space-md) var(--space-lg);
790
+ margin: var(--space-xl) 0;
791
+ font-family: var(--font-body);
792
+ font-style: normal;
793
+ font-size: var(--type-prose);
794
+ font-weight: var(--weight-body);
795
+ line-height: var(--leading-prose);
796
+ color: var(--color-on-surface);
797
+ }
798
+
799
+ /* A poem is set verse by verse at prose size — same as notes and body copy. */
800
+ .article-content blockquote .poem,
801
+ .article-content .poem {
802
+ font-family: var(--font-body);
803
+ font-style: normal;
804
+ font-size: var(--type-prose);
805
+ font-weight: var(--weight-body);
806
+ line-height: var(--leading-prose);
807
+ color: var(--color-on-surface);
808
+ }
809
+
810
+ .article-content .poem p {
811
+ font-size: inherit;
812
+ font-weight: inherit;
813
+ }
814
+
815
+ /* ── Theorem / proof / key-takeaways callout ──
816
+ Authored per article as `<div class="article-note" markdown="1">`; styled
817
+ here, never injected. Ochre wash + tertiary spine = scannable field note. */
818
+ .article-content .article-note {
819
+ position: relative;
820
+ background: color-mix(in srgb, var(--color-tertiary) 10%, var(--color-panel-contrast));
821
+ border: 1px solid color-mix(in srgb, var(--color-tertiary) 22%, var(--border-gentle-subtle));
822
+ padding: var(--space-lg) var(--space-lg) var(--space-lg) calc(var(--space-lg) + 3px);
823
+ margin: var(--space-xl) 0;
824
+ font-size: var(--type-subtitle);
825
+ font-weight: var(--weight-body);
826
+ line-height: var(--leading-prose);
827
+ color: var(--color-on-surface);
828
+ }
829
+
830
+ [data-theme="dark"] .article-content .article-note {
831
+ background: color-mix(in srgb, var(--color-tertiary) 6%, var(--color-panel-contrast));
832
+ }
833
+
834
+ .article-content .article-note::before {
835
+ content: "";
836
+ position: absolute;
837
+ inset: 0 auto 0 0;
838
+ width: 3px;
839
+ background: var(--color-tertiary);
840
+ }
841
+
842
+ .article-content .article-note strong {
843
+ color: var(--color-on-surface);
844
+ }
845
+
846
+ /* A formula band inside a callout, as in the mockup's axiomatic block. */
847
+ .article-content .article-note .math-display,
848
+ .article-content .article-note mjx-container[display="true"] {
849
+ background: var(--color-surface-dim);
850
+ padding: var(--space-sm) var(--space-md);
851
+ margin: var(--space-md) 0;
852
+ }
853
+
854
+ /* ── Benchmark and data tables ──
855
+ Authored as GFM; the ledger treatment is rules between rows, a label-voice
856
+ header, and figures that line up. */
857
+ /* No table surface: the header band and the hover row are the only fills, so
858
+ both read against the page canvas in either palette. */
859
+ .article-content table,
860
+ .article-content .table-csv {
861
+ margin: var(--space-xl) 0;
862
+ font-size: var(--type-dek);
863
+ font-variant-numeric: tabular-nums;
864
+ background: transparent;
865
+ }
866
+
867
+ .article-content th,
868
+ .article-content td,
869
+ .article-content .table-csv th,
870
+ .article-content .table-csv td {
871
+ border: 0;
872
+ border-bottom: 1px solid var(--border-gentle-subtle);
873
+ padding: var(--space-sm) var(--space-md);
874
+ }
875
+
876
+ /* GFM column alignment. Markdown's `|--:|` becomes align="right" on the cell,
877
+ which is a presentational hint and therefore loses to any author rule —
878
+ including the reset's `th { text-align: inherit }`. Numeric columns in a
879
+ results table are exactly where alignment carries meaning, so honour what
880
+ the author wrote. */
881
+ .article-content th[align="left"],
882
+ .article-content td[align="left"] {
883
+ text-align: left;
884
+ }
885
+
886
+ .article-content th[align="center"],
887
+ .article-content td[align="center"] {
888
+ text-align: center;
889
+ }
890
+
891
+ .article-content th[align="right"],
892
+ .article-content td[align="right"] {
893
+ text-align: right;
894
+ font-variant-numeric: tabular-nums;
895
+ }
896
+
897
+ .article-content thead th,
898
+ .article-content .table-csv thead th {
899
+ background: var(--color-surface-container);
900
+ font-family: var(--font-body);
901
+ font-size: var(--type-body);
902
+ font-weight: var(--weight-emphasis);
903
+ letter-spacing: normal;
904
+ color: var(--color-legend);
905
+ }
906
+
907
+ /* Zebra striping fights the row rules; the hover row is the reading aid. */
908
+ .article-content tbody tr:nth-child(even),
909
+ .article-content .table-csv tbody tr:nth-child(even) td {
910
+ background: transparent;
911
+ }
912
+
913
+ .article-content tbody tr:hover,
914
+ .article-content .table-csv tbody tr:hover {
915
+ background: var(--color-surface-container-low);
916
+ }
917
+
918
+ .article-content tbody tr:last-child td {
919
+ border-bottom: 0;
920
+ }
921
+
922
+ /* ── Notation on demand ── */
923
+ /* The Mermaid plate used to live here, at the container-low step. It has moved
924
+ to article.css and moved up one step, to the container step, because
925
+ container-low sits between the cluster fill (panel-contrast) and the node
926
+ fill (surface-card) that `mermaidConfig()` paints — so subgraph boxes and
927
+ nodes were dissolving into their own plate on this page. */
928
+
929
+ /* Display math takes its spacing from article.css, where the `.math-display`
930
+ wrapper carries a margin measured against the paragraph gap `.article-content
931
+ p` sets in this file. Do not override the margin here: the wrapper's padding
932
+ stays either way, so a larger margin only adds to a gap that is already the
933
+ one we want. */
934
+
935
+ /* A caption is a sentence about the figure — body face, description size
936
+ (body − 1pt), upright. Same treatment for image, video, YouTube, Plotly.
937
+ Softened toward variant so the legend reads as apparatus, not body. */
938
+ .article-content figcaption,
939
+ .article-content figure figcaption {
940
+ font-family: var(--font-body);
941
+ font-size: var(--type-subtitle);
942
+ font-style: normal;
943
+ font-weight: var(--weight-body);
944
+ line-height: 1.55;
945
+ letter-spacing: normal;
946
+ color: var(--color-legend);
947
+ margin-top: var(--space-sm);
948
+ }
949
+
950
+ .article-content hr {
951
+ margin: var(--space-2xl) 0;
952
+ }
953
+
954
+ /* ══ Marginalia: citations and the reference apparatus ══ */
955
+ .article-content .citation-sup {
956
+ font-family: var(--font-mono);
957
+ font-size: var(--type-meta);
958
+ }
959
+
960
+ .article-content .citation-sup a.citation {
961
+ color: var(--color-primary);
962
+ text-decoration: none;
963
+ }
964
+
965
+ .article-content .citation-sup a.citation:hover {
966
+ text-decoration: underline;
967
+ }
968
+
969
+ .article-content .article-references {
970
+ margin-top: var(--space-3xl);
971
+ padding-top: var(--space-lg);
972
+ border-top: 1px solid var(--border-gentle-divider);
973
+ }
974
+
975
+ /* The heading is an h2, so it would otherwise collect the section rule above
976
+ on top of the apparatus rule — two hairlines and a hole. Keep the ## face;
977
+ only drop the duplicated top rule. */
978
+ .article-content .article-references .section-title {
979
+ margin-top: 0;
980
+ padding-top: 0;
981
+ border-top: 0;
982
+ margin-bottom: var(--space-md);
983
+ }
984
+
985
+ .article-content .article-references .bibliography {
986
+ counter-reset: detail-ref;
987
+ list-style: none;
988
+ margin-left: 0;
989
+ padding-left: 0;
990
+ }
991
+
992
+ .article-content .article-references .bibliography>li {
993
+ counter-increment: detail-ref;
994
+ display: flex;
995
+ gap: var(--space-sm);
996
+ padding: 0.3rem var(--space-xs);
997
+ margin-bottom: 0;
998
+ font-family: var(--font-mono);
999
+ /* The floor of the apparatus, one step under the caption. It was 0.78rem,
1000
+ which in the notation face against a 1.1rem column was small enough that
1001
+ a reference read as a footnote to a footnote. */
1002
+ font-size: var(--type-meta);
1003
+ line-height: 1.55;
1004
+ color: var(--color-on-surface-variant);
1005
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-default);
1006
+ }
1007
+
1008
+ .article-content .article-references .bibliography>li:hover {
1009
+ background: var(--color-surface-container-low);
1010
+ }
1011
+
1012
+ .article-content .article-references .bibliography>li::before {
1013
+ content: "[" counter(detail-ref) "]";
1014
+ flex: none;
1015
+ color: var(--color-primary);
1016
+ }
1017
+
1018
+ /* ══ Prev / next ══
1019
+ Two panels, ordered by date upstream, replacing the bare text links. */
1020
+ nav.text-page-nav {
1021
+ display: grid;
1022
+ grid-template-columns: 1fr;
1023
+ gap: var(--space-md);
1024
+ align-items: stretch;
1025
+ margin-top: var(--space-xl);
1026
+ width: 100%;
1027
+ max-width: var(--reading-measure);
1028
+ }
1029
+
1030
+ @media (min-width: 640px) {
1031
+ nav.text-page-nav {
1032
+ grid-template-columns: 1fr 1fr;
1033
+ }
1034
+ }
1035
+
1036
+ nav.text-page-nav>a {
1037
+ display: flex;
1038
+ align-items: center;
1039
+ gap: var(--space-xs);
1040
+ padding: var(--space-md);
1041
+ background: var(--color-background);
1042
+ border: 1.5px solid var(--border-ink);
1043
+ border-radius: var(--radius-card);
1044
+ color: var(--color-on-surface);
1045
+ font-family: var(--font-label);
1046
+ font-size: var(--type-meta);
1047
+ letter-spacing: 0.04em;
1048
+ text-decoration: none;
1049
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-default),
1050
+ border-color var(--motion-duration-fast) var(--motion-ease-default),
1051
+ color var(--motion-duration-fast) var(--motion-ease-default);
1052
+ }
1053
+
1054
+ nav.text-page-nav>a:hover {
1055
+ background: var(--color-surface-container-high);
1056
+ border-color: var(--color-primary);
1057
+ color: var(--color-primary);
1058
+ text-decoration: none;
1059
+ }
1060
+
1061
+ nav.text-page-nav>a:last-child {
1062
+ justify-content: flex-end;
1063
+ text-align: right;
1064
+ }
1065
+
1066
+ /* First and last entries emit an empty <span> on the missing side, which is
1067
+ fine — it just holds the column. A nav with no link at all, though, would be
1068
+ nothing but a rule and a gap. */
1069
+ nav.text-page-nav:not(:has(a)) {
1070
+ display: none;
1071
+ }
1072
+
1073
+ /* ══ Cite this text block ══
1074
+ Dedicated block below controls, rendered using the standard code-block chrome. */
1075
+ .article-cite-block {
1076
+ margin-top: var(--space-xl);
1077
+ width: 100%;
1078
+ max-width: var(--reading-measure);
1079
+ }
1080
+
1081
+ .article-cite-label {
1082
+ margin: 0 0 var(--space-xs);
1083
+ color: var(--color-on-surface-variant);
1084
+ font-family: var(--font-label);
1085
+ font-size: var(--type-meta);
1086
+ letter-spacing: 0.08em;
1087
+ }
1088
+
1089
+ .article-cite-block .code-block {
1090
+ margin: 0;
1091
+ background: var(--color-background);
1092
+ border: 1.5px solid var(--border-ink);
1093
+ border-radius: var(--radius-card);
1094
+ overflow: hidden;
1095
+ }
1096
+
1097
+ .article-cite-block .code-block pre {
1098
+ padding: var(--space-md);
1099
+ font-family: var(--font-technical);
1100
+ font-size: var(--type-meta);
1101
+ line-height: 1.55;
1102
+ color: var(--color-on-surface);
1103
+ white-space: pre;
1104
+ }
1105
+
1106
+ /* ══ Column rhythm at the breakpoints ══ */
1107
+ @media (max-width: 767px) {
1108
+
1109
+ .text-page-main,
1110
+ .text-reading-column,
1111
+ .article-content {
1112
+ width: 100% !important;
1113
+ max-width: 100% !important;
1114
+ }
1115
+
1116
+ .text-detail-sidebar .article-sidebar-heading,
1117
+ .text-detail-sidebar .article-meta {
1118
+ padding-left: var(--space-md);
1119
+ padding-right: var(--space-md);
1120
+ }
1121
+
1122
+ .article-content h2 {
1123
+ font-size: var(--type-section-lg);
1124
+ }
1125
+
1126
+ /* A four-column benchmark table wanted 468px inside a 355px column at 390px
1127
+ and pushed the whole document sideways — every page on the site got a
1128
+ horizontal scrollbar because of one article. The ledger cell padding is
1129
+ most of the excess (35px per cell), so it comes down first; what is still
1130
+ too wide then scrolls inside the table rather than moving the page. The
1131
+ runtime emits the table with no wrapper, so the table itself has to be the
1132
+ scroll container. */
1133
+ .article-content table,
1134
+ .article-content .table-csv {
1135
+ display: block;
1136
+ width: 100%;
1137
+ max-width: 100%;
1138
+ overflow-x: auto;
1139
+ }
1140
+
1141
+ .article-content th,
1142
+ .article-content td,
1143
+ .article-content .table-csv th,
1144
+ .article-content .table-csv td {
1145
+ padding: var(--space-xs) var(--space-sm);
1146
+ }
1147
+ }
1148
+
1149
+ /* ══ Related Texts ══
1150
+ Neighbors from writing_index.json xcites. Section pills are neutral gray;
1151
+ card titles pick up the linked collection's triad colour on hover. */
1152
+ .article-related-box {
1153
+ margin-top: var(--space-xl);
1154
+ padding: 0 var(--space-md) var(--space-lg);
1155
+ }
1156
+
1157
+ @media (max-width: 1023px) {
1158
+ .article-related-box {
1159
+ display: none;
1160
+ }
1161
+ }
1162
+
1163
+ .article-related-title {
1164
+ font-family: var(--font-label);
1165
+ font-size: var(--type-label-lg);
1166
+ letter-spacing: 0.08em;
1167
+ text-transform: uppercase;
1168
+ color: var(--color-on-surface);
1169
+ font-weight: 600;
1170
+ margin: 0 0 var(--space-md);
1171
+ padding: 0;
1172
+ }
1173
+
1174
+ .article-related-section+.article-related-section {
1175
+ margin-top: var(--space-lg);
1176
+ }
1177
+
1178
+ .article-related-section-pill {
1179
+ display: flex;
1180
+ align-items: center;
1181
+ justify-content: center;
1182
+ width: fit-content;
1183
+ margin: 0 0 var(--space-md);
1184
+ padding: 0.2rem 0.7rem;
1185
+ min-height: 1.65rem;
1186
+ border-radius: 0;
1187
+ background: color-mix(in srgb, var(--color-on-surface-variant) 18%, transparent);
1188
+ color: var(--color-on-surface-variant);
1189
+ font-family: var(--font-label);
1190
+ font-size: var(--type-label);
1191
+ font-weight: var(--weight-label);
1192
+ letter-spacing: 0.04em;
1193
+ text-transform: uppercase;
1194
+ line-height: 1;
1195
+ }
1196
+
1197
+ .article-related-list {
1198
+ display: flex;
1199
+ flex-direction: column;
1200
+ gap: var(--space-2xs, 0.35rem);
1201
+ }
1202
+
1203
+ .article-related-card {
1204
+ display: flex;
1205
+ flex-direction: column;
1206
+ gap: var(--space-2xs, 0.25rem);
1207
+ padding: var(--space-sm) var(--space-md);
1208
+ background: var(--color-surface-container-low);
1209
+ border-radius: var(--radius-card);
1210
+ border: 1px solid var(--border-gentle-subtle);
1211
+ text-decoration: none;
1212
+ transition:
1213
+ border-color var(--motion-duration-fast) var(--motion-ease-default),
1214
+ background-color var(--motion-duration-fast) var(--motion-ease-default);
1215
+ }
1216
+
1217
+ .article-related-card:hover {
1218
+ border-color: color-mix(in srgb, var(--color-on-surface) 28%, var(--border-gentle-subtle));
1219
+ background: var(--color-surface-container);
1220
+ }
1221
+
1222
+ .article-related-card--articles:hover {
1223
+ border-color: color-mix(in srgb, var(--triad-secondary) 40%, var(--border-gentle-subtle));
1224
+ }
1225
+
1226
+ .article-related-card--posts:hover {
1227
+ border-color: color-mix(in srgb, var(--triad-tertiary) 40%, var(--border-gentle-subtle));
1228
+ }
1229
+
1230
+ .article-related-card--projects:hover {
1231
+ border-color: color-mix(in srgb, var(--triad-primary) 40%, var(--border-gentle-subtle));
1232
+ }
1233
+
1234
+ .article-related-card-title {
1235
+ font-family: var(--font-content-title);
1236
+ font-size: calc(var(--type-subtitle) - 1pt);
1237
+ font-weight: 400;
1238
+ line-height: 1.3;
1239
+ margin: 0;
1240
+ }
1241
+
1242
+ .article-related-card-title a {
1243
+ color: var(--color-on-surface);
1244
+ text-decoration: none;
1245
+ transition: color var(--motion-duration-fast) var(--motion-ease-default);
1246
+ }
1247
+
1248
+ .article-related-card--articles:hover .article-related-card-title a {
1249
+ color: var(--triad-secondary);
1250
+ }
1251
+
1252
+ .article-related-card--posts:hover .article-related-card-title a {
1253
+ color: var(--triad-tertiary);
1254
+ }
1255
+
1256
+ .article-related-card--projects:hover .article-related-card-title a {
1257
+ color: var(--triad-primary);
1258
+ }
1259
+
1260
+ .article-related-card-dates {
1261
+ font-family: var(--font-mono);
1262
+ font-size: var(--type-meta);
1263
+ color: var(--color-on-surface);
1264
+ line-height: 1.4;
1265
+ }
1266
+
1267
+ .article-related-card-dates-sep {
1268
+ color: var(--color-on-surface);
1269
+ opacity: 0.55;
1270
+ }