@charset "UTF-8";
/* NUMBERS ----------------------------------------------------------------------- */
/* SPACING ----------------------------------------------------------------------- */
/* One 4px-based scale for vertical rhythm. Use these instead of ad-hoc rem values so section, block and component spacing stay related. */
/* COLORS ------------------------------------------------------------------------ */
/* Light palette. The dark overrides live in _theme.scss; components consume the custom properties through these Sass aliases so both themes stay in one place. */
:root { color-scheme: light; --background: #f1f1f1; --color: #1a1a1a; --muted: #4a4d51; --rule: #d9dee2; --rule-strong: #b9c0c5; --surface: #fff; --surface-muted: #fafafa; --line: #e3e3e3; --link: #000; --accent: #006e52; /* Tag colours. Each label is already unique, so colour is a redundant cue. */ --tag-shopify: #006e52; --tag-open-data: #1f5f9e; --tag-iot: #6b3fa0; --tag-plugin: #8a4b00; --tag-cli: #006b73; --tag-diagnostics: #a5301f; --tag-contribution: #8a2c6a; --tag-civic-tech: #6f5200; --card-hover: rgba(74, 77, 81, 0.6); --footer-bg: #111; --footer-border: #3a3a3a; --footer-text: #a3a3a3; --footer-link: #f5f5f5; --alert-bg: #fbeae5; --alert-border: #f1c8bd; --alert-text: #8a1f0b; --alert-strong: #6b1708; --code-bg: #e8e8e8; --scroll: #d4d4d4; --syntax-comment: #5f6368; --syntax-keyword: #005a44; --syntax-string: #0a6b52; --syntax-number: #8a5a00; --syntax-error-bg: #fdecea; --syntax-error: #b3261e; --syntax-inserted: #006e52; }

/* TYPE -------------------------------------------------------------------------- */
/* Five text sizes. Anything smaller than the body uses the 14px label size; large type is only used for the page title and the intro statement. */
/* Header and footer chrome share the same size as dates and other labels; hierarchy comes from colour and weight instead. */
/* One shared style for dates, metadata, captions and other small mono labels. */
/* Quiet buttons share one shape across pagination, post actions and the 404 links: a mono label, a hairline border, equal space around an icon, and an inverted hover state. */
/* Blueprint line language. Object lines outline primary structure, fine lines carry detail, and centre lines use a long-short-long rhythm. */
/* A dimension line: a top rule with short downward end ticks, used above headings to mark their width. */
/* DARK THEME -------------------------------------------------------------------- */
/* Applied by the inline head script before first paint. The footer stays dark in both themes, so its tokens only shift to a deeper black. */
@media screen { :root[data-theme="dark"] { color-scheme: dark; --background: #111; --color: #e8e8e8; --muted: #a3a3a3; --rule: #2a2d30; --rule-strong: #3c4145; --surface: #191919; --surface-muted: #151515; --line: #2a2a2a; --link: #e8e8e8; --accent: #5fbfa0; --tag-shopify: #5fbfa0; --tag-open-data: #7cb8ef; --tag-iot: #c4a8f5; --tag-plugin: #e8b878; --tag-cli: #7fd4e0; --tag-diagnostics: #f0a898; --tag-contribution: #eba6d0; --tag-civic-tech: #e3c877; --card-hover: rgba(232, 232, 232, 0.32); --footer-bg: #0b0b0b; --footer-border: #262626; --footer-text: #a3a3a3; --footer-link: #f5f5f5; --alert-bg: #2a1512; --alert-border: #5a2a20; --alert-text: #f0a898; --alert-strong: #ffc7bb; --code-bg: #1d1d1d; --scroll: #3a3a3a; --syntax-comment: #8b9095; --syntax-keyword: #7fd1b9; --syntax-string: #9ed3a7; --syntax-number: #e3b877; --syntax-error-bg: #3a1512; --syntax-error: #ff9d92; --syntax-inserted: #7fd1b9; } }
/*! normalize.css 2012-03-11T12:53 UTC - http://github.com/necolas/normalize.css */
/* ============================================================================= HTML5 display definitions ========================================================================== */
/* Corrects block display not defined in IE6/7/8/9 & FF3 */
article, aside, details, figcaption, figure, footer, header, hgroup, nav, section, summary { display: block; }

/* Corrects inline-block display not defined in IE6/7/8/9 & FF3 */
audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; }

/* Prevents modern browsers from displaying 'audio' without controls Remove excess height in iOS5 devices */
audio:not([controls]) { display: none; height: 0; }

/* Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 Known issue: no IE6 support */
[hidden] { display: none; }

/* ============================================================================= Base ========================================================================== */
/* 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units http://clagnut.com/blog/348/#c790 2. Prevents iOS text size adjust after orientation change, without disabling user zoom www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ */
html { font-size: 100%; /* 1 */ -webkit-text-size-adjust: 100%; /* 2 */ -ms-text-size-adjust: 100%; /* 2 */ }

/* Addresses font-family inconsistency between 'textarea' and other form elements. */
html, button, input, select, textarea { font-family: sans-serif; }

/* Addresses margins handled incorrectly in IE6/7 */
body { margin: 0; }

/* ============================================================================= Links ========================================================================== */
/* Addresses outline displayed oddly in Chrome */
a:focus { outline: thin dotted; }

/* Improves readability when focused and also mouse hovered in all browsers people.opera.com/patrickl/experiments/keyboard/test */
a:hover, a:active { outline: 0; }

/* ============================================================================= Typography ========================================================================== */
/* Addresses font sizes and margins set differently in IE6/7 Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 */
h1 { font-size: 2em; margin: 0.67em 0; }

h2 { font-size: 1.5em; margin: 0.83em 0; }

h3 { font-size: 1.17em; margin: 1em 0; }

h4 { font-size: 1em; margin: 1.33em 0; }

h5 { font-size: 0.83em; margin: 1.67em 0; }

h6 { font-size: 0.75em; margin: 2.33em 0; }

/* Addresses styling not present in IE7/8/9, S5, Chrome */
abbr[title] { border-bottom: 1px dotted; }

/* Addresses style set to 'bolder' in FF3+, S4/5, Chrome
*/
b, strong { font-weight: bold; }

blockquote { margin: 1em 40px; }

/* Addresses styling not present in S5, Chrome */
dfn { font-style: italic; }

/* Addresses styling not present in IE6/7/8/9 */
mark { background: #ff0; color: #000; }

/* Addresses margins set differently in IE6/7 */
p, pre { margin: 1em 0; }

/* Corrects font family set oddly in IE6, S4/5, Chrome en.wikipedia.org/wiki/User:Davidgothberg/Test59 */
pre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; }

/* Improves readability of pre-formatted text in all browsers */
pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }

/* 1. Addresses CSS quotes not supported in IE6/7 2. Addresses quote property not supported in S4 */
/* 1 */
q { quotes: none; }

/* 2 */
q:before, q:after { content: ''; content: none; }

small { font-size: 75%; }

/* Prevents sub and sup affecting line-height in all browsers gist.github.com/413930 */
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }

