pandorga 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (376) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +77 -0
  3. data/DESIGN.md +1434 -0
  4. data/LICENSE +21 -0
  5. data/README.md +81 -0
  6. data/_data/themes/dark.yml +97 -0
  7. data/_data/themes/light.yml +120 -0
  8. data/_data/themes/shared.yml +37 -0
  9. data/_data/translations.yml +85 -0
  10. data/_includes/chord-tab.html +38 -0
  11. data/_includes/content-runtime/00-config.html +22 -0
  12. data/_includes/content-runtime/10-utils.html +176 -0
  13. data/_includes/content-runtime/20-markdown.html +264 -0
  14. data/_includes/content-runtime/25-code-blocks.html +315 -0
  15. data/_includes/content-runtime/30-includes-bib.html +190 -0
  16. data/_includes/content-runtime/30-includes-xcite.html +122 -0
  17. data/_includes/content-runtime/30-includes.html +365 -0
  18. data/_includes/content-runtime/40-listing.html +225 -0
  19. data/_includes/content-runtime/41-listing-helpers.html +71 -0
  20. data/_includes/content-runtime/42-listing-filter.html +208 -0
  21. data/_includes/content-runtime/43-blocks.html +379 -0
  22. data/_includes/content-runtime/50-detail.html +861 -0
  23. data/_includes/content-runtime/55-toc.html +176 -0
  24. data/_includes/content-runtime/60-fragments.html +567 -0
  25. data/_includes/content-runtime/90-bootstrap.html +76 -0
  26. data/_includes/cut-in.html +10 -0
  27. data/_includes/cv/cv-box.html +18 -0
  28. data/_includes/dict-card.html +24 -0
  29. data/_includes/figure.html +36 -0
  30. data/_includes/home/band-index.html +43 -0
  31. data/_includes/home/band-listing.html +49 -0
  32. data/_includes/mathjax.html +10 -0
  33. data/_includes/ongoing-product.html +1 -0
  34. data/_includes/page/content-runtime.html +20 -0
  35. data/_includes/page/date_evolution.html +19 -0
  36. data/_includes/page/figure-lightbox.html +102 -0
  37. data/_includes/page/formatted_date.html +15 -0
  38. data/_includes/page/hero-default.html +18 -0
  39. data/_includes/page/listing-runtime-attrs.html +8 -0
  40. data/_includes/page/mermaid.html +101 -0
  41. data/_includes/page/network-graph.html +1253 -0
  42. data/_includes/page/page-header.html +41 -0
  43. data/_includes/page/professional-connections.html +2 -0
  44. data/_includes/page/site-footer.html +101 -0
  45. data/_includes/page/timeline-section.html +868 -0
  46. data/_includes/plotly.html +22 -0
  47. data/_includes/theme/dev-reexport.html +100 -0
  48. data/_includes/theme/favicons.html +4 -0
  49. data/_includes/theme/font-loader.html +55 -0
  50. data/_includes/theme/shell-theme-boot.html +115 -0
  51. data/_includes/theme/site-logo.html +6 -0
  52. data/_includes/theme/tailwind-theme-colors.json.liquid +65 -0
  53. data/_includes/theme/tailwind-theme-fonts.json.liquid +6 -0
  54. data/_includes/theme/theme-context.liquid +20 -0
  55. data/_includes/theme/theme-toggle.html +94 -0
  56. data/_includes/theme/theme-vars-block.liquid +161 -0
  57. data/_includes/theme/theme-vars.html +16 -0
  58. data/_includes/transcription.html +80 -0
  59. data/_includes/tweet.html +22 -0
  60. data/_includes/video.html +11 -0
  61. data/_includes/xcite.html +3 -0
  62. data/_includes/youtube-short.html +26 -0
  63. data/_includes/youtube.html +12 -0
  64. data/_layouts/article.html +27 -0
  65. data/_layouts/articles.html +534 -0
  66. data/_layouts/bibliography.html +417 -0
  67. data/_layouts/blog.html +544 -0
  68. data/_layouts/cv.html +393 -0
  69. data/_layouts/home.html +319 -0
  70. data/_layouts/network.html +9 -0
  71. data/_layouts/pandorga/articles.html +4 -0
  72. data/_layouts/pandorga/bibliography.html +4 -0
  73. data/_layouts/pandorga/blog.html +4 -0
  74. data/_layouts/pandorga/cv.html +4 -0
  75. data/_layouts/pandorga/media.html +4 -0
  76. data/_layouts/pandorga/network.html +4 -0
  77. data/_layouts/pandorga/portfolio.html +4 -0
  78. data/_layouts/post.html +7 -0
  79. data/_layouts/product.html +5 -0
  80. data/_layouts/project.html +27 -0
  81. data/_layouts/projects.html +534 -0
  82. data/_layouts/resources.html +387 -0
  83. data/_layouts/shell.html +203 -0
  84. data/_layouts/text.html +246 -0
  85. data/_plugins/bibliography_artifact.rb +57 -0
  86. data/_plugins/content_backend.rb +37 -0
  87. data/_plugins/content_collections.rb +51 -0
  88. data/_plugins/content_detail_paths.rb +58 -0
  89. data/_plugins/content_headers.rb +73 -0
  90. data/_plugins/content_keys.rb +310 -0
  91. data/_plugins/content_local_serve.rb +168 -0
  92. data/_plugins/content_site_adapter.rb +159 -0
  93. data/_plugins/content_validators_hook.rb +20 -0
  94. data/_plugins/content_writing_index.rb +52 -0
  95. data/_plugins/content_writing_paths.rb +55 -0
  96. data/_plugins/force_utf8_encoding.rb +13 -0
  97. data/_plugins/git_chronology.rb +211 -0
  98. data/_plugins/git_metadata.rb +43 -0
  99. data/_plugins/job_sections.rb +25 -0
  100. data/_plugins/lib/content_validators.rb +27 -0
  101. data/_plugins/lib/resource_validator.rb +56 -0
  102. data/_plugins/lib/tag_validator.rb +153 -0
  103. data/_plugins/lib/text_excerpt.rb +56 -0
  104. data/_plugins/lib/writing_entry.rb +107 -0
  105. data/_plugins/local_detail_rewrites.rb +194 -0
  106. data/_plugins/timeline.rb +507 -0
  107. data/_plugins/writing_slug_limits.rb +70 -0
  108. data/assets/css/article.css +1330 -0
  109. data/assets/css/base.css +1432 -0
  110. data/assets/css/cv.css +13 -0
  111. data/assets/css/global-link.css +94 -0
  112. data/assets/css/ledger/bibliography.css +307 -0
  113. data/assets/css/ledger/chrome.css +118 -0
  114. data/assets/css/ledger/cv.css +836 -0
  115. data/assets/css/ledger/detail.css +1270 -0
  116. data/assets/css/ledger/footer.css +298 -0
  117. data/assets/css/ledger/home.css +1490 -0
  118. data/assets/css/ledger/network.css +106 -0
  119. data/assets/css/ledger/projects.css +716 -0
  120. data/assets/css/ledger/rascunhos.css +379 -0
  121. data/assets/css/ledger/sources.css +327 -0
  122. data/assets/css/ledger/writing.css +754 -0
  123. data/assets/tailwind-play.js +15 -0
  124. data/examples/full/Gemfile +7 -0
  125. data/examples/full/_config.yml +92 -0
  126. data/examples/full/_redirects +3 -0
  127. data/examples/full/content/collections/articles/2026-01-01-welcome.md +9 -0
  128. data/examples/full/content/collections/data/contacts.yml +3 -0
  129. data/examples/full/content/collections/data/education.yml +4 -0
  130. data/examples/full/content/collections/jobs/research-fellow.md +12 -0
  131. data/examples/full/content/collections/posts/2026-01-02-note.md +9 -0
  132. data/examples/full/content/collections/products/ledger-tool.md +6 -0
  133. data/examples/full/content/collections/projects/demo-kit.md +10 -0
  134. data/examples/full/content/collections/resources/style-guide.md +7 -0
  135. data/examples/full/content/pages/cv/summary.md +1 -0
  136. data/examples/full/content/pages/headers.yml +20 -0
  137. data/examples/full/index.html +4 -0
  138. data/examples/minimal/Gemfile +7 -0
  139. data/examples/minimal/_config.yml +47 -0
  140. data/examples/minimal/_redirects +3 -0
  141. data/examples/minimal/content/collections/articles/2026-01-01-hello.md +10 -0
  142. data/examples/minimal/content/collections/articles/2026-01-02-second.md +9 -0
  143. data/examples/minimal/content/pages/headers.yml +6 -0
  144. data/examples/minimal/index.html +4 -0
  145. data/exe/pandorga +7 -0
  146. data/functions/api/studio/[[path]].js +562 -0
  147. data/functions/api/studio/_lib/auth.js +131 -0
  148. data/functions/api/studio/_lib/blocks.js +156 -0
  149. data/functions/api/studio/_lib/gemini.js +62 -0
  150. data/functions/api/studio/_lib/github.js +278 -0
  151. data/functions/api/studio/_lib/guidelines.js +143 -0
  152. data/functions/api/studio/_lib/paths.js +76 -0
  153. data/functions/api/studio/_lib/prompts.js +165 -0
  154. data/functions/api/studio/_lib/rate-limit.js +40 -0
  155. data/functions/api/studio/_lib/rewrite.js +136 -0
  156. data/functions/api/studio/_lib/sanitize.js +143 -0
  157. data/lib/pandorga/cli.rb +66 -0
  158. data/lib/pandorga/commands/doctor.rb +66 -0
  159. data/lib/pandorga/commands/export.rb +30 -0
  160. data/lib/pandorga/commands/install_functions.rb +41 -0
  161. data/lib/pandorga/commands/new.rb +113 -0
  162. data/lib/pandorga/commands/publish.rb +53 -0
  163. data/lib/pandorga/commands/serve.rb +34 -0
  164. data/lib/pandorga/commands/studio_schema.rb +309 -0
  165. data/lib/pandorga/jekyll/generator.rb +74 -0
  166. data/lib/pandorga/jekyll/plugins/bibliography_artifact.rb +57 -0
  167. data/lib/pandorga/jekyll/plugins/content_backend.rb +37 -0
  168. data/lib/pandorga/jekyll/plugins/content_collections.rb +51 -0
  169. data/lib/pandorga/jekyll/plugins/content_detail_paths.rb +58 -0
  170. data/lib/pandorga/jekyll/plugins/content_headers.rb +73 -0
  171. data/lib/pandorga/jekyll/plugins/content_keys.rb +310 -0
  172. data/lib/pandorga/jekyll/plugins/content_local_serve.rb +168 -0
  173. data/lib/pandorga/jekyll/plugins/content_site_adapter.rb +159 -0
  174. data/lib/pandorga/jekyll/plugins/content_validators_hook.rb +20 -0
  175. data/lib/pandorga/jekyll/plugins/content_writing_index.rb +52 -0
  176. data/lib/pandorga/jekyll/plugins/content_writing_paths.rb +55 -0
  177. data/lib/pandorga/jekyll/plugins/force_utf8_encoding.rb +13 -0
  178. data/lib/pandorga/jekyll/plugins/git_chronology.rb +211 -0
  179. data/lib/pandorga/jekyll/plugins/git_metadata.rb +43 -0
  180. data/lib/pandorga/jekyll/plugins/job_sections.rb +25 -0
  181. data/lib/pandorga/jekyll/plugins/lib/content_validators.rb +27 -0
  182. data/lib/pandorga/jekyll/plugins/lib/resource_validator.rb +56 -0
  183. data/lib/pandorga/jekyll/plugins/lib/tag_validator.rb +153 -0
  184. data/lib/pandorga/jekyll/plugins/lib/text_excerpt.rb +56 -0
  185. data/lib/pandorga/jekyll/plugins/lib/writing_entry.rb +107 -0
  186. data/lib/pandorga/jekyll/plugins/local_detail_rewrites.rb +194 -0
  187. data/lib/pandorga/jekyll/plugins/timeline.rb +507 -0
  188. data/lib/pandorga/jekyll/plugins/writing_slug_limits.rb +70 -0
  189. data/lib/pandorga/jekyll.rb +15 -0
  190. data/lib/pandorga/object_store_env.rb +51 -0
  191. data/lib/pandorga/registry/schema.yml +115 -0
  192. data/lib/pandorga/registry.rb +279 -0
  193. data/lib/pandorga/version.rb +5 -0
  194. data/lib/pandorga.rb +19 -0
  195. data/scripts/cdn-allowlist.json +19 -0
  196. data/scripts/content/backfill-content-keys.rb +99 -0
  197. data/scripts/content/band_art.rb +362 -0
  198. data/scripts/content/export-content-json.rb +741 -0
  199. data/scripts/content/flatten-ansi-capture.mjs +153 -0
  200. data/scripts/content/hero_portraits.rb +28 -0
  201. data/scripts/content/publish-content-to-object-store.sh +78 -0
  202. data/scripts/content/publish-content-to-r2.sh +5 -0
  203. data/scripts/content/sync-content-json-to-r2.rb +167 -0
  204. data/scripts/content/sync-content-redirects.rb +12 -0
  205. data/scripts/generate-configuration-doc.rb +127 -0
  206. data/scripts/lib/content_license.rb +50 -0
  207. data/scripts/lib/content_storage_config.rb +70 -0
  208. data/scripts/lib/normalize-display-math.mjs +52 -0
  209. data/scripts/lib/r2_json_sync.rb +76 -0
  210. data/scripts/lib/withdrawn_public_objects.rb +23 -0
  211. data/scripts/lib/writing_index_contract.rb +61 -0
  212. data/scripts/pdf/md-to-pdf.mjs +1400 -0
  213. data/scripts/pdf/md-to-pdf.sh +27 -0
  214. data/scripts/test/fixtures/dual-template/_config.yml +32 -0
  215. data/scripts/test/fixtures/dual-template/index.html +4 -0
  216. data/scripts/test/render-parity/article-note.json +5 -0
  217. data/scripts/test/render-parity/blockquote.json +5 -0
  218. data/scripts/test/render-parity/blocks/ansi-basic.json +7 -0
  219. data/scripts/test/render-parity/blocks/ansi-bold-is-bright.json +7 -0
  220. data/scripts/test/render-parity/blocks/ansi-drops-cursor-moves.json +7 -0
  221. data/scripts/test/render-parity/blocks/ansi-escapes-markup.json +7 -0
  222. data/scripts/test/render-parity/blocks/ansi-extended-colour.json +7 -0
  223. data/scripts/test/render-parity/blocks/ansi-real-control-byte.json +7 -0
  224. data/scripts/test/render-parity/blocks/archive-card-date-once.json +19 -0
  225. data/scripts/test/render-parity/blocks/archive-card-empty-foot.json +7 -0
  226. data/scripts/test/render-parity/blocks/archive-card-footer-dates.json +22 -0
  227. data/scripts/test/render-parity/blocks/archive-card-meta-parts.json +26 -0
  228. data/scripts/test/render-parity/blocks/archive-card.json +32 -0
  229. data/scripts/test/render-parity/blocks/cta-link.json +13 -0
  230. data/scripts/test/render-parity/blocks/lang-pill-pt.json +8 -0
  231. data/scripts/test/render-parity/blocks/lang-pill.json +8 -0
  232. data/scripts/test/render-parity/blocks/ledger-row-meta-parts.json +29 -0
  233. data/scripts/test/render-parity/blocks/ledger-row.json +35 -0
  234. data/scripts/test/render-parity/blocks/meta-strip.json +27 -0
  235. data/scripts/test/render-parity/blocks/poster-media-image.json +8 -0
  236. data/scripts/test/render-parity/blocks/poster-media-video.json +9 -0
  237. data/scripts/test/render-parity/blocks/section-banner.json +13 -0
  238. data/scripts/test/render-parity/blocks/system-card-custom-meta.json +22 -0
  239. data/scripts/test/render-parity/blocks/system-card.json +48 -0
  240. data/scripts/test/render-parity/blocks/tag-chips-links.json +12 -0
  241. data/scripts/test/render-parity/blocks/tag-chips.json +9 -0
  242. data/scripts/test/render-parity/cut-in-blank-line.json +9 -0
  243. data/scripts/test/render-parity/cut-in-right.json +9 -0
  244. data/scripts/test/render-parity/cut-in.json +8 -0
  245. data/scripts/test/render-parity/figure.json +5 -0
  246. data/scripts/test/render-parity/load-runtime.mjs +337 -0
  247. data/scripts/test/render-parity/ongoing-product.json +5 -0
  248. data/scripts/test/render-parity/plotly.json +5 -0
  249. data/scripts/test/render-parity/poem.json +5 -0
  250. data/scripts/test/render-parity/run-absent-field-checks.mjs +232 -0
  251. data/scripts/test/render-parity/run-block-fixture.mjs +86 -0
  252. data/scripts/test/render-parity/run-detail-parity-checks.mjs +123 -0
  253. data/scripts/test/render-parity/run-field-flow-checks.mjs +227 -0
  254. data/scripts/test/render-parity/run-fixture.mjs +159 -0
  255. data/scripts/test/render-parity/selectors.mjs +43 -0
  256. data/scripts/test/render-parity/video.json +5 -0
  257. data/scripts/test/render-parity/youtube-short.json +10 -0
  258. data/scripts/test/render-parity/youtube.json +8 -0
  259. data/scripts/test/test-absent-field-absent-element.rb +43 -0
  260. data/scripts/test/test-cdn-pins.rb +44 -0
  261. data/scripts/test/test-cloudflare-watch-paths-doc.rb +31 -0
  262. data/scripts/test/test-configuration-doc.rb +36 -0
  263. data/scripts/test/test-content-detail-urls.rb +346 -0
  264. data/scripts/test/test-content-keys.rb +110 -0
  265. data/scripts/test/test-content-license.rb +45 -0
  266. data/scripts/test/test-content-render-parity.rb +38 -0
  267. data/scripts/test/test-content-validators.rb +12 -0
  268. data/scripts/test/test-css-structure.rb +72 -0
  269. data/scripts/test/test-cv-export-contract.rb +158 -0
  270. data/scripts/test/test-detail-render-parity.rb +40 -0
  271. data/scripts/test/test-dev-reexport.rb +122 -0
  272. data/scripts/test/test-doctor.rb +39 -0
  273. data/scripts/test/test-dual-template-instances.rb +87 -0
  274. data/scripts/test/test-dual-template-runtime.rb +167 -0
  275. data/scripts/test/test-examples-build.rb +60 -0
  276. data/scripts/test/test-export-content-json-incremental.rb +138 -0
  277. data/scripts/test/test-export.rb +53 -0
  278. data/scripts/test/test-exported-fields-reach-the-view.rb +60 -0
  279. data/scripts/test/test-form-control-theming.rb +143 -0
  280. data/scripts/test/test-git-chronology.rb +414 -0
  281. data/scripts/test/test-hero-portraits.mjs +97 -0
  282. data/scripts/test/test-hero-portraits.rb +162 -0
  283. data/scripts/test/test-home-band-art.rb +135 -0
  284. data/scripts/test/test-install-functions.rb +39 -0
  285. data/scripts/test/test-language-export.rb +79 -0
  286. data/scripts/test/test-listing-filter-contract.rb +119 -0
  287. data/scripts/test/test-math-row-breaks.mjs +79 -0
  288. data/scripts/test/test-math-row-breaks.rb +18 -0
  289. data/scripts/test/test-new.rb +113 -0
  290. data/scripts/test/test-no-personal-data.rb +85 -0
  291. data/scripts/test/test-public-cv-export.rb +164 -0
  292. data/scripts/test/test-r2-config-consistency.rb +57 -0
  293. data/scripts/test/test-r2-json-sync.rb +193 -0
  294. data/scripts/test/test-registry-home.rb +34 -0
  295. data/scripts/test/test-runtime-js-syntax.rb +64 -0
  296. data/scripts/test/test-slim-writing-index.rb +126 -0
  297. data/scripts/test/test-studio-api-contract.rb +99 -0
  298. data/scripts/test/test-studio-content-files.rb +79 -0
  299. data/scripts/test/test-studio-liquid-roundtrip.mjs +179 -0
  300. data/scripts/test/test-studio-rate-limit.mjs +70 -0
  301. data/scripts/test/test-studio-rewrite-api-contract.rb +59 -0
  302. data/scripts/test/test-studio-rewrite-sanitize.mjs +210 -0
  303. data/scripts/test/test-studio-schema-composition.rb +126 -0
  304. data/scripts/test/test-studio-slug-config.rb +167 -0
  305. data/scripts/test/test-template-packages.rb +51 -0
  306. data/scripts/test/test-text-excerpt.rb +51 -0
  307. data/scripts/test/test-unwrap-media-src.mjs +39 -0
  308. data/scripts/test/test-unwrap-media-src.rb +19 -0
  309. data/scripts/test/test-withdrawn-content-export.rb +69 -0
  310. data/scripts/test/test-writing-index-export-slugs.rb +84 -0
  311. data/scripts/test/test-writing-slug-limits.rb +61 -0
  312. data/scripts/test/test-xcite-chronology.rb +106 -0
  313. data/scripts/validate.sh +74 -0
  314. data/scripts/visual/build-cv-pages-json.js +33 -0
  315. data/scripts/visual/build-site-pages-json.js +94 -0
  316. data/scripts/visual/publish-visual-inspection-to-r2.sh +42 -0
  317. data/scripts/visual/take-screenshots.js +88 -0
  318. data/scripts/visual/visual-inspection-pages.json +113 -0
  319. data/studio/assets/index-B_j0pMD5.css +1 -0
  320. data/studio/assets/index-Ch4YEv9g.js +285 -0
  321. data/studio/index.html +27 -0
  322. data/studio/schema.yml +869 -0
  323. data/studio-app/README.md +27 -0
  324. data/studio-app/index.html +26 -0
  325. data/studio-app/package-lock.json +2499 -0
  326. data/studio-app/package.json +26 -0
  327. data/studio-app/skills-lock.json +47 -0
  328. data/studio-app/src/api/client.js +71 -0
  329. data/studio-app/src/editor/body-editor.js +70 -0
  330. data/studio-app/src/editor/liquid-core.js +382 -0
  331. data/studio-app/src/editor/liquid.js +77 -0
  332. data/studio-app/src/lib/document.js +207 -0
  333. data/studio-app/src/lib/list-controls.js +317 -0
  334. data/studio-app/src/lib/schema.js +66 -0
  335. data/studio-app/src/lib/yaml-doc.js +100 -0
  336. data/studio-app/src/main.js +3693 -0
  337. data/studio-app/src/media/browser.js +578 -0
  338. data/studio-app/src/preview/render.js +430 -0
  339. data/studio-app/src/styles/studio.css +2826 -0
  340. data/studio-app/vite.config.js +25 -0
  341. data/templates/articles/README.md +40 -0
  342. data/templates/articles/fixtures/article.md +9 -0
  343. data/templates/articles/screenshots/README.md +6 -0
  344. data/templates/articles/studio.yml +73 -0
  345. data/templates/articles/template.yml +21 -0
  346. data/templates/bibliography/README.md +41 -0
  347. data/templates/bibliography/fixtures/references.json +9 -0
  348. data/templates/bibliography/screenshots/README.md +6 -0
  349. data/templates/bibliography/studio.yml +66 -0
  350. data/templates/bibliography/template.yml +18 -0
  351. data/templates/blog/README.md +38 -0
  352. data/templates/blog/fixtures/post.md +9 -0
  353. data/templates/blog/screenshots/README.md +6 -0
  354. data/templates/blog/studio.yml +69 -0
  355. data/templates/blog/template.yml +20 -0
  356. data/templates/cv/README.md +34 -0
  357. data/templates/cv/fixtures/job.md +12 -0
  358. data/templates/cv/screenshots/README.md +6 -0
  359. data/templates/cv/studio.yml +200 -0
  360. data/templates/cv/template.yml +25 -0
  361. data/templates/media/README.md +38 -0
  362. data/templates/media/fixtures/resource.md +7 -0
  363. data/templates/media/screenshots/README.md +6 -0
  364. data/templates/media/studio.yml +58 -0
  365. data/templates/media/template.yml +17 -0
  366. data/templates/network/README.md +34 -0
  367. data/templates/network/fixtures/sources.json +3 -0
  368. data/templates/network/screenshots/README.md +6 -0
  369. data/templates/network/studio.yml +28 -0
  370. data/templates/network/template.yml +10 -0
  371. data/templates/portfolio/README.md +39 -0
  372. data/templates/portfolio/fixtures/project.md +10 -0
  373. data/templates/portfolio/screenshots/README.md +6 -0
  374. data/templates/portfolio/studio.yml +89 -0
  375. data/templates/portfolio/template.yml +18 -0
  376. metadata +505 -0
