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
data/DESIGN.md ADDED
@@ -0,0 +1,1434 @@
1
+ ---
2
+ version: "2.0"
3
+ name: "Brazilian Ledger"
4
+ description: >-
5
+ Editorial-brutalist personal site, rebuilt on the "Architectural Ledger"
6
+ component layer. Pure-white (or near-black) paper, square cards with thin
7
+ clear ink frames (Reimagined Borders), a slate/forest/ochre triad with
8
+ raised chroma (2026-09 vivid pass), serif display titles over a humanist
9
+ sans body, monospace for notation, and translucent "cubist shards" reduced
10
+ to two canonical polygons. Depth is a four-step surface ladder — canvas,
11
+ panel, card, hover — rather than shadow. Every card, row, chip, meta strip
12
+ and CTA comes from one shared JavaScript block library. Two complete
13
+ palettes, light and dark, selected by data-theme on <html>.
14
+ omitted:
15
+ - name: "Elevation"
16
+ reason: >-
17
+ Deliberate. Content carries no shadow at all; the only two box-shadows
18
+ in the build are on floating overlays. Depth is the surface ladder plus
19
+ hairline borders.
20
+
21
+ # ─────────────────────────────────────────────────────────────
22
+ # Colors. Two full palettes. Source of truth: _data/themes/*.yml
23
+ # rendered into CSS custom properties by _includes/theme/theme-vars-block.liquid.
24
+ # Light is also bound to :root, so it is the no-JS default.
25
+ # ─────────────────────────────────────────────────────────────
26
+ colors:
27
+ light:
28
+ primary: "#1c5a7a" # ice slate (vivid 2026-09)
29
+ secondary: "#287445" # forest green (vivid 2026-09)
30
+ tertiary: "#746f30" # ochre gold (vivid 2026-09)
31
+ primaryContainer: "#a8c5d4"
32
+ secondaryContainer: "#b4d4c0"
33
+ tertiaryContainer: "#d0ceb0"
34
+ onPrimaryContainer: "#0a1419"
35
+ onSecondaryContainer: "#0c1710"
36
+ onTertiaryContainer: "#17160c"
37
+ onPrimary: "#ffffff"
38
+ onSecondary: "#ffffff"
39
+ onTertiary: "#ffffff"
40
+ # ── The Architectural Ledger surface ladder ──
41
+ # White-paper edition 2026-09: canvas is pure #ffffff. Cards rest
42
+ # transparent (panel/canvas and shards show through) and take a fill
43
+ # only on hover. Panel contrast is near-neutral grey (#f4f4f4) — cream
44
+ # pulled ochre, blue-grey still read as slate; equal-RGB greys keep the
45
+ # ladder quiet. Stitch alabaster #fcf9f6 is deliberately not used
46
+ # (yellow cast on printed CV).
47
+ #
48
+ # The triad keeps the Ledger hues (slate / forest / ochre) with higher
49
+ # chroma — Stitch inspires vividness, it does not replace the system.
50
+ background: "#ffffff" # step 1 — canvas, white paper
51
+ backgroundSecondary: "#f4f4f4"
52
+ panelContrast: "#f4f4f4" # step 2 — panel / alternating band
53
+ surfaceCard: "transparent" # step 3 — card at rest (no fill)
54
+ surfaceCardContrast: "transparent" # step 3b — same on contrast panel
55
+ surfaceHigh: "#ebebeb" # step 4 — hover
56
+ surfaceHighContrast: "#e2e2e2" # step 4b — hover on contrast card
57
+ # ── The older Material surface ramp, still emitted and still used ──
58
+ surface: "#ffffff"
59
+ surfaceContainerLowest: "#ffffff"
60
+ surfaceContainerLow: "#f4f4f4"
61
+ surfaceContainer: "#ebebeb"
62
+ surfaceContainerHigh: "#e2e2e2"
63
+ surfaceContainerHighest: "#d8d8d8"
64
+ surfaceVariant: "#d8d8d8"
65
+ surfaceDim: "#d8d8d8"
66
+ panelGrey: "#f1f1f1" # color-mix(surfaceContainer 82%, background)
67
+ onSurface: "#000000"
68
+ onSurfaceVariant: "#5e5d59"
69
+ onBackground: "#000000"
70
+ outline: "#5c5c5c"
71
+ outlineVariant: "#b8b8b8" # every hairline is an opacity step of this
72
+ link: "#1c5a7a"
73
+ linkHover: "#2a6f92"
74
+ inversePrimary: "#a4c8da"
75
+ inverseSurface: "#303030"
76
+ inverseOnSurface: "#f2f2f2"
77
+ error: "#ba1a1a"
78
+ onError: "#ffffff"
79
+ errorContainer: "#ffdad6"
80
+ onErrorContainer: "#93000a"
81
+ resYoutube: "#c62828"
82
+ resPodcast: "#2e7d32"
83
+ resDoc: "#b45309"
84
+ chartProject: "#6fb387"
85
+ sidebarThumbnailBackground: "#ebebeb"
86
+ dark:
87
+ primary: "#9dc2d6"
88
+ secondary: "#8fc29f"
89
+ tertiary: "#d7d4a5"
90
+ primaryContainer: "#475760"
91
+ secondaryContainer: "#405748"
92
+ tertiaryContainer: "#67664f"
93
+ onPrimaryContainer: "#b6d1e0"
94
+ onSecondaryContainer: "#abd1b7"
95
+ onTertiaryContainer: "#e1dfbc"
96
+ onPrimary: "#0f1a22"
97
+ onSecondary: "#141a15"
98
+ onTertiary: "#1b1c12"
99
+ # ── The Architectural Ledger surface ladder ──
100
+ # Re-based 2026-09 in two moves. First, neutralised: the old values had G
101
+ # as the highest channel across the whole ladder, which read as an
102
+ # olive/green cast rather than black. Then widened: panel and hover keep
103
+ # the ~+4 / ~+6 L* steps; resting cards are transparent so shards and
104
+ # the panel show through. Vivid 2026-09: triad chroma raised.
105
+ background: "#0c0c0e" # step 1 — canvas, near-black, neutral
106
+ panelContrast: "#141418" # step 2 — panel / alternating band
107
+ surfaceCard: "transparent" # step 3 — card at rest (no fill)
108
+ surfaceHigh: "#26262b" # step 4 — hover, chip fill
109
+ # ── The older Material surface ramp, still emitted and still used ──
110
+ backgroundSecondary: "#141418"
111
+ surface: "#111114" # distinct from background in dark
112
+ surfaceContainerLowest: "#08080a"
113
+ surfaceContainerLow: "#17171b"
114
+ surfaceContainer: "#1f1f24"
115
+ surfaceContainerHigh: "#26262b"
116
+ surfaceContainerHighest: "#33342e" # documented here only — not a field in dark.yml
117
+ surfaceVariant: "#2e2e34"
118
+ surfaceDim: "#060607"
119
+ panelGrey: "#22231f" # documented here only — not a field in dark.yml
120
+ onSurface: "#ffffff"
121
+ onSurfaceVariant: "#b8b7ae"
122
+ onBackground: "#ffffff"
123
+ outline: "#93938d"
124
+ outlineVariant: "#45454c" # neutralised 2026-09; every hairline is an opacity step of this
125
+ link: "#a9cbdc"
126
+ linkHover: "#bfd7e4"
127
+ inversePrimary: "#255b75"
128
+ inverseSurface: "#e8e6e1"
129
+ inverseOnSurface: "#141413"
130
+ error: "#ffb4ab"
131
+ onError: "#690005"
132
+ errorContainer: "#93000a"
133
+ onErrorContainer: "#ffdad6"
134
+ resYoutube: "#de6461"
135
+ resPodcast: "#5dc462"
136
+ resDoc: "#e4b268"
137
+ chartProject: "#90c0a3"
138
+ sidebarThumbnailBackground: "#1f1f24"
139
+
140
+ # Borders are never a raw color. They are opacity steps of outlineVariant
141
+ # (gentle ladder) plus denser ink-frame mixes of onSurface for sketchbook
142
+ # cards (--border-ink / --border-ink-strong), identical token names in both
143
+ # themes.
144
+ borders:
145
+ gentleFaint: "color-mix(in srgb, {colors.*.outlineVariant} 22%, transparent)"
146
+ gentleSubtle: "color-mix(in srgb, {colors.*.outlineVariant} 32%, transparent)"
147
+ gentleDivider: "color-mix(in srgb, {colors.*.outlineVariant} 35%, transparent)"
148
+ gentle: "color-mix(in srgb, {colors.*.outlineVariant} 62%, transparent)"
149
+ gentleHover: "color-mix(in srgb, {colors.*.outlineVariant} 78%, transparent)"
150
+ ink: "color-mix(in srgb, {colors.*.onSurface} 72%, transparent)"
151
+ inkStrong: "color-mix(in srgb, {colors.*.onSurface} 88%, transparent)"
152
+
153
+ # ─────────────────────────────────────────────────────────────
154
+ # Typography. Root font-size is 110%, so 1rem ≈ 17.6px.
155
+ # Faces live once in `_data/themes/shared.yml` (not per colour theme).
156
+ # ─────────────────────────────────────────────────────────────
157
+ typography:
158
+ fontFamilies:
159
+ title: "'Cinzel Decorative', serif"
160
+ subtitle: "'Newsreader', serif"
161
+ label: "'Marcellus SC', serif"
162
+ body: "'Newsreader', sans-serif"
163
+ mono: "'Courier Prime', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace"
164
+ code: "'Courier Prime', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace"
165
+ rootFontSize: "110%"
166
+
167
+ pageTitle: # 404 only; the home hero no longer uses it
168
+ fontFamily: "{typography.fontFamilies.title}"
169
+ fontSize: "2.75rem"
170
+ fontSizeMd: "3.75rem"
171
+ fontWeight: 400
172
+ homeHeroTitle: # copy-column cqi; floor is the band title
173
+ fontFamily: "{typography.fontFamilies.title}"
174
+ fontSize: "clamp(var(--type-section), calc(100cqi * 0.92 / 14.5), var(--type-page-lg))"
175
+ fontSizeSm: "clamp(var(--type-section-lg), calc(100cqi * 0.92 / 14.5), var(--type-page-lg))" # ≥640px, same step as sectionTitle
176
+ fontWeight: 400
177
+ pageHeaderTitle: # listing pages
178
+ fontFamily: "{typography.fontFamilies.title}"
179
+ fontSize: "2.35rem"
180
+ fontSizeMd: "2.85rem"
181
+ fontWeight: 400
182
+ textTitle: # article / post detail title
183
+ fontFamily: "{typography.fontFamilies.subtitle}"
184
+ fontSize: "1.95rem"
185
+ fontSizeMd: "2.45rem"
186
+ fontWeight: 400
187
+ sectionTitle: # band headers, register rails, banners
188
+ fontFamily: "{typography.fontFamilies.title}"
189
+ fontSize: "1.75rem"
190
+ fontSizeSm: "2.05rem"
191
+ fontWeight: 400
192
+ cardTitle:
193
+ fontFamily: "{typography.fontFamilies.subtitle}"
194
+ fontSize: "1.4rem"
195
+ fontWeight: 400
196
+ subtitle:
197
+ fontFamily: "{typography.fontFamilies.subtitle}"
198
+ fontSize: "1.05rem"
199
+ fontWeight: 400
200
+ body:
201
+ fontFamily: "{typography.fontFamilies.body}"
202
+ fontSize: "1rem"
203
+ fontWeight: 300
204
+ lineHeight: 1.5
205
+ prose: # .article-content, long-form reading
206
+ fontFamily: "{typography.fontFamilies.body}"
207
+ fontSize: "1.2rem" # mobile prose base at 110% root
208
+ fontSizeMd: "1.25rem" # desktop prose base
209
+ sizeOffset: "-4pt … +4pt" # --prose-size-offset; 0 is this default
210
+ lineHeight: 1.6
211
+ dropCapLines: 2 # --drop-cap-lines; _config.yml drop_cap_lines
212
+ color: "{colors.*.onSurfaceVariant}"
213
+ paragraphMargin: "1.15em"
214
+ proseHeading2: # the display serif, with a rule above
215
+ fontFamily: "{typography.fontFamilies.title}"
216
+ fontSize: "1.7em" # 1.5em below 768px
217
+ fontWeight: 400
218
+ lineHeight: 1.2
219
+ borderTop: "1px solid {borders.gentleDivider}"
220
+ proseHeading3: # the label face, uppercase, primary
221
+ fontFamily: "{typography.fontFamilies.label}"
222
+ fontSize: "0.95em"
223
+ fontWeight: 400
224
+ letterSpacing: "0.1em"
225
+ textTransform: "uppercase"
226
+ color: "{colors.*.primary}"
227
+ proseHeading4:
228
+ fontFamily: "{typography.fontFamilies.body}"
229
+ fontSize: "1em"
230
+ fontWeight: 400
231
+ marginalia:
232
+ fontFamily: "{typography.fontFamilies.label}"
233
+ fontSize: "0.75rem"
234
+ fontWeight: 400
235
+ letterSpacing: "0.05em"
236
+ cardMeta:
237
+ fontFamily: "{typography.fontFamilies.label}"
238
+ fontSize: "0.8125rem"
239
+ fontWeight: 400
240
+ letterSpacing: "0.04em"
241
+ cardTag:
242
+ fontFamily: "{typography.fontFamilies.label}"
243
+ fontSize: "0.75rem"
244
+ fontWeight: 400
245
+ letterSpacing: "0.03em"
246
+ codeTechnical: # the fourth role — notation
247
+ fontFamily: "{typography.fontFamilies.mono}"
248
+ fontSize: "0.8125rem"
249
+ lineHeight: 1.45
250
+ fontVariantNumeric: "tabular-nums"
251
+ codeTechnicalSm:
252
+ fontFamily: "{typography.fontFamilies.mono}"
253
+ fontSize: "0.75rem"
254
+ lineHeight: 1.4
255
+ fontVariantNumeric: "tabular-nums"
256
+ code: # inline code inside prose
257
+ fontFamily: "{typography.fontFamilies.mono}"
258
+ fontSize: "0.85em"
259
+
260
+ # The apparatus inside an article descends from the prose in four steps, all
261
+ # in onSurfaceVariant. Every one of them is a sentence, which is why none
262
+ # takes the label or notation voice for its size.
263
+ proseQuote: # blockquote, `>` in Markdown
264
+ fontFamily: "{typography.fontFamilies.body}"
265
+ fontSize: "0.95rem" # identical to proseNote by intent
266
+ lineHeight: "{typography.body.lineHeight}"
267
+ plate: "surfaceContainerLow, 2px tertiary left rule"
268
+ proseNote: # .article-note, authored per article
269
+ fontFamily: "{typography.fontFamilies.body}"
270
+ fontSize: "0.95rem"
271
+ lineHeight: "{typography.body.lineHeight}"
272
+ plate: "surfaceCard, 3px tertiary bar"
273
+ proseCaption: # figcaption, and the lightbox caption
274
+ fontFamily: "{typography.fontFamilies.body}"
275
+ fontSize: "0.9rem"
276
+ lineHeight: 1.55
277
+ proseBibliography: # .article-references .bibliography li
278
+ fontFamily: "{typography.fontFamilies.mono}"
279
+ fontSize: "0.85rem"
280
+ lineHeight: 1.55
281
+
282
+ # ─────────────────────────────────────────────────────────────
283
+ # Shapes. Soft paper radius on cards; true pills stay circular.
284
+ # ─────────────────────────────────────────────────────────────
285
+ rounded:
286
+ none: "0"
287
+ card: "0" # --radius-card; square Borders Edition
288
+ full: "9999px" # footer contact discs, timeline dot key
289
+ tailwindScale: # remapped in assets/tailwind-play.js
290
+ DEFAULT: "0.125rem"
291
+ lg: "0.25rem"
292
+ xl: "0.5rem"
293
+ full: "0.75rem" # NOTE: not a pill
294
+
295
+ # The two canonical shard polygons, declared once in assets/css/base.css.
296
+ shards:
297
+ alpha: "polygon(26% 0, 100% 0, 78% 100%, 0 68%)" # --shard-alpha
298
+ beta: "polygon(0 0, 74% 18%, 100% 100%, 18% 84%)" # --shard-beta
299
+ opacity: "12%" # --cubist-shard-opacity
300
+ opacityStrong: "16%" # --cubist-shard-opacity-strong
301
+ utility: ".shard-accent / .shard-accent-beta"
302
+ suppressedBelow: "640px"
303
+
304
+ # ─────────────────────────────────────────────────────────────
305
+ # Spacing. A named scale in custom properties, in assets/css/base.css.
306
+ # Tailwind's default 0.25rem rungs are still in use in the Liquid chrome.
307
+ # ─────────────────────────────────────────────────────────────
308
+ spacing:
309
+ scale:
310
+ xs: "0.25rem" # --space-xs
311
+ sm: "0.5rem" # --space-sm
312
+ md: "1rem" # --space-md
313
+ lg: "1.5rem" # --space-lg
314
+ xl: "2rem" # --space-xl
315
+ "2xl": "3rem" # --space-2xl
316
+ "3xl": "4.5rem" # --space-3xl
317
+ gutter: "2rem" # --gutter
318
+ gutterMobile: "1rem" # --gutter-mobile
319
+ tailwindRungsInUse: ["0", "0.5", "1", "1.5", "2", "2.5", "3", "3.5", "4", "5", "6", "8", "10", "12", "16"]
320
+
321
+ layoutTokens:
322
+ shellMaxWidth: "80rem" # Tailwind max-w-7xl
323
+ navPaddingX: ["1.5rem", "2rem", "2.5rem"] # base / sm / md
324
+ listingPaddingX: ["2rem", "4rem"] # base / md — px-8 md:px-16
325
+ homeBandPaddingX: ["{spacing.gutterMobile}", "{spacing.gutter}"] # base / md
326
+ homeBandPaddingY: ["3rem", "4.5rem"] # base / md — space-2xl / 3xl
327
+ readingMeasure: "45rem" # --reading-measure; every page, prose and media alike
328
+ legacyContainer: "64rem"
329
+ breakpoints:
330
+ sm: "640px"
331
+ md: "768px"
332
+ lg: "1024px"
333
+ xl: "1280px"
334
+ ownBreakpoints: ["639px", "767px", "900px"] # max-width queries in ledger CSS
335
+
336
+ # ─────────────────────────────────────────────────────────────
337
+ # Components. The block library first — it renders most of the site.
338
+ # Source of truth: _includes/content-runtime/43-blocks.html for the markup,
339
+ # assets/css/base.css ("Architectural Ledger blocks") for the styling.
340
+ # ─────────────────────────────────────────────────────────────
341
+ components:
342
+ langPill: # renderLangPill(language)
343
+ typography: "{typography.fontFamilies.label} 0.725rem / 0.08em uppercase"
344
+ padding: "0.1rem 0.4rem"
345
+ border: "1px solid color-mix(in srgb, currentColor 35%, transparent)"
346
+ color: "{colors.*.primary} for EN, {colors.*.secondary} for PT"
347
+ backingField: "language"
348
+ chip: # renderTagChips(tags)
349
+ typography: "{typography.fontFamilies.label} 0.725rem / 0.08em uppercase"
350
+ padding: "0.15rem 0.45rem"
351
+ backgroundColor: "{colors.*.surfaceHigh}"
352
+ textColor: "{colors.*.onSurfaceVariant}"
353
+ hoverBackgroundColor: "{colors.*.surfaceContainerHigh}"
354
+ rounded: "{rounded.none}"
355
+ backingField: "tags"
356
+ metaStrip: # renderMetaStrip([parts])
357
+ typography: "{typography.codeTechnical}"
358
+ letterSpacing: "0.06em"
359
+ textTransform: "uppercase"
360
+ gap: "{spacing.scale.sm}"
361
+ separator: "/ in outlineVariant, only between surviving parts"
362
+ cta: # renderCtaLink(href, label)
363
+ typography: "{typography.fontFamilies.label} 0.8rem / 0.1em uppercase"
364
+ textColor: "{colors.*.primary}"
365
+ hoverTextColor: "{colors.*.linkHover}"
366
+ icon: "→ (U+2192) text glyph, aria-hidden, translateX(4px) on hover"
367
+ sectionBanner: # renderSectionBanner({kicker, title, note})
368
+ kicker: "{typography.codeTechnical}, 0.12em, onSurfaceVariant"
369
+ title: "{typography.sectionTitle}"
370
+ note: "{typography.codeTechnical}, 0.08em, onSurfaceVariant"
371
+ marginBottom: "{spacing.scale.xl}"
372
+ ledgerRow: # renderLedgerRow(item)
373
+ backgroundColor: "{colors.*.surfaceCard}"
374
+ hoverBackgroundColor: "{colors.*.surfaceHigh}"
375
+ padding: "{spacing.scale.lg}"
376
+ rounded: "{rounded.none}"
377
+ boxShadow: "none"
378
+ titleHoverColor: "{colors.*.primary}"
379
+ ledgerCard: # renderArchiveCard(item)
380
+ backgroundColor: "{colors.*.surfaceCard}"
381
+ hoverBackgroundColor: "{colors.*.surfaceHigh}"
382
+ borderColor: "{borders.gentleSubtle}"
383
+ borderWidth: "1px"
384
+ padding: "{spacing.scale.lg}"
385
+ rounded: "{rounded.none}"
386
+ footBorderTop: "1px solid {borders.gentleDivider}"
387
+ systemCard: # renderSystemCard(item)
388
+ inherits: "{components.ledgerCard}"
389
+ extras: "poster, label/status meta strip, repository + article + external links"
390
+ poster: # renderPosterMedia(src, alt)
391
+ aspectRatio: "16 / 9"
392
+ objectFit: "cover"
393
+ backgroundColor: "{colors.*.surfaceContainer}"
394
+ filter: "saturate(0.92) contrast(1.02)"
395
+ hoverFilter: "saturate(1.05) contrast(1)"
396
+ hoverTransform: "scale(1.02)" # suppressed under prefers-reduced-motion
397
+ videoBehaviour: "controls, preload=none — never autoplays"
398
+ pageHeaderStandfirst:
399
+ maxWidth: "{layoutTokens.readingMeasure}"
400
+ fontSize: "1.05rem"
401
+ lineHeight: 1.5 # --leading-body
402
+ backingField: "standfirst on the page header JSON"
403
+ buttonSolid: # .home-cta--solid, .writing-lead-cta
404
+ backgroundColor: "{colors.*.primary}"
405
+ textColor: "{colors.*.onPrimary}"
406
+ padding: "0.85rem {spacing.scale.lg}"
407
+ rounded: "{rounded.none}"
408
+ buttonOutline: # .home-cta--outline
409
+ backgroundColor: "{colors.*.surfaceCard}"
410
+ borderColor: "{borders.gentle}"
411
+ textColor: "{colors.*.primary}"
412
+ hoverBackgroundColor: "{colors.*.surfaceHigh}"
413
+ hoverBorderColor: "{colors.*.primary}"
414
+ filterChip: # .listing-filter-chip, shared controller
415
+ backgroundColor: "{colors.*.background}"
416
+ borderColor: "{borders.gentle}"
417
+ padding: "0.35rem 0.65rem"
418
+ rounded: "{rounded.none}"
419
+ activeBorderColor: "{colors.*.secondary}"
420
+ activeBackgroundColor: "color-mix(in srgb, {colors.*.secondary} 8%, {colors.*.background})"
421
+ input:
422
+ backgroundColor: "{colors.*.background}"
423
+ borderColor: "{borders.gentleSubtle}"
424
+ padding: "0.5rem 0.65rem"
425
+ rounded: "{rounded.none}"
426
+ focusOutline: "2px solid color-mix(in srgb, {colors.*.secondary} 45%, transparent)"
427
+ popover: # .listing-filter-panel, nav menu
428
+ backgroundColor: "{colors.*.surface}"
429
+ borderColor: "{borders.gentle}"
430
+ rounded: "{rounded.none}"
431
+ boxShadow: "0 12px 32px color-mix(in srgb, {colors.*.onSurface} 12%, transparent)"
432
+
433
+ motion:
434
+ stateChange: "0.15s ease" # 23 declarations — the de facto standard
435
+ legacyChrome: "0.2s ease" # 8 declarations, pre-redesign chrome
436
+ mediaFilter: "0.4s ease"
437
+ mediaTransform: ["0.6s ease", "0.7s ease"]
438
+ lightbox: ["160ms ease-out", "180ms ease-out"]
439
+ readingProgress: "scroll(root block) timeline, beNewsreader @supports"
440
+ reducedMotion: "5 rules — see Motion below"
441
+ ---
442
+
443
+ # DESIGN.md — Brazilian Ledger
444
+
445
+ This file **describes the site as it is built today**, in both themes. It is a
446
+ record, not an aspiration: it says what the browser actually renders, not what
447
+ a mockup or an earlier document intended. Where the built site and a mockup
448
+ disagree, the built site wins and this file records the built site.
449
+
450
+ Source of truth for the values above:
451
+
452
+ | Concern | File |
453
+ | :--- | :--- |
454
+ | Palettes | [`_data/themes/light.yml`](_data/themes/light.yml), [`_data/themes/dark.yml`](_data/themes/dark.yml) |
455
+ | Shared type / motion | [`_data/themes/shared.yml`](_data/themes/shared.yml) — fonts and non-colour tokens used by both palettes |
456
+ | Token emission | [`_includes/theme/theme-vars-block.liquid`](_includes/theme/theme-vars-block.liquid) |
457
+ | Tailwind binding | [`_includes/theme/tailwind-theme-colors.json.liquid`](_includes/theme/tailwind-theme-colors.json.liquid), [`_includes/theme/tailwind-theme-fonts.json.liquid`](_includes/theme/tailwind-theme-fonts.json.liquid), [`assets/tailwind-play.js`](assets/tailwind-play.js) |
458
+ | Type, spacing, shards, shared components | [`assets/css/base.css`](assets/css/base.css) |
459
+ | **Block library (markup)** | [`_includes/content-runtime/43-blocks.html`](_includes/content-runtime/43-blocks.html) |
460
+ | Per-page styles | [`assets/css/ledger/`](assets/css/ledger/) — `home`, `writing`, `rascunhos`, `projects`, `sources`, `detail`, `cv` |
461
+ | Long-form prose (superseded in part) | [`assets/css/article.css`](assets/css/article.css) |
462
+ | Print | [`assets/css/cv-print.css`](assets/css/cv-print.css) |
463
+ | Shell chrome | [`_layouts/shell.html`](_layouts/shell.html) |
464
+
465
+ A note on names. The palette files still call the system the **Brazilian
466
+ Ledger**; the component layer the redesign added calls itself the
467
+ **Architectural Ledger**. They are the same design system. Nothing in the code
468
+ reconciles the two names.
469
+
470
+ ---
471
+
472
+ ## Overview
473
+
474
+ The site is a personal editorial archive: CV, long-form articles in English,
475
+ `rascunhos` (drafts) in Portuguese, projects, a bibliography of sources, and
476
+ an interactive network of ideas. The register is **structural rigour with
477
+ artistic soul** — a draughtsman's sheet rather than a SaaS landing page.
478
+
479
+ Four ideas carry it:
480
+
481
+ 1. **Framed paper.** Cards and plates carry a thin, clear ink frame and stay
482
+ square (`--radius-card: 0`). Depth still comes from the surface ladder
483
+ and borders, not drop shadows. Drafting corner brackets and empty
484
+ crosshairs are decoration only — never labels. Italic section-title
485
+ halves and the hero family name take a marker highlight wash.
486
+ 2. **Paper, not glass.** No drop shadows on content. Depth comes from the
487
+ four-step surface ladder and from hairline / ink borders.
488
+ 3. **A ledger voice for data.** Dates, spans, classifications, hosts, counts
489
+ and reference numbers are set in monospace with tabular figures. This is
490
+ the fourth type role, and it is what makes the site read as a register
491
+ rather than a blog.
492
+ 4. **Cubist shards, rationed.** Two canonical polygons, one utility class,
493
+ five uses of it plus one hand-rolled pseudo-element. The motif is a
494
+ signature now rather than a texture. The logo repeats it as three coloured
495
+ facets.
496
+
497
+ **Theme switching.** `<html data-theme="light|dark">` is set before paint by
498
+ [`_includes/theme/shell-theme-boot.html`](_includes/theme/shell-theme-boot.html),
499
+ persisted under `localStorage["site-theme"]`, default `system`. Light is
500
+ additionally bound to `:root`, so it is the no-JS fallback. Both palettes emit
501
+ the *same token names*, so every component is theme-agnostic.
502
+
503
+ **How pages are built.** Listings and detail views are not Liquid. They render
504
+ in the browser from `_content_json` via JavaScript string templates in
505
+ [`_includes/content-runtime/`](_includes/content-runtime/), and the shared
506
+ block library is the bottom of that stack. Only the shell, the page headers,
507
+ the CV register frames, the timeline and the network graph are server-rendered.
508
+
509
+ ---
510
+
511
+ ## The block library
512
+
513
+ This is the most important thing to understand about the current build.
514
+ **Layouts no longer style their own cards.** Nine renderers in
515
+ [`_includes/content-runtime/43-blocks.html`](_includes/content-runtime/43-blocks.html)
516
+ produce every card, row, chip, pill, meta strip and CTA on the site; their CSS
517
+ lives in one block in [`assets/css/base.css`](assets/css/base.css) under
518
+ *Architectural Ledger blocks*. All nine are re-exported onto
519
+ `window.ContentRuntime` by `90-bootstrap.html`.
520
+
521
+ | Renderer | Renders | Root class | Backing field |
522
+ | :--- | :--- | :--- | :--- |
523
+ | `renderLangPill(language)` | a bordered `EN` or `PT` pill | `.lang-pill` | `language` |
524
+ | `renderTagChips(tags, {hrefFor, limit})` | row of filled chips, optionally links | `.ledger-chip-row` | `tags` |
525
+ | `renderMetaStrip([parts], {className})` | mono ledger line with `/` separators | `.ledger-meta.code-technical` | whatever the caller passes |
526
+ | `renderCtaLink(href, label)` | label + `→`, arrow slides on hover | `.ledger-cta` | `url` / an external link field |
527
+ | `renderSectionBanner({kicker, title, note})` | kicker over a section title, note right | `.ledger-section-banner` | static labels |
528
+ | `renderLedgerRow(item, opts)` | dense horizontal entry | `.ledger-row` | `title`, `date`, `language`, `tags`, preview — `opts.metaParts` replaces the date + pill strip |
529
+ | `renderArchiveCard(item, opts)` | vertical grid card | `.ledger-card` | `title`, `date`, `language`, `tags`, preview |
530
+ | `renderSystemCard(item, opts)` | project card with poster | `.ledger-system-card` | `project`, `thumbnail`, `label`, `status`, `tagline`, `tags`, `github`, `article_url`, `external_url` |
531
+ | `renderPosterMedia(src, alt)` | image, or video when the URL ends `.mp4`/`.webm`/`.mov` | `.ledger-poster` | `thumbnail` |
532
+
533
+ `blockFields(item)` is the shared reader underneath them. It takes
534
+ `front_matter.title | date | last_updated | language | tags`, falling back to
535
+ the item's own keys, and calls `listingPreview(item)` for the summary
536
+ (`description` → `excerpt` → an extract from `body_markdown`).
537
+
538
+ ### The house rule
539
+
540
+ > **A block renders nothing when its field is absent.**
541
+
542
+ This is the project's central invariant and it is enforced at the renderer, not
543
+ at the call site. `renderLangPill` returns an empty string for an undeclared
544
+ language. `renderTagChips` returns an empty string for an empty array.
545
+ `renderMetaStrip` drops empty parts *and the separators that would have sat
546
+ beside them*, so a missing date never leaves a dangling slash.
547
+ `renderSectionBanner` returns nothing without a title. `renderCtaLink` returns
548
+ nothing without both an href and a label.
549
+
550
+ Nothing in the library derives a value, counts anything, or substitutes a
551
+ placeholder. There are no reading times, word counts, entry numbers, sequence
552
+ counters, status pills, version badges or fabricated metrics anywhere in the
553
+ build, and an absent field is never rendered as a dash, a placeholder string
554
+ or an "Unknown".
555
+
556
+ This matters because the corpus is uneven: of 67 exported entries, **29
557
+ declare a language**. The pill is therefore absent on more than half the site,
558
+ by design. A card with no tags is a card with no chip row, not a card with an
559
+ empty chip row.
560
+
561
+ Two listings go further and suppress the pill even where the field is set,
562
+ because every entry in them shares one language and the pill would only be
563
+ marking which entries declared the field: the rascunhos ledger
564
+ (`_layouts/blog.html`, all Portuguese) and the writing index
565
+ (`_layouts/articles.html`, all English). It still renders on the home page
566
+ bands, on the sources index — where French, English and Portuguese genuinely
567
+ mix — and on the article detail view.
568
+
569
+ `renderPosterMedia` extends the rule to runtime failure: a thumbnail that
570
+ 404s removes its own `.ledger-poster` wrapper via an inline `onerror`, so a
571
+ dead third-party image leaves no empty plate beNewsreader.
572
+
573
+ ### Where the library is not yet the only source
574
+
575
+ Recorded honestly, because the rule above is the design goal and these are the
576
+ gaps:
577
+
578
+ * **`_layouts/resources.html`** hand-builds the whole source card
579
+ (`article.ledger-card.source-card`) rather than calling `renderArchiveCard`.
580
+ It also forks `renderLangPill` into a local `languagePill()` to get a third
581
+ `.lang-pill--other` variant for sources that are neither English nor
582
+ Portuguese, and forks the pagination controls.
583
+ * **`_layouts/cv.html`** carries three local card shapes — `.cv-entry`,
584
+ `.cv-language`, `.cv-product`. `renderProduct` in particular is structurally
585
+ an archive card built from scratch.
586
+ * **`_layouts/blog.html`** builds its own thumbnail plate
587
+ (`.rascunhos-row-media`) instead of calling `renderPosterMedia`, giving the
588
+ site two poster implementations with different failure behaviour (an inline
589
+ `onerror` attribute versus an attached `error` listener). It also re-parents
590
+ the shared row's chip row into the head and deletes `.ledger-row-foot`.
591
+ * **`_layouts/home.html`** wraps shared leaves in a local `.home-featured`
592
+ shape, and hand-writes its hero CTAs and four section banners in Liquid —
593
+ class-for-class identical to `renderCtaLink` and `renderSectionBanner`, but
594
+ duplicated, because those renderers are JavaScript.
595
+ * **`_layouts/articles.html`** wraps shared leaves in a local `.writing-lead`.
596
+ Its rows now pass their strip in through `renderLedgerRow`'s `metaParts`
597
+ (date, then a `span.writing-revised` when the entry has been rewritten)
598
+ rather than grafting the revision onto `.ledger-meta` after construction.
599
+ * **`_layouts/text.html`** duplicates `renderTagChips` and `renderLangPill` in
600
+ Liquid. This one is structural: `text.html` is the studio-only server-rendered
601
+ path and cannot call the JavaScript renderers. Its markup is kept
602
+ byte-compatible with theirs on purpose, so
603
+ [`assets/css/ledger/detail.css`](assets/css/ledger/detail.css) styles both.
604
+
605
+ [`_layouts/projects.html`](_layouts/projects.html) is the clean case: one
606
+ `renderSystemCard` call and nothing else.
607
+
608
+ ---
609
+
610
+ ## Colours
611
+
612
+ ### Brand triad
613
+
614
+ Three roles, configured per theme, used for **text, borders, rules, links,
615
+ solid call-to-action fills and cubist shards**. The redesign added the one
616
+ exception to the old rule that brand hues are never a fill: the primary
617
+ call-to-action (`.home-cta--solid`, `.writing-lead-cta`, the active filter tab
618
+ on the writing index) is a solid `primary` block with `onPrimary` text.
619
+
620
+ | Role | Light | Dark | Used for |
621
+ | :--- | :--- | :--- | :--- |
622
+ | Primary | `#255b75` ice slate | `#9dc2d6` | Links, family name, CTAs, active tab, `h3` in prose, reading-progress bar, register rules, text selection |
623
+ | Secondary | `#33774c` forest green | `#8fc29f` | Status labels, licence and marginalia chrome, footer, active filter chip |
624
+ | Tertiary | `#6e6c3d` ochre gold | `#d7d4a5` | Detail-page meta, print dates, the third card rule |
625
+
626
+ Contrast against each theme's canvas (re-measured after the 2026-09 vivid pass):
627
+
628
+ | Role | Light vs `#ffffff` | Dark vs `#0c0c0e` |
629
+ | :--- | :--- | :--- |
630
+ | Primary | 7.42 : 1 | 10.35 : 1 |
631
+ | Secondary | 5.41 : 1 | 9.66 : 1 |
632
+ | Tertiary | 5.42 : 1 | 12.89 : 1 |
633
+
634
+ All pass WCAG AA for body text on the canvas. `onPrimary` on `primary` stays
635
+ white-on-slate in light and dark-ink-on-light-slate in dark.
636
+
637
+ The 2026-09 vivid pass raises chroma on the existing Ledger hues rather than
638
+ adopting Stitch pigment hexes. In light the triad still separates mainly by
639
+ hue; in dark `tertiary` remains the brightest accent after body copy.
640
+
641
+ ### The surface ladder
642
+
643
+ The redesign replaced the two-tone band alternation with a four-step ladder.
644
+ Both palettes emit the same three new tokens on top of `background`:
645
+
646
+ | Step | Token | Light | Dark | Used for |
647
+ | :--- | :--- | :--- | :--- | :--- |
648
+ | 1 — canvas | `--color-background` | `#ffffff` | `#0c0c0e` | page, default home band |
649
+ | 2 — panel | `--color-panel-contrast` | `#f4f4f4` | `#141418` | alternating bands, filter decks, CV dossier, writing lead, detail sidebar heading, marginalia panels |
650
+ | 3 — card | `--color-surface-card` | `#ffffff` | `#0c0c0e` | every `.ledger-row`, `.ledger-card`, `.ledger-system-card` at rest (solid background matching ambient layer; masks ambient background grid/patterns) |
651
+ | 4 — hover | `--color-surface-high` | `#ebebeb` | `#26262b` | row and card hover, chip fill |
652
+
653
+ Cards sit with solid surface color matching their parent plane (`#ffffff` canvas / `#f4f4f4` contrast band in light; `#0c0c0e` canvas / `#141418` contrast band in dark) so that ambient mesh and background grid patterns do not bleed through card content. Hover rungs lift via `--color-surface-high`.
654
+
655
+ ### The older Material ramp
656
+
657
+ The eight-step Material surface ramp (`surfaceContainerLowest` through
658
+ `surfaceVariant`, plus `panelGrey`) is still emitted and still used — by the
659
+ nav, the footer, the filter popover, the prose `pre`/`code`/`blockquote` fills,
660
+ the timeline rails and the prev/next panels. It sits beside the ladder rather
661
+ than underneath it, and the two overlap: `surfaceCard` and
662
+ `surfaceContainerLow` differ by 1.037 : 1 in light and 1.014 : 1 in dark.
663
+ Two token families name nearly the same colour.
664
+
665
+ ### Borders
666
+
667
+ Never a raw colour. Five opacity steps of `outlineVariant`, shared by both
668
+ themes: `gentleFaint` 22%, `gentleSubtle` 32%, `gentleDivider` 35%, `gentle`
669
+ 62%, `gentleHover` 78%.
670
+
671
+ ### Semantic and media colours
672
+
673
+ `error` / `errorContainer`, plus three resource-type accents that lighten in
674
+ dark (`resYoutube` `#c62828` → `#ef5350`, `resPodcast` `#2e7d32` → `#66bb6a`,
675
+ `resDoc` `#b45309` → `#ffb74d`). Note that the sources page does **not** use
676
+ them: `assets/css/ledger/sources.css` colours its medium labels and filter
677
+ swatches from the brand triad instead (`primary` for video, `secondary` for
678
+ audio, `tertiary` for documents, `outline` for websites), and every label
679
+ carries its own word so the hue never has to carry the reading alone.
680
+
681
+ The CV timeline carries the one genuinely categorical scale in the build, in
682
+ `_data/themes/*.yml` under `timeline:` and emitted as `--timeline-employment`
683
+ … `--timeline-post`. Six kinds of event are drawn at once and have to be told
684
+ apart, which three inks cannot do — before this existed, employment shared
685
+ `primary` with article and product shared `tertiary` with post. Hues are
686
+ spread roughly 18 / 42 / 140 / 168 / 200 / 245 and desaturated to stay in the
687
+ editorial register; dark lifts each in lightness rather than reusing the light
688
+ values.
689
+
690
+ It is held outside `colors:` on purpose. A `.page-accent--*` class rebinds
691
+ `--color-primary`, and the CV reads green — a chart drawn from the rebound
692
+ triad comes out one hue, which is a chart that cannot be read. **A chart's
693
+ palette is not the page's accent.** The lane tokens are also what the legend
694
+ swatches paint from, so the key cannot drift from the thing it explains.
695
+
696
+ ### Terminal captures
697
+
698
+ The one palette on the site that does not follow the theme. An ` ```ansi `
699
+ fence in article prose renders a real terminal capture in colour, and the
700
+ sixteen base colours plus the ground are stated outright in
701
+ `assets/css/article.css` rather than drawn from the ladder:
702
+
703
+ | Property | Value | Note |
704
+ | :--- | :--- | :--- |
705
+ | `--ansi-bg` | `#12130f` | Fixed in both themes |
706
+ | `--ansi-fg` | `#d3d7cf` | Tango's white |
707
+ | `--ansi-fg-bold` | `#eeeeec` | Bold with no colour set |
708
+ | `--ansi-fg-dim` | `#9aa09a` | Resting copy control, 4.9 : 1 on the bar |
709
+ | `--ansi-0` … `--ansi-15` | Tango | Ubuntu's terminal palette |
710
+
711
+ Two reasons it is an exception rather than an oversight. ANSI colours are
712
+ defined against a dark terminal — bright yellow (`--ansi-11`, `#fce94f`) is
713
+ 1.2 : 1 on the light palette's white canvas and simply disappears. And a
714
+ capture is a picture of something: it brings its own surface the way a
715
+ photograph brings its own light, and repainting it per theme would be
716
+ repainting the subject.
717
+
718
+ Tango specifically, because it is what Ubuntu's terminal ships, and the one
719
+ capture the site currently publishes — the `neofetch` block in the WSL
720
+ article — was drawn with it. Indices past 15 are the 6×6×6 cube and the
721
+ 24-step grey ramp; those are arithmetic in the standard and are computed in
722
+ `_includes/content-runtime/25-code-blocks.html` rather than tabulated here.
723
+
724
+ Bold maps to the bright half of the palette in the renderer, not in CSS: it
725
+ is a terminal behaviour rather than a typographic one, and every tool that
726
+ paints a bold red means the bright one.
727
+
728
+ The type is `0.68rem / 1.15`, and both terms are set on the `pre` rather than
729
+ on the `code` inside it — `code` is inline, so the block establishes the line
730
+ boxes and a leading set on the `code` loses to the block's strut. 1.15 is a
731
+ terminal cell: VTE, which Ubuntu's terminal is built on, takes its row height
732
+ from the font's ascent plus descent and adds nothing, landing between 1.16 and
733
+ 1.20 for the faces it ships. This is the one place on the site where prose
734
+ leading is actively wrong rather than merely loose — box drawing made of `s`,
735
+ `o` and `/` stops being a shape at anything near it.
736
+
737
+ ---
738
+
739
+ ## Typography
740
+
741
+ Owner stack (2026-10-01): **Cinzel Decorative** for *page / display* titles,
742
+ **Newsreader** for body and content titles (**Light 300** / **SemiBold 600**),
743
+ **Newsreader** for subtitle / standfirst roles that still use `--font-subtitle`,
744
+ **Marcellus SC** for labels / marginalia, **Courier Prime** for notation and
745
+ code. Declared once in `_data/themes/shared.yml` — light and dark only swap
746
+ colour. Root **110%**.
747
+ Titles are **+2pt** over the Stitch rem steps; chrome (meta, dek,
748
+ labels, nav, CTAs, mono) stays on the bare rem steps so dates, pills, and
749
+ “Read article” links do not outgrow the prose. Detail prose is **1.2rem** below 1024px (about **21.1px**) and
750
+ **1.25rem** from there (about **22px**). A reader can step that default
751
+ by −4…+4 pt (`--prose-size-offset`) from a compact dropdown.
752
+
753
+ Print CV keeps its own Lora / Overlock path in `cv-print.css` (untouched).
754
+
755
+ Type sizes live as `--type-*` tokens on `html` in `base.css`, mapped from the
756
+ Stitch Borders scale (§19.4 of reimagined-borders). Page CSS must consume
757
+ those tokens — no one-off rem sizes for roles that already have a step.
758
+
759
+ | Role | Family | Weight | Size token |
760
+ | :--- | :--- | :--- | :--- |
761
+ | Display | Cinzel Decorative | 400 | `--type-display` / `-lg` |
762
+ | Page H1 | Cinzel Decorative | 400 | `--type-page` / `-lg` |
763
+ | Section | Cinzel Decorative | 400 | `--type-section` / `-lg` |
764
+ | Detail title | Newsreader | 600 | `--type-title` / `-lg` |
765
+ | Card / row title | Newsreader | 600 | `--type-card` / `-lg` |
766
+ | Prose | Newsreader | 300 | `--type-prose` |
767
+ | UI body | Newsreader | 300 | `--type-body` |
768
+ | Detail standfirst | Newsreader | 300 | `--type-subtitle` (= body − 1pt) |
769
+ | Dek / masthead tags | Newsreader | 300 | `--type-dek` |
770
+ | Meta / dates / kickers | Newsreader | 300 | `--type-meta` |
771
+ | Chips / nav / SC | Marcellus SC | 400 | `--type-label` / `-lg` |
772
+ | Notation | Courier Prime | 400 | `--type-mono` |
773
+ | Code | Courier Prime | 400 | `--font-code` |
774
+
775
+ ### Scale (Stitch-aligned)
776
+
777
+ | Token | rem | ~px at 110% root | Stitch analogue |
778
+ | :--- | :--- | :--- | :--- |
779
+ | `--type-display-lg` | 3.6515 | ~64 | display-hero |
780
+ | `--type-page-lg` | 2.9015 | ~51 | headline-lg |
781
+ | `--type-section-lg` | 2.1515 | ~38 | headline-md |
782
+ | `--type-title-lg` | 2.4015 | ~42 | detail H1 |
783
+ | `--type-card` | 1.4015 | ~25 | headline-sm |
784
+ | `--type-prose` | 1.2 | ~21.1 | reading body, under 1024px |
785
+ | `--type-prose` (≥1024px) | 1.25 | ~22 | reading body, desktop |
786
+ | `--type-body` | 1.0758 | ~19 | body-md |
787
+ | `--type-meta` | 0.8125 | ~14 | body-sm |
788
+ | `--type-label` | 0.8485 | ~15 | label-md |
789
+ | `--type-mono` | 0.8504 | ~15 | notation |
790
+
791
+ ---
792
+
793
+ ## Typography (implementation notes)
794
+
795
+ A page with `drop_cap: true` raises the first letter of the opening paragraph
796
+ across `--drop-cap-lines` (2 unless `_config.yml` says otherwise) in the
797
+ reading face, Newsreader, when the body does not open with a heading.
798
+
799
+ One leading token, `--leading-body: 1.5`, covers chrome and short body copy;
800
+ `--leading-prose: 1.6` covers long-form. Both are declared on `html` in
801
+ `base.css` and applied through `html body` rather than `body`, because
802
+ Tailwind's Preflight ships `body { line-height: inherit }` from the CDN
803
+ `<style>` that loads after this repo's sheets and wins a specificity tie on
804
+ source order.
805
+
806
+ Consumers spell `var(--type-*)`, `var(--leading-body)` or
807
+ `var(--leading-prose)` rather than repeat numbers. Code blocks, figure
808
+ captions, verse and chart labels may keep local leading; they are not body
809
+ copy.
810
+
811
+ ### The notation role
812
+
813
+
814
+
815
+ `.code-technical` (0.85rem / 1.5) and `.code-technical-sm` (0.75rem / 1.4) both
816
+ set `font-variant-numeric: tabular-nums`. This is the role that carries the
817
+ ledger reading: `renderMetaStrip` applies `.code-technical` unconditionally, so
818
+ every meta line on every card is mono, and the CV, the rascunhos ledger head,
819
+ the sources host line, the network hint and the detail back link all follow.
820
+ `.code-technical-sm` is defined but nothing uses it.
821
+
822
+ ### Display scale
823
+
824
+ | Token | Base | Larger | Line height |
825
+ | :--- | :--- | :--- | :--- |
826
+ | `pageTitle` | 3.625rem | 5.875rem (≥768px) | 0.95 |
827
+ | `homeHeroTitle` | `clamp(var(--type-section), calc(100cqi * 0.92 / 14.5), var(--type-page-lg))` | floor `--type-section-lg` (≥640px) | 1.05 |
828
+ | `pageHeaderTitle` | 2.5rem | 3.5rem (≥768px) | 1.15 → 1.1 |
829
+ | `textTitle` | 2.15rem | 2.65rem (≥768px) | 1.12 |
830
+ | `textTitle` in the dossier | 1.95rem | 2.25rem (≥1024px) | 1.15 |
831
+ | `sectionTitle` | 2rem | 2.375rem (≥640px) | 1.1 |
832
+ | `cardTitle` | 1.625rem | — | 1.2 |
833
+
834
+ The home hero sizes to its copy column through a container query on
835
+ `.home-hero-copy`. The preferred term is `100cqi * 0.92 / 14.5`, capped at
836
+ `--type-page-lg`. The floor is the band-title step (`--type-section`, then
837
+ `--type-section-lg` from 640px) so the name meets Articles, Rascunhos, and
838
+ Projects on a phone instead of falling to the card size. Below 768px the
839
+ name may wrap (`white-space: normal`); from 768px it stays one line. The
840
+ given name is `onSurface` and the family name is `primary` on the same
841
+ line. An earlier container-query fit —
842
+ `min(4.875rem, calc(100cqw * 0.96 / 4.5))`, whose `4.5` was the advance of
843
+ `"Hello, I am"` in Lora — remains in `base.css` but reaches no markup.
844
+
845
+ ### Prose
846
+
847
+ `.article-content` runs at `1.1rem / var(--leading-body)` inside a `45rem`
848
+ measure, paragraph margin `1.15em`. At 1280px and above that measure holds
849
+ about 81 characters to the line, which is long for the leading; the measure is
850
+ a known open question rather than a tuned value.
851
+
852
+ Its ink is `onSurface` — pure black in light, pure white in dark — so body
853
+ copy and titles share the full foreground. Meta, captions, chips and other
854
+ chrome stay on `onSurfaceVariant`.
855
+
856
+ * **`h2`** is the display serif at `1.6em / 400`, with a `gentleDivider` rule
857
+ above and `--space-2xl` of air, so a long article reads as a sequence of
858
+ plates. It drops to `1.4em` below 768px, and the first `h2` in the column
859
+ loses its rule.
860
+ * **`h3`** is the label face at `0.95em`, uppercase, `0.1em` tracking, in
861
+ `primary`.
862
+ * **`h4`** is the body sans at `1em / 600`.
863
+ * **`h1`** is untouched from the old build: body sans, 600, `1.4em`. It is
864
+ rare in prose and has no rule above it.
865
+
866
+ `pre` takes a 2px `outlineVariant` left rule on `surfaceContainer`;
867
+ `blockquote`, `.article-note`, tables with zebra `nth-child(even)` rows, MathJax
868
+ display blocks, Mermaid diagrams and the superscript citation apparatus are all
869
+ styled in [`assets/css/ledger/detail.css`](assets/css/ledger/detail.css), which
870
+ loads after and overrides much of `article.css`.
871
+
872
+ **Media sits at the measure, everywhere.** Every figure, video, Plotly embed
873
+ and Mermaid diagram is capped by `--reading-measure`, the same as the prose
874
+ around it, and a diagram wider than that scrolls inside its own plate rather
875
+ than pushing past the column. The projects page used to be the exception: it
876
+ released its whole body from the measure and handed the measure back to a
877
+ whitelist of text blocks, so a plate ran 18% wider than its paragraph at
878
+ 1440px and 6% wider at 1280px. Neither width read as a deliberate break out of
879
+ the column, and the same includes sat differently there than on an article.
880
+
881
+ **Every piece of media sits on a plate.** `.article-content figure` gives all
882
+ of them one ground in both themes, so a light Matplotlib export and a dark
883
+ simulation video do not arrive as two surfaces. That means the `figure`
884
+ element is the contract: an include that emits a bare `<div>` or a bare
885
+ `<img>` silently opts out of it, which is how YouTube embeds and cut-in
886
+ portraits went unplated for as long as they did.
887
+
888
+ **The apparatus scale.** Below the prose's `1.1rem` the article descends in
889
+ four steps, all set in the body ink: a quote and a note share `0.95rem`, a
890
+ caption takes `0.9rem`, a bibliography entry `0.85rem`. A quote and a note are
891
+ deliberately indistinguishable as type, since both are a passage set aside from
892
+ the argument; only the plate tells them apart. Captions are the one step that
893
+ moves in two files, because `article.css` scopes `.article-content figure
894
+ figcaption` at `(0,1,2)` and outranks `detail.css`'s `(0,1,1)` — change one
895
+ without the other and figures keep the old size while a bare caption moves.
896
+
897
+ The bibliography is the last thing in an article still set in the notation face
898
+ while being made of sentences, which the notation rule above says it should not
899
+ be. Left as-is for now; its size no longer compounds the problem.
900
+
901
+ **Code blocks.** A fenced block is wrapped in a `.code-block` container by
902
+ `_includes/content-runtime/25-code-blocks.html`, which gives it the chrome an
903
+ editor gives a file: a `surfaceContainerHigh` bar over a `surfaceContainer`
904
+ pane, the language on a tab drawn in the pane's own fill with a 2px `primary`
905
+ inset rule across its top edge, and a copy control at the right in the label
906
+ register. Square and hairline-bordered like the ledger cards. The container
907
+ carries the fill and the frame, so the `pre` inside is zeroed back to a scroll
908
+ box — that override is three classes deep and so beats the `detail.css` rule
909
+ above without depending on sheet order. A ` ```mermaid ` fence is skipped; an
910
+ ` ```ansi ` fence takes the terminal palette instead (see
911
+ [Terminal captures](#terminal-captures)).
912
+
913
+ ### Label register
914
+
915
+ Uppercase label and notation text is the site's connective tissue.
916
+ Letter-spacing in use, by frequency: `0.1em` (12), `0.08em` (10), `0.12em` (7),
917
+ `0.04em` (4), `0.14em` (3), `0.05em` (3), `0.2em` (2), `0.06em` (2), `0.02em`
918
+ (2), `0.25em` (1), `0.16em` (1), plus `-0.01em` (3) as a display pull.
919
+
920
+ ---
921
+
922
+ ## Layout
923
+
924
+ * **Shell.** `max-width: 80rem` centred. The nav pads `1.5rem` → `2rem` (sm) →
925
+ `2.5rem` (md); listing and detail mains pad `2rem` → `4rem` (md); home bands
926
+ pad `--gutter-mobile` → `--gutter` (md).
927
+ * **Nav.** Sticky, `bg-surface`, logo + `|` divider + theme toggle on the left,
928
+ a single **Explore** hamburger on the right *at every breakpoint* — there is
929
+ still no persistent horizontal nav, and no responsive visibility class
930
+ anywhere in the bar. The menu is a bordered popover grouped from `_data/nav`.
931
+ A `1px` tonal strip closes the bar.
932
+ * **Home.** Six full-bleed `.home-band` sections, `--space-2xl` → `--space-3xl`
933
+ (md) of vertical padding, each closed by a `gentleDivider` rule, alternating
934
+ `background` and `.home-band--contrast` (`panelContrast`): hero, featured,
935
+ writing, rascunhos, systems, ideas graph. The featured band carries a 6rem ×
936
+ 3px `primary` rule at its top-left corner as a structural mark. The three
937
+ writing cards take `primary` / `secondary` / `tertiary` 2px top rules in
938
+ rotation. The systems grid is a 12-column asymmetric pair — 7/5 then 5/7 —
939
+ from 1024px. Below 768px the hero portrait runs to the screen edge, pulled
940
+ out of the band's gutter by exactly `--gutter-mobile` on each side and losing
941
+ its side hairlines, which at the edge would read as a frame cut off.
942
+ * **Writing index.** Masthead over a hairline, then a `panelContrast` filter
943
+ deck (tag tabs, sort, keyword), then a `panelContrast` lead panel for the
944
+ newest entry, then an archive of `.ledger-row`s under a section banner.
945
+ * **Rascunhos.** Masthead with a shard, the shared filter panel unfolded into a
946
+ full-width strip, then a two-column body: a 3/1 split at 1024px of ledger
947
+ rows beside a sticky marginalia column. Rows become two-column
948
+ (`.rascunhos-row--illustrated`, a 7.5rem plate, 4rem on phones) only when an
949
+ image is actually present.
950
+ * **Projects.** A Tailwind `grid-cols-1 md:grid-cols-2` of system cards with
951
+ `align-items: start`, so a card without a poster does not stretch to match
952
+ one that has one.
953
+ * **Sources.** `.listing-card-grid--sources`: one column, two from 768px,
954
+ three from 1024px, posters at `16 / 9` — the video frame, which is what most
955
+ of the collection links to. Filtering hides pre-rendered cards with a class
956
+ rather than re-rendering them.
957
+ * **Detail pages.** Two columns from `768px` — a 3/9 split of a left `aside`
958
+ carrying a `panelContrast` dossier (title, description or thumbnail, tags,
959
+ language, dates) and the prose column at `45rem` on the right. The sticky
960
+ table of contents appears in that sidebar only from `1024px`; below it, the
961
+ same list is mounted as a `<details>` disclosure instead. A 2px `primary`
962
+ reading-progress bar is drawn on a pseudo-element of the detail section with
963
+ `animation-timeline: scroll(root block)`, beNewsreader an `@supports` guard, so
964
+ engines without scroll-driven animation simply get no bar.
965
+ * **CV.** A `panelContrast` dossier band with two shards, then five
966
+ `.cv-register` sections and a timeline band. Each register is a 3/9 grid at
967
+ 1024px with the section title in a sticky left rail under a 2.5rem `primary`
968
+ rule, and the entries running past on the right.
969
+ * **Breakpoints.** Tailwind's `640 / 768 / 1024 / 1280` for `min-width`; the
970
+ ledger sheets add `max-width` queries at `639px` (shard suppression, phone
971
+ row metrics), `767px` (prose `h2`, sidebar padding, filter facet stacking) and
972
+ `900px` (the writing deck's own column collapse).
973
+
974
+ ---
975
+
976
+ ## Elevation & depth
977
+
978
+ The model is **tonal layering plus hairlines, never shadow**.
979
+
980
+ * **Content shadows: none.** Exactly two non-`none` `box-shadow` declarations
981
+ exist in the whole build: the listing filter popover (`0 12px 32px` of
982
+ `onSurface` at 12%, dropped below 768px where the panel goes inline) and a
983
+ 1px ring on the timeline's hovered bar. The nav menu adds Tailwind
984
+ `shadow-sm`. The print sheet blanket-zeroes `box-shadow` on every descendant.
985
+ * **Hairline borders do the real work.** Because no rung of the ladder clears
986
+ 1.16 : 1, boundaries are carried by `1px solid` borders at one of the five
987
+ `gentle*` opacities. There are 63 such declarations in the hand-written
988
+ stylesheets — 13 in `base.css`, 9 in `article.css`, 37 across `ledger/`, 4 in
989
+ `cv-print.css`.
990
+ * **Hover never lifts.** It steps the surface (`surfaceCard` → `surfaceHigh`),
991
+ shifts the title to `primary`, shifts a border toward `gentleHover` or
992
+ `primary`, or warms and scales a poster.
993
+ * **The print sheet has no surfaces at all.** Paper gets rules, not bands: a
994
+ full-strength hairline for a section, a half-strength one for an entry.
995
+
996
+ ---
997
+
998
+ ## Motion
999
+
1000
+ **Five tokens, and nothing outside them.** The scale is emitted by
1001
+ `_includes/theme/theme-vars-block.liquid` onto `:root` and every `[data-theme]`,
1002
+ so it survives a palette flip like any other custom property:
1003
+
1004
+ ```css
1005
+ --motion-duration-fast: 0.15s; /* state change: colour, border, background */
1006
+ --motion-duration-base: 0.4s; /* poster filter warm-up */
1007
+ --motion-duration-slow: 0.7s; /* poster and plate scale */
1008
+ --motion-ease-default: ease;
1009
+ --motion-ease-out: ease-out;
1010
+ ```
1011
+
1012
+ Every `transition` and `animation` in the hand-written CSS reads them — 40
1013
+ declarations, 60 duration references, 53 of them `fast` — including the two
1014
+ inline `<style>` blocks in `_includes/page/`. Adopting the scale collapsed the
1015
+ near-duplicates the convention had accumulated: the old `0.2s`, `180ms` and
1016
+ `150ms` state changes are all `fast` now, the `0.6s` poster scales are `slow`,
1017
+ and the lightbox's `160ms`/`180ms ease-out` keyframes are `fast` plus
1018
+ `--motion-ease-out`, which is the token's only use.
1019
+
1020
+ Two literals survive on purpose, neither of them motion: the `99999s ... 0s`
1021
+ WebKit autofill suppression on the filter input (`base.css`), and the `1ms
1022
+ linear` on the reading-progress bar, where the duration is a placeholder that
1023
+ `animation-timeline: scroll(root block)` overrides.
1024
+
1025
+ **Scroll-driven animation** is new: the reading-progress bar in
1026
+ `ledger/detail.css` uses `animation-timeline: scroll(root block)` beNewsreader an
1027
+ `@supports` guard, with a 1ms `linear` animation scaling a fixed 2px `primary`
1028
+ bar from 0 to 1.
1029
+
1030
+ **`prefers-reduced-motion: reduce` is honoured globally.** One query in
1031
+ `_includes/theme/theme-vars.html` zeroes the three duration tokens, so every
1032
+ transition and animation that reads one stops dead. Nothing has to opt in, and
1033
+ a new rule cannot forget to.
1034
+
1035
+ Six local blocks remain, because a zero duration removes the *tween*, not the
1036
+ end state — a `transform` still lands, it just lands instantly:
1037
+
1038
+ | File | What it disables |
1039
+ | :--- | :--- |
1040
+ | `base.css` | the system card poster's `scale(1.02)` |
1041
+ | `ledger/home.css` | the hero plate image's `scale(1.02)` |
1042
+ | `ledger/writing.css` | the writing lead poster's `scale` |
1043
+ | `ledger/rascunhos.css` | the thumbnail filter transition |
1044
+ | `ledger/detail.css` | the reading-progress bar, which is scroll-driven and so outside the token scale |
1045
+ | `article.css` | the lightbox keyframes, belt-and-braces since they now read `fast` |
1046
+
1047
+ Colour and background fades are no longer an exception: they read `fast` like
1048
+ everything else, so the query catches them too.
1049
+
1050
+ ---
1051
+
1052
+ ## Shapes
1053
+
1054
+ * **`--radius-card: 0` keeps cards square.** Ledger cards, system cards,
1055
+ chips, emphasis CTAs, the home hero plate and index panels stay hard
1056
+ rectangles. Drafting corner brackets (`.ledger-frame-corners`) and empty
1057
+ crosshairs (`.ledger-frame-crosshairs`) are pure decoration with no text.
1058
+ Marker washes (`.ledger-marker`, `.section-title em`) highlight field-backed
1059
+ words only. `ledger/detail.css` still zeroes radius on prose `code`,
1060
+ `pre`, `blockquote` and `.article-note` where needed for reading;
1061
+ `cv-print.css` zeroes radius on every descendant for print.
1062
+ * **True pills stay circular.** Footer contact discs and the timeline legend
1063
+ mark use `9999px`. `404.html`'s `rounded-xl` buttons resolve through
1064
+ `assets/tailwind-play.js`'s remapped radius scale.
1065
+ * **Cubist shards** are now a primitive. Two polygons are declared once in
1066
+ `base.css`:
1067
+
1068
+ ```css
1069
+ --shard-alpha: polygon(26% 0, 100% 0, 78% 100%, 0 68%);
1070
+ --shard-beta: polygon(0 0, 74% 18%, 100% 100%, 18% 84%);
1071
+ ```
1072
+
1073
+ `.shard-accent` is an absolutely positioned, `pointer-events: none`,
1074
+ `z-index: 0` element clipped by `--shard-alpha` at
1075
+ `--cubist-shard-opacity: 12%`; `.shard-accent-beta` swaps in the other
1076
+ polygon. A single `@media (max-width: 639px)` rule hides every one of them.
1077
+ Placement, size and hue are set per use by the page stylesheet, never by the
1078
+ utility.
1079
+
1080
+ Fifteen uses site-wide, all `aria-hidden`. No panel carries two shards in
1081
+ the same corner or the same hue, and no two panels carry the same pair of
1082
+ corners, which is the whole rule the placement follows:
1083
+
1084
+ | Where | Class | Corner | Hue |
1085
+ | :--- | :--- | :--- | :--- |
1086
+ | Home hero band | `.home-hero-pandorga` (geometric kite & ribbon) | background | `primary` / `secondary` / `tertiary` |
1087
+ | Home hero portrait | `.shard-accent.home-hero-shard` | top-right | `primary` |
1088
+ | Home featured card | `.shard-accent.home-featured-shard` | top-left | `tertiary` |
1089
+ | Home featured card | `.shard-accent-beta.home-featured-shard--beta` | bottom-right | `secondary` |
1090
+ | Home index panel | `.shard-accent.home-index-shard` | bottom-right | `secondary` |
1091
+ | Home index panel | `.shard-accent-beta.home-index-shard--beta` | bottom-left | `primary` |
1092
+ | Writing index lead panel | `.shard-accent.writing-lead-shard` | bottom-left | `primary` |
1093
+ | Writing index lead panel | `.shard-accent-beta.writing-lead-shard--beta` | top-right | `tertiary` |
1094
+ | CV dossier | `.shard-accent.cv-dossier-shard` | top-right | `secondary` |
1095
+ | CV dossier | `.shard-accent-beta.cv-dossier-shard--beta` | bottom-left | `primary` |
1096
+ | Projects lead | `.shard-accent.projects-lead-shard` | top-right | `primary` |
1097
+ | Projects hero | `.shard-accent.projects-hero-shard` | bottom-right | `secondary` |
1098
+ | Rascunhos masthead | `.shard-accent.rascunhos-shard` | top-right | `secondary` |
1099
+ | Sources masthead | `.shard-accent.sources-masthead-shard` | top-right | `secondary` |
1100
+ | Sources foot | `.shard-accent-beta.sources-foot-shard` | bottom-left | `tertiary` |
1101
+ | Site footer | `.shard-accent.site-footer-shard` | bottom-right | `secondary` |
1102
+ | Site footer | `.shard-accent-beta.site-footer-shard--beta` | bottom-left | `primary` |
1103
+
1104
+ Two placement traps this build has already fallen into. A `--beta` element
1105
+ carries both class names, so any media query that repositions the pair has to
1106
+ restate the base rule's `auto` offsets on the beta: leave all four set and
1107
+ the browser resolves to top and left, and the pair collapses into one corner.
1108
+ And a shard beNewsreader an opaque plate is a shard nobody sees, which is why the
1109
+ home featured pair goes around its poster rather than across the card.
1110
+
1111
+ On the writing index the shard was for a long time written to stand down
1112
+ whenever the featured entry carried a thumbnail — and that panel selects the
1113
+ first entry that *has* one, so the condition was always true and the shard
1114
+ never rendered on a built page.
1115
+
1116
+ A further shard sits on the detail sidebar heading. It is a `::after`
1117
+ pseudo-element, so it cannot take the utility class and re-implements it by
1118
+ hand — including its own 639px suppression.
1119
+ * **Icons** are Material Symbols Outlined, `FILL 0, wght 400, GRAD 0, opsz 24`,
1120
+ sized explicitly per context (`1rem` in the back link, `1.05rem` on the deck
1121
+ search, `1.1rem` on a system card, `1.25rem` in chrome).
1122
+ * **Card media** is a `16 / 9` `object-fit: cover` crop — `4 / 3` on rascunhos
1123
+ rows — never letterboxed. The home page is the exception: both its plates are
1124
+ measured against the copy beside them rather than against a ratio. The
1125
+ featured plate takes `--home-featured-plate-height` (its title's line plus
1126
+ four lines of description), and the draft rows' plates `align-self: stretch`
1127
+ so they square off against the chip row that closes the entry.
1128
+
1129
+ ---
1130
+
1131
+ ## Page chrome
1132
+
1133
+ ### Bands and plates
1134
+
1135
+ `.home-band` is the universal full-bleed container: no border except the
1136
+ `gentleDivider` rule that closes it, no shadow, `panelContrast` when it carries
1137
+ `.home-band--contrast`. Four panel-step plates carry a shard —
1138
+ `.writing-lead`, `.rascunhos-panel`, `.cv-dossier` and
1139
+ `.text-detail-sidebar .article-sidebar-heading` — so each takes
1140
+ `overflow: hidden` and lifts its content clear with `position: relative;
1141
+ z-index: 1`. The filter decks and `.rascunhos-pagination` are plain
1142
+ `panelContrast` strips. `.home-hero-plate` and `.home-graph-plate` sit a rung
1143
+ lower, on `surfaceCard`, beNewsreader a `gentleSubtle` hairline.
1144
+
1145
+ ### Filter deck
1146
+
1147
+ Two shapes over one controller (`_includes/content-runtime/42-listing-filter.html`).
1148
+ The writing index renders it inline as a `panelContrast` strip of tag **tabs**
1149
+ — solid `surfaceCard` fills, the active one inverting to a solid `primary`
1150
+ block — plus a sort group and a mono keyword field. Rascunhos takes the shared
1151
+ `page-header.html` popover and unfolds it into a full-width strip, removing the
1152
+ trigger button so it cannot re-collapse. Sources and the network graph keep the
1153
+ popover as a popover. The classification axis changed from `category` to `tag`
1154
+ in the controller; resources still filter on `category`.
1155
+
1156
+ ### CV
1157
+
1158
+ `.cv-register` sections separated by a `gentleDivider` top rule and
1159
+ `--space-3xl`. Entries are hairline-separated rows, not boxes: a mono role
1160
+ line, a display institution line, a mono place line, body copy, and an optional
1161
+ chip row of related products. Languages are a name, a dotted leader and a mono
1162
+ level.
1163
+
1164
+ ### Print CV
1165
+
1166
+ [`assets/css/cv-print.css`](assets/css/cv-print.css) now runs the same four
1167
+ type roles on paper: Simonetta for the name and entry titles, Newsreader for
1168
+ prose, small-caps section labels and organisations, mono for
1169
+ dates, URLs, email and phone. The header carries a mono kicker above the name —
1170
+ the paper counterpart of the dossier strip. Sections are a small-caps label
1171
+ with a hairline running out to the margin; jobs are separated by half-strength
1172
+ rules rather than wrapped in cards, because the mockup's card shape would have
1173
+ put eight grey blocks on an A4 sheet. The layout pins `data-theme="light"`, so
1174
+ only the light palette is ever in play, and a local reset zeroes radius and
1175
+ shadow on every descendant because the sheet ships no utility framework.
1176
+
1177
+ ### Footer
1178
+
1179
+ `surfaceContainerLow` with a `gentleDivider` top border, `3rem` vertical
1180
+ padding, marginalia in `secondary`, contacts injected at runtime as circular
1181
+ discs.
1182
+
1183
+ ---
1184
+
1185
+ ## The field contract
1186
+
1187
+ An element renders only if a populated field backs it. The classification axes,
1188
+ as enforced by [`_plugins/lib/tag_validator.rb`](_plugins/lib/tag_validator.rb):
1189
+
1190
+ * **Articles** classify on `tags`, one to three, from a closed English
1191
+ vocabulary: `Aesthetics`, `AI`, `Data`, `DevOps`, `Mathematics`,
1192
+ `Robotics`, `Social`. All 22 carry them.
1193
+ * **Rascunhos** classify on `tags` too, by subject and in Portuguese, **zero to
1194
+ three**: `arte`, `brasilidade`, `carme`, `devaneio`, `engraçadinho`,
1195
+ `música`, `pesquisa`, `provocação`, `técnico`, `trabalho`. Unlike articles
1196
+ the field may be empty — a notebook holds pages that file nowhere. 29 of 30
1197
+ carry at least one.
1198
+ * `category` is rejected on both collections — one classification axis, not
1199
+ two.
1200
+ * **Projects** carry free `tags` (all 8 do), plus `label`, `status`,
1201
+ `start_date`, `end_date`, `icon`, `thumbnail`, `github`, `article_url`,
1202
+ `external_url`.
1203
+ * **Resources** are the exception that keeps `category` (`Academic`,
1204
+ `Communication`, `Humor`, `Music`, `Professional`) alongside a `type` of
1205
+ `youtube` / `podcast` / `website`.
1206
+ * **`language`** (`enus` / `ptbr`) is optional and exported into both
1207
+ `writing_index.json` and the per-object JSON. 29 of 67 entries declare one.
1208
+
1209
+ `writing_index.json` carries 29 keys per entry; detail views read the full
1210
+ `front_matter` plus `body_markdown` from
1211
+ `_content_json/collections/<coll>/<slug>.json`.
1212
+
1213
+ ---
1214
+
1215
+ ## Do's and Don'ts
1216
+
1217
+ ### Do
1218
+
1219
+ * **Do reach for a block before writing a card.** If a layout needs a card,
1220
+ row, chip, pill, meta strip or CTA, call the renderer in `43-blocks.html`.
1221
+ Another local card shape is the failure mode this library exists to stop.
1222
+ * **Do let an absent field render nothing.** No placeholder, no dash, no
1223
+ derived stand-in, no counter.
1224
+ * **Do keep every corner at `0`.**
1225
+ * **Do take depth from the ladder**, and pair it with a hairline — no rung
1226
+ clears 1.16 : 1 on its own.
1227
+ * **Do set notation in `.code-technical`** — dates, spans, hosts, counts,
1228
+ reference numbers, kickers.
1229
+ * **Do keep long-form prose at `var(--reading-measure)`.**
1230
+ * **Do reach for `--space-*` and `--gutter`** in any new CSS rather than a
1231
+ literal length.
1232
+ * **Do use `--shard-alpha` / `--shard-beta`.** Do not draw a new polygon.
1233
+ * **Do emit identical token names in both palettes** so components never branch
1234
+ on theme.
1235
+ * **Do set `font-synthesis: none` on title text.** Simonetta ships 400 and 900 and nothing between; the gap must not be faked.
1236
+ * **Do reuse `.ledger-cta` / `.ledger-cta--emphasis` / `.ledger-cta--emphasis-alt`
1237
+ (`base.css`) for any CTA that used to reach for a padded, solid-filled
1238
+ button.** `--emphasis` is the primary hue tinted fill; `--emphasis-alt` swaps
1239
+ to the secondary hue for a second button-level action beside it (e.g. the
1240
+ home hero's "Full curriculum vitæ" / "Contacts" pair). Don't add another one-off
1241
+ button class — the old `.home-cta` / `--solid` / `--outline` family was
1242
+ retired in 2026-09 specifically because every page had started growing its
1243
+ own.
1244
+ * **Do scope a page's accent with `.page-accent--<color>` on its top-level
1245
+ container.** The two classes live in `base.css` (`--green` for Articles and
1246
+ the CV, `--ochre` for Rascunhos and Network of Ideas; Projects and Sources
1247
+ need none, blue is the default primary). Each rebinds `--color-primary` /
1248
+ `--color-link` / `--color-link-hover` to an existing triad token — never a
1249
+ new hex — and everything under it that reads those variables (card titles,
1250
+ CTAs, section-title `<em>` accents, filter chips) follows automatically
1251
+ through CSS inheritance. Categorical drawings that must keep the absolute
1252
+ slate / forest / ochre map (network graph nodes, type-keyed ambient pools)
1253
+ read `--triad-primary` / `--triad-secondary` / `--triad-tertiary` instead —
1254
+ those aliases are frozen in the theme vars block and are not rebound. Add a
1255
+ variant in `base.css`, not in a `ledger/*.css`: the green one was duplicated
1256
+ across two page stylesheets within a week of being written.
1257
+ * **Do let tag chips carry the page accent.** `.ledger-chip` mixes
1258
+ `--color-primary` into both its fill (10%) and its ink (78%), so a chip is
1259
+ blue on Projects and Sources, green on Articles and the CV, ochre on
1260
+ Rascunhos and Network, with no per-page rule. Hover steps the fill to 22%
1261
+ and adds a 35% accent hairline as an inset ring rather than a border — a
1262
+ border would have to exist at rest to avoid a 2px jump, and its padding
1263
+ compensation only nets to zero at integer device pixel ratios. The
1264
+ page-header tag line (`data-content-header-tags`, e.g. the CV's
1265
+ "Engineering Leadership. Team Building…") is deliberately not a chip and
1266
+ stays neutral.
1267
+ * **Do introduce a home band with a kicker, a title and a note**, all three
1268
+ from `content/pages/headers.yml` so the owner can edit them in Studio. The
1269
+ title is the same field the band's listing page reads for its own header.
1270
+ A band that opens straight onto cards gives the reader no way in. Each of
1271
+ the three renders nothing when its field is empty.
1272
+ * **Do mix a hover tone toward `--color-on-surface`, not toward a
1273
+ `*-fixed-dim` token.** Those tokens carry the same value in both palettes,
1274
+ so they lighten a link on hover in *both* — right against a near-black
1275
+ canvas, wrong against white paper, where emphasis has to go darker. The
1276
+ foreground ink is dark in light and light in dark, so mixing toward it
1277
+ moves the correct direction in each theme on its own.
1278
+ * **Do give a project-card link an icon that names its destination**
1279
+ (`renderCtaLink`'s `icon` option in `43-blocks.html`: `code` for a
1280
+ repository, `article` for a write-up, `public` for an external site, the
1281
+ entry's own `fm.icon` for "Details"). A destination icon is a *leading*
1282
+ marker and renders before the label, in `.ledger-cta-icon`, with no hover
1283
+ slide. Every other CTA keeps the trailing directional glyph — since 2026-09
1284
+ a Material `chevron_right` rather than a `→` — in `.ledger-cta-arrow`,
1285
+ which does slide on hover.
1286
+
1287
+ ### Don't
1288
+
1289
+ * **Don't write a hex.** Everything resolves through `_data/themes/*.yml` → CSS
1290
+ custom properties → Tailwind token names. A literal is correct in one palette
1291
+ and wrong in the other.
1292
+ * **Don't fill a panel, card or row with a brand hue.** A CTA earns a tinted
1293
+ fill (`.ledger-cta--emphasis`, `~14%` of the accent colour, brightening to a
1294
+ full fill on hover) for the one or two truly primary actions per page — that
1295
+ is the sanctioned exception, not a flat opaque brand-colour block.
1296
+ * **Don't add drop shadows to content.** Shadows are for floating overlays.
1297
+ Depth on a card or panel is the ladder rung plus its hairline, and on hover
1298
+ the rung plus a hairline warmed toward the page accent (`.ledger-card:hover`
1299
+ mixes `--color-primary` at 40% into the border — the reference mockup's
1300
+ "hairline transitions to warm antique brass at 40%", made accent-aware).
1301
+ One sanctioned exception, added 2026-09: a wide, very low opacity accent
1302
+ pool sits *beNewsreader* a band. It is a backdrop, not a content effect — no card,
1303
+ panel or row gains a glow, and the pool is blurred past any visible edge so
1304
+ it reads as ambient light in the room rather than as a shape. Three of them
1305
+ ship: the home hero at 7% (`.home-band--hero::before`), the home Sources and
1306
+ Network band at 5% (`.home-band--index::before`), and one beNewsreader the Network
1307
+ of Ideas graph at 7%, whose centre and width are read off the settled D3
1308
+ layout rather than fixed (`assets/css/ledger/network.css`).
1309
+
1310
+ **All three are dark-palette only**, gated on `[data-theme="dark"]`. The
1311
+ hero pool used to be unconditional, on the written assumption that it
1312
+ disappeared against a white light-theme canvas; the canvas has been warm
1313
+ paper since the ladder was inverted, and `--color-primary` inverts polarity
1314
+ between the palettes, so on light the rule painted a dark accent over paper.
1315
+ Near-equal RGB distance (20.3 dark, 19.3 light) resolves to 0.53 points of
1316
+ luminance on the near-black canvas and 9.11 on paper — about seventeen times
1317
+ the shift, in the palette the effect was never designed for. There is no
1318
+ light variant: paper has no headroom to lighten into.
1319
+ * **Don't tint a page background off-brand.** Dark is a neutral-to-blue
1320
+ near-black (`#0c0c0e` canvas) — not olive or green. Light *is* warm paper
1321
+ (`#f4f2ed`), and that is deliberate: it is where the *gauchismo* half of
1322
+ the identity actually lives. This rule used to read "light is pure
1323
+ `#ffffff`, never cream" — that was written when cards were grey on white,
1324
+ and it was reversed in 2026-09 along with the ladder. Cream as a *canvas*
1325
+ is correct; cream as a random panel tint still is not.
1326
+ * **Do step a card toward the light, in both themes.** Dark: canvas `#0c0c0e`
1327
+ → card `#1a1a1e`. Light: canvas `#f4f2ed` → card `#fffdf9`. A card that
1328
+ steps away from the light reads as recessed and, on white, as disabled —
1329
+ which is what the light theme did until 2026-09.
1330
+ * **Don't introduce a fourth webfont.** Four roles, three families plus the
1331
+ system mono stack.
1332
+ * **Don't autoplay a poster.** Project captures run to several megabytes;
1333
+ `renderPosterMedia` ships `preload="none"` and `controls`.
1334
+ * **Don't invent content furniture** — no reading times, word counts, entry
1335
+ numbers, sequence counters, invented status pills, fabricated metrics,
1336
+ benchmarks, commit hashes or version badges.
1337
+ * **Don't edit `43-blocks.html` for one page's needs.** A block that only one
1338
+ layout wants belongs in that layout's `ledger/*.css` and markup.
1339
+
1340
+ ---
1341
+
1342
+ ## Known inconsistencies in the current build
1343
+
1344
+ Recorded so the next pass resolves them deliberately rather than re-copying
1345
+ them.
1346
+
1347
+ 1. **Two names for one system.** `_data/themes/*.yml` says "Brazilian Ledger";
1348
+ `base.css`, `cv-print.css` and every `ledger/*.css` header say
1349
+ "Architectural Ledger". Nothing reconciles them.
1350
+ 2. **The block library is not yet the only source of cards.** Five layouts
1351
+ still hand-build card or row markup — see *Where the library is not yet the
1352
+ only source*. The sources card and the CV's `renderProduct` are archive
1353
+ cards rebuilt from scratch; rascunhos is a second poster implementation with
1354
+ different failure behaviour; `renderLangPill` is forked twice (sources'
1355
+ `lang-pill--other`, `text.html`'s Liquid copy).
1356
+ 3. ~~**The ladder's rungs are smaller than the step they replaced.**~~
1357
+ **Addressed 2026-09.** The dark canvas dropped to `#0c0c0e` and the rungs
1358
+ were respread to roughly +4.3 / +3.1 / +6.0 L\*, which is the mockup's own
1359
+ spacing; light was deepened more modestly, since white paper has less room
1360
+ to give and its warm cast is deliberate. Note that contrast *ratio* is the
1361
+ wrong instrument down here — the +0.05 flare term in the WCAG formula
1362
+ flattens every near-black pair to ≈1.05 : 1 no matter how far apart they
1363
+ look — so the rungs are specified in L\* instead.
1364
+ 4. **Two surface families name the same colours.** `surfaceCard` vs
1365
+ `surfaceContainerLow` and `surfaceHigh` vs `surfaceContainerHigh` still
1366
+ name near-identical values. The consequence this entry used to cite —
1367
+ `.ledger-chip`'s hover being invisible, since it stepped from one family
1368
+ to the other — was fixed in 2026-09 by giving the chip an accent-tinted
1369
+ fill and hairline of its own instead of a ladder rung (it was a literal
1370
+ no-op in dark: the two values were identical). Collapsing the two families
1371
+ into one is still the real fix and is still open.
1372
+ 5. **Two spacing systems.** `--space-*` governs the hand CSS; 176 Tailwind
1373
+ rungs govern the Liquid chrome. `home.html` is fully on the former,
1374
+ `shell.html`, `text.html` and `404.html` fully on the latter. The Tailwind
1375
+ config extends colours and fonts but **not** spacing, so the named scale is
1376
+ unavailable as a utility.
1377
+ 6. **The type scale is still not a scale.** 55 distinct `font-size` values
1378
+ across the eleven screen stylesheets, plus 25 more in `cv-print.css`; 19
1379
+ `line-height` values; 12 uppercase `letter-spacing` values. No ratio
1380
+ connects them.
1381
+ 7. **`article.css` was never updated.** It is 723 lines describing the old
1382
+ prose voice, kept alive only because `ledger/detail.css` loads after it and
1383
+ overrides most of it. Its radius declarations and its body-sans heading
1384
+ block are dead in the browser but live in the file. `.article-content h1`
1385
+ escaped the override and is still body sans at 600 / `1.4em`.
1386
+ 8. **About a third of `base.css`'s class selectors are dead.** `.home-panel`,
1387
+ `.home-panel-inner`, `.home-band-grey`, `.home-gateway`, `.home-hero-panel*`
1388
+ (including the stale container-query hero fit measured for Lora),
1389
+ `.home-portrait-*`, `.home-cv-*`, `.home-header-grid`, `.geometric-panel`,
1390
+ `.cv-box`, `.card-footer*`, `.card-thumbnail`, `.card-tag`, `.cat-link`,
1391
+ `.listing-card-grid--3`, `.serif-font`, `.font-headline`, `.font-editorial`
1392
+ — 33 of its 96 class selectors reach no markup. `.code-technical-sm` is the mirror
1393
+ case: a new token nothing consumes.
1394
+ 9. ~~**The CV timeline is stuck in the light palette.**~~ **Fixed 2026-09.**
1395
+ The chart now reads its lane colours from live CSS custom properties
1396
+ (`--timeline-*`) instead of Liquid-baked hex, and redraws on the
1397
+ `shell-theme-change` event, so it follows `data-theme` like everything
1398
+ else. Two related defects went with it: six event types were sharing four
1399
+ colours (employment collided with article, product with post), and the
1400
+ density bars and legend swatches read `--color-primary` / `--color-tertiary`
1401
+ — rebound to green on the CV by `.page-accent--green`, which flattened the
1402
+ chart into one hue. The lane palette is now its own categorical scale in
1403
+ `_data/themes/*.yml` under `timeline:`, deliberately outside the UI triad:
1404
+ see *Semantic and media colours*.
1405
+ 10. **`404.html` was not rebuilt.** It still carries `rounded-xl` on two
1406
+ buttons, `shadow-sm`, and a `bg-gradient-to-b from-primary
1407
+ to-primary-container` fill — three of the system's rules broken on one
1408
+ page.
1409
+ 11. **Navigation is still fully hidden.** A rich, deep site sits beNewsreader one
1410
+ hamburger at every breakpoint, so its structure is invisible on arrival.
1411
+ This survived the redesign untouched.
1412
+ 12. **Tailwind runs from the Play CDN in production**
1413
+ (`cdn.tailwindcss.com?plugins=forms,container-queries`), so utilities are
1414
+ unpurged, unversioned, and injected *after* the hand CSS. The redesign now
1415
+ depends on beating it twice: `base.css` re-specifies the filter input
1416
+ because the forms plugin paints it white in dark mode, and
1417
+ `ledger/detail.css` documents writing rules with two compound parts on
1418
+ purpose for the same reason.
1419
+ 13. ~~**`surface` still diverges between themes.**~~ **Fixed 2026-09.** Light
1420
+ had `surface == background == #ffffff`, so any component distinguishing
1421
+ the two — the nav, the filter popover — was invisible there. The ladder
1422
+ inversion gave `surface` the card value (`#fffdf9`) against a paper
1423
+ canvas, so the two now differ in both themes.
1424
+ 14. ~~**Light has no `sidebar_thumbnail` block.**~~ **Fixed 2026-09.** Light
1425
+ defines its own block now, so neither theme falls through to
1426
+ `_config.yml`.
1427
+ 15. **The Mermaid theme map is duplicated verbatim** in
1428
+ `_includes/page/mermaid.html` and `_includes/content-runtime/20-markdown.html`
1429
+ — nine `themeColor(token, '#hex')` fallbacks in each, byte-identical, with
1430
+ nothing keeping them in step.
1431
+ 16. **`renderArchiveCard`'s `showFooterDates` path is dead.** No layout passes
1432
+ the option, and `ledger/home.css` additionally hides
1433
+ `.ledger-card-foot .date-evolution` with CSS — a belt for a brace that is
1434
+ already off.