sup { top: -0.5em; }

sub { bottom: -0.25em; }

/* ============================================================================= Lists ========================================================================== */
/* Addresses margins set differently in IE6/7 */
dl, menu, ol, ul { margin: 1em 0; }

dd { margin: 0 0 0 40px; }

/* Addresses paddings set differently in IE6/7 */
menu, ol, ul { padding: 0 0 0 40px; }

/* Corrects list images handled incorrectly in IE7 */
nav ul, nav ol { list-style: none; list-style-image: none; }

/* ============================================================================= Embedded content ========================================================================== */
/* 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 2. Improves image quality when scaled in IE7 code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ */
img { border: 0; /* 1 */ -ms-interpolation-mode: bicubic; /* 2 */ }

/* Corrects overflow displayed oddly in IE9 */
svg:not(:root) { overflow: hidden; }

/* ============================================================================= Figures ========================================================================== */
/* Addresses margin not present in IE6/7/8/9, S5, O11 */
figure { margin: 0; }

/* ============================================================================= Forms ========================================================================== */
/* Corrects margin displayed oddly in IE6/7 */
form { margin: 0; }

/* Define consistent border, margin, and padding */
fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; }

/* 1. Corrects color not being inherited in IE6/7/8/9 2. Corrects text not wrapping in FF3 3. Corrects alignment displayed oddly in IE6/7 */
legend { border: 0; /* 1 */ padding: 0; white-space: normal; /* 2 */ *margin-left: -7px; /* 3 */ }

/* 1. Corrects font size not being inherited in all browsers 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome 3. Improves appearance and consistency in all browsers */
button, input, select, textarea { font-size: 100%; /* 1 */ margin: 0; /* 2 */ vertical-align: baseline; /* 3 */ *vertical-align: middle; /* 3 */ }

/* Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet */
button, input { line-height: normal; /* 1 */ }