@@ -0,0 +1,1432 @@
1
+ /* ── Material Symbols ── */
2
+ .material-symbols-outlined {
3
+ font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ vertical-align: middle;
8
+ line-height: 1;
9
+ text-transform: none;
10
+ }
11
+
12
+ /* Cubist triad — ~+25% over the 1.25rem theme-toggle box (rounded to 1.5rem). */
13
+ .site-logo {
14
+ display: block;
15
+ width: 1.5rem;
16
+ height: 1.5rem;
17
+ overflow: visible;
18
+ flex-shrink: 0;
19
+ }
20
+
21
+ #theme-toggle-icon.material-symbols-outlined {
22
+ font-size: var(--type-card);
23
+ width: 1.25rem;
24
+ height: 1.25rem;
25
+ line-height: 1;
26
+ }
27
+
28
+ /* Light page → pastel triad; dark page → production triad. Each theme paints
29
+ the mark in the *other* theme's primary/secondary/tertiary, which is why
30
+ these are literals: a palette only ever exposes its own three as custom
31
+ properties, so there is no token here to reach for. Tokenising them needs
32
+ inverse_secondary / inverse_tertiary in `_data/themes/*.yml` (and
33
+ inverse_primary corrected to the triad value) wired through
34
+ `_includes/theme/theme-vars-block.liquid`. */
35
+ :root .site-logo-primary,
36
+ [data-theme="light"] .site-logo-primary {
37
+ fill: #a4c8da;
38
+ }
39
+
40
+ :root .site-logo-secondary,
41
+ [data-theme="light"] .site-logo-secondary {
42
+ fill: #95c8a6;
43
+ }
44
+
45
+ :root .site-logo-tertiary,
46
+ [data-theme="light"] .site-logo-tertiary {
47
+ fill: #ddd8a8;
48
+ }
49
+
50
+ [data-theme="dark"] .site-logo-primary {
51
+ fill: #1c5a7a;
52
+ }
53
+
54
+ [data-theme="dark"] .site-logo-secondary {
55
+ fill: #287445;
56
+ }
57
+
58
+ [data-theme="dark"] .site-logo-tertiary {
59
+ fill: #746f30;
60
+ }
61
+
62
+ /* Same contract as Studio `.studio-mark-frame`: hairline in on-surface-variant
63
+ (light page → dark edge, dark page → light edge). Public site token is
64
+ `--color-on-surface-variant` (Studio uses `--on-surface-variant`). */
65
+ .site-logo-frame {
66
+ fill: none;
67
+ stroke: var(--color-on-surface-variant);
68
+ stroke-width: 1;
69
+ stroke-linejoin: miter;
70
+ vector-effect: non-scaling-stroke;
71
+ }
72
+
73
+ /* ── Base typography ──
74
+ Scale maps Stitch Borders tokens (§19.4) onto rem at root 110%:
75
+ display-hero → --type-display · headline-lg → --type-page ·
76
+ headline-md → --type-section · headline-sm → --type-card ·
77
+ body-lg → --type-prose · body-md → --type-body · body-sm → --type-meta ·
78
+ label-md → --type-label · label-lg → --type-label-lg.
79
+ Type tokens are literal rem at html { font-size: 110% } (1rem = 17.6px).
80
+ They already include the old +2pt / +1pt / +1.3pt steps. Prose is
81
+ 1.2rem below 1024px and 1.25rem from there, plus `--prose-size-offset`
82
+ (−4pt … +4pt). Selectors consume tokens directly. */
83
+ html {
84
+ font-size: 110%;
85
+
86
+ /* Display — Cinzel Decorative 400 */
87
+ --type-display: 2.9015rem;
88
+ --type-display-lg: 3.6515rem;
89
+ --type-page: 2.4015rem;
90
+ --type-page-lg: 2.9015rem;
91
+ --type-section: 1.9015rem;
92
+ --type-section-lg: 2.1515rem;
93
+
94
+ /* Content titles — Newsreader Regular 400 */
95
+ --type-title: 2.0015rem;
96
+ --type-title-lg: 2.4015rem;
97
+ --type-card: 1.4015rem;
98
+ --type-card-lg: 1.5515rem;
99
+
100
+ /* Reading + UI — Newsreader Light 300.
101
+ Default reading size is 1.2rem below 1024px and 1.25rem from there.
102
+ `--prose-size-offset` is the detail-page step (−4pt through +4pt); the
103
+ shell sets it from the reader's saved choice. The desktop step is in the
104
+ media query below. `--drop-cap-lines` is the fallback; the shell
105
+ overrides it from `_config.yml` `drop_cap_lines`. */
106
+ --prose-size-offset: 0pt;
107
+ --type-prose: calc(1.2rem + var(--prose-size-offset));
108
+ --drop-cap-lines: 2;
109
+ --type-body: 1.0758rem;
110
+ /* Card/row blurbs, standfirsts, project/source copy — same as body. */
111
+ --type-subtitle: var(--type-body);
112
+ --type-dek: 0.9375rem;
113
+ --type-meta: 0.8125rem;
114
+
115
+ /* Chrome — quiet relative to body (tuned for Marcellus SC cap-height to match icons) */
116
+ --type-label: 0.8485rem;
117
+ --type-label-lg: 0.9735rem;
118
+ --type-nav: 1.036rem;
119
+ --type-cta: 0.911rem;
120
+ --type-mono: 0.8504rem;
121
+ --type-micro: 0.75rem;
122
+
123
+ /* Soft apparatus ink — legends, card excerpts, muted descriptions. */
124
+ --color-legend: color-mix(in srgb, var(--color-on-surface) 58%, var(--color-on-surface-variant));
125
+
126
+ --font-size-body: var(--type-body);
127
+ --leading-body: 1.5;
128
+ --leading-prose: 1.6;
129
+
130
+ --weight-display: 400;
131
+ --weight-title: 400;
132
+ --weight-body: 300;
133
+ --weight-label: 400;
134
+ --weight-emphasis: 500;
135
+
136
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-default), color var(--motion-duration-fast) var(--motion-ease-default);
137
+ }
138
+
139
+ @media (min-width: 1024px) {
140
+ html {
141
+ --type-prose: calc(1.25rem + var(--prose-size-offset));
142
+ }
143
+ }
144
+
145
+ /* `html body`, not `body`: Tailwind's Preflight ships `body { line-height:
146
+ inherit }` from the CDN <style> that loads after this file, so a bare `body`
147
+ selector here loses the tie on source order and the leading silently becomes
148
+ Preflight's. That is how the old 1.65 in this rule came to be inert. */
149
+ html body {
150
+ font-family: var(--font-body);
151
+ font-size: var(--font-size-body);
152
+ font-weight: var(--weight-body);
153
+ background-color: var(--color-background);
154
+ /* Quiet square grid on both palettes; only --color-background changes. */
155
+ background-image:
156
+ linear-gradient(color-mix(in srgb, var(--color-outline) 4%, transparent) 1px, transparent 1px),
157
+ linear-gradient(90deg, color-mix(in srgb, var(--color-outline) 4%, transparent) 1px, transparent 1px);
158
+ background-size: 1.25rem 1.25rem;
159
+ background-attachment: fixed;
160
+ color: var(--color-on-surface);
161
+ line-height: var(--leading-body);
162
+ letter-spacing: 0.01em;
163
+ -webkit-font-smoothing: antialiased;
164
+ }
165
+
166
+ /* Display face only when opted in — do not paint every heading Cinzel
167
+ (article / card titles use --font-content-title). */
168
+ .font-title {
169
+ font-family: var(--font-title);
170
+ font-synthesis: none;
171
+ }
172
+
173
+ /* ── Utility class used in various places ── */
174
+ .marginalia {
175
+ font-family: var(--font-label);
176
+ font-size: var(--type-nav);
177
+ font-weight: var(--weight-label);
178
+ letter-spacing: 0.05em;
179
+ }
180
+
181
+ /* ── Display title scale (home hero, 404) ── */
182
+ .page-title {
183
+ font-family: var(--font-title);
184
+ font-size: var(--type-display);
185
+ font-weight: var(--weight-display);
186
+ line-height: 1.05;
187
+ color: var(--color-on-surface);
188
+ letter-spacing: normal;
189
+ font-synthesis: none;
190
+ font-optical-sizing: auto;
191
+ }
192
+
193
+ @media (min-width: 768px) {
194
+ .page-title {
195
+ font-size: var(--type-display-lg);
196
+ }
197
+ }
198
+
199
+ /* Listing page headers (CV, Articles, Portfolio, Rascunhos, …) */
200
+ .page-header-title {
201
+ font-family: var(--font-title);
202
+ font-size: var(--type-page);
203
+ font-weight: var(--weight-display);
204
+ line-height: 1.15;
205
+ color: var(--color-on-surface);
206
+ letter-spacing: normal;
207
+ margin-bottom: 0.5rem;
208
+ font-synthesis: none;
209
+ font-optical-sizing: auto;
210
+ }
211
+
212
+ @media (min-width: 768px) {
213
+ .page-header-title {
214
+ font-size: var(--type-page-lg);
215
+ line-height: 1.1;
216
+ }
217
+ }
218
+
219
+ /* Article / post / project detail titles — content title voice.
220
+ Regular 400 only — never Medium/bold, and never synthesize a heavier cut. */
221
+ .text-title {
222
+ font-family: var(--font-content-title);
223
+ font-size: var(--type-title);
224
+ font-weight: var(--weight-title);
225
+ font-synthesis: none;
226
+ line-height: 1.2;
227
+ color: var(--color-on-surface);
228
+ letter-spacing: normal;
229
+ font-optical-sizing: auto;
230
+ }
231
+
232
+ @media (min-width: 768px) {
233
+ .text-title {
234
+ font-size: var(--type-title-lg);
235
+ }
236
+ }
237
+
238
+ .section-title {
239
+ font-family: var(--font-title);
240
+ font-size: var(--type-section);
241
+ font-weight: var(--weight-display);
242
+ line-height: 1.15;
243
+ color: var(--color-on-surface);
244
+ font-synthesis: none;
245
+ font-optical-sizing: auto;
246
+ }
247
+
248
+ @media (min-width: 640px) {
249
+ .section-title {
250
+ font-size: var(--type-section-lg);
251
+ }
252
+ }
253
+
254
+ /* Featured article lead on the writing index is a content title, not a page name. */
255
+ .writing-lead-title.section-title {
256
+ font-family: var(--font-content-title);
257
+ font-weight: var(--weight-title);
258
+ font-synthesis: none;
259
+ }
260
+
261
+ /* The italicized second half of a section or page title ("Articles & Notes",
262
+ "Personal Projects & Experiments") carries the page's accent colour plus a
263
+ marker highlight — felt-pen underlay, not invented copy. Same wash on home
264
+ band titles (`.section-title em`) and listing mastheads (`.page-header-title
265
+ em`) so the two never diverge. */
266
+ .section-title em,
267
+ .page-header-title em {
268
+ font-style: italic;
269
+ color: var(--color-primary);
270
+ background: linear-gradient(104deg,
271
+ transparent 0.9%,
272
+ color-mix(in srgb, var(--color-primary) 16%, transparent) 2.4%,
273
+ color-mix(in srgb, var(--color-primary) 12%, transparent) 58%,
274
+ color-mix(in srgb, var(--color-primary) 4%, transparent) 98%);
275
+ box-decoration-break: clone;
276
+ -webkit-box-decoration-break: clone;
277
+ padding: 0 0.1em;
278
+ }
279
+
280
+ /* Dark ink paper: dim the felt wash so primary/ochre highlighter does not glow. */
281
+ [data-theme="dark"] .section-title em,
282
+ [data-theme="dark"] .page-header-title em {
283
+ background: linear-gradient(104deg,
284
+ transparent 0.9%,
285
+ color-mix(in srgb, var(--color-primary) 9%, transparent) 2.4%,
286
+ color-mix(in srgb, var(--color-primary) 6%, transparent) 58%,
287
+ color-mix(in srgb, var(--color-primary) 2%, transparent) 98%);
288
+ }
289
+
290
+
291
+
292
+ /* When headers.yml marks only half the title, the outer loading span's
293
+ blanket italic must not italicize the unmarked half. */
294
+ .page-header-title:has(em) [data-content-header-title] {
295
+ font-style: normal;
296
+ }
297
+
298
+ /* Marker wash for any field-backed highlight (hero family name, etc.). */
299
+ .ledger-marker {
300
+ background: linear-gradient(104deg,
301
+ transparent 0.9%,
302
+ color-mix(in srgb, var(--color-tertiary) 20%, transparent) 2.4%,
303
+ color-mix(in srgb, var(--color-tertiary) 16%, transparent) 58%,
304
+ color-mix(in srgb, var(--color-tertiary) 5%, transparent) 98%);
305
+ box-decoration-break: clone;
306
+ -webkit-box-decoration-break: clone;
307
+ padding: 0 0.1em;
308
+ }
309
+
310
+ [data-theme="dark"] .ledger-marker {
311
+ background: linear-gradient(104deg,
312
+ transparent 0.9%,
313
+ color-mix(in srgb, var(--color-tertiary) 10%, transparent) 2.4%,
314
+ color-mix(in srgb, var(--color-tertiary) 7%, transparent) 58%,
315
+ color-mix(in srgb, var(--color-tertiary) 2%, transparent) 98%);
316
+ }
317
+
318
+ .ledger-marker--primary {
319
+ background: linear-gradient(104deg,
320
+ transparent 0.9%,
321
+ color-mix(in srgb, var(--color-primary) 16%, transparent) 2.4%,
322
+ color-mix(in srgb, var(--color-primary) 12%, transparent) 58%,
323
+ color-mix(in srgb, var(--color-primary) 4%, transparent) 98%);
324
+ }
325
+
326
+ [data-theme="dark"] .ledger-marker--primary {
327
+ background: linear-gradient(104deg,
328
+ transparent 0.9%,
329
+ color-mix(in srgb, var(--color-primary) 9%, transparent) 2.4%,
330
+ color-mix(in srgb, var(--color-primary) 6%, transparent) 58%,
331
+ color-mix(in srgb, var(--color-primary) 2%, transparent) 98%);
332
+ }
333
+
334
+ /* ── Page-level accent ────────────────────────────────────────────────
335
+ The shared triad (theme/theme-vars-block.liquid) declares one primary per
336
+ theme, but each section of the site reads in its own hue: Projects and
337
+ Sources blue, Articles and the CV green, Rascunhos and Network ochre.
338
+ Rather than forking the palette, a page puts one of these classes on its
339
+ top-level container and the class rebinds the same custom property names
340
+ theme-vars-block.liquid already defines, pointing them at an *existing*
341
+ triad token — never a new literal. Custom properties inherit, so every
342
+ rule anywhere (here or in a ledger/*.css) that reads `var(--color-primary)`
343
+ picks the page's hue up for free: card titles, CTA fills and borders,
344
+ section-title italics.
345
+
346
+ Parameterisation: set `accent: green|ochre` in the listing page front
347
+ matter and in `content/pages/headers.yml` (home bands apply it at
348
+ hydrate time). Collection defaults in `_config.yml` cover studio/detail
349
+ paths. Omit the field (or use `blue`) for the default primary.
350
+
351
+ The hover tone is mixed toward `--color-on-surface` rather than borrowing
352
+ a `*-fixed-dim` token. Those tokens hold the same value in both palettes,
353
+ so they lighten the link on hover in *both* — correct against a near-black
354
+ canvas, wrong against white paper, where hover has to go darker to read as
355
+ emphasis. Mixing toward the foreground ink moves the right direction in
356
+ each theme automatically, because that ink is dark in light and light in
357
+ dark. Blue needs no class: it is the default primary. Categorical drawings
358
+ that must keep the absolute triad (network graph) read `--triad-*` instead.
359
+
360
+ These lived in cv.css and writing.css as two byte-similar copies until
361
+ the third page needed one. One definition, here, beside the rule that
362
+ consumes it. */
363
+ .page-accent--green {
364
+ --color-primary: var(--color-secondary);
365
+ --color-link: var(--color-secondary);
366
+ --color-link-hover: color-mix(in srgb, var(--color-secondary) 72%, var(--color-on-surface));
367
+ }
368
+
369
+ .page-accent--ochre {
370
+ --color-primary: var(--color-tertiary);
371
+ --color-link: var(--color-tertiary);
372
+ --color-link-hover: color-mix(in srgb, var(--color-tertiary) 72%, var(--color-on-surface));
373
+ }
374
+
375
+ /* Card / row / collection entry titles — content-title voice (Regular 400). */
376
+ .card-title,
377
+ .ledger-card-title,
378
+ .ledger-row-title,
379
+ .source-card-title {
380
+ font-family: var(--font-content-title);
381
+ font-size: var(--type-card);
382
+ font-weight: var(--weight-title);
383
+ font-synthesis: none;
384
+ line-height: 1.25;
385
+ color: var(--color-on-surface);
386
+ font-optical-sizing: auto;
387
+ }
388
+
389
+ /* Label-face meta on CV identity lines, etc. */
390
+ .card-meta {
391
+ font-family: var(--font-label);
392
+ font-size: var(--type-meta);
393
+ font-weight: var(--weight-label);
394
+ letter-spacing: 0.04em;
395
+ }
396
+
397
+ .card-meta-block {
398
+ display: flex;
399
+ flex-direction: column;
400
+ align-items: flex-start;
401
+ gap: 0.5rem;
402
+ }
403
+
404
+ /* Notation — tabular mono only. */
405
+ .code-technical {
406
+ font-family: var(--font-mono);
407
+ font-size: var(--type-mono);
408
+ line-height: 1.45;
409
+ font-variant-numeric: tabular-nums;
410
+ }
411
+
412
+ /* Masthead tag line under the page H1. */
413
+ [data-content-header-tags],
414
+ .page-header-tags {
415
+ font-size: var(--type-dek);
416
+ line-height: 1.45;
417
+ letter-spacing: 0.04em;
418
+ }
419
+
420
+ /* Dates, kickers, section notes — body face at meta size. */
421
+ .ledger-meta,
422
+ .ledger-kicker,
423
+ .ledger-section-note {
424
+ font-family: var(--font-body);
425
+ font-size: var(--type-meta);
426
+ font-weight: var(--weight-body);
427
+ line-height: 1.45;
428
+ font-variant-numeric: tabular-nums;
429
+ }
430
+
431
+ /* ── Detail-page return link ── */
432
+ .back-link {
433
+ color: var(--color-primary);
434
+ text-decoration: none;
435
+ font-size: var(--type-label-lg);
436
+ display: inline-block;
437
+ margin-bottom: 2rem;
438
+ }
439
+
440
+ .back-link:hover {
441
+ text-decoration: underline;
442
+ }
443
+
444
+ /* Text selection: quiet ink wash at high transparency, matching the
445
+ neutral shard/hero highlights rather than a primary-container spotlight. */
446
+ ::selection {
447
+ background: color-mix(in srgb, var(--color-on-surface) 12%, transparent);
448
+ color: inherit;
449
+ }
450
+
451
+ ::-moz-selection {
452
+ background: color-mix(in srgb, var(--color-on-surface) 12%, transparent);
453
+ color: inherit;
454
+ }
455
+
456
+ /* ── Shared layout tokens ── */
457
+ /* Light densifies paper rules. Prefer :root:not([data-theme=dark]) over
458
+ html:root so a missing attribute (pre-boot) still densifies light without
459
+ also matching dark once data-theme=dark is set. Dark keeps Liquid defaults
460
+ from theme-vars-block — do not re-dense them here. */
461
+ html:root,
462
+ html[data-theme="light"] {
463
+ --cubist-shard-opacity: 18%;
464
+ --cubist-shard-opacity-strong: 26%;
465
+ }
466
+
467
+ :root:not([data-theme="dark"]),
468
+ html[data-theme="light"] {
469
+ /* Light paper rules: denser outline mixes so white-on-white cards keep a
470
+ Stitch-like margin. */
471
+ --border-gentle-faint: color-mix(in srgb, var(--color-outline-variant) 40%, transparent);
472
+ --border-gentle-subtle: color-mix(in srgb, var(--color-outline-variant) 58%, transparent);
473
+ --border-gentle-divider: color-mix(in srgb, var(--color-outline-variant) 64%, transparent);
474
+ --border-gentle: color-mix(in srgb, var(--color-outline-variant) 82%, transparent);
475
+ --border-gentle-hover: color-mix(in srgb, var(--color-outline-variant) 94%, transparent);
476
+ /* Card / chip frames: same hairline as the CV disclaimer box
477
+ (`.cv-disclaimer-box` → `--border-gentle-subtle`). Opaque mid-ink was
478
+ heavier than the rest of the light ledger; this outline-variant mix
479
+ keeps a clear edge without a second border family. */
480
+ --border-ink: var(--border-gentle-subtle);
481
+ --border-ink-strong: var(--border-gentle-subtle);
482
+ }
483
+
484
+ html:root,
485
+ html[data-theme="light"] {
486
+ /* Spacing scale. Rhythm comes from deliberate jumps on this ladder rather
487
+ than from repeated rules, so components step xl -> 2xl -> 3xl between
488
+ bands and stay on sm/md inside a card. */
489
+ --space-xs: 0.25rem;
490
+ --space-sm: 0.5rem;
491
+ --space-md: 1rem;
492
+ --space-lg: 1.5rem;
493
+ --space-xl: 2rem;
494
+ --space-2xl: 3rem;
495
+ --space-3xl: 4.5rem;
496
+ --gutter: 2rem;
497
+ --gutter-mobile: 1rem;
498
+
499
+ /* The two canonical shard polygons. Every accent uses one of these; the
500
+ five ad hoc variants the old home and CV panels carried went out with
501
+ those panels, so nothing outside this pair is left. */
502
+ --shard-alpha: polygon(26% 0, 100% 0, 78% 100%, 0 68%);
503
+ --shard-beta: polygon(0 0, 74% 18%, 100% 100%, 18% 84%);
504
+ }
505
+
506
+ html[data-theme="dark"] {
507
+ --cubist-shard-opacity: 10%;
508
+ --cubist-shard-opacity-strong: 14%;
509
+ /* Dark Borders: sit on the canvas, barely a step above background. */
510
+ --border-ink: color-mix(in srgb, var(--color-on-surface) 5%, var(--color-background));
511
+ --border-ink-strong: color-mix(in srgb, var(--color-on-surface) 8%, var(--color-background));
512
+ }
513
+
514
+ /* Soft paper radius token (0 = square Borders Edition). Pills and status
515
+ dots stay fully round; shards stay clipped polygons. */
516
+ .shard-accent {
517
+ content: "";
518
+ position: absolute;
519
+ pointer-events: none;
520
+ z-index: 0;
521
+ clip-path: var(--shard-alpha);
522
+ opacity: var(--cubist-shard-opacity);
523
+ }
524
+
525
+ .shard-accent-beta {
526
+ clip-path: var(--shard-beta);
527
+ }
528
+
529
+ /* Shards are decoration, not information: below the tablet breakpoint they
530
+ cost layout without earning it. */
531
+ @media (max-width: 639px) {
532
+ .shard-accent {
533
+ display: none;
534
+ }
535
+ }
536
+
537
+ .professional-connections-grid {
538
+ display: grid;
539
+ grid-template-columns: 1fr;
540
+ gap: 1rem;
541
+ }
542
+
543
+ @media (min-width: 640px) {
544
+ .professional-connections-grid {
545
+ grid-template-columns: repeat(2, minmax(0, 1fr));
546
+ }
547
+ }
548
+
549
+ .professional-connections-grid .group {
550
+ position: relative;
551
+ min-height: 100%;
552
+ align-items: flex-start;
553
+ padding: 0.95rem 1rem;
554
+ background: transparent;
555
+ border: 1px solid var(--border-gentle);
556
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-default), border-color var(--motion-duration-fast) var(--motion-ease-default);
557
+ }
558
+
559
+ .professional-connections-grid .group:hover {
560
+ background: var(--color-surface-high);
561
+ border-color: var(--border-gentle-hover);
562
+ }
563
+
564
+ /* Also remove underline for elements using the utility class 'no-underline' */
565
+ a.no-underline,
566
+ a.no-underline:hover,
567
+ a.no-underline:focus,
568
+ a.no-underline:active {
569
+ text-decoration: none;
570
+ }
571
+
572
+ /* Interactive applets follow surface / on-surface tokens (light stays white) */
573
+ .applet-canvas {
574
+ background-color: var(--color-surface);
575
+ color: var(--color-on-surface);
576
+ }
577
+
578
+ .applet-canvas .timeline-applet-control,
579
+ .applet-canvas .timeline-zoom-btn {
580
+ background-color: var(--color-surface);
581
+ border-color: color-mix(in srgb, var(--color-outline-variant) 55%, transparent);
582
+ color: var(--color-on-surface-variant);
583
+ }
584
+
585
+ .applet-canvas .timeline-applet-control:hover,
586
+ .applet-canvas .timeline-zoom-btn:hover {
587
+ border-color: var(--color-primary);
588
+ color: var(--color-primary);
589
+ }
590
+
591
+ /* ── Listing pages: full-width card grid + header filter ── */
592
+ .sr-only {
593
+ position: absolute;
594
+ width: 1px;
595
+ height: 1px;
596
+ padding: 0;
597
+ margin: -1px;
598
+ overflow: hidden;
599
+ clip: rect(0, 0, 0, 0);
600
+ white-space: nowrap;
601
+ border: 0;
602
+ }
603
+
604
+ .listing-card-grid {
605
+ --listing-grid-cols: 2;
606
+ display: grid;
607
+ grid-template-columns: 1fr;
608
+ gap: 2rem;
609
+ }
610
+
611
+ @media (min-width: 768px) {
612
+ .listing-card-grid {
613
+ grid-template-columns: repeat(var(--listing-grid-cols), minmax(0, 1fr));
614
+ }
615
+ }
616
+
617
+ .page-header-row {
618
+ display: flex;
619
+ flex-direction: column;
620
+ gap: 1.25rem;
621
+ }
622
+
623
+ @media (min-width: 768px) {
624
+ .page-header-row--with-filter {
625
+ flex-direction: row;
626
+ align-items: flex-end;
627
+ justify-content: space-between;
628
+ gap: 2rem;
629
+ }
630
+
631
+ .page-header-row--with-filter .listing-filter {
632
+ flex-shrink: 0;
633
+ align-self: flex-end;
634
+ }
635
+ }
636
+
637
+ .listing-filter {
638
+ position: relative;
639
+ }
640
+
641
+ .listing-filter-trigger {
642
+ display: inline-flex;
643
+ align-items: center;
644
+ gap: 0.5rem;
645
+ padding: 0.35rem 0.75rem;
646
+ border: 1.5px solid var(--border-ink);
647
+ border-radius: var(--radius-card);
648
+ background: var(--color-surface-card);
649
+ color: var(--color-on-surface-variant);
650
+ cursor: pointer;
651
+ transition: color var(--motion-duration-fast) var(--motion-ease-default),
652
+ border-color var(--motion-duration-fast) var(--motion-ease-default),
653
+ background-color var(--motion-duration-fast) var(--motion-ease-default);
654
+ }
655
+
656
+ .listing-filter-trigger:hover,
657
+ .listing-filter-trigger[aria-expanded="true"] {
658
+ color: var(--color-secondary);
659
+ border-color: var(--color-secondary);
660
+ background: var(--color-surface-high);
661
+ }
662
+
663
+ .listing-filter-trigger .material-symbols-outlined {
664
+ font-size: var(--type-card);
665
+ }
666
+
667
+ .listing-filter-panel {
668
+ position: absolute;
669
+ z-index: 30;
670
+ top: calc(100% + 0.5rem);
671
+ right: 0;
672
+ width: min(22rem, calc(100vw - 4rem));
673
+ padding: 1rem 1.125rem;
674
+ background: var(--color-surface);
675
+ border: 1px solid var(--border-gentle);
676
+ box-shadow: 0 12px 32px color-mix(in srgb, var(--color-on-surface) 12%, transparent);
677
+ }
678
+
679
+ @media (max-width: 767px) {
680
+ .listing-filter-panel {
681
+ position: static;
682
+ width: 100%;
683
+ margin-top: 0.25rem;
684
+ box-shadow: none;
685
+ }
686
+ }
687
+
688
+ .listing-filter-search-label {
689
+ display: block;
690
+ margin-bottom: 0.75rem;
691
+ }
692
+
693
+ .listing-filter-search {
694
+ width: 100%;
695
+ padding: 0.5rem 0.65rem;
696
+ border: 1px solid var(--border-gentle-subtle);
697
+ background: var(--color-background);
698
+ --listing-search-value: color-mix(in srgb, black 75%, white);
699
+ color: var(--listing-search-value);
700
+ -webkit-text-fill-color: var(--listing-search-value);
701
+ caret-color: var(--listing-search-value);
702
+ font-family: var(--font-body);
703
+ font-size: var(--type-subtitle);
704
+ font-weight: var(--weight-body);
705
+ line-height: 1.4;
706
+ }
707
+
708
+ [data-theme="dark"] .listing-filter-search {
709
+ --listing-search-value: var(--color-on-surface);
710
+ }
711
+
712
+ /* The Tailwind forms plugin paints text inputs white from a runtime stylesheet
713
+ injected after this file, and its [type='text'] selector outranks a single
714
+ class. In dark mode that leaves the keyword box as a white hole in the
715
+ panel, on every listing page. Two classes take the specificity back. */
716
+ .listing-filter-panel .listing-filter-search,
717
+ .listing-filter .listing-filter-search {
718
+ background: var(--color-background);
719
+ border: 1px solid var(--border-gentle-subtle);
720
+ color: var(--listing-search-value);
721
+ -webkit-text-fill-color: var(--listing-search-value);
722
+ }
723
+
724
+ .listing-filter-search::placeholder {
725
+ font-family: var(--font-body);
726
+ font-size: var(--type-subtitle);
727
+ font-weight: var(--weight-body);
728
+ color: color-mix(in srgb, var(--color-on-surface) 50%, var(--color-on-surface-variant));
729
+ -webkit-text-fill-color: color-mix(in srgb, var(--color-on-surface) 50%, var(--color-on-surface-variant));
730
+ opacity: 1;
731
+ }
732
+
733
+ .listing-filter-search:-webkit-autofill,
734
+ .listing-filter-search:-webkit-autofill:focus {
735
+ -webkit-text-fill-color: var(--listing-search-value);
736
+ transition: background-color 99999s ease-out 0s;
737
+ }
738
+
739
+ /* The forms plugin's [type='search']:focus is equal specificity and injected
740
+ later, so a single-class rule here never applied and the ring stayed its
741
+ 1px blue. Two classes take it back, and its box-shadow has to go with it. */
742
+ .listing-filter-panel .listing-filter-search:focus,
743
+ .listing-filter .listing-filter-search:focus {
744
+ outline: 2px solid var(--color-primary);
745
+ outline-offset: 1px;
746
+ box-shadow: none;
747
+ border-color: var(--color-primary);
748
+ }
749
+
750
+ .listing-filter-group+.listing-filter-group {
751
+ margin-top: 0.85rem;
752
+ }
753
+
754
+ .listing-filter-group-label {
755
+ margin: 0 0 0.5rem;
756
+ }
757
+
758
+ .listing-filter-chips {
759
+ display: flex;
760
+ flex-wrap: wrap;
761
+ gap: 0.5rem;
762
+ }
763
+
764
+ .listing-filter-chip {
765
+ display: inline-flex;
766
+ align-items: center;
767
+ justify-content: center;
768
+ gap: 0.35rem;
769
+ padding: 0.35rem 0.55rem;
770
+ border: 1.5px solid var(--border-ink);
771
+ border-radius: var(--radius-card);
772
+ background: transparent;
773
+ color: var(--color-on-surface-variant);
774
+ font-family: var(--font-label);
775
+ font-size: var(--type-label);
776
+ font-weight: var(--weight-label);
777
+ letter-spacing: 0.02em;
778
+ line-height: 1.2;
779
+ cursor: pointer;
780
+ transition: border-color var(--motion-duration-fast) var(--motion-ease-default), color var(--motion-duration-fast) var(--motion-ease-default), background-color var(--motion-duration-fast) var(--motion-ease-default);
781
+ }
782
+
783
+ .listing-filter-chip:hover {
784
+ border-color: color-mix(in srgb, var(--color-primary) 55%, var(--border-ink));
785
+ color: var(--color-primary);
786
+ }
787
+
788
+ .listing-filter-chip.is-active {
789
+ border-color: var(--color-primary);
790
+ color: var(--color-primary);
791
+ /* High-transparency accent wash — selected cue without a solid green plate. */
792
+ background: color-mix(in srgb, var(--color-primary) 8%, transparent);
793
+ }
794
+
795
+ .listing-filter-swatch {
796
+ display: inline-block;
797
+ width: 0.75rem;
798
+ height: 0.75rem;
799
+ flex-shrink: 0;
800
+ }
801
+
802
+ /* ══ Architectural Ledger blocks ══
803
+ Styles for the shared renderers in content-runtime/43-blocks.html. Depth
804
+ comes from the surface ladder, never from shadow, and every corner is
805
+ square. Each block is invisible when its backing field is absent, so these
806
+ rules only ever describe the populated case. */
807
+
808
+ .lang-pill {
809
+ font-family: var(--font-label);
810
+ font-size: var(--type-label);
811
+ font-weight: var(--weight-label);
812
+ letter-spacing: 0.03em;
813
+ padding: 0.1rem 0.4rem;
814
+ border: 1px solid color-mix(in srgb, currentColor 35%, transparent);
815
+ white-space: nowrap;
816
+ }
817
+
818
+ .lang-pill--enus {
819
+ color: var(--color-primary);
820
+ }
821
+
822
+ .lang-pill--ptbr {
823
+ color: var(--color-secondary);
824
+ }
825
+
826
+ .ledger-chip-row {
827
+ display: flex;
828
+ flex-wrap: wrap;
829
+ gap: var(--space-xs);
830
+ align-items: center;
831
+ }
832
+
833
+ .ledger-chip {
834
+ font-family: var(--font-label);
835
+ font-size: var(--type-cta);
836
+ font-weight: var(--weight-label);
837
+ letter-spacing: 0.03em;
838
+ padding: 0.15rem 0.45rem;
839
+ /* Tags carry the page's accent rather than a neutral grey. Both values mix
840
+ `--color-primary`, which `.page-accent--*` rebinds per page, so a chip is
841
+ blue on Projects, green on Articles and the CV, ochre on Rascunhos with
842
+ no per-page rule anywhere. The fill stays mostly surface so a row of
843
+ chips reads as metadata and not as a row of buttons; the ink carries
844
+ most of the hue, which is what makes the tag legible as a tag.
845
+ Note this is `.ledger-chip` only: the page-header tag line (the CV's
846
+ "Engineering Leadership. Team Building…") is a separate
847
+ `data-content-header-tags` span and is deliberately left alone. */
848
+ background: color-mix(in srgb, var(--color-primary) 16%, var(--color-surface-high));
849
+ color: color-mix(in srgb, var(--color-primary) 82%, var(--color-on-surface));
850
+ text-decoration: none;
851
+ border-radius: var(--radius-card);
852
+ /* The hover hairline arrives as an inset ring rather than a border, and is
853
+ declared transparent here so it has something to transition from. This is
854
+ not the shadow the system forbids — nothing is offset, blurred or spread,
855
+ so it draws a 1px line on the chip's own edge, exactly where a border
856
+ would sit. A real border would be the idiom, but it would have to be
857
+ present at rest to avoid a 2px jump under the pointer, and the padding
858
+ compensation that costs only lands exactly at integer device pixel
859
+ ratios: at 1.75 the browser snaps the border to 0.571px and the chip
860
+ quietly loses most of a pixel on both axes. The ring takes no space at
861
+ any ratio, so the resting chip keeps its size, its fill and its square
862
+ corners. */
863
+ box-shadow: inset 0 0 0 1px transparent;
864
+ transition: color var(--motion-duration-fast) var(--motion-ease-default), background-color var(--motion-duration-fast) var(--motion-ease-default), box-shadow var(--motion-duration-fast) var(--motion-ease-default), border-color var(--motion-duration-fast) var(--motion-ease-default);
865
+ }
866
+
867
+ /* Hover reads `.ledger-card:hover` — one tonal step plus an accent-warmed
868
+ hairline — at chip scale.
869
+
870
+ The step cannot be a ladder rung: the chip's resting fill is already the
871
+ ladder's top one. What it used to do instead was borrow a rung from the
872
+ other surface family (`surfaceHigh` → `surfaceContainerHigh`), which is
873
+ the defect DESIGN.md records under *Two surface families name the same
874
+ colours* — in dark the two are the same value to the byte, so the hover
875
+ measured 1.000 : 1 and only the text changed, and in light they are close
876
+ enough to have read the same way. Tinting the resting fill with the page
877
+ accent instead is a real step in both palettes — down on paper, up on the
878
+ dark canvas — and stays right whichever way the light ladder is pointed,
879
+ which the borrowed rung did not. 8% is the value
880
+ `.listing-filter-chip.is-active` already uses for an accent-tinted fill.
881
+
882
+ The hairline carries the rest, at 35% accent over plain `--border-gentle`
883
+ rather than the card's 40% over `--border-gentle-hover`. A line drawn
884
+ around something this small is a far larger share of its ink than the same
885
+ line around a card, so the quieter mix is the proportionate one. */
886
+ a.ledger-chip:hover {
887
+ color: var(--color-on-surface);
888
+ /* Resting fill is 16% accent in light; hover must clear that. */
889
+ background: color-mix(in srgb, var(--color-primary) 28%, var(--color-surface-high));
890
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 45%, var(--border-gentle));
891
+ /* No-op for the filled chip, which has no border. It matters on the sources
892
+ facets, the one place that outlines the chip instead of filling it: the
893
+ ring would otherwise be drawn just inside that outline and the two
894
+ hairlines would read as one 2px line. Handing the edge to the ring keeps
895
+ the hover at a single line there too, and the fill that page expects on
896
+ hover still arrives. */
897
+ border-color: transparent;
898
+ }
899
+
900
+ [data-theme="dark"] a.ledger-chip {
901
+ background: color-mix(in srgb, var(--color-primary) 10%, var(--color-surface-high));
902
+ color: color-mix(in srgb, var(--color-primary) 78%, var(--color-on-surface));
903
+ }
904
+
905
+ [data-theme="dark"] a.ledger-chip:hover {
906
+ background: color-mix(in srgb, var(--color-primary) 22%, var(--color-surface-high));
907
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 35%, var(--border-gentle));
908
+ }
909
+
910
+ /* Dates live here. The chips and language pills inside the strip set their
911
+ own uppercase, so the strip itself leaves case alone — running small caps
912
+ over a date is heavier than the date deserves. */
913
+ .ledger-meta {
914
+ display: flex;
915
+ flex-wrap: wrap;
916
+ align-items: center;
917
+ gap: var(--space-sm);
918
+ color: var(--color-on-surface-variant);
919
+ letter-spacing: 0.04em;
920
+ }
921
+
922
+ /* A separator is ink, not a border. --color-outline-variant is tuned for
923
+ hairlines and reads at 1.31:1 here, which is invisible rather than quiet. */
924
+ .ledger-meta-sep {
925
+ color: var(--color-outline);
926
+ }
927
+
928
+ .ledger-cta {
929
+ display: inline-flex;
930
+ align-items: center;
931
+ gap: var(--space-xs);
932
+ font-family: var(--font-label);
933
+ font-size: var(--type-cta);
934
+ font-weight: var(--weight-label);
935
+ letter-spacing: 0.04em;
936
+ color: var(--color-primary);
937
+ text-decoration: none;
938
+ white-space: nowrap;
939
+ line-height: 1;
940
+ }
941
+
942
+ .ledger-cta>span {
943
+ display: inline-flex;
944
+ align-items: center;
945
+ line-height: 1;
946
+ }
947
+
948
+ /* Text CTAs carry a gentle accent underline so the clickable cue is not the
949
+ same gesture as the marker wash on titles. Emphasis buttons keep a box
950
+ fill instead — underline would fight the border. */
951
+ .ledger-cta:not(.ledger-cta--emphasis):not(.ledger-cta--emphasis-alt)>span:not(.ledger-cta-arrow):not(.ledger-cta-icon) {
952
+ text-decoration: underline;
953
+ text-decoration-color: color-mix(in srgb, var(--color-primary) 50%, transparent);
954
+ text-decoration-thickness: 1.5px;
955
+ text-underline-offset: 0.28em;
956
+ text-underline-position: under;
957
+ }
958
+
959
+ .ledger-cta-icon,
960
+ .ledger-cta-arrow {
961
+ display: inline-flex;
962
+ align-items: center;
963
+ justify-content: center;
964
+ font-size: 1.35em;
965
+ line-height: 1;
966
+ text-decoration: none;
967
+ }
968
+
969
+ .ledger-cta:hover {
970
+ color: var(--color-link-hover);
971
+ }
972
+
973
+ .ledger-cta:not(.ledger-cta--emphasis):not(.ledger-cta--emphasis-alt):hover>span:not(.ledger-cta-arrow):not(.ledger-cta-icon) {
974
+ text-decoration-color: color-mix(in srgb, var(--color-link-hover) 75%, transparent);
975
+ }
976
+
977
+ .ledger-cta-arrow {
978
+ transition: transform var(--motion-duration-fast) var(--motion-ease-default);
979
+ }
980
+
981
+ .ledger-cta:hover .ledger-cta-arrow {
982
+ transform: translateX(4px);
983
+ }
984
+
985
+ /* Shared discrete-button replacement for the old per-page `.home-cta` /
986
+ `.home-cta--solid` / `.home-cta--outline` pattern (assets/css/ledger/home.css).
987
+ Those set real button padding (0.85rem vertical, --space-lg horizontal) and a
988
+ solid filled background for the primary case. `.ledger-cta--emphasis` is the
989
+ one or two truly primary actions per page (e.g. hero "Full Curriculum
990
+ Vitæ"): still a `.ledger-cta`, just with tight padding and a tinted fill
991
+ instead of a heavy solid block, so it reads as a real button — with color
992
+ and contrast — without becoming a bulky box. Reuse this instead of adding
993
+ another one-off CTA class. `--emphasis-alt` swaps the hue to secondary, for
994
+ a second hero-level action (e.g. "Contacts") that needs to read as a
995
+ distinct button next to the primary one rather than a duplicate of it. */
996
+ .ledger-cta--emphasis {
997
+ padding: 0.35rem 0.85rem;
998
+ border: 1px solid var(--color-primary);
999
+ border-radius: var(--radius-card);
1000
+ background: color-mix(in srgb, var(--color-primary) 22%, var(--color-background));
1001
+ transition: color var(--motion-duration-fast) var(--motion-ease-default),
1002
+ border-color var(--motion-duration-fast) var(--motion-ease-default),
1003
+ background-color var(--motion-duration-fast) var(--motion-ease-default);
1004
+ }
1005
+
1006
+ .ledger-cta--emphasis:hover {
1007
+ color: var(--color-on-primary, var(--color-surface));
1008
+ border-color: var(--color-link-hover);
1009
+ background: var(--color-primary);
1010
+ }
1011
+
1012
+ .ledger-cta--emphasis-alt {
1013
+ padding: 0.35rem 0.85rem;
1014
+ border: 1px solid var(--color-secondary);
1015
+ border-radius: var(--radius-card);
1016
+ background: color-mix(in srgb, var(--color-secondary) 22%, var(--color-background));
1017
+ color: var(--color-secondary);
1018
+ transition: color var(--motion-duration-fast) var(--motion-ease-default),
1019
+ border-color var(--motion-duration-fast) var(--motion-ease-default),
1020
+ background-color var(--motion-duration-fast) var(--motion-ease-default);
1021
+ }
1022
+
1023
+ [data-theme="dark"] .ledger-cta--emphasis {
1024
+ background: color-mix(in srgb, var(--color-primary) 18%, var(--color-background));
1025
+ }
1026
+
1027
+ [data-theme="dark"] .ledger-cta--emphasis-alt {
1028
+ background: color-mix(in srgb, var(--color-secondary) 18%, var(--color-background));
1029
+ }
1030
+
1031
+ /* Dark resting mixes sit after the shared :hover fill and match its
1032
+ specificity (attr + class vs class + :hover), so without these the
1033
+ primary button kept the 14% wash under the pointer while the ink
1034
+ stepped to on-primary — dark text on a near-black hero. Alt escaped
1035
+ because its :hover rule already follows the dark rest. */
1036
+ [data-theme="dark"] .ledger-cta--emphasis:hover {
1037
+ background: var(--color-primary);
1038
+ }
1039
+
1040
+ [data-theme="dark"] .ledger-cta--emphasis-alt:hover {
1041
+ background: var(--color-secondary);
1042
+ }
1043
+
1044
+ .ledger-cta--emphasis-alt:hover {
1045
+ color: var(--color-on-secondary, var(--color-surface));
1046
+ border-color: var(--color-secondary);
1047
+ background: var(--color-secondary);
1048
+ }
1049
+
1050
+ .ledger-section-banner {
1051
+ display: flex;
1052
+ flex-wrap: wrap;
1053
+ align-items: baseline;
1054
+ justify-content: space-between;
1055
+ gap: var(--space-md);
1056
+ margin-bottom: var(--space-xl);
1057
+ }
1058
+
1059
+ .ledger-kicker {
1060
+ display: block;
1061
+ color: var(--color-on-surface-variant);
1062
+ letter-spacing: 0.12em;
1063
+ margin-bottom: var(--space-xs);
1064
+ }
1065
+
1066
+ .ledger-section-note {
1067
+ color: var(--color-on-surface-variant);
1068
+ letter-spacing: 0.08em;
1069
+ }
1070
+
1071
+ /* ── Ledger row: dense horizontal entry ── */
1072
+ .ledger-row {
1073
+ display: flex;
1074
+ flex-direction: column;
1075
+ gap: var(--space-sm);
1076
+ padding: var(--space-lg);
1077
+ background: var(--color-surface-card);
1078
+ border: 1.5px solid var(--border-ink);
1079
+ border-radius: var(--radius-card);
1080
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-default),
1081
+ border-color var(--motion-duration-fast) var(--motion-ease-default);
1082
+ }
1083
+
1084
+ .ledger-row:hover {
1085
+ background: var(--color-surface-high);
1086
+ border-color: var(--border-gentle-hover);
1087
+ }
1088
+
1089
+ .ledger-row-title a,
1090
+ .ledger-card-title a {
1091
+ color: var(--color-on-surface);
1092
+ text-decoration: none;
1093
+ transition: color var(--motion-duration-fast) var(--motion-ease-default);
1094
+ }
1095
+
1096
+ .ledger-row:hover .ledger-row-title a,
1097
+ .ledger-card:hover .ledger-card-title a,
1098
+ .ledger-system-card:hover .ledger-card-title a {
1099
+ color: var(--color-primary);
1100
+ }
1101
+
1102
+ .ledger-row-copy,
1103
+ .ledger-card-copy {
1104
+ margin: 0;
1105
+ font-family: var(--font-body);
1106
+ font-size: var(--type-subtitle);
1107
+ font-weight: var(--weight-body);
1108
+ color: var(--color-legend);
1109
+ }
1110
+
1111
+ /* The footer is a rule with links under it, so it needs enough air not to sit
1112
+ on the line. --space-sm is twice what this used to carry, and it is now the
1113
+ one value for every card of this shape — the home system grid and the
1114
+ projects page each used to override it with a larger one of their own. */
1115
+ .ledger-row-foot,
1116
+ .ledger-card-foot {
1117
+ display: flex;
1118
+ flex-wrap: wrap;
1119
+ align-items: center;
1120
+ justify-content: space-between;
1121
+ gap: var(--space-sm);
1122
+ padding-top: var(--space-sm);
1123
+ }
1124
+
1125
+ /* ── Ledger card: vertical grid cell ── */
1126
+ .ledger-card,
1127
+ .ledger-system-card {
1128
+ display: flex;
1129
+ flex-direction: column;
1130
+ background: var(--color-surface-card);
1131
+ border: 1.5px solid var(--border-ink);
1132
+ border-radius: var(--radius-card);
1133
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-default), border-color var(--motion-duration-fast) var(--motion-ease-default);
1134
+ }
1135
+
1136
+ /* Light paper: match the CV disclaimer edge — 1px outline hairline, not the
1137
+ 1.5px ink frame dark mode needs for faint canvas borders. */
1138
+ :root:not([data-theme="dark"]) .ledger-card,
1139
+ :root:not([data-theme="dark"]) .ledger-system-card,
1140
+ html[data-theme="light"] .ledger-card,
1141
+ html[data-theme="light"] .ledger-system-card {
1142
+ border-width: 1px;
1143
+ border-color: var(--border-ink);
1144
+ }
1145
+
1146
+ /* Triad top hairline — reusable edge accent (home grids + any listing). */
1147
+ .ledger-card--edge-primary {
1148
+ border-top-width: 2px;
1149
+ border-top-style: solid;
1150
+ border-top-color: var(--color-primary);
1151
+ }
1152
+
1153
+ .ledger-card--edge-secondary {
1154
+ border-top-width: 2px;
1155
+ border-top-style: solid;
1156
+ border-top-color: var(--color-secondary);
1157
+ }
1158
+
1159
+ .ledger-card--edge-tertiary {
1160
+ border-top-width: 2px;
1161
+ border-top-style: solid;
1162
+ border-top-color: var(--color-tertiary);
1163
+ }
1164
+
1165
+ /* Drafting corner brackets — four L-corners, no labels. Pure decoration.
1166
+ Outline ink (not primary): the corners are drafting marks on the plate,
1167
+ same register as `.ledger-frame-crosshairs`, not a brand accent. */
1168
+ .ledger-frame-corners {
1169
+ --frame-corner-size: 0.85rem;
1170
+ --frame-corner-thickness: 1px;
1171
+ position: absolute;
1172
+ inset: 0.35rem;
1173
+ pointer-events: none;
1174
+ z-index: 2;
1175
+ background:
1176
+ linear-gradient(var(--color-outline), var(--color-outline)) top left / var(--frame-corner-size) var(--frame-corner-thickness) no-repeat,
1177
+ linear-gradient(var(--color-outline), var(--color-outline)) top left / var(--frame-corner-thickness) var(--frame-corner-size) no-repeat,
1178
+ linear-gradient(var(--color-outline), var(--color-outline)) top right / var(--frame-corner-size) var(--frame-corner-thickness) no-repeat,
1179
+ linear-gradient(var(--color-outline), var(--color-outline)) top right / var(--frame-corner-thickness) var(--frame-corner-size) no-repeat,
1180
+ linear-gradient(var(--color-outline), var(--color-outline)) bottom left / var(--frame-corner-size) var(--frame-corner-thickness) no-repeat,
1181
+ linear-gradient(var(--color-outline), var(--color-outline)) bottom left / var(--frame-corner-thickness) var(--frame-corner-size) no-repeat,
1182
+ linear-gradient(var(--color-outline), var(--color-outline)) bottom right / var(--frame-corner-size) var(--frame-corner-thickness) no-repeat,
1183
+ linear-gradient(var(--color-outline), var(--color-outline)) bottom right / var(--frame-corner-thickness) var(--frame-corner-size) no-repeat;
1184
+ }
1185
+
1186
+ /* Empty drafting crosshairs outside a plate — no coordinates, no labels. */
1187
+ .ledger-frame-crosshairs {
1188
+ --cross-arm: 0.55rem;
1189
+ --cross-thick: 1px;
1190
+ position: absolute;
1191
+ inset: -0.35rem;
1192
+ pointer-events: none;
1193
+ z-index: 3;
1194
+ background:
1195
+ linear-gradient(var(--color-outline), var(--color-outline)) top left / var(--cross-arm) var(--cross-thick) no-repeat,
1196
+ linear-gradient(var(--color-outline), var(--color-outline)) top left / var(--cross-thick) var(--cross-arm) no-repeat,
1197
+ linear-gradient(var(--color-outline), var(--color-outline)) top right / var(--cross-arm) var(--cross-thick) no-repeat,
1198
+ linear-gradient(var(--color-outline), var(--color-outline)) top right / var(--cross-thick) var(--cross-arm) no-repeat,
1199
+ linear-gradient(var(--color-outline), var(--color-outline)) bottom left / var(--cross-arm) var(--cross-thick) no-repeat,
1200
+ linear-gradient(var(--color-outline), var(--color-outline)) bottom left / var(--cross-thick) var(--cross-arm) no-repeat,
1201
+ linear-gradient(var(--color-outline), var(--color-outline)) bottom right / var(--cross-arm) var(--cross-thick) no-repeat,
1202
+ linear-gradient(var(--color-outline), var(--color-outline)) bottom right / var(--cross-thick) var(--cross-arm) no-repeat;
1203
+ }
1204
+
1205
+ /* Lithography section divider — double hairline with a short primary tick. */
1206
+ .ledger-section-rule {
1207
+ display: block;
1208
+ height: 0;
1209
+ margin: 0 0 var(--space-xl);
1210
+ border: 0;
1211
+ border-top: 1px solid var(--border-gentle-divider);
1212
+ box-shadow: 0 3px 0 -2px var(--border-gentle-faint);
1213
+ position: relative;
1214
+ }
1215
+
1216
+ .ledger-section-rule::after {
1217
+ content: "";
1218
+ position: absolute;
1219
+ left: 0;
1220
+ top: -1.5px;
1221
+ width: 2.5rem;
1222
+ height: 2px;
1223
+ background: var(--color-primary);
1224
+ }
1225
+
1226
+ .ledger-section-rule--dashed {
1227
+ border-top-style: dashed;
1228
+ box-shadow: none;
1229
+ }
1230
+
1231
+ /* Hover lifts the card one rung and warms its hairline toward the page's
1232
+ accent. The reference mockup states this as "the line transitions to
1233
+ subtle charcoal slate or warm antique brass at 40% opacity" — the accent
1234
+ mix is that idea, made page-accent-aware rather than hardcoded to one hue.
1235
+ The card itself stays flat: no shadow, no glow. Depth is the rung plus
1236
+ the line, which is the whole premise of the ladder. */
1237
+ .ledger-card:hover,
1238
+ .ledger-system-card:hover {
1239
+ background: var(--color-surface-high);
1240
+ border-color: color-mix(in srgb, var(--color-primary) 55%, var(--border-gentle-hover));
1241
+ }
1242
+
1243
+ [data-theme="dark"] .ledger-card:hover,
1244
+ [data-theme="dark"] .ledger-system-card:hover {
1245
+ border-color: color-mix(in srgb, var(--color-primary) 10%, var(--border-ink));
1246
+ }
1247
+
1248
+ /* Cards and rows sitting on contrast bands keep the same transparent rest
1249
+ as the canvas cards; hover steps to surface-high-contrast. */
1250
+ .home-band--contrast .ledger-card,
1251
+ .home-band--contrast .ledger-row,
1252
+ .home-band--contrast .home-index-panel {
1253
+ background: var(--color-surface-card-contrast);
1254
+ }
1255
+
1256
+ .home-band--contrast .ledger-card:hover,
1257
+ .home-band--contrast .ledger-row:hover,
1258
+ .home-band--contrast .home-index-panel:hover {
1259
+ background: var(--color-surface-high-contrast);
1260
+ }
1261
+
1262
+ /* A project card used to end in a "Details" link pointing at the same place
1263
+ its own title points at, which is a second copy of the destination the
1264
+ reader is already looking at. The title's anchor now covers the whole card
1265
+ instead, so the poster, the tagline and the chips all navigate, and the
1266
+ footer is left to the links that actually leave the site.
1267
+
1268
+ One anchor, stretched: screen readers still announce a single link named
1269
+ after the project, and the keyboard still stops once. The outbound links
1270
+ and the tag chips are lifted above the overlay so they stay clickable. */
1271
+ .ledger-system-card {
1272
+ position: relative;
1273
+ }
1274
+
1275
+ .ledger-card-stretch::after {
1276
+ content: "";
1277
+ position: absolute;
1278
+ inset: 0;
1279
+ }
1280
+
1281
+ .ledger-system-card .ledger-card-links,
1282
+ .ledger-system-card .ledger-chip-row {
1283
+ position: relative;
1284
+ z-index: 1;
1285
+ }
1286
+
1287
+ .ledger-card-body,
1288
+ .ledger-system-card {
1289
+ display: flex;
1290
+ flex-direction: column;
1291
+ gap: var(--space-sm);
1292
+ padding: var(--space-lg);
1293
+ flex: 1;
1294
+ }
1295
+
1296
+ .ledger-card-copy {
1297
+ flex: 1;
1298
+ }
1299
+
1300
+ .ledger-card-foot {
1301
+ margin-top: auto;
1302
+ border-top: 1px solid var(--border-gentle-divider);
1303
+ }
1304
+
1305
+ .ledger-card-links {
1306
+ justify-content: flex-start;
1307
+ gap: var(--space-md);
1308
+ }
1309
+
1310
+ .ledger-status {
1311
+ color: var(--color-secondary);
1312
+ }
1313
+
1314
+ /* Optional page standfirst. Sits under the title at the prose measure so a
1315
+ long intro does not run the full page width. */
1316
+ .page-header-standfirst {
1317
+ max-width: var(--reading-measure, 45rem);
1318
+ margin-top: var(--space-md);
1319
+ font-size: var(--type-body);
1320
+ line-height: var(--leading-body);
1321
+ }
1322
+
1323
+ /* Poster media on system cards. Fixed ratio so a grid of projects keeps its
1324
+ baseline whether a card illustrates itself with a still or a capture. */
1325
+ .ledger-poster {
1326
+ position: relative;
1327
+ width: 100%;
1328
+ aspect-ratio: 16 / 9;
1329
+ overflow: hidden;
1330
+ background: var(--color-surface-container);
1331
+ }
1332
+
1333
+ /* Drafting corners on every poster plate — CSS-only, no invented labels. */
1334
+ .ledger-poster::after {
1335
+ content: "";
1336
+ --frame-corner-size: 0.7rem;
1337
+ --frame-corner-thickness: 1px;
1338
+ position: absolute;
1339
+ inset: 0.3rem;
1340
+ pointer-events: none;
1341
+ z-index: 2;
1342
+ background:
1343
+ linear-gradient(var(--color-primary), var(--color-primary)) top left / var(--frame-corner-size) var(--frame-corner-thickness) no-repeat,
1344
+ linear-gradient(var(--color-primary), var(--color-primary)) top left / var(--frame-corner-thickness) var(--frame-corner-size) no-repeat,
1345
+ linear-gradient(var(--color-primary), var(--color-primary)) top right / var(--frame-corner-size) var(--frame-corner-thickness) no-repeat,
1346
+ linear-gradient(var(--color-primary), var(--color-primary)) top right / var(--frame-corner-thickness) var(--frame-corner-size) no-repeat,
1347
+ linear-gradient(var(--color-primary), var(--color-primary)) bottom left / var(--frame-corner-size) var(--frame-corner-thickness) no-repeat,
1348
+ linear-gradient(var(--color-primary), var(--color-primary)) bottom left / var(--frame-corner-thickness) var(--frame-corner-size) no-repeat,
1349
+ linear-gradient(var(--color-primary), var(--color-primary)) bottom right / var(--frame-corner-size) var(--frame-corner-thickness) no-repeat,
1350
+ linear-gradient(var(--color-primary), var(--color-primary)) bottom right / var(--frame-corner-thickness) var(--frame-corner-size) no-repeat;
1351
+ }
1352
+
1353
+ .ledger-poster img,
1354
+ .ledger-poster video {
1355
+ width: 100%;
1356
+ height: 100%;
1357
+ object-fit: cover;
1358
+ display: block;
1359
+ filter: saturate(0.92) contrast(1.02);
1360
+ transition: filter var(--motion-duration-base) var(--motion-ease-default), transform var(--motion-duration-slow) var(--motion-ease-default);
1361
+ }
1362
+
1363
+ .ledger-system-card:hover .ledger-poster img,
1364
+ .ledger-system-card:hover .ledger-poster video {
1365
+ filter: saturate(1.05) contrast(1);
1366
+ transform: scale(1.02);
1367
+ }
1368
+
1369
+ @media (prefers-reduced-motion: reduce) {
1370
+
1371
+ .ledger-system-card:hover .ledger-poster img,
1372
+ .ledger-system-card:hover .ledger-poster video {
1373
+ transform: none;
1374
+ }
1375
+ }
1376
+
1377
+ /* Project-specific meta furniture grafted onto the shared system card. */
1378
+ .material-symbols-outlined.ledger-system-icon {
1379
+ font-size: var(--type-card);
1380
+ line-height: 1;
1381
+ color: var(--color-secondary);
1382
+ }
1383
+
1384
+ .ledger-span {
1385
+ color: var(--color-on-surface-variant);
1386
+ }
1387
+
1388
+ /* ── Listing pagination ──
1389
+ The shared renderer used to hand these buttons a string of Tailwind
1390
+ utilities and no component, so three page stylesheets each restated the
1391
+ same metrics at whatever specificity it took to beat the utilities. One
1392
+ definition here; the pages keep only what is genuinely theirs. */
1393
+ .pagination-nav-btn {
1394
+ padding: var(--space-xs) var(--space-md);
1395
+ min-height: 2.5rem;
1396
+ background: var(--color-surface-card);
1397
+ border: 1.5px solid var(--border-ink);
1398
+ border-radius: var(--radius-card);
1399
+ color: var(--color-on-surface-variant);
1400
+ font-family: var(--font-label);
1401
+ font-size: var(--type-label-lg);
1402
+ letter-spacing: 0.04em;
1403
+ cursor: pointer;
1404
+ transition: color var(--motion-duration-fast) var(--motion-ease-default), border-color var(--motion-duration-fast) var(--motion-ease-default), background-color var(--motion-duration-fast) var(--motion-ease-default);
1405
+ }
1406
+
1407
+ .pagination-nav-btn:hover:not(:disabled) {
1408
+ color: var(--color-on-surface);
1409
+ border-color: var(--border-gentle-hover);
1410
+ background: var(--color-surface-high);
1411
+ }
1412
+
1413
+ .pagination-nav-btn:focus-visible {
1414
+ outline: 2px solid var(--color-primary);
1415
+ outline-offset: 2px;
1416
+ }
1417
+
1418
+ .pagination-nav-btn:disabled {
1419
+ opacity: 0.4;
1420
+ cursor: default;
1421
+ }
1422
+
1423
+ /* Page count shares the Previous/Next label face; tabular nums keep "1 / 3"
1424
+ from jumping width as the page changes. */
1425
+ .pagination-count {
1426
+ font-family: var(--font-label);
1427
+ font-size: var(--type-label-lg);
1428
+ font-variant-numeric: tabular-nums;
1429
+ letter-spacing: 0.04em;
1430
+ color: var(--color-on-surface-variant);
1431
+ white-space: nowrap;
1432
+ }