/* =========================================================================
   filipsalo.se — bold editorial redesign
   Token-driven, responsive, light/dark. The layout is a single "breakout"
   grid (.prose): prose sits in a readable measure, while figures and code
   can widen to .wide / .full bands or float into the margin (.side).
   ========================================================================= */

/* ---- Self-hosted fonts (variable, latin subset) ------------------------ */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/static/fonts/inter-latin-wght-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: italic;
  font-weight: 100 900;
  font-display: swap;
  src: url("/static/fonts/inter-latin-wght-italic.woff2") format("woff2");
}
@font-face {
  font-family: "Cascadia Code";
  font-style: normal;
  font-weight: 200 700;
  font-display: swap;
  src: url("/static/fonts/cascadia-code-latin-wght-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Source Serif 4";
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url("/static/fonts/source-serif-4-latin-wght-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Source Serif 4";
  font-style: italic;
  font-weight: 200 900;
  font-display: swap;
  src: url("/static/fonts/source-serif-4-latin-wght-italic.woff2") format("woff2");
}

/* ---- Design tokens ----------------------------------------------------- */
:root {
  color-scheme: dark;

  /* Inter for all text; Cascadia Code reserved for code. */
  --font-display: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-body: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "Cascadia Code", ui-monospace, "SFMono-Regular", Menlo, monospace;
  --font-serif: "Source Serif 4", Georgia, "Times New Roman", serif;

  /* Fluid type scale (1.25 ratio, clamped between mobile and desktop). */
  --step--1: clamp(0.83rem, 0.80rem + 0.15vw, 0.92rem);
  --step-0:  clamp(1.00rem, 0.95rem + 0.25vw, 1.13rem);
  --step-1:  clamp(1.20rem, 1.10rem + 0.50vw, 1.50rem);
  --step-2:  clamp(1.44rem, 1.25rem + 0.95vw, 2.00rem);
  --step-3:  clamp(1.73rem, 1.40rem + 1.65vw, 2.75rem);
  --step-4:  clamp(2.07rem, 1.55rem + 2.60vw, 3.75rem);

  /* Spacing scale */
  --space-2xs: clamp(0.5rem, 0.45rem + 0.25vw, 0.625rem);
  --space-xs:  clamp(0.75rem, 0.68rem + 0.35vw, 0.94rem);
  --space-s:   clamp(1rem, 0.92rem + 0.4vw, 1.25rem);
  --space-m:   clamp(1.5rem, 1.36rem + 0.7vw, 1.9rem);
  --space-l:   clamp(2.25rem, 2rem + 1.25vw, 3rem);
  --space-xl:  clamp(3.5rem, 3rem + 2.5vw, 5rem);

  /* Layout. The post body (.prose) is a 10-column grid built from --colw
     (one column) and --gut (the gutter between columns). --band is the full
     10-column field (what .full figures span and the grid overlay draws); the
     header and footer snap to it so their edges land on columns 1 and 10.
     --measure sizes the list pages (home / archive). */
  --measure: 60ch;
  --gutter: clamp(1.1rem, 4vw, 2.5rem);
  --colw: clamp(0rem, 8vw, 5.75rem);
  --gut: clamp(0.6rem, 1.7vw, 1.3rem);
  --band: calc(10 * var(--colw) + 9 * var(--gut));
  --radius: 12px;
  --radius-s: 8px;

  /* Brand hue drives accents + tag chips so themes recolor in one place. */
  --hue: 210;

  /* Pizzazz gradients: the avatar ring sweeps violet→blue→green→amber and
     the name picks up amber→pink→violet, so the two read as one rainbow. */
  --grad-avatar: linear-gradient(115deg, hsl(268 82% 64%), hsl(210 88% 56%) 35%, hsl(162 70% 48%) 66%, hsl(45 95% 58%));
  --grad-name: linear-gradient(135deg, hsl(28 95% 58%), hsl(330 85% 62%) 55%, hsl(280 80% 64%));
  /* A quieter gradient for post titles: a soft teal→violet shift — two distinct
     colours, but far gentler than the hero name's saturated amber→pink→violet. */
  --grad-title: linear-gradient(135deg, hsl(178 55% 74%), 38%, hsl(268 52% 78%));
  --glow-color: hsl(210 82% 56% / 0.5);

  /* Semantic colors — a single soft dark theme. Lifted ~7% in lightness from
     the old near-black (bg 9%→16%); every layer keeps its relative step up the
     ladder, so the surfaces stay distinct without any jarring jump. */
  --bg:        hsl(228 18% 16%);
  --bg-tint:   hsl(var(--hue) 20% 19%);
  --surface:   hsl(228 16% 20%);
  --surface-2: hsl(228 15% 24%);
  /* Scales the language tint in the home cards. */
  --card-tint-scale: 1;
  --text:      hsl(225 24% 93%);
  --text-soft: hsl(225 16% 80%);
  --muted:     hsl(225 12% 64%);
  --border:    hsl(228 13% 28%);
  --border-strong: hsl(228 12% 37%);
  --accent:    hsl(var(--hue) 90% 76%);
  --accent-strong: hsl(var(--hue) 95% 82%);
  --accent-soft: hsl(var(--hue) 38% 27%);
  --on-accent: hsl(228 25% 10%);

  /* Callout (note) — yellow. A translucent tint reads as yellow over the dark
     surface, where a solid low-lightness amber went muddy/brown. */
  --note-bg:   hsl(48 90% 55% / 0.12);
  --note-border: hsl(48 75% 55% / 0.5);
  --note-text: hsl(48 80% 83%);

  /* Code surface */
  --code-bg:   hsl(228 16% 22%);
  --code-text: hsl(225 22% 86%);

  --shadow: 0 1px 2px hsl(0 0% 0% / 0.3), 0 10px 30px hsl(0 0% 0% / 0.35);
  /* Brighter than the highlight fill, so a selection stays visible over a mark. */
  --selection: hsl(var(--hue) 70% 46%);
  --mark-text:  hsl(210 30% 91%);        /* faint cool tint on highlighted text */
  /* Highlighter-pen border-image source: blue by default, pink for the
     `.pink` accent variant. Each bakes its own colour (border-image can't read
     a token), so the fill is the only thing that differs between them. The
     fills are lifted to track the lighter surface. */
  --mark-edge:      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 48' width='120' height='48'%3E%3Cfilter id='h' x='-4%25' y='-25%25' width='108%25' height='150%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.004 0.195' numOctaves='2' seed='90' result='n'/%3E%3CfeDisplacementMap in='SourceGraphic' in2='n' scale='13' xChannelSelector='R' yChannelSelector='G'/%3E%3C/filter%3E%3Crect x='4' y='13' width='112' height='22' rx='9' fill='hsl(210 85%25 60%25 / 0.16)' filter='url(%23h)'/%3E%3C/svg%3E");
  --mark-edge-pink: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 48' width='120' height='48'%3E%3Cfilter id='h' x='-4%25' y='-25%25' width='108%25' height='150%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.004 0.195' numOctaves='2' seed='90' result='n'/%3E%3CfeDisplacementMap in='SourceGraphic' in2='n' scale='13' xChannelSelector='R' yChannelSelector='G'/%3E%3C/filter%3E%3Crect x='4' y='13' width='112' height='22' rx='9' fill='hsl(335 85%25 60%25 / 0.16)' filter='url(%23h)'/%3E%3C/svg%3E");
}

/* ---- Reset / base ------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html { -webkit-text-size-adjust: 100%; }

body {
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.65;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

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

img, picture, svg, video { display: block; max-width: 100%; }
img { height: auto; }

a {
  color: var(--accent-strong);
  text-decoration-color: color-mix(in oklab, var(--accent) 40%, transparent);
  text-underline-offset: 0.15em;
}
a:hover { text-decoration-color: currentColor; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 3px;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

p, ul, ol, blockquote, pre, figure, table { text-wrap: pretty; }

/* ---- The breakout grid ------------------------------------------------- */
/* Default children sit in [content]; opt into wider bands by class. */
/* The post body is a real 9-column grid: 6 content columns with a 2-column
   margin on the left and a 1-column margin on the right, gutters between.
   Mobile collapses to one full-width column. Figures break asymmetrically
   into the margins: large +1 col right, huge +2 cols left, full both. The
   overlay shares the same --colw / --gut so it draws the exact columns. */
.prose {
  display: grid;
  grid-template-columns: [content-start] minmax(0, 1fr) [content-end];
  padding-inline: var(--gutter);
}
.prose > * { grid-column: content; }

@media (min-width: 52rem) {
  .prose {
    padding-inline: 0;
    grid-template-columns:
      [bleed-start] minmax(var(--gutter), 1fr)
      [c1] var(--colw) var(--gut)
      [wide-start] var(--colw) var(--gut)
      [content-start] var(--colw) var(--gut)
      var(--colw) var(--gut)
      var(--colw) var(--gut)
      var(--colw) var(--gut)
      var(--colw) var(--gut)
      var(--colw) [content-end]
      var(--gut) var(--colw) [wide-end]
      var(--gut) var(--colw) [edge-end bleed-end]
      minmax(var(--gutter), 1fr);
  }
  /* Centered, two columns wider than the text on each side. */
  .prose > .wide,
  .prose > figure.large,
  .prose > pre.large { grid-column: wide-start / wide-end; }
  .prose > .full,  .prose > figure.full { grid-column: c1 / edge-end; }

  /* On wide figures the image breaks out, but the caption stays within the
     regular text measure (the 6-column content width), centered under it. */
  .prose > .wide > figcaption,
  .prose > figure.large > figcaption,
  .prose > .full > figcaption,
  .prose > figure.full > figcaption {
    max-width: calc(6 * var(--colw) + 5 * var(--gut));
    margin-inline: auto;
  }
}

/* Vertical rhythm */
.prose > * + * { margin-top: var(--space-s); }
.prose > h2 { margin-top: var(--space-l); }
.prose > h3, .prose > h4 { margin-top: var(--space-m); }
.prose > h2 + * { margin-top: var(--space-xs); }
.prose > h3 + *, .prose > h4 + * { margin-top: var(--space-2xs); }

/* ---- Page frame: header, main, footer ---------------------------------- */
.site-header { padding-inline: var(--gutter); }

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
  flex-wrap: wrap;
  /* Snap to the grid band: content box = --band, so the home icon and nav
     cluster land on columns 1 and 10. (padding-inline adds the gutter back.) */
  max-width: calc(var(--band) + 2 * var(--gutter));
  margin-inline: auto;
  width: 100%;
  padding-block: var(--space-m);
}

/* Home icon (replaces the old wordmark), styled like the nav buttons. */
.home-link {
  display: inline-grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  color: var(--muted);
  background: transparent;
  border: 1px solid transparent;
  transition: color 0.2s, background 0.2s, border-color 0.2s;
}
.home-link:hover { color: var(--accent); background: var(--surface); border-color: var(--border); }
.home-link svg { width: 22px; height: 22px; }
body.home .home-link { display: none; }   /* already home — no need for the link */

/* Tight social/nav cluster (matches the filipsalo.se top bar). */
.site-nav {
  display: flex;
  align-items: center;
  gap: 0.15rem;
  margin-left: auto;   /* stay right even when the home link is hidden */
}
.site-nav a {
  display: inline-grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  color: var(--muted);
  background: transparent;
  border: 1px solid transparent;
  cursor: pointer;
  transition: color 0.2s, background 0.2s, border-color 0.2s;
}
.site-nav a:hover {
  color: var(--accent);
  background: var(--surface);
  border-color: var(--border);
}
/* Each social icon lights up in its own brand color on hover.
   (GitHub's brand is monochrome, so it goes full-contrast.) */
.site-nav a[aria-label="GitHub"]:hover   { color: var(--text); }
.site-nav a[aria-label="Codeberg"]:hover { color: #4793d8; }
.site-nav a[aria-label="Mastodon"]:hover { color: #6364ff; }
.site-nav a[aria-label="Bluesky"]:hover  { color: #1185fe; }
.site-nav a[aria-label="LinkedIn"]:hover { color: #0a66c2; }
.site-nav a[aria-label="Instagram"]:hover { color: #e1306c; }
.site-nav svg { width: 20px; height: 20px; }

main { flex: 1; padding-block: var(--space-l) var(--space-xl); position: relative; }

/* ---- Layout grid overlay (opt-in via `grid: true` front matter) -------- */
.grid-overlay {
  position: fixed;          /* full viewport, so the columns run behind the
                               header and footer too — not just the article */
  inset: 0;
  z-index: 5;
  pointer-events: none;
  visibility: hidden;
}
body.grid-on .grid-overlay { visibility: visible; }
/* The 10 columns (tinted) with transparent gutters between them, centered to
   match .prose's centered 10-column group. */
.grid-overlay::before {
  content: "";
  display: block;
  height: 100%;
  width: var(--band);
  max-width: calc(100% - 2 * var(--gutter));
  margin-inline: auto;
  background: repeating-linear-gradient(to right,
    hsl(var(--hue) 85% 55% / 0.11) 0, hsl(var(--hue) 85% 55% / 0.11) var(--colw),
    transparent var(--colw), transparent calc(var(--colw) + var(--gut)));
}

.grid-toggle {
  position: fixed;
  bottom: 1rem;
  right: 1rem;
  z-index: 6;
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font: 600 var(--step--1) / 1 var(--font-body);
  padding: 0.55rem 0.9rem;
  border-radius: 999px;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
  cursor: pointer;
}
.grid-toggle[aria-pressed="true"] { background: var(--accent); color: var(--on-accent); border-color: transparent; }
.grid-toggle kbd {
  font: inherit;
  opacity: 0.6;
  border: 1px solid currentColor;
  border-radius: 4px;
  padding: 0 0.3em;
}

.site-footer {
  /* Inset band: a fill darker than the page so the content above reads as
     sinking into a recessed well. On a dark fill a dark inner shadow has no
     contrast to work with, so the top lip is defined by a light catch-light
     (border + 1px inset highlight); the soft inner shadow just deepens it. */
  border-top: 1px solid hsl(228 16% 19%);
  background: hsl(228 22% 9%);
  color: var(--muted);
  padding-block: var(--space-m);
  box-shadow:
    inset 0 1px 0 hsl(0 0% 100% / 0.05),
    inset 0 6px 11px -4px hsl(0 0% 0% / 0.4);
}
/* Mirror the header frame exactly: a full-width bar whose inner content snaps
   to the same grid band as .site-header, so the two line up edge to edge. */
.site-footer .inner {
  max-width: calc(var(--band) + 2 * var(--gutter));
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
}
.site-footer p { font-size: var(--step--1); }
.site-footer a { color: var(--text-soft); }

/* Footer icon links (archive, email), styled like the header nav buttons. */
.footer-links { display: flex; align-items: center; gap: 0.15rem; }
.footer-links a {
  display: inline-grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 50%;
  color: var(--muted);
  border: 1px solid transparent;
  transition: color 0.2s, background 0.2s, border-color 0.2s;
}
.footer-links a:hover { color: var(--accent); background: var(--surface); border-color: var(--border); }
.footer-links svg { width: 18px; height: 18px; }

/* ---- Heading sizes (global; components may override) ------------------- */
h1, .post-title { font-size: var(--step-4); font-weight: 800; letter-spacing: -0.035em; }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }
h5, h6 { font-size: var(--step-0); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-soft); }

/* Centered single-column container for list pages (home, archive). */
.page {
  max-width: calc(var(--measure) + 2 * var(--colw));
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.page > * + * { margin-top: var(--space-s); }
.page.home { max-width: 56rem; }   /* wider, to give the three promo cards room */
.page.home > .card-group,
.page.home > .home-foot { margin-top: clamp(2rem, 5vw, 3rem); }
/* Tools index: same generous width as home so the cards have room. */
.page.tools { max-width: 56rem; }
.page.tools > .card-group { margin-top: clamp(2rem, 5vw, 3rem); }
.tools-head .lead { margin-top: 0.4rem; }

/* ---- Lead paragraph ---------------------------------------------------- */
.intro, .prose > .intro {
  font-family: var(--font-display);
  font-size: var(--step-1);
  line-height: 1.4;
  color: var(--text-soft);
  font-weight: 400;
}

/* ---- Inline elements --------------------------------------------------- */
strong, b { font-weight: 650; }
/* Highlighter pen: a turbulence-roughened rounded rect (the themed --mark-edge
   source) used as a border-image. The fixed corners keep the ends rough AND
   covered at any width, while the long top/bottom strokes stretch across the
   body — so long marks don't fray or repeat. Honors box-decoration-break, so
   every line-wrapped fragment gets its own rough ends. */
mark {
  color: hsl(210 80% 88%); /* blue text, close to white, matching the fill */
  background: transparent; /* clear the UA mark yellow; the border-image is the fill */
  font-weight: 650;
  padding: 0.55em 0.55em;
  margin: 0 -0.35em;
  border: 0 solid transparent;
  border-image-source: var(--mark-edge);
  border-image-slice: 15 24 fill;
  border-image-width: 15px 24px;
  border-image-repeat: stretch;
  border-image-outset: 2px 3px;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
/* Hotter accent variant — <mark class="pink">…</mark>. Pink text to match the
   pink fill, rather than inheriting the blue. */
mark.pink {
  border-image-source: var(--mark-edge-pink);
  color: hsl(335 80% 88%);
}
small { font-size: var(--step--1); }

code {
  font-family: var(--font-mono);
  font-size: 0.88em;
  background: var(--accent-soft);
  padding: 0.15em 0.4em;
  border-radius: 5px;
}

/* ---- Lists ------------------------------------------------------------- */
.prose ul, .prose ol { padding-inline-start: 1.4em; }
.prose li + li { margin-top: 0.35em; }
.prose li::marker { color: var(--accent); }

/* ---- Blockquote -------------------------------------------------------- */
blockquote {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: var(--step-0);
  line-height: 1.55;
  color: var(--text-soft);
  border-inline-start: 3px solid var(--accent);
  padding-inline-start: var(--space-s);
}

/* ---- Code blocks ------------------------------------------------------- */
pre {
  font-family: var(--font-mono);
  background: var(--code-bg);
  color: var(--code-text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-s) var(--space-m);
  overflow-x: auto;
  font-size: var(--step--1);
  line-height: 1.6;
  -webkit-overflow-scrolling: touch;
}
pre code { background: none; padding: 0; font-size: inherit; }

/* Language marker on fenced code blocks — a small corner tab echoing the home
   cards' language badge. The wrapper (added by JS) doesn't scroll, so the tab
   stays pinned while the <pre> scrolls horizontally beneath it. */
.code-wrap { position: relative; }
.code-wrap::before {
  content: attr(data-lang);
  position: absolute;
  top: 1px;
  right: 1px;
  z-index: 1;
  font-family: var(--font-body);
  font-size: 0.64rem;
  font-weight: 650;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--lang-color, var(--accent-strong));
  background: color-mix(in oklab, var(--lang-color, var(--accent)) 20%, var(--surface-2));
  padding: 0.3em 0.75em;
  border-radius: 0 calc(var(--radius) - 1px) 0 0.6rem;   /* follow the pre's rounded top-right; round the inner corner */
  pointer-events: none;
}
/* Per-language tints, echoing the home cards' brand colours; others fall back
   to the site accent. */
.code-wrap[data-lang="python"]     { --lang-color: hsl(207 82% 70%); }
.code-wrap[data-lang="go"]         { --lang-color: hsl(192 78% 62%); }
.code-wrap[data-lang="shell"],
.code-wrap[data-lang="bash"]       { --lang-color: hsl(142 48% 64%); }
.code-wrap[data-lang="sql"]        { --lang-color: hsl(265 72% 78%); }
.code-wrap[data-lang="css"]        { --lang-color: hsl(286 65% 75%); }
.code-wrap[data-lang="c"]          { --lang-color: hsl(210 30% 74%); }
.code-wrap[data-lang="json"]       { --lang-color: hsl(40 80% 68%); }
.code-wrap[data-lang="javascript"] { --lang-color: hsl(48 85% 66%); }

/* ---- Figures & the flexible widths ------------------------------------ */
figure { margin-block: var(--space-s); }
figure img { width: 100%; border-radius: var(--radius-s); }
figcaption {
  margin-top: 0.6em;
  font-size: var(--step--1);
  color: var(--muted);
}

/* Divisible figures sit side by side, stacking on small screens. */
figure.divisible { display: flex; flex-wrap: wrap; gap: var(--space-s); }
figure.divisible > figure { flex: 1 1 12rem; margin: 0; }
figure.divisible > figure.two { flex: 2 1 20rem; }

/* Small figures / margin notes: sit in the margin column beside the text on
   wide screens (left by default, right with .right). Stack inline on narrow
   screens. Only .small / .side opt in, so full-width callouts (.note) are
   unaffected. */
.prose > .small,
.prose > .side { grid-column: content; margin-block: var(--space-s); }
@media (min-width: 52rem) {
  /* Place them in the margin column rather than the content column. Grid items
     can't float (what the old rule attempted), so a margin note in the content
     column reserved — and blanked out — a whole content row. Living in the
     margin column lets the body text keep flowing full-width beside it. */
  .prose > .small,
  .prose > .side {
    grid-column: c1 / content-start;      /* the 2-col left margin */
    align-self: start;
    /* Match the body's block spacing on top so the note's first line lines up
       with the adjacent paragraph (whose --space-s top margin would otherwise
       leave the note riding ~20px high in the shared grid row). */
    margin-block: var(--space-s) 0;
    margin-right: var(--gut);             /* keep the content-facing gutter clear */
    /* A note taller than the body element it shares a grid row with would
       stretch that row, opening a gap in the content column beside it. Drop its
       height contribution to zero and let it overflow down into the (empty)
       margin, so the body keeps its natural spacing. */
    max-height: 0;
    overflow: visible;
  }
  .prose > .small.right,
  .prose > .side.right {
    grid-column: content-end / edge-end;  /* the 2-col right margin */
    margin-right: 0;
    margin-left: var(--gut);              /* keep the content-facing gutter clear */
  }
}
/* On narrow screens the aside stacks inline in the body; a faint bordered card
   lifts it off the page so it reads as an aside rather than a stray paragraph.
   On wide screens it lives borderless in the margin (above), so scope to narrow. */
@media (max-width: 52rem) {
  .prose > .side {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-s);
    padding: var(--space-xs) var(--space-s);
  }
}
aside {
  font-size: var(--step--1);
  color: var(--text-soft);
}
aside blockquote {
  font-size: var(--step-0);
  border-inline-start-width: 2px;
}

/* ---- Note callout ------------------------------------------------------ */
.note {
  background: var(--note-bg);
  border: 1px solid var(--note-border);
  color: var(--note-text);
  border-radius: var(--radius);
  padding: var(--space-s) var(--space-m);
}
.note h4 {
  font-family: var(--font-body);
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 0.3em;
}
.note p { margin: 0; }

/* ---- Tables ------------------------------------------------------------ */
.prose table { border-collapse: collapse; width: 100%; font-size: var(--step--1); }
.prose th, .prose td { text-align: left; padding: 0.5em 0.75em; border-bottom: 1px solid var(--border); }
.prose th { font-weight: 650; }

/* ---- Post header / metadata ------------------------------------------- */
.post-title { margin-bottom: var(--space-s); }
/* Keep the date tucked under the title on post pages, and pull the title up
   close to the tag kicker above it (drop the heading's own top margin). The
   bottom padding extends the gradient's paint box so last-line descenders
   aren't clipped; the negative margin keeps the lead's position unchanged. */
.post .post-title { margin-top: 0; padding-bottom: 0.12em; margin-bottom: -0.12em; }
/* Let the title run one grid column wider than the body, into the right
   margin, while its left edge stays aligned with the text below. */
@media (min-width: 52rem) {
  .prose > .post-title { grid-column: content-start / wide-end; }
}
/* A soft teal→violet gradient on the post title, far quieter than the hero
   name's colour gradient. Falls back to solid --text. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .post-title {
    background: var(--grad-title);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
  /* The gradient's fill is transparent, so selected glyphs would vanish into a
     solid block of selection colour. Restore a visible fill while selected. */
  .post-title::selection {
    -webkit-text-fill-color: var(--text);
    color: var(--text);
  }
}
/* Date + reading time (the byline lives at the end now). */
.post-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.3rem;
  color: var(--muted);
  font-size: var(--step--1);
  margin-top: var(--space-2xs);
  margin-bottom: var(--space-2xs);
}
.post-meta .post-when { color: var(--muted); display: inline-flex; align-items: center; }
.post-meta time { color: var(--muted); font-weight: 400; }
.post-meta .reading { margin-left: 0.7em; display: inline-flex; align-items: center; }   /* a little space before the clock icon */
/* Dim calendar / clock icon, rendered inline before each line. */
.post-meta .meta-icon {
  display: inline-block;
  width: 15px;
  height: 15px;
  color: var(--muted);
}
.post-meta .post-when .meta-icon { margin-right: 0.4em; }

/* Summary rendered as a standfirst between the title and the date — larger and
   lighter than body text, echoing the home hero's lead. The date (.post-meta)
   tucks just under it via its own small top margin. */
.post-lead {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: var(--step-1);
  line-height: 1.45;
  color: var(--text-soft);
  margin-top: var(--space-s);
  margin-bottom: 0;
}

/* End-of-article block: a compact author byline beside a rail of related
   posts. Stacks on narrow screens; sits side by side (byline | related) on
   wide ones, broken out by the byline's own padding so the row frames the
   text rather than nesting inside it. */
.post-end { margin-top: var(--space-xl); }
/* Side by side only when a related rail is present; an untagged post (no rail)
   keeps just the compact byline rather than stretching across an empty grid. */
@media (min-width: 52rem) {
  .post-end:has(.related) {
    grid-column: wide-start / wide-end;   /* two columns wider than the text, centered */
    display: grid;
    grid-template-columns: minmax(0, 21rem) minmax(0, 1fr);
    gap: var(--space-l);
    align-items: start;   /* let the byline hug its content, not stretch tall */
  }
  .post-end:has(.related) .post-byline { width: auto; }
}

/* The byline: a compact card echoing the home hero (gradient ring + name).
   fit-content keeps it hugging its contents when it stands alone; in the
   related-rail grid it's widened back to fill its column (above). */
.post-byline {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  width: fit-content;
  padding: var(--space-s);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: var(--shadow);
}
/* Smaller version of the home .avatar ring; tame the glow inside the card. */
.byline-avatar { padding: 3px; }
.byline-avatar::before { inset: -9px; filter: blur(8px); }
.post-byline .byline-photo {
  position: relative;
  z-index: 1;
  width: 72px; height: 72px;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid var(--surface);   /* gap between ring and photo */
}
.post-byline .byline { display: flex; flex-direction: column; gap: 0.1em; min-width: 0; }
.post-byline .author {
  width: fit-content;
  font-weight: 700;
  font-size: var(--step-1);
  letter-spacing: -0.02em;
  color: var(--text);
}
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .post-byline .author {
    background: var(--grad-name);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}
.post-byline .byline-role { color: var(--accent-strong); font-weight: 600; font-size: var(--step--1); }
/* Contact icons, tucked under the name/role inside the byline. */
.byline-socials { display: flex; gap: 0.75rem; margin-top: 0.5em; }
.byline-socials a { display: inline-flex; color: var(--muted); transition: color 0.15s; }
.byline-socials a:hover { color: var(--accent-strong); }
.byline-socials svg { width: 20px; height: 20px; display: block; }

/* Related-posts rail beside the byline: "More posts about <topic>" + links. */
.related { min-width: 0; }
.related-topic {
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--muted);
  margin-bottom: var(--space-s);
}
/* Topic/tag mark — used for the related-rail topic and the post-header tags.
   Tags are cased manually in front matter (so DNS, macOS, step3 read right),
   so no text-transform here. */
.related-tag {
  letter-spacing: 0;
  font-weight: 700;
}
/* Post-header tags: a row of marked tags sitting above the title as a
   category/kicker, a touch larger than body text. */
.post-tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4em 0.9em;
  margin-top: 0;
  margin-bottom: var(--space-2xs);
  font-size: var(--step-0);
}
.post-tags mark { margin-inline: 0; }   /* gap spaces them; drop the bleed-in margin */
/* The rail reuses the front-page .entry / .entry-title / .entry-date styling
   for consistency. Only the list reset is scoped here, above .prose's ul/li
   rules, so it doesn't inherit the article-list indent and inter-item margin. */