/* 1. Improves usability and consistency of cursor style between image-type 'input' and others 2. Corrects inability to style clickable 'input' types in iOS 3. Removes inner spacing in IE7 without affecting normal text inputs Known issue: inner spacing remains in IE6 */
button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; /* 1 */ -webkit-appearance: button; /* 2 */ *overflow: visible; /* 3 */ }

/* Re-set default cursor for disabled elements */
button[disabled], input[disabled] { cursor: default; }

/* 1. Addresses box sizing set to content-box in IE8/9 2. Removes excess padding in IE8/9 3. Removes excess padding in IE7 Known issue: excess padding remains in IE6 */
input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ *height: 13px; /* 3 */ *width: 13px; /* 3 */ }

/* 1. Addresses appearance set to searchfield in S5, Chrome 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) */
input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; }

/* Removes inner padding and search cancel button in S5, Chrome on OS X */
input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }

/* Removes inner padding and border in FF3+ www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ */
button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }

/* 1. Removes default vertical scrollbar in IE6/7/8/9 2. Improves readability and alignment in all browsers */
textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ }

/* ============================================================================= Tables ========================================================================== */
/* Remove most spacing between table cells */
table { border-collapse: collapse; border-spacing: 0; }

body, button, input, select, textarea { font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: var(--color); }

body { font-size: 1rem; line-height: 1.6; font-weight: 400; font-optical-sizing: auto; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }

h1, h2, h3, h4, h5, h6 { margin: 0 0 0.75rem; font-weight: 400; line-height: 1.1; letter-spacing: -0.02em; color: var(--color); overflow-wrap: anywhere; }

h1 { font-size: clamp(2.25rem, 5vw, 3.25rem); }

h2 { margin-top: 2.5rem; font-size: 1.75rem; }

h3 { margin-top: 2rem; font-size: 1.25rem; font-weight: 500; line-height: 1.3; }

h4, h5, h6 { margin-top: 2rem; font-size: 1rem; font-weight: 500; }

p { margin: 0 0 1rem; text-wrap: pretty; }

strong { font-weight: 600; }

time { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-size: 0.875rem; font-weight: 500; letter-spacing: 0.08em; line-height: 1.3; text-transform: uppercase; display: block; margin-bottom: 0.75rem; color: var(--muted); font-variant-numeric: tabular-nums; }

/* LISTS ------------------------------------------------------------------------- */
ul, ol { margin: 0 0 1rem; padding-left: 1.25rem; }

li + li { margin-top: 0.25rem; }

li > ul, li > ol { margin-top: 0.5rem; margin-bottom: 0; }

/* BLOCKQUOTES ------------------------------------------------------------------- */
blockquote { max-width: 68ch; margin: 2.5rem 0; padding: 0; text-align: center; font-family: inherit; font-style: normal; line-height: 1.5; color: var(--muted); /* Inline the quotation marks so they wrap the quote itself, rather than setting a separate oversized mark above it. */ }
blockquote p { /* The blockquote keeps the body font size so its 68ch measure matches the article column; only the quote itself steps up a size. */ max-width: 40ch; margin: 0 auto; font-size: 1.25rem; text-wrap: pretty; }
blockquote p + p { margin-top: 0.75rem; }
blockquote p:first-child::before { content: '\201C'; }
blockquote p:last-child::after { content: '\201D'; }

/* CODE -------------------------------------------------------------------------- */
code, kbd, samp, pre { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-variant-ligatures: none; }

code { padding: 0.1em 0.3em; border: 0; background: var(--code-bg); color: var(--color); font-size: 0.9em; overflow-wrap: anywhere; box-decoration-break: clone; -webkit-box-decoration-break: clone; }

pre { display: block; margin: 2rem 0; padding: 1rem 1.25rem; background: var(--surface-muted); border: 1px solid var(--line); color: var(--color); font-size: 0.875rem; line-height: 1.6; tab-size: 4; white-space: pre; word-wrap: normal; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: thin; scrollbar-color: var(--scroll) transparent; }
pre::-webkit-scrollbar { height: 8px; }
pre::-webkit-scrollbar-thumb { background: var(--scroll); }
pre code { padding: 0; border: 0; background: none; font-size: inherit; white-space: inherit; }

/* Rouge wrappers produced by kramdown's fenced code blocks. The card styling lives on the inner <pre>, so keep the wrapper transparent. The box bleeds into the gutter by its own padding, which keeps the code text aligned with the prose measure instead of indenting it inside the card. Scope these to block wrappers only: kramdown also puts `highlighter-rouge` on inline code. */
div.highlighter-rouge, figure.highlight { margin: 2rem 0; }

div.highlighter-rouge pre, figure.highlight pre { margin: 0; }

