Engineering notes
- Andras Schmelczer, software engineer. Writeups
- of finished projects, with the tradeoffs left in.
+ Andras Schmelczer sharing cool projects and lessons learned
- Most started because I couldn't yet do the thing: an 8-bit ALU, a mobile GPU, a
- single static HTML file, a cross-language ABI. The About page
- covers the patterns I keep returning to.
+ I enjoy learning by building exciting things that sometimes even turn out to be useful. This page is dedicated to the projects I actually finished. To learn more about me, check out the About page.
diff --git a/src/styles/base.css b/src/styles/base.css
new file mode 100644
index 0000000..c1defcd
--- /dev/null
+++ b/src/styles/base.css
@@ -0,0 +1,57 @@
+/* Base layer: html/body defaults, links, focus rings, text selection. */
+@layer base {
+ html {
+ background: var(--color-bg);
+ scroll-behavior: smooth;
+ }
+
+ body {
+ min-block-size: 100vh;
+ min-block-size: 100dvh;
+ display: flex;
+ flex-direction: column;
+ background: var(--color-bg);
+ color: var(--color-fg);
+ font-family: var(--font-sans);
+ font-size: var(--fs-body);
+ line-height: var(--leading-snug);
+ transition:
+ background-color 200ms ease,
+ color 200ms ease;
+ }
+
+ address {
+ font-style: normal;
+ }
+
+ a {
+ color: var(--color-link);
+ text-decoration-thickness: 0.08em;
+ text-underline-offset: 0.18em;
+ transition: color 150ms ease;
+ }
+
+ a:hover {
+ color: var(--color-link-hover);
+ }
+
+ a:active {
+ opacity: 0.85;
+ transition: opacity 80ms ease;
+ }
+
+ :focus-visible {
+ outline: 2px solid var(--color-accent);
+ outline-offset: 3px;
+ }
+
+ main:focus-visible {
+ outline: 2px solid var(--color-accent);
+ outline-offset: -2px;
+ }
+
+ ::selection {
+ background: var(--color-selection-bg);
+ color: var(--color-fg);
+ }
+}
diff --git a/src/styles/components/headings-and-tags.css b/src/styles/components/headings-and-tags.css
new file mode 100644
index 0000000..6e82b97
--- /dev/null
+++ b/src/styles/components/headings-and-tags.css
@@ -0,0 +1,160 @@
+/* Section headings, breadcrumbs, tag cloud / list / filter. */
+@layer components {
+ .eyebrow {
+ margin: 0 0 var(--space-3);
+ color: var(--color-muted);
+ font-size: var(--fs-caption);
+ line-height: 1.4;
+ font-weight: var(--weight-semibold);
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ }
+
+ .section-heading {
+ width: 100%;
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: var(--space-4);
+ flex-wrap: wrap;
+ margin-bottom: var(--space-4);
+ padding-top: var(--space-6);
+ }
+
+ :where(.section-heading, .archive-year, .project-section, .facts, .at-a-glance) h2 {
+ font-size: var(--fs-lg);
+ font-weight: var(--weight-semibold);
+ line-height: var(--leading-snug);
+ }
+
+ .section-heading a {
+ min-height: 44px;
+ display: inline-flex;
+ align-items: center;
+ color: var(--color-muted);
+ font-size: var(--fs-caption);
+ text-decoration: none;
+ }
+
+ .section-heading a:hover {
+ color: var(--color-fg);
+ text-decoration: underline;
+ text-underline-offset: 0.25em;
+ }
+
+ .section-heading__text {
+ flex: 1 1 11rem;
+ min-width: 0;
+ }
+
+ .section-heading__text p {
+ max-width: var(--measure);
+ margin-top: var(--space-1);
+ color: var(--color-muted);
+ font-size: var(--fs-caption);
+ line-height: 1.4;
+ }
+
+ .breadcrumbs {
+ margin: 0 0 var(--space-3);
+ padding: 0;
+ list-style: none;
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-2);
+ color: var(--color-muted);
+ font-size: var(--fs-caption);
+ }
+
+ .breadcrumbs li {
+ display: inline-flex;
+ align-items: baseline;
+ min-width: 0;
+ }
+
+ .breadcrumbs li:not(:last-child)::after {
+ content: '›';
+ margin-left: var(--space-2);
+ color: var(--color-rule-medium);
+ flex: none;
+ }
+
+ .breadcrumbs a {
+ color: var(--color-muted);
+ text-decoration: none;
+ }
+
+ .breadcrumbs a:hover {
+ color: var(--color-fg);
+ text-decoration: underline;
+ text-underline-offset: 0.25em;
+ }
+
+ .breadcrumbs [aria-current='page'] {
+ color: var(--color-fg);
+ }
+
+ .tag-cloud {
+ margin-top: var(--space-2);
+ }
+
+ .tag-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-1) var(--space-2);
+ margin: var(--space-2) 0 0;
+ padding: 0;
+ list-style: none;
+ color: var(--color-muted);
+ font-size: var(--fs-caption);
+ }
+
+ .tag-list a {
+ min-height: 2rem;
+ display: inline-flex;
+ align-items: center;
+ padding-inline: var(--space-1);
+ margin-inline: calc(-1 * var(--space-1));
+ color: var(--color-muted);
+ text-decoration: none;
+ }
+
+ .tag-list a::before {
+ content: '#';
+ color: var(--color-rule-medium);
+ }
+
+ .tag-list .tag-more::before {
+ content: none;
+ }
+
+ .tag-list .tag-count {
+ margin-inline-start: 0.35em;
+ padding: 0 0.4em;
+ border-radius: var(--radius-pill);
+ background: var(--color-code-bg);
+ color: var(--color-fg);
+ font-size: var(--fs-caption);
+ font-variant-numeric: tabular-nums;
+ }
+
+ .tag-list a:hover,
+ .tag-list a[aria-current='page'] {
+ color: var(--color-fg);
+ }
+
+ .tag-filter {
+ display: flex;
+ align-items: baseline;
+ flex-wrap: wrap;
+ gap: var(--space-2) var(--space-4);
+ margin: 0 0 var(--space-8);
+ padding-block: var(--space-3);
+ border-block: 1px solid var(--color-rule);
+ }
+
+ .tag-filter > span {
+ color: var(--color-muted);
+ font-size: var(--fs-caption);
+ }
+}
diff --git a/src/styles/components/listings.css b/src/styles/components/listings.css
new file mode 100644
index 0000000..871171b
--- /dev/null
+++ b/src/styles/components/listings.css
@@ -0,0 +1,198 @@
+/* Article list (+ timeline variant), project-list grid, entry thumbnails. */
+@layer components {
+ .article-list,
+ .project-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ }
+
+ .project-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
+ gap: var(--space-4);
+ align-items: stretch;
+ }
+
+ .article-list > li {
+ display: grid;
+ grid-template-columns: 4.5rem minmax(0, 1fr) minmax(6rem, 8rem);
+ grid-template-areas: 'date content thumb';
+ align-items: center;
+ gap: var(--space-4);
+ padding-block: var(--space-6);
+ border-top: 1px solid var(--color-rule);
+ }
+
+ .article-list > li:first-child {
+ border-top: 0;
+ }
+
+ .article-list time {
+ grid-area: date;
+ color: var(--color-muted);
+ font-size: var(--fs-caption);
+ text-align: end;
+ }
+
+ .article-list > li > article {
+ grid-area: content;
+ min-width: 0;
+ padding-right: var(--space-3);
+ }
+
+ .article-list h3,
+ .project-list h3 {
+ font-size: var(--fs-base);
+ line-height: var(--leading-snug);
+ }
+
+ .project-list h3 {
+ color: var(--color-fg);
+ font-weight: var(--weight-semibold);
+ }
+
+ .article-list .entry-title {
+ display: inline-flex;
+ align-items: center;
+ min-height: 28px;
+ color: var(--color-fg);
+ font-weight: var(--weight-semibold);
+ text-decoration: none;
+ }
+
+ .article-list .entry-title:hover {
+ color: var(--color-link-hover);
+ }
+
+ .article-list p,
+ .project-list p {
+ margin: var(--space-1) 0 0;
+ color: var(--color-muted);
+ font-size: var(--fs-base);
+ line-height: var(--leading-snug);
+ }
+
+ .entry-thumbnail {
+ display: block;
+ overflow: hidden;
+ border: 1px solid var(--color-rule);
+ border-radius: var(--radius-md);
+ background: var(--color-code-bg);
+ aspect-ratio: 4 / 3;
+ transition: border-color 150ms ease;
+ }
+
+ .entry-thumbnail img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center center;
+ }
+
+ a.entry-thumbnail {
+ text-decoration: none;
+ }
+
+ a.entry-thumbnail:hover,
+ a.entry-thumbnail:focus-visible {
+ border-color: var(--color-rule-strong);
+ }
+
+ .article-list > li:focus-within .entry-thumbnail {
+ border-color: var(--color-rule-strong);
+ }
+
+ .article-thumbnail {
+ grid-area: thumb;
+ align-self: center;
+ }
+
+ .article-list--timeline {
+ --timeline-date-column: 6.25rem;
+ --timeline-marker-column: 1.125rem;
+ --timeline-marker-offset: 0.5625rem;
+ --timeline-gap: var(--space-4);
+ --timeline-date-offset: 1.125rem;
+ --timeline-dot-top: 50%;
+ --timeline-marker-center: calc(
+ var(--timeline-date-column) + var(--timeline-gap) + var(--timeline-marker-offset)
+ );
+ }
+
+ .article-list--timeline > li {
+ position: relative;
+ grid-template-columns:
+ var(--timeline-date-column) var(--timeline-marker-column) minmax(0, 1fr)
+ minmax(6rem, 8rem);
+ grid-template-areas: 'date marker content thumb';
+ column-gap: var(--timeline-gap);
+ border-top: 0;
+ }
+
+ .article-list--timeline > li::before,
+ .article-list--timeline > li::after {
+ content: '';
+ position: absolute;
+ left: var(--timeline-marker-center);
+ z-index: 0;
+ pointer-events: none;
+ }
+
+ .article-list--timeline > li::before {
+ inset-block: 0;
+ width: 1px;
+ transform: translateX(-50%);
+ background: var(--color-rule-medium);
+ }
+
+ .article-list--timeline > li:first-child::before {
+ inset-block-start: var(--timeline-dot-top);
+ }
+
+ .article-list--timeline > li:last-child::before {
+ inset-block-end: calc(100% - var(--timeline-dot-top));
+ }
+
+ .article-list--timeline > li:only-child::before {
+ content: none;
+ }
+
+ .article-list--timeline > li::after {
+ top: var(--timeline-dot-top);
+ width: 0.75rem;
+ height: 0.75rem;
+ border: 2px solid var(--color-bg);
+ border-radius: var(--radius-pill);
+ background: var(--color-accent);
+ box-shadow: 0 0 0 1px var(--color-rule-strong);
+ transform: translate(-50%, -50%);
+ }
+
+ .article-list--timeline > li > article,
+ .article-list--timeline time,
+ .article-list--timeline .entry-thumbnail {
+ position: relative;
+ z-index: 1;
+ }
+
+ .article-list--timeline time {
+ justify-self: end;
+ align-self: center;
+ width: max-content;
+ min-block-size: 0;
+ margin-inline-end: calc(-1 * var(--timeline-date-offset));
+ padding-inline-end: 4px;
+ display: inline-block;
+ color: var(--color-muted);
+ font-size: var(--fs-sm);
+ line-height: 1.2;
+ text-align: center;
+ white-space: nowrap;
+ top: 5px;
+ transform: rotate(-45deg);
+ transform-origin: right center;
+ }
+}
diff --git a/src/styles/components/post.css b/src/styles/components/post.css
new file mode 100644
index 0000000..56e9bdb
--- /dev/null
+++ b/src/styles/components/post.css
@@ -0,0 +1,212 @@
+/* At-a-glance, facts, media figures, post nav, TOC, gallery, related posts. */
+@layer components {
+ .at-a-glance {
+ margin-top: var(--space-8);
+ padding-block: var(--space-5);
+ border-block: 1px solid var(--color-rule);
+ }
+
+ .at-a-glance dl,
+ .facts dl {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+ margin: var(--space-4) 0 0;
+ }
+
+ .at-a-glance__row,
+ .facts dl > div {
+ display: grid;
+ grid-template-columns: minmax(6rem, max-content) minmax(0, 1fr);
+ gap: var(--space-4);
+ }
+
+ .at-a-glance dt,
+ .facts dt {
+ color: var(--color-muted);
+ font-size: var(--fs-caption);
+ }
+
+ .facts {
+ margin-top: var(--space-8);
+ padding-top: var(--space-6);
+ border-top: 1px solid var(--color-rule);
+ }
+
+ /* Let prose wrap beside .at-a-glance and continue below it. */
+ @media (min-width: 1100px) {
+ .post > .at-a-glance {
+ float: right;
+ width: min(18rem, 42%);
+ margin-top: var(--space-8);
+ margin-right: 0;
+ margin-bottom: var(--space-4);
+ margin-left: var(--space-8);
+ }
+
+ .post > .post-media,
+ .post > .post-gallery,
+ .related-posts,
+ .post-nav {
+ clear: both;
+ }
+ }
+
+ .post-media {
+ max-inline-size: min(100%, var(--measure-wide));
+ margin: var(--space-8) 0 0;
+ }
+
+ .post-media img,
+ .post-media video {
+ border: 1px solid var(--color-rule);
+ border-radius: var(--radius-md);
+ background: var(--color-code-bg);
+ width: 100%;
+ }
+
+ .post-media figcaption,
+ .media-transcript {
+ max-width: var(--measure);
+ margin-top: var(--space-2);
+ color: var(--color-muted);
+ font-size: var(--fs-caption);
+ line-height: 1.45;
+ }
+
+ .media-transcript strong {
+ color: var(--color-fg);
+ font-weight: var(--weight-semibold);
+ }
+
+ .post-nav {
+ margin-top: var(--space-12);
+ padding-top: var(--space-6);
+ border-top: 1px solid var(--color-rule);
+ }
+
+ .post-nav__list {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
+ gap: var(--space-4);
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ .post-nav__next {
+ justify-self: end;
+ }
+
+ .post-nav a {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+ padding: var(--space-3) var(--space-4);
+ border: 1px solid var(--color-rule);
+ border-radius: var(--radius-md);
+ color: var(--color-fg);
+ text-decoration: none;
+ min-height: 44px;
+ transition: border-color 150ms ease;
+ }
+
+ .post-nav a:hover,
+ .post-nav a:focus-visible {
+ border-color: var(--color-rule-strong);
+ }
+
+ .post-nav a.next {
+ text-align: end;
+ }
+
+ .post-nav .post-nav__label {
+ color: var(--color-muted);
+ font-size: var(--fs-caption);
+ }
+
+ .post-nav .post-nav__title {
+ color: var(--color-fg);
+ font-weight: var(--weight-semibold);
+ }
+
+ .post-toc {
+ margin-top: var(--space-6);
+ padding: var(--space-3) var(--space-4);
+ border-inline-start: 2px solid var(--color-rule);
+ font-size: var(--fs-caption);
+ color: var(--color-muted);
+ max-height: 60vh;
+ overflow-y: auto;
+ }
+
+ .post-header h1,
+ .post-nav .post-nav__title,
+ .project-card h3 {
+ overflow-wrap: anywhere;
+ }
+
+ .post-toc ol {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+ }
+
+ .post-toc a {
+ display: inline-flex;
+ align-items: center;
+ min-block-size: 24px;
+ padding-block: 2px;
+ color: var(--color-muted);
+ text-decoration: none;
+ }
+
+ .post-toc a:hover,
+ .post-toc a:focus-visible {
+ color: var(--color-fg);
+ }
+
+ .post-gallery {
+ list-style: none;
+ padding: 0;
+ margin: var(--space-8) 0 0;
+ display: grid;
+ gap: var(--space-6);
+ }
+
+ .post > .post-gallery {
+ width: 100%;
+ }
+
+ .post-gallery .post-media {
+ max-inline-size: 100%;
+ margin: 0;
+ }
+
+ .external-link-icon {
+ display: inline-block;
+ margin-inline-start: 0.25em;
+ vertical-align: -0.125em;
+ opacity: 0.85;
+ }
+
+ .related-posts {
+ margin-top: var(--space-12);
+ padding-top: var(--space-6);
+ border-top: 1px solid var(--color-rule);
+ }
+
+ .related-posts h2 {
+ font-size: var(--fs-lg);
+ font-weight: var(--weight-semibold);
+ margin-bottom: var(--space-4);
+ }
+
+ .empty-state {
+ max-width: var(--measure);
+ padding-block: var(--space-6);
+ }
+}
diff --git a/src/styles/components/project-card.css b/src/styles/components/project-card.css
new file mode 100644
index 0000000..975666d
--- /dev/null
+++ b/src/styles/components/project-card.css
@@ -0,0 +1,155 @@
+/* Project card: thumbnail, summary, stretched-link Open control, links. */
+@layer components {
+ .project-card {
+ position: relative;
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-template-areas:
+ 'thumb'
+ 'summary';
+ min-width: 0;
+ overflow: hidden;
+ border: 1px solid var(--color-rule);
+ border-radius: var(--radius-lg);
+ background: var(--color-bg);
+ transition: border-color 150ms ease;
+ }
+
+ .project-card:hover,
+ .project-card:focus-within,
+ .project-card:target {
+ border-color: var(--color-rule-strong);
+ }
+
+ .project-card .project-thumbnail {
+ grid-area: thumb;
+ width: 100%;
+ height: auto;
+ border: 0;
+ border-bottom: 1px solid var(--color-rule);
+ border-radius: 0;
+ aspect-ratio: 4 / 3;
+ }
+
+ .project-card__summary {
+ grid-area: summary;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: var(--space-3);
+ min-width: 0;
+ padding: var(--space-3) var(--space-4);
+ }
+
+ .project-card p {
+ margin: 0;
+ }
+
+ .project-card .project-description {
+ color: var(--color-fg);
+ font-size: var(--fs-base);
+ line-height: var(--leading-snug);
+ }
+
+ .project-card__head {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+ min-width: 0;
+ }
+
+ .project-card__actions {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: var(--space-2) var(--space-3);
+ margin-top: var(--space-3);
+ }
+
+ /* Stretched link: the Open control's overlay covers the whole card so any
+ click opens the project website. The Article link sits above the overlay
+ via a higher z-index and stays independently clickable. */
+ .project-card__open::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+ /* Cover the whole card, not the Open button's pill radius. The card's own
+ overflow:hidden + border-radius clips this to the rounded card shape. */
+ border-radius: 0;
+ }
+
+ .project-card__open {
+ margin-left: auto;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3em;
+ min-height: 36px;
+ padding: 0.25em 0.85em;
+ border: 1px solid var(--color-rule-strong);
+ border-radius: var(--radius-pill);
+ background: var(--color-bg);
+ color: var(--color-link);
+ font-size: var(--fs-caption);
+ font-weight: var(--weight-semibold);
+ text-decoration: none;
+ transition:
+ background-color 150ms ease,
+ border-color 150ms ease,
+ color 150ms ease;
+ }
+
+ .project-card:hover .project-card__open,
+ .project-card:focus-within .project-card__open {
+ border-color: var(--color-link);
+ background: var(--color-callout-bg);
+ color: var(--color-link-hover);
+ }
+
+ .project-article-link {
+ position: relative;
+ z-index: 2;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3em;
+ min-height: 36px;
+ color: var(--color-link);
+ font-size: var(--fs-caption);
+ font-weight: var(--weight-medium);
+ text-decoration: none;
+ }
+
+ .project-article-link:hover,
+ .project-article-link:focus-visible {
+ color: var(--color-link-hover);
+ text-decoration: underline;
+ }
+
+ .project-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-1) var(--space-3);
+ margin: var(--space-3) 0 0;
+ padding: 0;
+ list-style: none;
+ }
+
+ .project-links a {
+ min-height: 44px;
+ min-width: 44px;
+ display: inline-flex;
+ align-items: center;
+ color: var(--color-link);
+ }
+
+ .project-links a:hover,
+ .project-links a:focus-visible {
+ color: var(--color-link-hover);
+ }
+
+ .project-links a .download-indicator {
+ margin-left: 0.25em;
+ color: var(--color-muted);
+ font-size: 0.85em;
+ }
+}
diff --git a/src/styles/components/prose.css b/src/styles/components/prose.css
new file mode 100644
index 0000000..2def8bb
--- /dev/null
+++ b/src/styles/components/prose.css
@@ -0,0 +1,196 @@
+/* Long-form article prose: headings, lists, quotes, tables, code (Shiki). */
+@layer components {
+ .prose {
+ max-inline-size: var(--measure);
+ line-height: var(--leading-prose);
+ hyphens: auto;
+ hyphenate-limit-chars: 7 3 3;
+ }
+
+ .prose > * + * {
+ margin-top: 1.05em;
+ }
+
+ .prose > h2:first-child,
+ .prose > h3:first-child {
+ margin-top: 0;
+ }
+
+ .prose p {
+ text-wrap: pretty;
+ }
+
+ .prose h2,
+ .prose h3 {
+ position: relative;
+ color: var(--color-fg);
+ line-height: var(--leading-tight);
+ scroll-margin-top: var(--space-8);
+ }
+
+ .prose h2 {
+ margin-top: var(--space-12);
+ font-size: var(--fs-xl);
+ font-weight: var(--weight-semibold);
+ }
+
+ .prose h3 {
+ margin-top: var(--space-8);
+ font-size: var(--fs-lg);
+ font-weight: var(--weight-semibold);
+ }
+
+ .prose h2:target,
+ .prose h3:target {
+ background: var(--color-callout-bg);
+ border-inline-start: 2px solid var(--color-accent);
+ padding-inline-start: var(--space-3);
+ margin-inline-start: calc(-1 * var(--space-3) - 2px);
+ }
+
+ .prose .heading-anchor {
+ margin-inline-start: 0.4em;
+ color: var(--color-muted);
+ font-weight: var(--weight-regular);
+ font-size: 0.85em;
+ text-decoration: none;
+ opacity: 0;
+ transition: opacity 150ms ease;
+ }
+
+ .prose .heading-anchor:focus-visible {
+ opacity: 1;
+ text-decoration: underline;
+ }
+
+ .prose .heading-anchor::before {
+ content: '#';
+ }
+
+ .prose h2:hover .heading-anchor,
+ .prose h3:hover .heading-anchor,
+ .prose .heading-anchor:hover {
+ opacity: 1;
+ }
+
+ @media (hover: none) {
+ .prose .heading-anchor {
+ opacity: 1;
+ }
+ }
+
+ .prose ul,
+ .prose ol {
+ padding-inline-start: 1.25rem;
+ }
+
+ .prose li + li {
+ margin-top: var(--space-1);
+ }
+
+ .prose strong {
+ font-weight: var(--weight-bold);
+ color: var(--color-fg);
+ }
+
+ .prose em {
+ font-style: italic;
+ }
+
+ .prose blockquote {
+ margin-inline: 0;
+ padding: var(--space-2) var(--space-4);
+ border-inline-start: 3px solid var(--color-rule-medium);
+ background: var(--color-callout-bg);
+ color: var(--color-muted);
+ font-style: italic;
+ }
+
+ .prose blockquote > * + * {
+ margin-top: 0.6em;
+ }
+
+ .prose hr {
+ margin-block: var(--space-8);
+ border: 0;
+ height: 1px;
+ background: var(--color-rule);
+ }
+
+ .prose table {
+ width: 100%;
+ max-width: 100%;
+ border-collapse: collapse;
+ font-size: 0.95em;
+ display: block;
+ overflow-x: auto;
+ }
+
+ .prose thead {
+ background: var(--color-code-bg);
+ text-align: start;
+ }
+
+ .prose th,
+ .prose td {
+ padding: var(--space-2) var(--space-3);
+ border-bottom: 1px solid var(--color-rule);
+ vertical-align: top;
+ }
+
+ .prose th {
+ font-weight: var(--weight-semibold);
+ }
+
+ .prose code {
+ font-family: var(--font-mono);
+ font-size: 0.88em;
+ background: var(--color-code-bg);
+ border-radius: var(--radius-sm);
+ padding: 0.08em 0.25em;
+ }
+
+ .prose pre {
+ max-width: 100%;
+ overflow-x: auto;
+ scrollbar-gutter: stable;
+ padding: var(--space-4);
+ background: var(--color-code-bg);
+ border: 1px solid var(--color-rule);
+ border-radius: var(--radius-md);
+ line-height: 1.55;
+ scrollbar-width: thin;
+ scrollbar-color: var(--color-rule-medium) var(--color-code-bg);
+ }
+
+ .prose pre::-webkit-scrollbar {
+ height: 8px;
+ }
+
+ .prose pre::-webkit-scrollbar-track {
+ background: var(--color-code-bg);
+ }
+
+ .prose pre::-webkit-scrollbar-thumb {
+ background: var(--color-rule-medium);
+ border-radius: var(--radius-sm);
+ }
+
+ .prose pre code {
+ background: transparent;
+ padding: 0;
+ }
+
+ /* Shiki dual-theme: defaultColor: false emits --shiki-light / --shiki-dark
+ vars on every token; light-dark() picks the active variant from
+ color-scheme on :root. */
+ .prose pre.astro-code,
+ .prose pre.astro-code code,
+ .prose pre.astro-code span {
+ color: light-dark(var(--shiki-light), var(--shiki-dark));
+ background-color: light-dark(
+ var(--shiki-light-bg, var(--color-code-bg)),
+ var(--shiki-dark-bg, var(--color-code-bg))
+ );
+ }
+}
diff --git a/src/styles/components/sections.css b/src/styles/components/sections.css
new file mode 100644
index 0000000..398ab95
--- /dev/null
+++ b/src/styles/components/sections.css
@@ -0,0 +1,125 @@
+/* Post-content width wrappers, archive/project sections, about, starting points. */
+@layer components {
+ .post > .post-media {
+ max-width: var(--measure);
+ margin-inline: auto;
+ }
+
+ .post > .prose {
+ max-width: var(--measure-wide);
+ max-inline-size: var(--measure-wide);
+ margin-inline: auto;
+ }
+
+ .post > .at-a-glance,
+ .post > .post-thumbnail,
+ .post > .post-gallery,
+ .post-nav {
+ max-width: var(--measure-wide);
+ margin-inline: auto;
+ }
+
+ .archive-year,
+ .project-section {
+ width: 100%;
+ }
+
+ .archive-year + .archive-year,
+ .project-section + .project-section {
+ margin-top: var(--space-10);
+ }
+
+ .archive-year h2,
+ .project-section h2 {
+ margin-bottom: var(--space-3);
+ }
+
+ .about-section {
+ width: 100%;
+ margin-top: var(--space-10);
+ }
+
+ .about-section--starting-points {
+ margin-top: var(--space-6);
+ padding-top: var(--space-6);
+ border-top: 1px solid var(--color-rule);
+ }
+
+ .about-section--starting-points .section-heading {
+ align-items: flex-start;
+ padding-top: 0;
+ }
+
+ .about-section.facts {
+ max-width: none;
+ }
+
+ .about-section.facts > .prose {
+ margin-top: var(--space-4);
+ }
+
+ .prose.about-copy {
+ width: 100%;
+ max-inline-size: none;
+ }
+
+ .starting-points {
+ display: grid;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: var(--space-3);
+ margin: 0;
+ padding: var(--space-4) 0 0;
+ list-style: none;
+ }
+
+ .starting-points > li {
+ min-width: 0;
+ }
+
+ .starting-point__thumbnail {
+ aspect-ratio: 4 / 3;
+ }
+
+ .starting-point__body {
+ min-width: 0;
+ padding-block-start: var(--space-2);
+ }
+
+ .starting-point__body h3 {
+ font-size: var(--fs-sm);
+ font-weight: var(--weight-semibold);
+ line-height: var(--leading-snug);
+ }
+
+ .starting-point__body a {
+ color: var(--color-fg);
+ text-decoration: none;
+ }
+
+ .starting-point__body a:hover {
+ color: var(--color-link-hover);
+ text-decoration: underline;
+ text-underline-offset: 0.2em;
+ }
+
+ .starting-point__body p {
+ margin-top: var(--space-1);
+ color: var(--color-muted);
+ font-size: var(--fs-xs);
+ line-height: var(--leading-snug);
+ }
+
+ .starting-points > li:focus-within .entry-thumbnail {
+ border-color: var(--color-rule-strong);
+ }
+
+ .about-links {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-1) var(--space-4);
+ }
+
+ .post > .prose {
+ margin-top: var(--space-8);
+ }
+}
diff --git a/src/styles/components/theme-switcher.css b/src/styles/components/theme-switcher.css
new file mode 100644
index 0000000..51add57
--- /dev/null
+++ b/src/styles/components/theme-switcher.css
@@ -0,0 +1,118 @@
+/* Light/dark theme toggle switch + forced-colors fallback. */
+@layer components {
+ .theme-switcher {
+ --switcher-w: 2.75rem;
+ --switcher-h: 1.5rem;
+ --switcher-icon: 1.05rem;
+ --switcher-gap: 0.22rem;
+
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: var(--switcher-w);
+ height: var(--switcher-h);
+ /* Adjacent header targets remain at least 44px apart while the visual
+ track stays compact. */
+ margin: max(var(--space-2), calc((44px - var(--switcher-h)) / 2)) 0;
+ overflow: hidden;
+ border: 1px solid var(--color-rule-medium);
+ border-radius: var(--radius-pill);
+ appearance: none;
+ cursor: pointer;
+ background: var(--theme-switcher-track);
+ color: inherit;
+ transition:
+ background-color 200ms ease,
+ border-color 150ms ease;
+ box-shadow: inset 0 1px 2px rgb(0 0 0 / 18%);
+ }
+
+ .theme-switcher:hover {
+ border-color: var(--color-rule-strong);
+ }
+
+ .no-js .theme-switcher {
+ display: none !important;
+ }
+
+ .theme-switcher-icon {
+ position: absolute;
+ top: 50%;
+ left: 0;
+ width: var(--switcher-icon);
+ height: var(--switcher-icon);
+ pointer-events: none;
+ transition:
+ transform 180ms ease,
+ opacity 180ms ease,
+ color 180ms ease;
+ }
+
+ .theme-switcher-icon-sun {
+ color: var(--theme-switcher-icon-light);
+ }
+
+ .theme-switcher-icon-moon {
+ color: var(--theme-switcher-icon-dark);
+ }
+
+ .theme-switcher[aria-pressed='false'] .theme-switcher-icon-sun {
+ transform: translateY(-50%)
+ translateX(calc(var(--switcher-w) - var(--switcher-icon) - var(--switcher-gap)));
+ opacity: 1;
+ }
+
+ .theme-switcher[aria-pressed='false'] .theme-switcher-icon-moon {
+ transform: translateY(-50%) translateX(calc(-1 * var(--switcher-icon)));
+ opacity: 0;
+ }
+
+ .theme-switcher[aria-pressed='true'] .theme-switcher-icon-sun {
+ transform: translateY(-50%) translateX(var(--switcher-w));
+ opacity: 0;
+ }
+
+ .theme-switcher[aria-pressed='true'] .theme-switcher-icon-moon {
+ transform: translateY(-50%) translateX(var(--switcher-gap));
+ opacity: 1;
+ }
+
+ /* High-contrast / forced-colors fallback: render a text label. */
+ @media (forced-colors: active) {
+ .theme-switcher {
+ width: auto;
+ height: auto;
+ min-block-size: 44px;
+ min-inline-size: 44px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--space-1) var(--space-2);
+ overflow: visible;
+ background: ButtonFace;
+ color: ButtonText;
+ border: 1px solid ButtonBorder;
+ box-shadow: none;
+ }
+
+ .theme-switcher-icon {
+ display: none;
+ }
+
+ .theme-switcher::before {
+ content: 'Light';
+ position: static;
+ width: auto;
+ height: auto;
+ transform: none;
+ background: transparent;
+ border-radius: 0;
+ }
+
+ .theme-switcher[aria-pressed='true']::before {
+ content: 'Dark';
+ transform: none;
+ }
+ }
+}
diff --git a/src/styles/components/thumbnails.css b/src/styles/components/thumbnails.css
new file mode 100644
index 0000000..a5b59f7
--- /dev/null
+++ b/src/styles/components/thumbnails.css
@@ -0,0 +1,243 @@
+/* Post thumbnail (iframe embed) and click-to-play video thumbnail. */
+@layer components {
+ .post-thumbnail {
+ margin: 0;
+ }
+
+ .post-thumbnail img {
+ width: 100%;
+ border: 1px solid var(--color-rule);
+ border-radius: var(--radius-md);
+ background: var(--color-code-bg);
+ }
+
+ .post-thumbnail--iframe {
+ position: relative;
+ aspect-ratio: var(--post-thumbnail-aspect, 16 / 9);
+ overflow: hidden;
+ border: 1px solid var(--color-rule);
+ border-radius: var(--radius-md);
+ background: var(--color-code-bg);
+ }
+
+ .post-thumbnail--iframe picture,
+ .post-thumbnail--iframe img {
+ width: 100%;
+ height: 100%;
+ }
+
+ .post-thumbnail--iframe picture {
+ display: block;
+ }
+
+ .post-thumbnail--iframe img {
+ object-fit: cover;
+ object-position: center center;
+ border: 0;
+ border-radius: 0;
+ }
+
+ .post-thumbnail__play {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ border: 0;
+ padding: 0;
+ display: grid;
+ place-items: center;
+ background: color-mix(in oklch, #000 22%, transparent);
+ color: var(--color-fg);
+ cursor: pointer;
+ }
+
+ .post-thumbnail__play:hover,
+ .post-thumbnail__play:focus-visible {
+ background: color-mix(in oklch, #000 30%, transparent);
+ }
+
+ .post-thumbnail__play-icon {
+ width: clamp(3.25rem, 9vw, 4.75rem);
+ aspect-ratio: 1;
+ display: grid;
+ place-items: center;
+ border: 1px solid var(--color-rule-strong);
+ border-radius: var(--radius-pill);
+ background: color-mix(in oklch, var(--color-bg) 88%, transparent);
+ box-shadow: 0 0.75rem 2rem color-mix(in oklch, #000 28%, transparent);
+ transition:
+ background-color 150ms ease,
+ transform 150ms ease;
+ }
+
+ .post-thumbnail__play:hover .post-thumbnail__play-icon,
+ .post-thumbnail__play:focus-visible .post-thumbnail__play-icon {
+ background: var(--color-bg);
+ transform: scale(1.04);
+ }
+
+ .post-thumbnail__play svg {
+ width: 42%;
+ height: 42%;
+ transform: translateX(8%);
+ fill: currentColor;
+ }
+
+ .post-thumbnail__iframe {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ border: 0;
+ background: var(--color-code-bg);
+ }
+
+ .post-thumbnail--iframe.is-active picture,
+ .post-thumbnail--iframe.is-active .post-thumbnail__play {
+ display: none;
+ }
+
+ .post-thumbnail__noscript {
+ position: absolute;
+ inset-inline: var(--space-3);
+ inset-block-end: var(--space-3);
+ padding: var(--space-2) var(--space-3);
+ border-radius: var(--radius-md);
+ background: var(--color-bg);
+ font-size: var(--fs-caption);
+ }
+
+ /* Click-to-play header video: a poster with a play button that swaps in an
+ inline