.related .related-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--space-s); }
.related .related-list .entry { margin-top: 0; }
/* Fallback when a post has no tagged siblings: the latest post + an archive
   link. The link sits a step below the post above it. */
.related-archive {
  display: inline-block;
  margin-top: var(--space-s);
  font-weight: 600;
  color: var(--text-soft);
  text-decoration: underline;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 0.15em;
  transition: color 0.15s;
}
.related-archive:hover { color: var(--accent-strong); text-decoration-color: currentColor; }
@media (max-width: 36rem) {
  .post-byline .byline { flex: 1 1 60%; }   /* name/role/socials fill beside the avatar */
}

/* ---- Home: hero --------------------------------------------------------- */
.hero {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: var(--space-l);
  align-items: start;
  margin-bottom: var(--space-l);
}
/* Avatar in column 1; name, role, lead, history stack in column 2, so the
   text below the name/role lines up with them. */
.hero > .avatar { grid-column: 1; grid-row: 1 / span 4; }
.hero-name     { grid-column: 2; grid-row: 1; }
.hero .role    { grid-column: 2; grid-row: 2; }
.hero .lead    { grid-column: 2; grid-row: 3; }
.hero .history { grid-column: 2; grid-row: 4; }
.avatar {
  position: relative;
  flex: none;
  border-radius: 50%;
  padding: 4px;                 /* gradient ring thickness */
  background: var(--grad-avatar);
  line-height: 0;
}
.avatar::before {               /* soft glow behind the photo */
  content: "";
  position: absolute;
  inset: -16px;
  z-index: 0;
  border-radius: 50%;
  background: radial-gradient(closest-side, var(--glow-color), transparent 72%);
  filter: blur(10px);
}
.hero-photo {
  position: relative;
  z-index: 1;
  display: block;
  width: clamp(112px, 26vw, 200px);
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 50%;
  border: 3px solid var(--surface);   /* gap between ring and photo */
}
.hero-name { font-size: var(--step-4); letter-spacing: -0.035em; width: fit-content; max-width: 100%; }
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .hero-name {
    background: var(--grad-name);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}