.post div.highlighter-rouge, .post figure.highlight { width: calc(100% + 2.5rem); max-width: calc(68ch + 2.5rem); margin: 2rem -1.25rem; }

.post > pre { width: calc(100% + 2.5rem); max-width: calc(68ch + 2.5rem); margin: 2rem -1.25rem; }

/* SYNTAX HIGHLIGHTING ----------------------------------------------------------- */
.highlight { color: var(--color); }
.highlight .c, .highlight .cm, .highlight .c1, .highlight .cs, .highlight .cd { color: var(--syntax-comment); font-style: italic; }
.highlight .k, .highlight .kc, .highlight .kd, .highlight .kn, .highlight .kp, .highlight .kr, .highlight .kt { color: var(--syntax-keyword); font-weight: 600; }
.highlight .s, .highlight .s1, .highlight .s2, .highlight .sb, .highlight .sc, .highlight .sd, .highlight .se, .highlight .sh, .highlight .si, .highlight .sx, .highlight .sr, .highlight .ss { color: var(--syntax-string); }
.highlight .m, .highlight .mf, .highlight .mh, .highlight .mi, .highlight .mo, .highlight .il { color: var(--syntax-number); }
.highlight .na, .highlight .nt { color: var(--syntax-number); }
.highlight .o, .highlight .ow { color: var(--muted); }
.highlight .p { color: var(--muted); }
.highlight .err { padding: 0 0.2em; background: var(--syntax-error-bg); color: var(--syntax-error); }
.highlight .gd { color: var(--syntax-error); }
.highlight .gi { color: var(--syntax-inserted); }
.highlight .gh, .highlight .gu { color: var(--syntax-inserted); font-weight: 600; }

/* OTHER ------------------------------------------------------------------------- */
hr { height: 1px; border: 0; background-image: repeating-linear-gradient(to right, var(--rule-strong) 0 24px, transparent 24px 28px, var(--rule-strong) 28px 32px, transparent 32px 36px); margin: 2rem 0; }

img { max-width: 100%; height: auto; }
img.left, img.right { max-width: 35%; }
img.left { float: left; margin: 0 2rem 0.5rem -1.25rem; }
img.right { float: right; margin: 0 -1.25rem 0.5rem 1rem; }
img.center { display: block; margin: 2rem auto; max-width: 100%; max-height: 80vh; }

figure { margin: 2rem 0; }
figure.center { margin: 2rem auto; text-align: center; max-width: 100%; }
figure.center img { max-height: 80vh; border: 1px solid var(--rule); background: var(--surface); }

.post figure { counter-increment: figure; /* Keep a figure on the same measure as the article text instead of centring a narrow image across the full content width. */ max-width: 68ch; }
.post figure.center { margin-left: 0; margin-right: auto; }

.post figure img, .post > p img.center { border: 1px solid var(--rule); background: var(--surface); }

figcaption { margin-top: 0.5rem; color: var(--muted); font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-size: 0.875rem; letter-spacing: 0.06em; line-height: 1.5; text-align: center; text-transform: uppercase; }

.post figcaption::before { color: var(--muted); content: "FIG. " counter(figure) " / "; }

#map { height: 300px; }

.embed { position: relative; overflow: hidden; margin: 2rem 0; height: 0; padding-bottom: 56.25%; /* 16:9 ratio */ }
.embed.four-three { padding-bottom: 75%; /* 4:3 ratio */ }
.embed iframe, .embed object, .embed embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }

html { background: var(--background); }

body { min-height: 100vh; background-color: var(--background); }

.content { max-width: 1040px; margin: 0 auto; padding: 1.25rem 1.5rem; }

/* HEADER ------------------------------------------------------------------------ */
[role=banner] { position: sticky; top: 0; z-index: 20; background: var(--background); border-bottom: 1px solid var(--line); color: var(--muted); }
[role=banner] .content { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding-top: 1rem; padding-bottom: 1rem; }
[role=banner] nav { display: flex; align-items: center; gap: 0.25rem; flex-shrink: 0; /* The 44px controls centre 16px icons, so the icon sits 14px inside the nav's right edge. Pull the nav out by that inset so the theme icon lines up with the content edge instead of looking indented. */ margin-right: -14px; }

.brand { display: flex; align-items: baseline; gap: 0.5rem; min-width: 0; }

.brand-name { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-size: 0.875rem; font-weight: 500; letter-spacing: 0.08em; line-height: 1.3; text-transform: uppercase; color: var(--color); font-weight: 600; white-space: nowrap; }

