/* ---------------------------------------------------------------------------
   Theme tokens injected from content/site.yaml at build time.
   Re-skinning the site is a config change, not a redesign.
   --------------------------------------------------------------------------- */
:root {
  --accent: #1976d2;
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}
@media (prefers-color-scheme: dark) {
  :root {
    --accent: #90caf9;
  }
}

/* ---------------------------------------------------------------------------
   Display face — GFS Neohellenic (SIL OFL, /assets/fonts/OFL.txt). Drawn for
   Greek first, which is the right way round on a bilingual site. Subsetted from
   ~450 KB TTFs to ~16 KB woff2 each. Headings only: body text stays on the
   system sans. These URLs are hashed BEFORE the stylesheet is — see
   generator/src/assets.js.
   --------------------------------------------------------------------------- */
@font-face {
  font-family: 'GFS Neohellenic';
  src: url('/assets/fonts/gfs-neohellenic-400.707b6640.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  /* Matches the subset, so the browser skips the file for out-of-range text
     rather than downloading it to find out. */
  unicode-range: U+0020-007E, U+00A0-00FF, U+0370-03FF, U+2010-2015, U+2018-201A,
    U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+20AC, U+2122;
}
@font-face {
  font-family: 'GFS Neohellenic';
  src: url('/assets/fonts/gfs-neohellenic-700.46983e68.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0020-007E, U+00A0-00FF, U+0370-03FF, U+2010-2015, U+2018-201A,
    U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+20AC, U+2122;
}

/* ---------------------------------------------------------------------------
   Design tokens
   --------------------------------------------------------------------------- */
:root {
  --bg: #fdfdfd;
  --surface: #ffffff;
  --surface-sunken: #f2f4f6;
  --text: #111417;
  --text-muted: #596570;
  --border: #e2e6ea;
  --border-strong: #c4ccd4;
  --accent-contrast: #ffffff;
  /* The old card, kept exactly. Used as a filled panel with white text, never
     as link colour: it is 3.12:1 on white, so small text on or in it fails AA. */
  /* Not a site.yaml knob: it names the .woff2 files this repo ships, so a
     config override would silently fall back to the system sans. */
  --font-display: 'GFS Neohellenic', var(--font-sans);
  --card-bg: #2196f3;
  /* The two darker stops of the card gradient. Tokens, not literals, because
     dark mode re-darkens the whole card and a hardcoded gradient would quietly
     put the light palette back. */
  --card-bg-mid: #1565c0;
  --card-bg-deep: #0d47a1;
  --card-text: #ffffff;
  /* The old blog was two colours, not one: a blue header block and a GREEN
     category rail. Green comes back on the tags, which is what that rail was
     for. NOT the original #4CAF50 — white on it is 2.78:1 and the old rail
     failed AA outright. #2f6f4f is 5.99:1 on white and 5.28:1 on the tint. */
  --accent-2: #2f6f4f;
  --accent-2-bg: #e6f4ec;
  --accent-2-border: #b9dcc8;
  --shadow: 0 1px 2px rgb(17 20 23 / 6%), 0 4px 12px rgb(17 20 23 / 5%);

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4.5rem;

  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 14px;

  --measure: 68ch;
  --wrap: 72rem;

  --code-bg: #f5f5f0;
  --code-border: #e4e4dc;
  --code-text: #24292f;
  --code-comment: #6a737d;
  --code-keyword: #a6237a;
  --code-string: #0a6b45;
  --code-number: #9a4b00;
  --code-title: #1f4bb8;
  --code-attr: #6f42c1;
  --code-meta: #57606a;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14161a;
    --surface: #1b1e23;
    --surface-sunken: #22262c;
    --text: #e7eaee;
    --text-muted: #a3acb6;
    --border: #2b3038;
    --border-strong: #3b424c;
    --accent-contrast: #0b1b2b;
    /* Darkened so white text on the card still clears AA on a dark ground.
       #1565c0 is 5.75:1 against white and is the LIGHTEST stop here, so every
       point of the gradient clears it. */
    --card-bg: #1565c0;
    --card-bg-mid: #0d47a1;
    --card-bg-deep: #072a63;
    --card-text: #ffffff;
    --accent-2: #9ad7b0;
    --accent-2-bg: #16281f;
    --accent-2-border: #2c4a39;
    --shadow: 0 1px 2px rgb(0 0 0 / 40%), 0 4px 14px rgb(0 0 0 / 30%);

    --code-bg: #1a1d22;
    --code-border: #2b3038;
    --code-text: #d7dbdf;
    --code-comment: #7f8a95;
    --code-keyword: #f08bc4;
    --code-string: #9ad7b0;
    --code-number: #ffb86b;
    --code-title: #8ab4ff;
    --code-attr: #c8a6ff;
    --code-meta: #97a1ab;
  }
}

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

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

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: clamp(1rem, 0.97rem + 0.15vw, 1.0625rem);
  line-height: 1.65;
  min-height: 100vh;
  /* 100vh on mobile Safari is the chrome-less height, i.e. taller than what you
     can actually see, which guarantees a scrollbar on a page that fits. dvh is
     the visible height; browsers without it keep the vh above. */
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, picture, video, canvas, svg { display: block; max-width: 100%; height: auto; }
input, button, textarea, select { font: inherit; color: inherit; }
p, h1, h2, h3, h4, li { overflow-wrap: break-word; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  line-height: 1.2;
  /* 700, not 640: two weights are shipped and 640 resolves to the bold anyway.
     Tracking eased off — the display face crowds at the old -0.015em. */
  font-weight: 700;
  letter-spacing: -0.005em;
  /* GFS Neohellenic sets a 0.398em word space against a 0.387em x-height —
     more than twice the usual ratio, because it was drawn for schoolbook text
     at small sizes. Untouched, headings read as loose words rather than a
     phrase. -0.11em brings the space to ~0.29em, in line with the body sans.
     Anything using --font-display needs this; it is not a global fix. */
  word-spacing: -0.11em;
  text-wrap: balance;
}
h1 { font-size: clamp(1.85rem, 1.5rem + 1.6vw, 2.6rem); }
h2 { font-size: clamp(1.35rem, 1.2rem + 0.7vw, 1.7rem); }
h3 { font-size: 1.2rem; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
a:hover { text-decoration-thickness: 2px; }

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

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--space-5);
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: var(--space-2);
  background: var(--surface);
  color: var(--text);
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  z-index: 100;
}
.skip-link:focus { left: var(--space-4); }

