/* ==========================================================================
   Abscode — abscode.com's design system, mapped onto Quark 2
   --------------------------------------------------------------------------
   Loaded LAST by Quark 2 (priority 80), after theme.css (90) and
   blades.min.css (95), so nothing here needs !important.

   This is a port of the main site's system, not an imitation of it: the same
   OKLCH brand source, the same derived ramps, the same curve language and the
   same ambient "quiet blueprint" graphics. Everything is mixed off --brand, so
   when a visitor repaints the site from the "Make it yours" panel on
   abscode.com, the blog repaints with it — see js/abscode-theme.js.

   Source of truth: D:\Git\abscode\abscode.com/src/styles.css
   See docs/knowledge/11-customization-plan.md
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Typefaces

   Served from the main site at /fonts/. The blog lives at abscode.com/blog/,
   so these are SAME-ORIGIN absolute paths — no CORS, no extra connection, and
   already warm in cache for anyone arriving from abscode.com. Both are
   variable; the weight range matters or headings snap to 400.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: 'Inter var';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/fonts/inter-var.woff2') format('woff2');
}

@font-face {
  font-family: 'JetBrains Mono var';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url('/fonts/jetbrains-mono-var.woff2') format('woff2');
}

/* --------------------------------------------------------------------------
   2. Brand — one OKLCH triplet drives the entire palette

   js/abscode-theme.js overwrites --brand-h / -c / -l inline on <html> from the
   shared `ab_theme` cookie before first paint. These are the fallbacks, and
   match abscode.com's own defaults (hue 263, an indigo).

   Do not hardcode a brand hex anywhere below this block. Every colour is mixed
   off --brand so the whole thing re-skins from the one knob.
   -------------------------------------------------------------------------- */