.role {
  margin-top: 0.35rem;
  color: var(--accent-strong);
  font-weight: 600;
  font-size: var(--step-1);
}
.history {
  margin-top: var(--space-s);   /* match the space above the lead */
  max-width: 40rem;
  color: var(--muted);
  font-size: var(--step--1);
  line-height: 1.5;
}
.history b { color: var(--text-soft); font-weight: 650; }
@media (max-width: 36rem) {
  /* Smaller avatar with name + role beside it; lead + history drop to full
     width underneath, left-aligned under the avatar. */
  .hero { column-gap: var(--space-s); }
  .hero-photo { width: clamp(80px, 24vw, 104px); }
  .hero > .avatar { grid-row: 1 / span 2; }
  .hero .lead,
  .hero .history { grid-column: 1 / -1; }
}

/* Lead paragraph under the home hero. */
.lead {
  margin-top: var(--space-s);
  font-size: var(--step-1);
  line-height: 1.5;
  color: var(--text-soft);
  max-width: 42rem;
}

/* Promo cards, grouped under a shared heading instead of a per-card kind. */
.card-group { margin-top: clamp(2rem, 5vw, 3rem); }
.cards-heading {
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--accent-strong);
  margin-bottom: var(--space-xs);
  margin-left: 1.4rem;   /* align with the text inside the cards (matches .card padding-inline) */
}
.cards {
  display: grid;
  grid-template-columns: 1fr;          /* stacked on phones / small tablets */
  gap: clamp(0.85rem, 1.5vw, 1.25rem);
  align-items: stretch;                /* cards in a row share the tallest card's height */
}
@media (min-width: 48rem) {
  /* Reach past the text column toward the page margins (heading + cards
     together) so the three cards keep gaining width as the window grows.
     Collapses to 0 where there's no spare margin. */
  .card-group {
    --reach: clamp(0rem, (100vw - 56rem) / 2 - 1.5rem, 7rem);
    margin-inline: calc(-1 * var(--reach));
  }
  .cards { grid-template-columns: repeat(3, 1fr); }
  .cards.cards-pair { grid-template-columns: repeat(2, 1fr); }   /* two tees, half-width each */
}
.card {
  position: relative;
  overflow: hidden;
  display: block;
  /* fill (padding box) + edge (hover border). Gradients so the multi-layer
     hover background below stays valid even with no language tint. */
  --card-fill: linear-gradient(var(--surface), var(--surface));
  --card-edge: var(--grad-avatar);
  background: var(--card-fill);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 1.25rem 1.4rem;
  text-decoration: none;
  color: inherit;
  transition: transform .15s, border-color .15s, box-shadow .15s;
}
/* Project cards stack as a column so the Code/Docs links can sit at the bottom. */
.cards .card { display: flex; flex-direction: column; }
/* Faint glow in the card + a matching hover border, in the language's colour. */
.card:has(.lang-go)     { --card-fill: radial-gradient(135% 125% at 100% 0%, color-mix(in oklab, #00add8 calc(24% * var(--card-tint-scale)), var(--surface)), var(--surface) 64%); --card-edge: linear-gradient(135deg, hsl(192 85% 55%), hsl(193 92% 38%)); }
.card:has(.lang-swift)  { --card-fill: radial-gradient(135% 125% at 100% 0%, color-mix(in oklab, #f05138 calc(20% * var(--card-tint-scale)), var(--surface)), var(--surface) 64%); --card-edge: linear-gradient(135deg, hsl(11 95% 66%), hsl(6 82% 52%)); }
.card:has(.lang-python) { --card-fill: radial-gradient(135% 125% at 100% 0%, color-mix(in oklab, #3776ab calc(24% * var(--card-tint-scale)), var(--surface)), var(--surface) 64%); --card-edge: linear-gradient(135deg, hsl(207 78% 62%), hsl(207 58% 42%)); }
.card:has(.lang-kod)    { --card-fill: radial-gradient(135% 125% at 100% 0%, color-mix(in oklab, #7c5cff calc(22% * var(--card-tint-scale)), var(--surface)), var(--surface) 64%); --card-edge: linear-gradient(135deg, hsl(256 90% 72%), hsl(264 76% 54%)); }
.card:has(.lang-c)      { --card-fill: radial-gradient(135% 125% at 100% 0%, color-mix(in oklab, #a8b9cc calc(22% * var(--card-tint-scale)), var(--surface)), var(--surface) 64%); --card-edge: linear-gradient(135deg, hsl(210 32% 70%), hsl(214 26% 52%)); }
.card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow);
  border-color: transparent;
  /* gradient border: the card fill stays in the padding box, the edge gradient
     shows through the (transparent) border, respecting the rounded corners */
  background:
    var(--card-fill) padding-box,
    var(--card-edge) border-box;
}
/* Faint watermark icon peeking from the bottom-right corner. */
.card-icon {
  position: absolute;
  bottom: -0.5rem;
  right: -0.5rem;
  width: 4.5rem;
  height: 4.5rem;
  color: var(--text);
  opacity: 0.07;
  pointer-events: none;
}
.card h3 { font-size: var(--step-1); font-weight: 700; }
/* Language badge in the top-right corner, in each language's brand colors. */
.card .lang {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 1;
  font-size: 0.68rem;
  font-weight: 650;
  letter-spacing: 0.02em;
  line-height: 1.4;
  padding: 0.3em 0.9em;
  border-radius: 0 0 0 0.7rem;   /* flush to the corner; round only the inner (bottom-left) corner */
}
.lang-go     { background: linear-gradient(135deg, hsl(192 92% 31%), hsl(194 97% 23%)); color: #fff; }
.lang-swift  { background: linear-gradient(135deg, hsl(11 92% 64%), hsl(6 82% 51%)); color: #fff; }
.lang-python { background: linear-gradient(135deg, hsl(207 52% 53%), hsl(207 54% 37%)); color: #fff; }
.lang-kod    { background: linear-gradient(135deg, hsl(258 64% 56%), hsl(266 68% 44%)); color: #fff; }
.lang-c      { background: linear-gradient(135deg, hsl(212 22% 48%), hsl(216 26% 36%)); color: #fff; }
.card p { margin-top: .4rem; color: var(--text-soft); font-size: var(--step--1); line-height: 1.5; }
/* Code/Docs links: equal-width columns filling the card, icon + left-aligned text. */
/* Equal-width columns sized to the widest link (not stretched across the card),
   flowing as a compact inline-ish block. */
.pypi-badge { line-height: 0; }
.pypi-badge img { height: 20px; width: auto; display: block; }
.card-links {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  width: max-content;
  max-width: 100%;
  align-items: center;
  gap: 0.8rem;
  margin-top: auto;        /* push to the bottom of the (flex-column) card */
  padding-top: 0.6rem;     /* min gap from the text when the card isn't tall */
  font-size: var(--step--1);
}
.card-links a { display: flex; align-items: center; gap: 0.4em; }
/* With a PyPI badge present, Code/Docs pack on the left and the badge sits right. */
.card-links:has(.pypi-badge) { display: flex; width: 100%; }
.card-links .pypi-badge { margin-left: auto; }
.card-links .link-icon { width: 14px; height: 14px; flex: none; }

/* Apparel: each tee is its own card; the image is cropped to its top half. */
.card.tee { padding: 0; }
.card.tee img {
  width: 100%;
  display: block;
  aspect-ratio: 2 / 1;          /* top half of the square 480x480 source */
  object-fit: cover;
  object-position: top;
}

/* ---- Home: recent posts (toned-down, secondary list) ------------------- */
.section-label {
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  font-weight: 600;
  padding-bottom: var(--space-2xs);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-2xs);
}
/* "Latest posts" heading: title left, feed icon right-aligned. */
.recent-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.feed-link {
  display: inline-flex;
  color: var(--muted);
  transition: color 0.2s;
}
.feed-link:hover { color: var(--accent-strong); }
.feed-link svg { width: 16px; height: 16px; display: block; }
/* No dividers — spacing comes from the gap between entries. */
.entries { list-style: none; padding: 0; display: flex; flex-direction: column; gap: var(--space-s); }
.entry-row { line-height: 1.35; }
.entry-title {
  font-weight: 600;
  color: var(--text);
  text-decoration: underline;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 0.15em;
}
.entry-title:hover { color: var(--accent-strong); text-decoration-color: currentColor; }
/* Date sits inline, right after the title. */
.entry-date { margin-left: 0.55em; color: var(--muted); font-size: var(--step--1); font-variant-numeric: tabular-nums; }
.entry-summary { margin-top: 0.3rem; color: var(--text-soft); font-size: var(--step--1); line-height: 1.5; max-width: 62ch; }

.more { margin-top: var(--space-m); font-size: var(--step-0); font-weight: 600; }
.more a { color: var(--accent-strong); text-decoration: none; }
.more a:hover { text-decoration: underline; }

/* Home bottom: apparel boxes stacked in a column beside the latest-posts list. */
.home-foot { display: grid; gap: var(--space-l); }
@media (min-width: 44rem) {
  .home-foot {
    grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
    gap: clamp(1.5rem, 4vw, 3rem);
    align-items: start;
  }
}
.home-foot .recent { margin-top: 0; }
.apparel-stack {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(0.85rem, 1.5vw, 1.25rem);
}

/* ---- Archive ----------------------------------------------------------- */
.archive-month {
  font-size: var(--step-1);
  color: var(--muted);
  font-weight: 600;
  margin-top: var(--space-l);
  margin-bottom: var(--space-xs);
  padding-bottom: 0.3em;
  border-bottom: 1px solid var(--border);
}
.archive-list { list-style: none; padding: 0; }
.archive-list li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-s);
  padding: 0.3rem 0;
}
.archive-entry { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4em 0.6em; min-width: 0; }
.archive-list a { color: var(--text); text-decoration: none; }
.archive-list a:hover { color: var(--accent-strong); text-decoration: underline; }
.archive-date { flex: none; font-size: var(--step--1); color: var(--muted); font-variant-numeric: tabular-nums; }

/* ---- Experience timeline ----------------------------------------------- */
.experience { max-width: 52rem; }
.timeline {
  position: relative;
  list-style: none;
  margin-top: var(--space-l);
  padding: 0;
}
/* The thread running through the markers — a vertical take on the brand rainbow. */
.timeline::before {
  content: "";
  position: absolute;
  left: 6px;
  top: 0.55rem;
  bottom: 0.55rem;
  width: 2px;
  border-radius: 2px;
  background: linear-gradient(to bottom,
    hsl(268 82% 64%), hsl(210 88% 56%) 33%, hsl(162 70% 48%) 66%, hsl(45 95% 58%));
}
.tl-item {
  position: relative;
  padding-left: 2.1rem;
  padding-bottom: var(--space-xl);
  display: grid;
  gap: var(--space-s) var(--space-xl);
}
.tl-item:last-child { padding-bottom: var(--space-2xs); }
/* The node sitting on the line. */
.tl-item::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.4rem;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--bg);
  border: 3px solid var(--accent);
  box-shadow: 0 0 0 3px var(--bg);   /* mask the line cleanly behind the node */
  z-index: 1;
}
@media (min-width: 46rem) {
  .tl-item { grid-template-columns: minmax(0, 1fr) minmax(11rem, 15rem); align-items: start; }
}
.tl-date {
  font-size: var(--step--1);
  font-weight: 650;
  letter-spacing: 0.02em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.tl-role { font-size: var(--step-1); margin: 0.15em 0 0.4em; }
.tl-role .tl-org { color: var(--accent-strong); }
.tl-main > p { color: var(--text-soft); max-width: 46ch; }
.tl-aside { font-size: var(--step--1); color: var(--text-soft); line-height: 1.5; }
.tl-aside-label {
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent-strong);
  margin-bottom: 0.55em;
}
.tl-aside ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.45em; }
.tl-aside li { position: relative; padding-left: 1.05em; }
.tl-aside li::before { content: "\2192"; position: absolute; left: 0; color: var(--accent); }

/* ---- Syntax highlighting (highlight.js tokens, theme-aware) ----------- */
.hljs-comment, .hljs-quote { color: var(--muted); font-style: italic; }
.hljs-keyword, .hljs-selector-tag, .hljs-meta .hljs-keyword, .hljs-doctag, .hljs-section {
  color: hsl(var(--hue) 70% 60%);
}
.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta .hljs-string {
  color: hsl(150 50% 65%);
}
.hljs-number, .hljs-literal, .hljs-variable, .hljs-template-variable, .hljs-tag .hljs-attr {
  color: hsl(28 85% 68%);
}
.hljs-title, .hljs-title.function_, .hljs-name, .hljs-selector-id, .hljs-selector-class {
  color: hsl(210 85% 72%);
}
.hljs-built_in, .hljs-class .hljs-title, .hljs-type { color: hsl(190 70% 65%); }
.hljs-symbol, .hljs-bullet, .hljs-link { color: hsl(330 70% 58%); }
.hljs-emphasis { font-style: italic; }
.hljs-strong { font-weight: 700; }
.hljs-deletion { color: hsl(0 70% 55%); }

/* ---- Motion ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}