/* ---------------------------------------------------------------------------
   Preview ribbon & draft badge
   --------------------------------------------------------------------------- */
.preview-ribbon {
  position: sticky;
  top: 0;
  z-index: 50;
  background: repeating-linear-gradient(
    135deg, #8a5a00, #8a5a00 10px, #7a4f00 10px, #7a4f00 20px
  );
  color: #fff;
  text-align: center;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  padding: var(--space-2) var(--space-4);
}

.draft-badge {
  display: inline-block;
  vertical-align: middle;
  margin-inline-start: var(--space-3);
  padding: 0.15em 0.6em;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #8a5a00;
  background: #ffe9b8;
  border: 1px solid #e0bb63;
  border-radius: 999px;
}
@media (prefers-color-scheme: dark) {
  .draft-badge { color: #ffd88a; background: #3a2c0a; border-color: #6b5316; }
}

/* ---------------------------------------------------------------------------
   Header & footer
   --------------------------------------------------------------------------- */
.site-header {
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}
.site-header__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-block: var(--space-4);
}
.site-title {
  font-family: var(--font-display);
  font-weight: 700;
  word-spacing: -0.11em;
  /* Up from 1.0625rem — the display face has a smaller x-height, so an equal
     size read as smaller. */
  font-size: 1.1875rem;
  letter-spacing: 0;
  color: var(--text);
  text-decoration: none;
}
.site-title:hover { color: var(--accent); }

.site-nav__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  list-style: none;
  padding: 0;
}
.site-nav__list a {
  color: var(--text-muted);
  text-decoration: none;
  font-size: 0.9375rem;
  padding-block: var(--space-1);
  border-bottom: 2px solid transparent;
}
.site-nav__list a:hover { color: var(--text); }
.site-nav__list a[aria-current="page"] {
  color: var(--text);
  border-bottom-color: var(--accent);
}

main { flex: 1; padding-block: var(--space-7) var(--space-8); }

.site-footer {
  border-top: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
  font-size: 0.875rem;
}
.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-5);
  align-items: center;
  justify-content: space-between;
  padding-block: var(--space-5);
}
.social { display: flex; gap: var(--space-4); list-style: none; padding: 0; }