.brand-tagline { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-size: 0.875rem; font-weight: 500; letter-spacing: 0.08em; line-height: 1.3; text-transform: uppercase; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* FOOTER ------------------------------------------------------------------------ */
[role=contentinfo] { background: var(--footer-bg); border-top: 3px solid var(--footer-border); color: var(--footer-text); }
[role=contentinfo] .content { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-size: 0.875rem; font-weight: 500; letter-spacing: 0.08em; line-height: 1.3; text-transform: uppercase; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.75rem 1rem; /* The social links are 44x44 hit areas around 16px icons, so the visible icons sit 14px inside their boxes. Compensate here so the visual gap from the footer edge to the icons matches the 24px side padding. */ padding-top: 0.625rem; /* 10px + 14px icon inset = 24px */ padding-bottom: 0.625rem; }

[role=main] { background: transparent; border: 0; }
[role=main] .content { padding-top: 2.5rem; padding-bottom: 2.5rem; }
[role=main]:focus { outline: none; }

.intro { max-width: 48rem; margin: 0; }
.intro .intro-title { max-width: 24ch; margin: 0 0 1.25rem; color: var(--color); font-size: clamp(2.25rem, 5vw, 3.25rem); font-weight: 400; line-height: 1.1; letter-spacing: -0.02em; text-wrap: balance; }
.intro p { color: var(--muted); max-width: 65ch; margin: 0 0 1rem; }

.section-heading { /* Shrink to the heading text so the dimension line below can match its width. */ width: fit-content; max-width: 100%; /* Optical: balance the space from the dimension line down to the text with the space from the text down to the content rule below (14px each). */ margin: 0 0 calc(1rem + 1px); color: var(--color); font-size: 1.75rem; font-weight: 400; line-height: 1.1; letter-spacing: -0.02em; text-wrap: balance; }
.section-heading::before { height: 6px; border: 1px solid var(--rule-strong); border-bottom: 0; display: block; width: 100%; margin-bottom: calc(0.75rem + 3px); content: ""; }

/* PROJECTS ---------------------------------------------------------------------- */
.projects { /* Intro margin (16px) + padding gives the same 48px above the heading line as the other sections. */ padding-top: 2rem; }

/* Card text sits 24px inside a 1px grid edge; match it outside the grid. */
/* The grids bleed into the page gutter by the card inset, keeping the card text on the same reading column as the section headings. */
.project-grid { position: relative; display: grid; grid-template-columns: 1fr; gap: 0; margin-inline: -1.5rem; border-left: 1px solid var(--rule); }
.project-grid::before { position: absolute; top: -2px; right: 0; left: 0; height: 2px; background: var(--rule-strong); content: ""; pointer-events: none; }
.project-grid::after { position: absolute; right: 0; bottom: 0; left: 0; height: 1px; background: var(--rule-strong); content: ""; pointer-events: none; }

@media (min-width: 40rem) { .project-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.project-card { display: flex; flex-direction: column; gap: 0.75rem; background: var(--surface); border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule); padding: 1.5rem calc(1.5rem - 1px); }

.project-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; }

.project-meta { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-size: 0.875rem; font-weight: 500; letter-spacing: 0.08em; line-height: 1.3; text-transform: uppercase; flex-shrink: 0; margin: 0; color: var(--tag-color, currentColor); text-align: right; white-space: nowrap; }

/* Tag colours live on the label itself; the text already names the category. */
[data-tag="shopify"] { --tag-color: var(--tag-shopify); }

[data-tag="open-data"] { --tag-color: var(--tag-open-data); }

[data-tag="iot"] { --tag-color: var(--tag-iot); }

[data-tag="plugin"] { --tag-color: var(--tag-plugin); }

[data-tag="cli"] { --tag-color: var(--tag-cli); }

[data-tag="diagnostics"] { --tag-color: var(--tag-diagnostics); }

[data-tag="contribution"] { --tag-color: var(--tag-contribution); }

[data-tag="civic-tech"] { --tag-color: var(--tag-civic-tech); }

.project-title { margin: 0; font-size: 1.25rem; font-weight: 500; line-height: 1.3; }

.project-description { flex: 1; margin: 0; color: var(--muted); font-size: 0.875rem; line-height: 1.5; }

/* OPEN SOURCE ------------------------------------------------------------------- */
.open-source { margin-top: 3rem; }

.open-source-heading { width: fit-content; max-width: 100%; margin: 0 0 calc(1rem - 1px); color: var(--color); font-size: 1.25rem; font-weight: 500; line-height: 1.3; }
.open-source-heading::before { height: 6px; border: 1px solid var(--rule-strong); border-bottom: 0; display: block; width: 100%; margin-bottom: 0.75rem; content: ""; }