:root {
  --brand-h: 263;
  --brand-c: 0.215;
  --brand-l: 0.547;

  --brand:      oklch(var(--brand-l) var(--brand-c) var(--brand-h));
  --brand-050:  color-mix(in srgb, var(--brand) 4%,  white);
  --brand-100:  color-mix(in srgb, var(--brand) 10%, white);
  --brand-200:  color-mix(in srgb, var(--brand) 18%, white);
  --brand-700:  color-mix(in srgb, var(--brand) 84%, black);
  --brand-ink:  color-mix(in srgb, var(--brand) 22%, #0B0F14);

  /* Readable text ON a brand fill, flipped by brand lightness rather than
     guessed. Lifted from the main site. */
  --brand-fg: hsl(0 0% clamp(0%, (0.62 - var(--brand-l)) * 100000%, 100%));

  /* Link colour. On the dark canvas the brand alone is too dark to pass AA at
     body size (3.30:1 at the default hue), so lightness is raised there. */
  --brand-link-dark: oklch(calc(var(--brand-l) + 0.11) var(--brand-c) var(--brand-h));

  /* ---- Ink + surfaces (light) ---- */
  --ink-900: #14181C;
  --ink-700: #333A40;
  --ink-500: #5D686F;
  --ink-400: #8A939A;

  --surface-0: #FFFFFF;
  --surface-1: color-mix(in srgb, var(--brand) 3.5%, white);
  --surface-2: color-mix(in srgb, var(--brand) 7.5%, white);

  --code-bg:     var(--brand-ink);
  --code-text:   color-mix(in srgb, var(--brand-100) 55%, white);
  --code-accent: color-mix(in srgb, var(--brand) 62%, white);

  /* ---- Shape — curved ---- */
  --r-sm:    12px;
  --r:       16px;
  --r-lg:    24px;
  --r-xl:    24px;
  --r-block: 28px;
  --pill:    999px;

  /* ---- Motion ---- */
  --ease:       cubic-bezier(0.22, 0.61, 0.36, 1);
  --speed:      0.3s;
  --speed-fast: 0.18s;
  --ambient-drift: 64s;

  /* ---- Ambient "quiet blueprint" graphics ----
     All mixed off --brand and kept ultra-faint, so the backdrop re-skins with
     the hue knob and stays surface-dominant. */
  --brand-mist:   color-mix(in srgb, var(--brand) 6%, transparent);
  --brand-mist-2: color-mix(in srgb, var(--brand) 5%, transparent);
  --brand-dot:    color-mix(in srgb, var(--brand) 9%, transparent);
  --grid-cell:    46px;
  --bp-module:    var(--grid-cell);
  --bp-arc:       color-mix(in srgb, var(--brand) 10%, transparent);
}

:root[data-theme="dark"] {
  --ink-900: #EEF2F6;
  --ink-700: #C3CBD2;
  --ink-500: #8E99A2;
  --ink-400: #626D75;

  --surface-0: color-mix(in srgb, var(--brand) 5%,  #0B0F14);
  --surface-1: color-mix(in srgb, var(--brand) 9%,  #10151B);
  --surface-2: color-mix(in srgb, var(--brand) 15%, #161C23);

  --brand-050: color-mix(in srgb, var(--brand) 14%, #10151B);
  --brand-100: color-mix(in srgb, var(--brand) 24%, #151B22);
  --brand-200: color-mix(in srgb, var(--brand) 36%, #151B22);

  --bp-arc:    color-mix(in srgb, var(--brand) 16%, transparent);
  --brand-dot: color-mix(in srgb, var(--brand) 17%, transparent);
}

/* --------------------------------------------------------------------------
   3. Bind to Quark 2's tokens

   Everything Quark 2 draws — nav, cards, forms, blog list, sidebar, pagination
   — reads these. Rebinding them restyles the whole theme without touching a
   single component rule, and keeps us inheriting upstream fixes.
   -------------------------------------------------------------------------- */
:root,
:root[data-theme="light"],
:root[data-theme="dark"] {
  --q2-bg:          var(--surface-0);
  --q2-bg-elev:     var(--surface-1);
  --q2-bg-muted:    var(--surface-2);

  --q2-text:        var(--ink-700);
  --q2-text-strong: var(--ink-900);
  --q2-text-muted:  var(--ink-500);

  --q2-accent:      var(--brand);
  --q2-link:        var(--brand);
  --q2-focus-ring:  color-mix(in srgb, var(--brand) 45%, transparent);
  --q2-accent-contrast: var(--brand-fg);

  --q2-border-ring:   color-mix(in srgb, var(--brand) 10%, transparent);
  --q2-border-strong: color-mix(in srgb, var(--brand) 18%, transparent);
  --q2-divider:       color-mix(in srgb, var(--brand) 9%,  transparent);

  /* abscode.com is markedly rounder than stock Quark 2 (16px base against 8px).
     Matching the curve language is most of the family resemblance. */
  --q2-radius-xs: 6px;
  --q2-radius-sm: 10px;
  --q2-radius:    var(--r-sm);
  --q2-radius-md: var(--r);
  --q2-radius-lg: var(--r-lg);

  --pico-font-family-sans-serif:
    'Inter var', Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
    Helvetica, Arial, sans-serif;
  /* The main site sets headings in Inter, with no separate display face, so
     Quark 2's Cal Sans is deliberately dropped rather than inherited. */
  --pico-font-family-display:
    'Inter var', Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
    sans-serif;
  --pico-font-family-monospace:
    'JetBrains Mono var', 'JetBrains Mono', 'SF Mono', Monaco, 'Cascadia Code',
    'Roboto Mono', 'Courier New', monospace;
}

:root,
:root[data-theme="light"] {
  --q2-nav-bg:     rgba(255, 255, 255, 0.78);
  --q2-nav-border: transparent;   /* borderless, like the main site's .nav */
}

:root[data-theme="dark"] {
  --q2-link:       var(--brand-link-dark);
  --q2-accent:     var(--brand-link-dark);
  --q2-nav-bg:     color-mix(in srgb, var(--surface-0) 78%, transparent);
  --q2-nav-border: transparent;
}

/* --------------------------------------------------------------------------
   4. Ambient ground — the soft brand field the whole site floats on

   One fixed layer above the canvas and beneath content, at ~5-6% brand so it
   stays surface-dominant. This is the only perpetual animation on the page:
   transform-only, composited, and switched off under reduced-motion.
   -------------------------------------------------------------------------- */
html { background: var(--surface-0); }

body { background: transparent; }

body::before {
  content: "";
  position: fixed;
  inset: -20vmax;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(42vmax 42vmax at 16% 8%,  var(--brand-mist),   transparent 60%),
    radial-gradient(48vmax 48vmax at 88% 88%, var(--brand-mist-2), transparent 62%);
  transform: translateZ(0);
}

@media (prefers-reduced-motion: no-preference) {
  body::before {
    animation: absAuroraDrift var(--ambient-drift) var(--ease) infinite alternate;
    will-change: transform;
  }
}

@keyframes absAuroraDrift {
  from { transform: translate3d(-1.5%, -1%,   0) scale(1.04); }
  to   { transform: translate3d( 1.5%,  1.2%, 0) scale(1.09); }
}

/* --------------------------------------------------------------------------
   5. Concentric registration arcs

   The main site's "quiet blueprint" motif: one 46px module drives a ring field
   anchored to the page's top-right, masked so it fades before it reaches the
   content. It reads as scanner registration paper, which is on-topic for a
   document-tooling company.

   Anchored to the page wrapper rather than per-section, so the rings stay one
   continuous circle system across seams instead of restarting.
   -------------------------------------------------------------------------- */
#page-wrapper { position: relative; z-index: 0; }

#page-wrapper::before {
  content: "";
  position: absolute;
  top: -60px;
  left: 0;
  right: 0;
  height: 1560px;
  z-index: -1;
  pointer-events: none;
  background: repeating-radial-gradient(circle at 108% -6%,
    transparent 0 calc(var(--bp-module) - 1.5px),
    var(--bp-arc) calc(var(--bp-module) - 1.5px) var(--bp-module));
  -webkit-mask-image: radial-gradient(92% 118% at 106% -6%, #000 16%, transparent 74%);
          mask-image: radial-gradient(92% 118% at 106% -6%, #000 16%, transparent 74%);
}

/* --------------------------------------------------------------------------
   6. Typography
   -------------------------------------------------------------------------- */
body {
  font-family: var(--pico-font-family-sans-serif);
  letter-spacing: -0.011em;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--pico-font-family-display);
  letter-spacing: -0.022em;
  font-weight: 650;
}

h1 { font-weight: 700; letter-spacing: -0.03em; }

/* Quark 2 gives h2 an accent bar; the main site doesn't. */
h2::before { content: none; }

code, kbd, samp, pre {
  font-family: var(--pico-font-family-monospace);
  font-variant-ligatures: none;
}

pre {
  background: var(--code-bg);
  color: var(--code-text);
  border-radius: var(--r-sm);
  border: none;
}

pre code { background: none; color: inherit; }

:not(pre) > code {
  background: var(--brand-100);
  color: var(--brand-700);
  border-radius: 6px;
  padding: 0.15em 0.4em;
  font-size: 0.9em;
}

:root[data-theme="dark"] :not(pre) > code { color: var(--code-accent); }

/* --------------------------------------------------------------------------
   7. The blog header — a deeply-curved card, as on the main site

   The hero there is a contained, heavily-rounded block floating on the page
   rather than a full-bleed colour wash, carrying its own dot-field.
   -------------------------------------------------------------------------- */
.blog-page-header {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-block);
  padding: 56px 40px 52px;
  margin-bottom: 2.5rem;
  background:
    radial-gradient(760px 420px at 88% -16%, color-mix(in srgb, var(--brand) 12%, transparent), transparent 60%),
    radial-gradient(560px 380px at 2% 112%, color-mix(in srgb, var(--brand) 8%,  transparent), transparent 62%),
    var(--surface-1);
}

/* Blueprint dot-field — scanner registration paper, masked to a whisper. */
.blog-page-header::before {
  content: "";
  position: absolute;
  inset: -12px;
  z-index: 0;
  pointer-events: none;
  background-image: radial-gradient(circle at 1px 1px, var(--brand-dot) 1px, transparent 1.6px);
  background-size: 24px 24px;
  -webkit-mask-image: radial-gradient(62% 72% at 80% 44%, #000, transparent 72%);
          mask-image: radial-gradient(62% 72% at 80% 44%, #000, transparent 72%);
  opacity: 0.55;
}

.blog-page-header > * { position: relative; z-index: 1; }

@media (max-width: 640px) {
  .blog-page-header {
    border-radius: var(--r-lg);
    padding: 36px 22px 32px;
  }
}

/* --------------------------------------------------------------------------
   8. Blog cards
   Generous radius, a hairline brand-tinted ring instead of a shadow, and the
   whole card lifting on hover — the main site's card idiom.
   -------------------------------------------------------------------------- */
.blog-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 1.5rem;
}

.blog-grid .card {
  background: var(--surface-1);
  border: 0;
  border-radius: var(--r-lg);
  overflow: hidden;
  transition:
    transform var(--speed-fast) var(--ease),
    background var(--speed-fast) var(--ease);
}

.blog-grid .card:hover {
  transform: translateY(-2px);
  background: var(--surface-2);
}

.blog-grid .card h2,
.blog-grid .card h3 { letter-spacing: -0.02em; }

.blog-grid .card a { text-decoration: none; }
.blog-grid .card a:hover { text-decoration: underline; }

@media (prefers-reduced-motion: reduce) {
  .blog-grid .card,
  .blog-grid .card:hover { transition: none; transform: none; }
}

/* --------------------------------------------------------------------------
   9. Taxonomy pills
   -------------------------------------------------------------------------- */
.p-category,
.tags a,
.taxonomy a {
  display: inline-block;
  padding: 0.2em 0.7em;
  border-radius: var(--pill);
  background: var(--brand-100);
  color: var(--brand-700);
  font-size: 0.8125rem;
  font-weight: 500;
  text-decoration: none;
  transition: background var(--speed-fast) var(--ease);
}

.p-category:hover,
.tags a:hover,
.taxonomy a:hover {
  background: var(--brand-200);
  text-decoration: none;
}

:root[data-theme="dark"] .p-category,
:root[data-theme="dark"] .tags a,
:root[data-theme="dark"] .taxonomy a { color: var(--brand-link-dark); }

.dt-published,
time {
  color: var(--ink-500);
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   10. Reading measure

   Long-form posts read badly at Quark 2's full content width. The main site
   holds body copy to roughly 68 characters; media and code break out.
   -------------------------------------------------------------------------- */
.content-item > p,  .e-content > p,
.content-item > ul, .e-content > ul,
.content-item > ol, .e-content > ol,
.content-item > blockquote, .e-content > blockquote,
.content-item > h2, .e-content > h2,
.content-item > h3, .e-content > h3,
.content-item > h4, .e-content > h4 {
  max-width: 68ch;
}

/* Wide content must scroll inside itself, never the page. */
.content-item > pre,   .e-content > pre,
.content-item > table, .e-content > table,
.content-item > figure, .e-content > figure {
  max-width: 100%;
  overflow-x: auto;
}

/* --------------------------------------------------------------------------
   11. Logo
   Two wordmark files: dark ink for light backgrounds, white for dark ones.
   partials/logo.html.twig emits both; CSS picks one, so the swap survives the
   pre-paint theme script with no JavaScript of its own.
   -------------------------------------------------------------------------- */
.navbar-brand .abs-wordmark {
  display: block;
  height: 26px;
  width: auto;
}

.navbar-brand .abs-wordmark--dark { display: none; }

:root[data-theme="dark"] .navbar-brand .abs-wordmark--light { display: none; }
:root[data-theme="dark"] .navbar-brand .abs-wordmark--dark  { display: block; }

/* --------------------------------------------------------------------------
   12. Footer
   Copyright mirrors abscode.com; legal links point back at the main site.
   -------------------------------------------------------------------------- */
#footer .abs-footer {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  align-items: baseline;
  justify-content: space-between;
  font-size: 0.875rem;
  color: var(--ink-500);
}

#footer .abs-footer p { margin: 0; }

#footer .abs-footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
}

#footer .abs-footer a {
  color: var(--ink-500);
  text-decoration: none;
  transition: color var(--speed-fast) var(--ease);
}

#footer .abs-footer a:hover {
  color: var(--brand);
  text-decoration: underline;
}

:root[data-theme="dark"] #footer .abs-footer a:hover { color: var(--brand-link-dark); }

#footer .abs-dot { color: var(--brand); }
:root[data-theme="dark"] #footer .abs-dot { color: var(--brand-link-dark); }

@media (max-width: 560px) {
  #footer .abs-footer { flex-direction: column; gap: 0.75rem; }
}

/* --------------------------------------------------------------------------
   13. Blog index — featured carousel, topic chips + search, card grid

   Ported from the main site's listing (src/styles.css, "BLOG INDEX").
   Monochrome by construction: every colour is a mix off --brand, so the whole
   listing re-skins with the hue wheel and flips to dark for free.
   Behaviour: js/abscode-blog.js.
   -------------------------------------------------------------------------- */

/* The four-step brand ladder the fallback covers use, mirroring the main site's
   --hue-N-wash / --hue-N-tint (where hue-1..4 all resolve to the brand hue). */
:root {
  --cover-wash-1: color-mix(in srgb, var(--brand)  5%, white);
  --cover-wash-2: color-mix(in srgb, var(--brand)  8%, white);
  --cover-wash-3: color-mix(in srgb, var(--brand) 11%, white);
  --cover-wash-4: color-mix(in srgb, var(--brand) 14%, white);
  --cover-tint-1: color-mix(in srgb, var(--brand) 12%, white);
  --cover-tint-2: color-mix(in srgb, var(--brand) 16%, white);
  --cover-tint-3: color-mix(in srgb, var(--brand) 20%, white);
  --cover-tint-4: color-mix(in srgb, var(--brand) 24%, white);
}

:root[data-theme="dark"] {
  --cover-wash-1: color-mix(in srgb, var(--brand)  9%, #0B0F14);
  --cover-wash-2: color-mix(in srgb, var(--brand) 12%, #0B0F14);
  --cover-wash-3: color-mix(in srgb, var(--brand) 15%, #0B0F14);
  --cover-wash-4: color-mix(in srgb, var(--brand) 18%, #0B0F14);
  --cover-tint-1: color-mix(in srgb, var(--brand) 22%, #161C23);
  --cover-tint-2: color-mix(in srgb, var(--brand) 26%, #161C23);
  --cover-tint-3: color-mix(in srgb, var(--brand) 30%, #161C23);
  --cover-tint-4: color-mix(in srgb, var(--brand) 34%, #161C23);
}

/* ---- featured carousel ---- */
.blog-featured { position: relative; margin-bottom: 3rem; }

.blog-featured-track {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* Claim horizontal pans only, so a vertical swipe scrolls the page. */
  touch-action: pan-x;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  gap: 0;
  border-radius: var(--r-block);
}

.blog-featured-track::-webkit-scrollbar { display: none; }

.blog-feat {
  flex: 0 0 100%;
  scroll-snap-align: center;
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  min-height: 320px;
  background: var(--surface-1);
  border-radius: var(--r-block);
  overflow: hidden;
}

/* Deep brand cover. Two-class specificity so it beats the .blog-cover-N tints. */
.blog-feat .blog-feat-cover {
  position: relative;
  padding: 36px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  background: linear-gradient(140deg, var(--brand), color-mix(in srgb, var(--brand) 74%, #0B0F14));
  color: var(--brand-fg);
  overflow: hidden;
}

/* Faint registration rings, echoing the page header graphic. */
.blog-feat .blog-feat-cover::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.5;
  background: repeating-radial-gradient(circle at 12% 8%,
    transparent 0 calc(var(--bp-module) - 1.5px),
    rgba(255, 255, 255, 0.14) calc(var(--bp-module) - 1.5px) var(--bp-module));
  -webkit-mask-image: radial-gradient(80% 80% at 12% 8%, #000, transparent 72%);
          mask-image: radial-gradient(80% 80% at 12% 8%, #000, transparent 72%);
}

.blog-feat-cover-title {
  position: relative;
  color: var(--brand-fg);
  font-size: 30px;
  font-weight: 700;
  line-height: 1.12;
  letter-spacing: -0.02em;
  margin: 0;
}

.blog-feat-cover-foot {
  position: relative;
  margin-top: 14px;
  font-size: 12px;
  font-weight: 600;
  opacity: 0.78;
  letter-spacing: 0.02em;
}

.blog-feat-body {
  padding: 40px 38px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.blog-feat-title {
  font-size: 23px;
  font-weight: 700;
  line-height: 1.24;
  letter-spacing: -0.02em;
  color: var(--ink-900);
  margin: 12px 0;
}

.blog-feat-title a { color: inherit; text-decoration: none; }
.blog-feat-title a:hover { color: var(--brand); }
:root[data-theme="dark"] .blog-feat-title a:hover { color: var(--brand-link-dark); }

.blog-feat-excerpt {
  font-size: 15px;
  color: var(--ink-500);
  line-height: 1.6;
  margin-bottom: 16px;
}

.blog-feat-link { font-weight: 600; text-decoration: none; color: var(--brand); }
.blog-feat-link:hover { text-decoration: underline; }
:root[data-theme="dark"] .blog-feat-link { color: var(--brand-link-dark); }

/* ---- category chip + badges (shared by featured and grid) ---- */
.blog-card-cat {
  align-self: flex-start;
  display: inline-block;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.12em;
  padding: 5px 11px;
  border-radius: var(--pill);
}

.blog-feat-cover .blog-card-cat {
  position: relative;
  background: rgba(255, 255, 255, 0.92);
  color: color-mix(in srgb, var(--brand) 68%, black);
  margin-bottom: auto;
}

.blog-card-cover .blog-card-cat {
  position: absolute;
  top: 14px;
  left: 14px;
  z-index: 2;
  background: var(--brand);
  color: var(--brand-fg);
}

.blog-card-badge {
  align-self: flex-start;
  display: inline-block;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.12em;
  padding: 4px 10px;
  border-radius: var(--pill);
  background: var(--brand-100);
  color: var(--brand-700);
  margin-bottom: 12px;
}

.blog-card-badge--new {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 2;
  margin: 0;
  background: var(--ink-900);
  color: var(--surface-0);
}

/* ---- carousel dots (the arrows were removed - see section 18) ---- */
.blog-feat-dots { display: flex; justify-content: center; gap: 8px; margin-top: 18px; }

.blog-feat-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--pill);
  border: 0;
  cursor: pointer;
  padding: 0;
  background: color-mix(in srgb, var(--brand) 24%, transparent);
  transition: width var(--speed) var(--ease), background var(--speed) var(--ease);
}

.blog-feat-dot.is-active { width: 26px; background: var(--brand); }

/* ---- all-posts heading ---- */
.blog-allposts-head { text-align: center; margin-bottom: 26px; }

.blog-eyebrow {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--brand);
  margin-bottom: 10px;
}

:root[data-theme="dark"] .blog-eyebrow { color: var(--brand-link-dark); }

.blog-allposts-title {
  font-size: clamp(28px, 4vw, 40px);
  font-weight: 700;
  letter-spacing: -0.03em;
  color: var(--ink-900);
  margin: 0;
}

/* ---- post grid ----
   Overrides section 8's auto-fill grid: this listing is a fixed three-up so it
   reads like the main site's, and the card is our own markup, not Quark 2's. */
.blog-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 26px; }

.blog-card {
  background: var(--surface-1);
  border-radius: var(--r-lg);
  overflow: hidden;
  transition: transform var(--speed) var(--ease);
}

.blog-card:hover { transform: translateY(-4px); background: var(--surface-2); }
.blog-card[hidden] { display: none; }

.blog-card-link {
  display: flex;
  flex-direction: column;
  height: 100%;
  text-decoration: none;
  color: inherit;
}

.blog-card-link:hover { text-decoration: none; }

.blog-card-cover {
  position: relative;
  height: 148px;
  overflow: hidden;
}

/* A post's own hero image fills the cover and hides the gradient beneath it. */
.blog-card-img,
.blog-card-img img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Faint arc motif, so a cover with no image still has texture. */
.blog-card-cover::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.6;
  background: repeating-radial-gradient(circle at 90% 12%,
    transparent 0 calc(var(--bp-module) - 1.5px),
    color-mix(in srgb, var(--brand) 12%, transparent) calc(var(--bp-module) - 1.5px) var(--bp-module));
  -webkit-mask-image: radial-gradient(70% 80% at 90% 12%, #000, transparent 74%);
          mask-image: radial-gradient(70% 80% at 90% 12%, #000, transparent 74%);
}

/* An image already carries its own texture. */
.blog-card-cover:has(.blog-card-img)::after { display: none; }

.blog-cover-1 { background: linear-gradient(135deg, var(--cover-wash-1), var(--cover-tint-2)); }
.blog-cover-2 { background: linear-gradient(135deg, var(--cover-wash-2), var(--cover-tint-3)); }
.blog-cover-3 { background: linear-gradient(135deg, var(--cover-wash-3), var(--cover-tint-4)); }
.blog-cover-4 { background: linear-gradient(135deg, var(--cover-tint-1), var(--cover-wash-4)); }

.blog-card-body {
  padding: 20px 22px 24px;
  display: flex;
  flex-direction: column;
  flex: 1;
}

.blog-card-title {
  font-size: 17px;
  font-weight: 700;
  line-height: 1.32;
  letter-spacing: -0.015em;
  color: var(--ink-900);
  margin: 0 0 9px;
}

.blog-card-excerpt {
  font-size: 13.5px;
  color: var(--ink-500);
  line-height: 1.55;
  margin-bottom: 16px;
}

.blog-card-meta {
  margin-top: auto;
  font-size: 12px;
  color: var(--ink-400);
  font-weight: 500;
}

.blog-empty { text-align: center; color: var(--ink-500); padding: 40px 0; font-size: 15px; }
.blog-empty[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .blog-featured-track { scroll-behavior: auto; }
  .blog-card:hover { transform: none; }
}

@media (max-width: 900px) {
  .blog-feat { grid-template-columns: 1fr; min-height: 0; }
  .blog-feat .blog-feat-cover { min-height: 180px; padding: 28px; }
  .blog-feat-cover-title { font-size: 25px; }
  .blog-feat-body { padding: 28px; }
  .blog-grid { grid-template-columns: repeat(2, 1fr); gap: 20px; }
}

@media (max-width: 560px) {
  .blog-grid { grid-template-columns: 1fr; }
}

/* --------------------------------------------------------------------------
   14. Post page — header, meta and the "On this page" index

   Quark 2 centres the post header and renders the H1 through a partial that
   wraps it in a self-link, so the title painted as a giant blue underlined
   heading while the body sat left-aligned to a reading measure. The template
   override (partials/blog-item.html.twig) fixes the markup; this styles it.
   -------------------------------------------------------------------------- */
.post-header {
  text-align: left;
  margin-bottom: 2.25rem;
  padding-bottom: 0.5rem;
}

.post-title {
  font-size: clamp(30px, 4.4vw, 46px);
  font-weight: 700;
  line-height: 1.14;
  letter-spacing: -0.03em;
  color: var(--ink-900);
  max-width: 20ch;
  margin: 0 0 0.6rem;
  text-wrap: balance;
}

/* The title is plain text on its own page, but guard against any inherited
   link styling if a hero variant ever renders it as an anchor. */
.post-title a { color: inherit; text-decoration: none; }

.post-subtitle {
  font-size: 1.125rem;
  line-height: 1.55;
  color: var(--ink-500);
  max-width: 60ch;
  margin: 0 0 1rem;
}

/* Date, read time and tags on one baseline instead of three stacked blocks. */
.post-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
  font-size: 0.875rem;
  color: var(--ink-400);
}

.post-meta .blog-date,
.post-meta .post-readtime { display: inline-flex; align-items: center; gap: 0.4em; }
.post-meta .tags { display: inline-flex; flex-wrap: wrap; gap: 0.4rem; }

/* --------------------------------------------------------------------------
   "On this page" — the post index in the sidebar
   -------------------------------------------------------------------------- */
.post-toc-block[hidden] { display: none; }

.post-toc-block {
  position: sticky;
  top: 1.5rem;
  background: var(--surface-1);
  border-radius: var(--r);
  padding: 1.1rem 1.2rem;
}

.post-toc-block h4 {
  margin: 0 0 0.7rem;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-400);
}

.post-toc ul { list-style: none; margin: 0; padding: 0; }

.post-toc li { margin: 0; border-radius: var(--r-sm); }

/* Nested headings indent, so the index reads as the post's shape. */
.post-toc-h3 a { padding-left: 1.6rem; font-size: 0.8125rem; }

.post-toc a {
  display: block;
  padding: 0.38rem 0.7rem;
  font-size: 0.875rem;
  line-height: 1.4;
  color: var(--ink-500);
  text-decoration: none;
  transition: color var(--speed-fast) var(--ease);
}

.post-toc a:hover { color: var(--brand); text-decoration: none; }
:root[data-theme="dark"] .post-toc a:hover { color: var(--brand-link-dark); }

/* Reading position, set by the IntersectionObserver in abscode-blog.js. */
/* Reading position by fill, the way the main site marks state. */
.post-toc li.is-active { background: color-mix(in srgb, var(--brand) 10%, transparent); }
.post-toc li.is-active > a { color: var(--ink-900); font-weight: 600; }

/* Anchored headings must clear the sticky nav when jumped to. */
.post-body h2,
.post-body h3 { scroll-margin-top: 90px; }

@media (prefers-reduced-motion: reduce) {
  .post-toc a { transition: none; }
}

/* Below the sidebar breakpoint the index would push the post down the page;
   it is redundant there anyway, since the headings are a short scroll away. */
@media (max-width: 900px) {
  .post-toc-block { display: none; }
}

/* --------------------------------------------------------------------------
   15. Post cover and inline diagrams

   Covers are generated PNGs committed as page media (scripts/gen-post-covers.mjs).
   Diagrams are inline SVG in the post body, drawn entirely from the theme tokens
   so they follow the hue wheel and flip to dark with everything else — no
   hardcoded colour, and no image request.
   -------------------------------------------------------------------------- */
.post-cover {
  margin: 0 0 1.75rem;
  border-radius: var(--r-lg);
  overflow: hidden;
  line-height: 0;
}

.post-cover img {
  display: block;
  width: 100%;
  height: auto;
}

/* Figures break out of the 68ch reading measure — they are meant to be looked
   at, not read at text width. */
.post-figure {
  margin: 2rem 0;
  max-width: 100%;
}

.post-figure figcaption {
  margin-top: 0.7rem;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--ink-400);
  max-width: 62ch;
}

.post-diagram {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* ---- diagram primitives ---- */
.post-diagram .d-box {
  fill: var(--surface-0);
  stroke: color-mix(in srgb, var(--brand) 26%, transparent);
  stroke-width: 1.5;
}

.post-diagram .d-box--muted {
  fill: color-mix(in srgb, var(--brand) 6%, var(--surface-0));
  stroke-dasharray: 4 4;
}

.post-diagram .d-box--brand {
  fill: color-mix(in srgb, var(--brand) 92%, black);
  stroke: none;
}

.post-diagram .d-label {
  fill: var(--ink-900);
  font-size: 15px;
  font-weight: 600;
  text-anchor: middle;
  font-family: inherit;
}

.post-diagram .d-label--sm { font-size: 13px; font-weight: 500; fill: var(--ink-500); }
.post-diagram .d-label--on-brand { fill: var(--brand-fg); }
.post-diagram .d-anchor-start { text-anchor: start; }

.post-diagram .d-lane {
  fill: var(--ink-400);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1.4px;
  font-family: inherit;
}

.post-diagram .d-lane--on-brand { fill: var(--brand-fg); opacity: 0.75; }

.post-diagram .d-flow {
  stroke: color-mix(in srgb, var(--brand) 45%, transparent);
  stroke-width: 2;
  fill: none;
  marker-end: none;
  stroke-linecap: round;
}

/* Weight carries the meaning: the heavy line is the 14 MB upload. */
.post-diagram .d-flow--heavy { stroke-width: 9; stroke: color-mix(in srgb, var(--brand) 55%, transparent); }
.post-diagram .d-flow--light { stroke-width: 2.5; }

.post-diagram .d-wire {
  fill: var(--ink-500);
  font-size: 12.5px;
  font-weight: 600;
  text-anchor: middle;
  font-family: inherit;
}

.post-diagram .d-wire--heavy { fill: var(--ink-900); }

.post-diagram .d-bar { fill: var(--ink-900); }
.post-diagram .d-bar--on-brand { fill: var(--brand-fg); opacity: 0.9; }

.post-diagram .d-rule {
  stroke: color-mix(in srgb, var(--brand) 22%, transparent);
  stroke-width: 3;
  stroke-linecap: round;
}

.post-diagram .d-rule--on-brand { stroke: var(--brand-fg); opacity: 0.35; }

/* A live QR reads as brand-coloured hatching; a masked one is flat and inert. */
.post-diagram .d-qr {
  fill: color-mix(in srgb, var(--brand) 18%, var(--surface-0));
  stroke: color-mix(in srgb, var(--brand) 45%, transparent);
  stroke-width: 1.5;
}

.post-diagram .d-qr--masked {
  fill: color-mix(in srgb, var(--brand-fg) 22%, transparent);
  stroke: color-mix(in srgb, var(--brand-fg) 40%, transparent);
}

.post-diagram .d-warn { fill: var(--ink-900); font-size: 12.5px; font-weight: 600; font-family: inherit; }
.post-diagram .d-ok { fill: var(--brand-fg); font-size: 12.5px; font-weight: 600; font-family: inherit; opacity: 0.9; }

@media (max-width: 640px) {
  .post-figure { margin: 1.5rem 0; }
  .post-figure figcaption { font-size: 0.8125rem; }
}

/* --------------------------------------------------------------------------
   Featured cover art — the post's own image behind the brand wash
   -------------------------------------------------------------------------- */
.blog-feat-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}

/* With art behind it the gradient becomes a scrim, so the chip and title stay
   readable over any image. The registration rings drop out — the artwork has
   its own. */
.blog-feat .blog-feat-cover.has-image {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--brand) 12%, transparent) 0%,
    color-mix(in srgb, #0B0F14 62%, transparent) 78%);
}

.blog-feat .blog-feat-cover.has-image::before { display: none; }

.blog-feat-cover.has-image .blog-card-cat,
.blog-feat-cover.has-image .blog-feat-cover-title,
.blog-feat-cover.has-image .blog-feat-cover-foot { position: relative; z-index: 2; }

/* The scrim sits above the image but below the copy. */
.blog-feat .blog-feat-cover.has-image::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(180deg, transparent 28%, color-mix(in srgb, #0B0F14 72%, transparent) 100%);
}

.blog-feat-cover.has-image .blog-feat-cover-title,
.blog-feat-cover.has-image .blog-feat-cover-foot { color: #fff; }

@media (max-width: 900px) {   /* search first on narrow screens */
}

/* ---- featured slide motion ----
   The art and the copy enter separately: the image settles from a slight
   over-scale while the copy lifts in just behind it. Transform and opacity
   only, so it composites on the GPU and never lands on the scroll path. */
.blog-feat-img {
  opacity: 0;
  transform: scale(1.04);
  transition:
    opacity 620ms var(--ease),
    transform 900ms var(--ease);
  will-change: opacity, transform;
}

.blog-feat.is-current .blog-feat-img {
  opacity: 1;
  transform: scale(1);
}

.blog-feat-body > * {
  opacity: 0;
  transform: translateY(10px);
  transition:
    opacity 520ms var(--ease),
    transform 520ms var(--ease);
}

.blog-feat.is-current .blog-feat-body > * { opacity: 1; transform: none; }

/* the stagger — each line follows the one above it */
.blog-feat.is-current .blog-feat-body > *:nth-child(1) { transition-delay: 90ms; }
.blog-feat.is-current .blog-feat-body > *:nth-child(2) { transition-delay: 150ms; }
.blog-feat.is-current .blog-feat-body > *:nth-child(3) { transition-delay: 210ms; }
.blog-feat.is-current .blog-feat-body > *:nth-child(4) { transition-delay: 270ms; }
.blog-feat.is-current .blog-feat-body > *:nth-child(5) { transition-delay: 330ms; }

/* Without JS nothing gets .is-current, so the slide must not stay invisible. */
.no-js .blog-feat-img,
.no-js .blog-feat-body > * { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .blog-feat-img,
  .blog-feat-body > * {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}

/* ---- cover art fit ----
   The covers are 1200x510. Letting the panel drive the ratio kept cropping the
   baked-in title, so the panel takes the art's aspect and the whole image is
   shown. */
.blog-feat .blog-feat-cover.has-image {
  padding: 0;
  background: var(--surface-2);
  display: block;
}

.blog-feat .blog-feat-cover.has-image::after { display: none; }

/* The covers are 1200x510 with their title set in the LEFT half, while this
   panel is far squarer - so a centred crop sliced the title off both ends
   (that is the clipped "...king, / r own hardware" in the report). Anchoring
   the crop left keeps the designed text whole at every panel width. */
.blog-feat-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: left center;
}

/* Stacked, the panel is wider than it is tall and much closer to the art's own
   ratio, so the whole cover reads - centre it there. */
@media (max-width: 900px) {
  .blog-feat-img { object-position: center; }
}

/* the slide is a two-up on desktop; the art keeps its own height */
.blog-feat { align-items: stretch; }

/* --------------------------------------------------------------------------
   Mobile
   -------------------------------------------------------------------------- */
@media (max-width: 900px) {
  /* Stacked, the art must not eat the fold before the headline. */
  .blog-feat { grid-template-columns: 1fr; }
  .blog-feat .blog-feat-cover.has-image { aspect-ratio: 16 / 9; }
  .blog-feat-body { padding: 22px 20px 26px; }
  .blog-feat-title { font-size: 20px; }
  .blog-feat-excerpt { font-size: 14.5px; }

  .blog-allposts-title { font-size: clamp(24px, 6vw, 32px); }
}

@media (max-width: 640px) {
  .blog-featured { margin-bottom: 2rem; }
  .blog-feat { border-radius: var(--r-lg); }
  .blog-featured-track { border-radius: var(--r-lg); }
  .blog-feat .blog-feat-cover.has-image { aspect-ratio: 3 / 2; }

  .post-title { max-width: none; }
  .post-meta { gap: 0.35rem 0.75rem; font-size: 0.8125rem; }
  .post-cover { border-radius: var(--r); margin-bottom: 1.25rem; }
  .post-figure { margin: 1.25rem 0; }
}

/* Carousel: dots only now, so give them the whole width to sit under. */
.blog-feat-dots { margin-top: 16px; }

/* ==========================================================================
   19. Listing toolbar — topic chips, search, type-ahead, result count

   THE SINGLE DEFINITION. Sections 13-18 grew four overlapping passes at this
   strip and they fought each other in the cascade; they were removed and
   replaced by this block. Keep it that way: change values here rather than
   adding another override later in the file.

   The governing idea is that the chips and the search are ONE control family.
   They share a height, a radius, a border and a surface, so the row reads as a
   single strip rather than a rail of pills with a form control parked beside it.
   ========================================================================== */

:root {
  --tb-h: 34px;                                   /* one height for the strip */
}

/* ---- the row ---- */
.blog-toolbar {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: start;              /* search aligns to the FIRST chip row */
  gap: 12px 20px;
  margin-bottom: 14px;
}

/* ---- topic chips ---- */
.blog-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 8px;
  min-width: 0;
}

.blog-chip {
  flex: 0 0 auto;                  /* never stretch to fill a wrapped row */
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: var(--tb-h);
  padding: 0 13px;
  border: 0;
  border-radius: var(--pill);
  background: var(--surface-1);
  color: var(--ink-700);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background var(--speed-fast) var(--ease),
    color var(--speed-fast) var(--ease);
}

.blog-chip:hover { background: var(--surface-2); color: var(--ink-900); }

.blog-chip.is-active {
  background: var(--brand);
  color: var(--brand-fg);
}

.blog-chip:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* the count reads as a quiet annotation, not a second label */
.blog-chip-n {
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  opacity: 0.55;
}

.blog-chip.is-active .blog-chip-n { opacity: 0.85; }

/* ---- search ---- */
.blog-search-box {
  position: relative;
  width: clamp(220px, 26vw, 280px);
}

.blog-search-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  height: var(--tb-h);
  padding: 0 6px 0 12px;
  border: 0;
  border-radius: var(--pill);
  background: var(--surface-1);
  cursor: text;
  transition: background var(--speed-fast) var(--ease), outline-color var(--speed-fast) var(--ease);
  outline: 2px solid transparent;
  outline-offset: -2px;
}

.blog-search-wrap:hover { background: var(--surface-2); }

.blog-search-wrap:focus-within {
  background: var(--surface-2);
  outline-color: var(--brand);
}

.blog-search-wrap > svg {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  color: var(--ink-400);
}

.blog-search-wrap:focus-within > svg { color: var(--brand); }

/* Quark 2 is built on Pico, which gives input[type=search] its own border,
   radius and a background-image magnifier. Left alone that painted a second
   rounded box with a second icon inside this pill. Reset to a bare field. */
.blog-search-wrap .blog-search,
.blog-search-wrap input[type="search"] {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  outline: 0;
  border-radius: 0;
  background: none;
  background-image: none;
  box-shadow: none;
  appearance: none;
  -webkit-appearance: none;
  font-family: inherit;
  font-size: 13.5px;
  line-height: 1;
  color: var(--ink-900);
}

.blog-search-wrap input[type="search"]::placeholder { color: var(--ink-400); opacity: 1; }

/* the browser's own clear affordance would be a third control in one pill */
.blog-search-wrap input[type="search"]::-webkit-search-decoration,
.blog-search-wrap input[type="search"]::-webkit-search-cancel-button,
.blog-search-wrap input[type="search"]::-webkit-search-results-button {
  -webkit-appearance: none;
  display: none;
}

.blog-search-clear {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: var(--pill);
  background: transparent;
  color: var(--ink-400);
  cursor: pointer;
  transition: background var(--speed-fast) var(--ease), color var(--speed-fast) var(--ease);
}

.blog-search-clear[hidden] { display: none; }
.blog-search-clear:hover { background: color-mix(in srgb, var(--brand) 12%, transparent); color: var(--ink-900); }
.blog-search-clear svg { width: 12px; height: 12px; }

/* ---- type-ahead ---- */
.blog-suggest {
  position: absolute;
  z-index: 30;
  top: calc(100% + 8px);
  right: 0;
  left: auto;
  width: max(100%, 280px);
  margin: 0;
  padding: 6px;
  list-style: none;
  background: var(--surface-2);
  border-radius: var(--r);
  max-height: 320px;
  overflow-y: auto;
}

.blog-suggest[hidden] { display: none; }
.blog-suggest-item { margin: 0; }

.blog-suggest-item a {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 11px;
  border-radius: var(--r-sm);
  font-size: 13.5px;
  line-height: 1.35;
  color: var(--ink-700);
  text-decoration: none;
}

.blog-suggest-item.is-active a,
.blog-suggest-item a:hover {
  background: color-mix(in srgb, var(--brand) 10%, transparent);
  color: var(--ink-900);
  text-decoration: none;
}

/* the matched run, not a highlighter pen */
.blog-suggest mark { background: none; color: var(--brand); font-weight: 700; padding: 0; }
:root[data-theme="dark"] .blog-suggest mark { color: var(--brand-link-dark); }

.blog-suggest-topic {
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 600;
  color: var(--ink-400);
}

/* ---- result count ---- */
.blog-count {
  font-size: 13px;
  color: var(--ink-500);
  margin-bottom: 22px;
}

/* ---- narrow ---- */
@media (max-width: 900px) {
  .blog-toolbar { grid-template-columns: 1fr; gap: 12px; }
  .blog-search-box { width: 100%; order: -1; }   /* search first */
  .blog-suggest { width: 100%; }
}

@media (max-width: 640px) {
  /* One chip row that scrolls sideways beats four stacked rows pushing the
     posts off the screen. */
  .blog-filters {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity;
    padding-bottom: 2px;
    margin-inline: -2px;
    padding-inline: 2px;
  }
  .blog-filters::-webkit-scrollbar { display: none; }
  .blog-chip { scroll-snap-align: start; }
  .blog-count { margin-bottom: 16px; }
  .blog-suggest { max-height: 260px; }
}

@media (prefers-reduced-motion: reduce) {
  .blog-chip,
  .blog-search-wrap,
  .blog-search-clear { transition: none; }
}

/* --------------------------------------------------------------------------
   20. Cross-property links

   The blog is one Abscode property among several, so the nav and the footer
   both point back to the others. In the nav they are the LAST items and read
   as secondary: this install's own pages come first.
   -------------------------------------------------------------------------- */
.navigation > .abs-nav-x > a { color: var(--ink-500); }
.navigation > .abs-nav-x > a:hover { color: var(--brand); }
:root[data-theme="dark"] .navigation > .abs-nav-x > a:hover { color: var(--brand-link-dark); }

/* In the mobile tree the items stack, so they only need the same muting. */
.tree.navigation > .abs-nav-x > a { color: var(--ink-500); }

.abs-footer__sites {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.1rem;
  font-size: 0.875rem;
}

.abs-footer__sites a { color: var(--ink-500); text-decoration: none; }
.abs-footer__sites a:hover { color: var(--brand); text-decoration: underline; }
:root[data-theme="dark"] .abs-footer__sites a:hover { color: var(--brand-link-dark); }

@media (max-width: 560px) {
  .abs-footer__sites { gap: 0.35rem 0.9rem; }
}