/* The self-hosting line: its own full-width row under the two space-between
   lines, centered and quieter than them so it reads as a sign-off, not a nav. */
.site-footer__onprem {
  flex-basis: 100%;
  margin: 0;
  text-align: center;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

/* The live "last outage" line, filled by the footer script from /api/uptime.
   Same row treatment as the on-prem sign-off; quieter still. */
.site-footer__uptime {
  flex-basis: 100%;
  margin: 0;
  text-align: center;
  font-size: 0.75rem;
  color: var(--text-muted);
}

/* ---------------------------------------------------------------------------
   Prose
   --------------------------------------------------------------------------- */
.prose { max-width: var(--measure); }
.prose > * + * { margin-top: var(--space-4); }
.prose h2 { margin-top: var(--space-7); }
.prose h3 { margin-top: var(--space-6); }
.prose ul, .prose ol { padding-inline-start: var(--space-5); }
.prose li + li { margin-top: var(--space-2); }

.prose blockquote {
  border-inline-start: 3px solid var(--accent);
  padding-inline-start: var(--space-4);
  color: var(--text-muted);
  font-style: italic;
}

.prose hr { border: 0; border-top: 1px solid var(--border); margin-block: var(--space-6); }

/* Migrated YouTube and Spotify embeds. A class rather than inline styles,
   because html-validate enforces no-inline-style across every built page. */
.embed {
  display: block;
  border: 0;
  max-width: 100%;
  border-radius: var(--radius-md);
}

.prose table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9375rem;
}
.prose th, .prose td {
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
}
.prose th { font-weight: 640; }

.prose img { border-radius: var(--radius-md); margin-block: var(--space-5); }

/* ---------------------------------------------------------------------------
   Front card

   The old meglis.com front page was a centred w3.css panel in Material Blue
   with white text and a soft shadow. That is what this reproduces, on the
   site's own type scale.

   Everything inside it must clear AA against #2196f3, which is only 3.12:1
   against white. That is fine for the name and role at display sizes (large
   text needs 3:1) but NOT for anything at body size — hence the tinted panel
   behind the small print rather than plain white-on-blue.
   --------------------------------------------------------------------------- */
.card {
  max-width: 44rem;
  margin-inline: auto;
  margin-bottom: var(--space-7);
  padding: clamp(var(--space-6), 5vw, var(--space-8)) var(--space-5);
  text-align: center;
  color: var(--card-text);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  overflow: hidden;

  /* Two layers drifting at different speeds: a black sheen over a field of
     blues.

     CONTRAST BOUND, and the reason both layers are what they are. --card-bg-mid
     and --card-bg-deep are componentwise darker than --card-bg in both schemes,
     so every interpolated point is too and the lightest the surface can ever be
     is --card-bg itself: 3.12:1 on white in light mode, which carries the
     display-size name and role and nothing smaller, and 5.75:1 in dark. The
     sheen is pure black, so it can only darken further. A light stop in either
     layer breaks this and nothing would catch it — axe cannot evaluate a
     gradient. Measured on white: #2196f3 3.12:1, #1565c0 5.75, #0d47a1 8.63. */
  background-color: var(--card-bg);
  background-image:
    linear-gradient(115deg, rgb(0 0 0 / 0%) 32%, rgb(0 0 0 / 14%) 50%, rgb(0 0 0 / 0%) 68%),
    linear-gradient(125deg,
      var(--card-bg-mid) 0%, var(--card-bg) 30%,
      var(--card-bg-deep) 65%, var(--card-bg) 100%);
  background-size: 300% 300%, 200% 200%;
  background-position: 0% 50%, 0% 50%;
  animation: card-sheen 31s linear infinite, card-drift 24s ease-in-out infinite;
}

@keyframes card-drift {
  0%, 100% { background-position: 0% 50%, 0% 50%; }
  50%      { background-position: 0% 50%, 100% 50%; }
}
@keyframes card-sheen {
  0%   { background-position: 0% 50%, 0% 50%; }
  100% { background-position: 100% 50%, 0% 50%; }
}

/* The global clamp collapses animation-duration to 0.01ms, which on an
   infinite keyframe means "stop wherever you land". Pin the first frame
   instead, so reduced motion gets a chosen surface rather than a random one. */
@media (prefers-reduced-motion: reduce) {
  .card {
    animation: none !important;
    background-position: 0% 50%, 0% 50%;
  }
}