.open-source-grid { position: relative; display: grid; grid-template-columns: 1fr; gap: 0; margin-inline: -1.5rem; border-left: 1px solid var(--rule); }
.open-source-grid::before { position: absolute; top: -2px; right: 0; left: 0; height: 2px; background: var(--rule-strong); content: ""; pointer-events: none; }
.open-source-grid::after { position: absolute; right: 0; bottom: 0; left: 0; height: 1px; background: var(--rule-strong); content: ""; pointer-events: none; }

@media (min-width: 40rem) { .open-source-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 64rem) { .open-source-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.open-source-card { display: flex; flex-direction: column; gap: 0.75rem; padding: 1.5rem calc(1.5rem - 1px); background: var(--surface-muted); border-right: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }

/* Stretch the title link over the whole card so the full box is clickable. */
.project-card.has-link, .open-source-card.has-link { position: relative; }

.project-card.has-link .project-title a::after, .open-source-card.has-link .open-source-title a::after { position: absolute; inset: 0; content: ""; }

.project-card.has-link .project-title a:focus-visible, .open-source-card.has-link .open-source-title a:focus-visible { outline: none; }

/* Keep card hover monochrome: darken the frame and text instead of the title. */
.project-card .project-title a:hover, .project-card .project-title a:focus, .project-card .project-title a:active, .open-source-card .open-source-title a:hover, .open-source-card .open-source-title a:focus, .open-source-card .open-source-title a:active { color: inherit; }

.project-card.has-link:hover, .project-card.has-link:focus-within, .open-source-card.has-link:hover, .open-source-card.has-link:focus-within { box-shadow: inset 0 0 0 1px var(--card-hover); }

.project-card.has-link:hover .project-description, .project-card.has-link:focus-within .project-description, .open-source-card.has-link:hover .open-source-description, .open-source-card.has-link:focus-within .open-source-description { color: var(--color); }

.open-source-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }

.open-source-meta { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-size: 0.875rem; font-weight: 500; letter-spacing: 0.08em; line-height: 1.3; text-transform: uppercase; flex-shrink: 0; margin: 0; color: var(--tag-color, currentColor); text-align: right; white-space: nowrap; }

.open-source-title { margin: 0; font-size: 1rem; font-weight: 500; line-height: 1.3; letter-spacing: -0.02em; }

.open-source-description { margin: 0; color: var(--muted); font-size: 0.875rem; line-height: 1.5; }

/* POSTS ------------------------------------------------------------------------- */
.posts-heading { margin-top: 3rem; /* The post rows' top rule sits on their box edge, while the project grids draw theirs 2px above, so this needs 2px less to look the same. */ margin-bottom: calc(1rem - 1px); }

.posts { margin: 0; }
.posts article { /* Stretch the title link over the whole row so the date and padding are clickable too. The ::before overlay leaves ::after free for the external-link arrow. */ position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: baseline; gap: 1rem; border-top: 1px solid var(--line); padding: 1.5rem 0; }
.posts h3 { grid-column: 1; grid-row: 1; margin: 0; font-size: 1rem; font-weight: 500; line-height: 1.3; }
.posts h3 a::before { position: absolute; inset: 0; content: ""; }
.posts h3 a:hover, .posts h3 a:focus, .posts h3 a:active { color: var(--link); text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 0.15em; }
.posts h3 a[href^="http"]::after { content: "↗" / ""; display: inline-block; margin-left: 0.3em; color: inherit; font-size: 0.85em; opacity: 0.4; }
.posts time { grid-column: 2; grid-row: 1; margin: 0; white-space: nowrap; }
.posts nav[role=navigation] { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1.5rem 0; }
.posts nav a { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-size: 0.875rem; font-weight: 500; letter-spacing: 0.08em; line-height: 1.3; text-transform: uppercase; display: inline-flex; align-items: center; justify-content: center; /* 14px keeps the icon-to-label gap equal to the side padding, with a little more breathing room than the 12px base step. */ gap: 0.875rem; padding: 0.9rem 0.875rem; border: 1px solid var(--rule-strong); background: transparent; color: var(--color); /* Match the 16px icons so icon and text buttons share one height. */ line-height: 1rem; cursor: pointer; text-decoration: none; }
.posts nav a:hover, .posts nav a:focus { border-color: var(--color); background: var(--color); color: var(--background); text-decoration: none; }
.posts nav a:disabled { cursor: default; opacity: 0.6; }
.posts nav a[rel=prev] { margin-right: auto; }
.posts nav a[rel=next] { margin-left: auto; }