/* ---------------------------------------------------------------------------
   The front page is laid out against the VIEWPORT, not the document.

   It holds one card and nothing else, so it should sit in the middle of
   whatever room the header and footer leave rather than pushing the page
   taller than the screen. Every vertical measurement here is therefore
   vh-scaled: on a short window the spacing gives way before the page does.

   Scoped to .home (set in build.js) — every other page is a document and
   scrolls normally.

   Deliberately NOT `overflow: hidden`. If the content genuinely cannot fit —
   text scaled to 200%, a very short window — clipping it would fail WCAG 1.4.4
   and 1.4.10 by making it unreachable. Scrolling is the correct failure.
   --------------------------------------------------------------------------- */
.home main {
  display: flex;
  align-items: center;
  justify-content: center;
  padding-block: clamp(var(--space-4), 3vh, var(--space-7));
}
.home .card {
  width: 100%;
  margin-bottom: 0;
  padding-block: clamp(var(--space-5), 6vh, var(--space-8));
}
.home .card__body > * + * { margin-top: clamp(var(--space-2), 1.6vh, var(--space-4)); }
.home .card__body h2 { margin-top: clamp(var(--space-2), 1.2vh, var(--space-3)); }
.home .card__latest { margin-top: clamp(var(--space-4), 3.2vh, var(--space-6)); }
/* The self-hosting footer line adds a third row; on the viewport-bound front
   page that tipped the tightest sizes (el at 360×640) into a scrollbar. Give
   the footer a little less breathing room here so the one-screen guarantee
   holds — the front page is the one place vertical space is rationed. */
/* Two extra footer rows (the on-prem sign-off and the live uptime line) crowd
   the viewport-bound front page; tighten the footer's padding and row gap there
   so the one-screen guarantee holds down to 360×640 in both languages. */
.home .site-footer__inner { padding-block: var(--space-2); gap: var(--space-2) var(--space-5); }

/* Portrait and latest-post strip. Both are template chrome (home.njk), not
   markdown, so the front page stays editable as prose in the admin. */
.card__portrait {
  width: 160px;
  height: 160px;
  margin: 0 auto var(--space-5);
  border-radius: 50%;
  object-fit: cover;
  /* A ring, not a hard edge: warm photo against blue card read as a cutout. */
  box-shadow: 0 0 0 4px rgb(255 255 255 / 85%), 0 8px 24px rgb(0 0 0 / 28%);
}

/* On the same 26%-black inset as the credential line, for the same reason: it
   carries body-size text, which needs 4.5:1 and cannot get it from white on
   the raw surface. The inset gives 5.29:1 for pure white and 4.73:1 at the 92%
   below, so label, title and date all clear AA. */
.card__latest {
  margin-top: var(--space-6);
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-md);
  background: rgb(0 0 0 / 26%);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: var(--space-2) var(--space-3);
  font-size: 0.9375rem;
}
.card__latest-label {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgb(255 255 255 / 92%);
}
.card__latest a {
  font-family: var(--font-display);
  word-spacing: -0.11em;
  font-size: 1.0625rem;
  font-weight: 700;
  color: var(--card-text);
  text-decoration-color: rgb(255 255 255 / 55%);
  text-underline-offset: 0.22em;
}
.card__latest a:hover { text-decoration-color: var(--card-text); }
.card__latest time { color: rgb(255 255 255 / 92%); font-variant-numeric: tabular-nums; }
.card__body { max-width: none; }
.card__body > * + * { margin-top: var(--space-4); }

.card__body h1 {
  color: var(--card-text);
  font-size: clamp(2rem, 1.5rem + 2.4vw, 3rem);
}
/* The role line: large-text threshold, so 3:1 is the bar it must clear. */
.card__body h2 {
  color: var(--card-text);
  margin-top: var(--space-3);
  font-size: clamp(1.25rem, 1.1rem + 0.7vw, 1.5rem);
  font-weight: 500;
  letter-spacing: 0;
}
.card__body p { color: var(--card-text); }

/* Small print — the credential line — on a darkened inset.
   White on raw #2196f3 is 3.12:1, which is fine for the display-size name and
   role (large text needs 3:1) but fails at body size. A 26% black overlay
   lifts it to ~5.3:1, so this clears AA outright rather than leaning on the
   large-text threshold with a 4% margin.
   Selected by "has no links" so the button row below is unaffected — position
   selectors would break the moment the card gains a line. */
.card__body p:not(:has(a)) {
  display: inline-block;
  padding: 0.3em 1.1em;
  border-radius: 999px;
  background: rgb(0 0 0 / 26%);
  font-size: 1rem;
  font-weight: 500;
}

/* Links become buttons, as they were on the original card. Solid white with
   dark blue text: #1565c0 on white is 5.75:1, so the labels clear AA at any
   size, which white-on-card never could. */
.card__body p a {
  display: inline-block;
  margin: var(--space-2) var(--space-1) 0;
  padding: 0.45em 1.1em;
  background: #ffffff;
  color: #1565c0;
  font-size: 0.9375rem;
  font-weight: 600;
  border-radius: 999px;
  text-decoration: none;
  box-shadow: 0 1px 2px rgb(0 0 0 / 18%);
}
.card__body p a:hover { background: #e8f2fd; }
.card__body :focus-visible { outline-color: var(--card-text); outline-offset: 3px; }

/* ---------------------------------------------------------------------------
   Language switcher — a segmented control, not two loose links
   --------------------------------------------------------------------------- */
.lang-switch__list {
  display: flex;
  align-items: stretch;
  list-style: none;
  margin: 0;
  padding: 2px;
  gap: 2px;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: 999px;
}
.lang-switch__list a {
  display: block;
  padding: 0.25rem 0.7rem;
  border-radius: 999px;
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.4;
  color: var(--text-muted);
  text-decoration: none;
  white-space: nowrap;
  transition: background-color 120ms ease, color 120ms ease;
}
.lang-switch__list a:hover { color: var(--text); background: var(--surface); }
.lang-switch__list a[aria-current="true"] {
  background: var(--accent);
  color: var(--accent-contrast);
}
.lang-switch__list a[aria-current="true"]:hover { color: var(--accent-contrast); }

.section-heading { margin-bottom: var(--space-5); }
.section-meta { color: var(--text-muted); margin-bottom: var(--space-5); }
.more-link { margin-top: var(--space-6); }
.empty-state { color: var(--text-muted); }

/* ---------------------------------------------------------------------------
   Code
   --------------------------------------------------------------------------- */
code, kbd, samp, pre { font-family: var(--font-mono); font-size: 0.875em; }

:not(pre) > code {
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.1em 0.35em;
}

pre.hljs {
  position: relative;
  background: var(--code-bg);
  color: var(--code-text);
  border: 1px solid var(--code-border);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  overflow-x: auto;
  line-height: 1.55;
  tab-size: 2;
}
pre.hljs code { background: none; border: 0; padding: 0; font-size: inherit; }

pre.hljs[data-lang]::after {
  content: attr(data-lang);
  position: absolute;
  top: 0;
  right: 0;
  padding: 0.15em 0.6em;
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--code-meta);
  background: var(--code-border);
  border-bottom-left-radius: var(--radius-sm);
}

/* highlight.js tokens — light and dark share these vars */
.hljs-comment, .hljs-quote { color: var(--code-comment); font-style: italic; }
.hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-doctag { color: var(--code-keyword); }
.hljs-string, .hljs-regexp, .hljs-addition { color: var(--code-string); }
.hljs-number, .hljs-symbol, .hljs-bullet { color: var(--code-number); }
.hljs-title, .hljs-title_, .hljs-title.function_, .hljs-section, .hljs-name { color: var(--code-title); }
.hljs-attr, .hljs-attribute, .hljs-variable, .hljs-template-variable { color: var(--code-attr); }
.hljs-type, .hljs-class .hljs-title, .hljs-built_in { color: var(--code-title); }
.hljs-meta, .hljs-comment .hljs-doctag { color: var(--code-meta); }
.hljs-emphasis { font-style: italic; }
.hljs-strong { font-weight: 700; }
.hljs-deletion { color: #b3261e; }

/* ---------------------------------------------------------------------------
   Post cards, tags, pagination
   --------------------------------------------------------------------------- */
.post-grid { display: grid; gap: var(--space-5); }

.post-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  box-shadow: var(--shadow);
}
.post-card__title { font-size: 1.25rem; }
/* Blue, not black. Every post title on the old blog was a blue link, and that
   is most of what gave the listing its colour; rendering them as body text made
   the whole site read monochrome. */