/* POST -------------------------------------------------------------------------- */
.post { counter-reset: figure; /* Post actions sit outside the article's grid item, so their margin cannot collapse with the last block's margin. Clear the last margin so the gap above the buttons matches the gap below them. */ }
.post > h1 { padding-bottom: 1rem; border-bottom: 2px solid var(--rule-strong); }
.post > p, .post > ul, .post > ol, .post > h1, .post > h2, .post > h3, .post > h4, .post > h5, .post > h6, .post > hr { max-width: 68ch; }
.post > :last-child { margin-bottom: 0; }

.post-layout { display: grid; /* Grid items default to min-width: auto, which lets a wide child (such as a long code block) push the whole column past the viewport. Let the article shrink and scroll its own overflow instead. */ }
.post-layout > * { min-width: 0; }

/* Post pages follow the actions directly with the post list, so give the list the same 2rem gap the buttons have above them. */
.post-layout + .posts { margin-top: 2rem; }

.post-actions { margin-top: 2rem; }

/* On wide screens, move the post actions into the empty gutter to the right of the 68ch article column and keep them in view while reading. */
@media (min-width: 56rem) { .post-layout { grid-template-columns: minmax(0, 68ch) minmax(12rem, 1fr); gap: 2.5rem; align-items: start; }
  .post-layout .post-actions { position: sticky; top: 6rem; align-self: start; margin-top: 0; }
  .post-layout .post-actions .action-row, .post-layout .post-actions .action-buttons { flex-direction: column; align-items: flex-start; }
  .post-layout .post-actions .action-button { justify-content: flex-start; } }
.not-found p { max-width: 60ch; }
.not-found .not-found-secondary { color: var(--muted); }
.not-found .action-row { margin-top: 1.5rem; }

/* SKIP LINK --------------------------------------------------------------------- */
.skip-link { position: absolute; top: 0.75rem; left: 0.75rem; z-index: 100; padding: 0.5rem 1rem; background: var(--color); color: var(--background); font-size: 0.875rem; font-weight: 600; transform: translateY(-200%); text-decoration: none; }
.skip-link:hover, .skip-link:focus { color: var(--background); text-decoration: none; transform: translateY(0); }
.skip-link:focus-visible { outline-color: var(--background); }

@media (max-width: 30rem) { [role=banner] .content { /* The 44px controls set the minimum height; trim the vertical padding so the sticky header takes less room on small screens. */ padding-top: 0.5rem; padding-bottom: 0.5rem; }
  .brand { flex-direction: column; align-items: flex-start; gap: 0; }
  .brand-tagline { display: none; }
  /* The header already names the site; drop the repeated name from the footer credit so it fits alongside the social links on phones. */
  .copyright-name { display: none; }
  /* Keep the mobile page bottom to the same 24px rhythm as the rest of the layout instead of the desktop 40px. */
  [role=main] .content { padding-bottom: 1.5rem; }
  /* The post rows carry their own bottom padding. When the list has no pagination, drop it from the final row and let the page padding above supply the gap to the footer. */
  .posts > article:last-child { padding-bottom: 0; }
  /* The final row's padding already separates it from the pagination button; give the button the same 24px below it before the footer. */
  .posts nav[role=navigation] { padding-top: 0; padding-bottom: 0; } }
@media (max-width: 22.625rem) { /* Below this width the credit wraps under the social links. The 44px icon boxes already add 14px around the 16px icons, so use a tighter top padding and row gap to keep the visual spacing even all round. */
  [role=contentinfo] .content { gap: 0.375rem 1rem; padding-top: 0.625rem; padding-bottom: 1.25rem; } }
::selection { background: var(--color); color: var(--background); text-shadow: none; }

[role=contentinfo] ::selection { background: var(--footer-link); color: var(--footer-bg); }

/* LINKS ------------------------------------------------------------------------- */
a { color: var(--link); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 0.15em; border: 0; }
a:hover, a:focus, a:active { color: var(--link); text-decoration-thickness: 2px; }

a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

[role=contentinfo] a:focus-visible { outline-color: #fff; }

h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: inherit; border: 0; text-decoration: none; }
h1 a:hover, h1 a:focus, h1 a:active, h2 a:hover, h2 a:focus, h2 a:active, h3 a:hover, h3 a:focus, h3 a:active, h4 a:hover, h4 a:focus, h4 a:active, h5 a:hover, h5 a:focus, h5 a:active, h6 a:hover, h6 a:focus, h6 a:active { color: var(--accent); text-decoration: none; }

[role=contentinfo] a { color: var(--footer-link); }
[role=contentinfo] a:hover, [role=contentinfo] a:focus, [role=contentinfo] a:active { color: #fff; text-decoration: underline; }

.brand-name, .brand-name:hover, .brand-name:focus { color: var(--color); text-decoration: none; }

/* NAVIGATION -------------------------------------------------------------------- */
.nav-link { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-size: 0.875rem; font-weight: 500; letter-spacing: 0.08em; line-height: 1.3; text-transform: uppercase; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; padding: 0 0.25rem; }

/* THEME TOGGLE ------------------------------------------------------------------ */
.theme-toggle { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; padding: 0; border: 0; background: none; color: var(--muted); cursor: pointer; }
.theme-toggle svg { display: block; width: 16px; height: 16px; }
.theme-toggle .sun { display: none; }
.theme-toggle:hover, .theme-toggle:focus, .theme-toggle:active { background: none; color: var(--color); }
.theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

:root[data-theme="dark"] .theme-toggle .moon { display: none; }
:root[data-theme="dark"] .theme-toggle .sun { display: block; }

/* ALERTS ------------------------------------------------------------------------ */
.alert { background: var(--alert-bg); border: 1px solid var(--alert-border); color: var(--alert-text); }
.alert strong { color: var(--alert-strong); }
.alert a { color: var(--alert-text); }
.alert a:hover, .alert a:focus { color: var(--alert-strong); }

/* BUTTONS ----------------------------------------------------------------------- */
button, .button, input[type=submit] { display: inline-flex; align-items: center; justify-content: center; background: #008060; color: #fff; padding: 0.75rem 1.25rem; border: 0; font-family: inherit; font-size: 1rem; font-weight: 600; line-height: 1.1; cursor: pointer; text-decoration: none; }
button:hover, button:focus, .button:hover, .button:focus, input[type=submit]:hover, input[type=submit]:focus { background: #006e52; color: #fff; text-decoration: none; }

/* Quiet action buttons follow the pagination style: mono label, hairline border, and an inverted hover state. The extra vertical padding keeps them comfortable to hit on touch screens. */
.action-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; }

.action-buttons { display: flex; flex-wrap: wrap; gap: 0.75rem; }

.action-button { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-size: 0.875rem; font-weight: 500; letter-spacing: 0.08em; line-height: 1.3; text-transform: uppercase; display: inline-flex; align-items: center; justify-content: center; /* 14px keeps the icon-to-label gap equal to the side padding, with a little more breathing room than the 12px base step. */ gap: 0.875rem; padding: 0.9rem 0.875rem; border: 1px solid var(--rule-strong); background: transparent; color: var(--color); /* Match the 16px icons so icon and text buttons share one height. */ line-height: 1rem; cursor: pointer; text-decoration: none; }
.action-button:hover, .action-button:focus { border-color: var(--color); background: var(--color); color: var(--background); text-decoration: none; }
.action-button:disabled { cursor: default; opacity: 0.6; }
.action-button[aria-busy=true] { cursor: progress; }

.action-icon { flex: none; width: 16px; height: 16px; }

/* Secondary label inside a quiet button, such as the Swedish "Startsida" alongside the English "Home". Opacity keeps it muted in both themes and in the inverted hover state. */
.action-secondary { opacity: 0.55; }

/* Feedback from the post actions is announced by a live region but shown in the button label, so the message does not appear outside the button. */
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* SOCIAL LINKS ------------------------------------------------------------------ */
.links { display: flex; flex-wrap: wrap; gap: 0.25rem; margin: 0 0 0 -14px; /* (44px target - 16px icon) / 2 */ padding: 0; list-style: none; }
.links li { display: inline-block; margin: 0; }
.links a { display: block; width: 44px; height: 44px; border: 0; text-decoration: none; text-indent: -9999em; opacity: 0.5; background-position: center; background-repeat: no-repeat; background-size: 16px 16px; }
.links a:hover, .links a:focus, .links a:active { opacity: 1; }
.links .x { background-image: url("../images/x.svg"); }
.links .linkedin { background-image: url("../images/linkedin.svg"); }
.links .dribbble { background-image: url("../images/dribbble.svg"); }
.links .github { background-image: url("../images/github.svg"); }
.links .instagram { background-image: url("../images/instagram.svg"); }

.flattr { border: 1px solid #82a255; border-top-color: #8fb15f; border-bottom-color: #728f4a; background: #91bc5d; color: #fff; padding: .4em .8em; text-decoration: none; }
.flattr:hover, .flattr:focus { border-color: #728f4a; color: #fff; background: #86ac57; }