.post-card__title a { color: var(--accent); text-decoration: none; }
.post-card__title a:hover { text-decoration: underline; }
.post-card__meta { color: var(--text-muted); font-size: 0.8125rem; margin-top: var(--space-2); }
.post-card__excerpt { margin-top: var(--space-3); color: var(--text-muted); }

.post-header { margin-bottom: var(--space-6); }
.post-meta { color: var(--text-muted); font-size: 0.875rem; margin-top: var(--space-2); }
.post-footer-nav { max-width: var(--measure); margin-top: var(--space-7); padding-top: var(--space-5); border-top: 1px solid var(--border); }

.tag-list { display: flex; flex-wrap: wrap; gap: var(--space-2); list-style: none; padding: 0; margin-top: var(--space-3); }

.tag {
  display: inline-block;
  font-size: 0.8125rem;
  padding: 0.15em 0.7em;
  border-radius: 999px;
  background: var(--accent-2-bg);
  border: 1px solid var(--accent-2-border);
  color: var(--accent-2);
  text-decoration: none;
}
.tag:hover { border-color: var(--accent-2); text-decoration: underline; }
.tag--heading { font-size: 0.5em; vertical-align: middle; }

.tag-cloud { display: flex; flex-wrap: wrap; gap: var(--space-3); list-style: none; padding: 0; }
.tag-cloud li { display: flex; align-items: center; gap: var(--space-2); }
.tag-count { color: var(--text-muted); font-size: 0.8125rem; }

.pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-top: var(--space-7);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
}
.pagination__status { color: var(--text-muted); font-size: 0.875rem; }

/* ---------------------------------------------------------------------------
   Forms — contact & search
   --------------------------------------------------------------------------- */
.field { display: flex; flex-direction: column; gap: var(--space-2); }
.field label { font-size: 0.875rem; font-weight: 600; }

input[type="text"], input[type="email"], input[type="search"], textarea {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  padding: var(--space-3);
}
input:focus, textarea:focus { border-color: var(--accent); }
textarea { resize: vertical; min-height: 8rem; }

.button {
  align-self: flex-start;
  background: var(--accent);
  color: var(--accent-contrast);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-5);
  font-weight: 600;
  cursor: pointer;
}
.button:hover { filter: brightness(1.08); }
.button:disabled { opacity: 0.55; cursor: not-allowed; }

.contact-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  max-width: var(--measure);
  margin-top: var(--space-6);
}

/* Off-screen rather than display:none — bots skip hidden inputs. */
.honeypot {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.form-status { font-size: 0.9375rem; min-height: 1.5em; }
.form-status[data-state="error"] { color: #b3261e; }
.form-status[data-state="success"] { color: var(--accent); }
@media (prefers-color-scheme: dark) { .form-status[data-state="error"] { color: #ff9d92; } }

.search { max-width: var(--measure); margin-top: var(--space-6); }
.search__label { display: block; font-size: 0.875rem; font-weight: 600; margin-bottom: var(--space-2); }
.search__status { color: var(--text-muted); font-size: 0.875rem; margin-top: var(--space-3); min-height: 1.5em; }
.search__results { list-style: none; padding: 0; max-width: var(--measure); display: grid; gap: var(--space-4); }
.search__result a { font-weight: 600; }
.search__excerpt { color: var(--text-muted); font-size: 0.9375rem; margin-top: var(--space-1); }
.search__empty, .search__fallback { color: var(--text-muted); }

/* ---------------------------------------------------------------------------
   Responsive & motion
   --------------------------------------------------------------------------- */
@media (min-width: 46rem) {
  .post-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 32rem) {
  .wrap { padding-inline: var(--space-4); }

  /* Two rows, not three: wordmark and switcher share the top, the nav takes a
     full row below because it is the part that actually needs the width.
     Stacking as a column put the switcher under the nav, which read as an
     afterthought.

     This is a DELIBERATE visual reorder — markup order is title, nav, switcher,
     so tabbing goes wordmark → nav → switcher and jumps back up to the
     switcher. The alternative kept focus order intact but wedged the switcher
     against a two-line nav at 390px. Left as-is on purpose; don't "fix" it
     without re-reading that trade-off. */
  .site-header__inner { align-items: center; gap: var(--space-3); }
  .site-title { order: 1; }
  .lang-switch { order: 2; }
  .site-nav { order: 3; flex-basis: 100%; }

  main { padding-block: var(--space-5) var(--space-7); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

