/* ---------------------------------------------------------------------------
   Fonts. Self-hosted variable woff2, subset to latin + latin-ext.
   The `latin-ext` faces carry the Polish diacritics (ą ć ę ł ń ś ź ż) and are
   only fetched when such a character actually renders -- so the English pages
   never pay for them.
   `format('woff2')` and not the deprecated `format('woff2-variations')`, which
   some browsers ignore outright, silently dropping the face.
--------------------------------------------------------------------------- */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 300 600;
  font-display: swap;
  src: url("../fonts/ibm-plex-sans-latin-normal.992074e6a151.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 300 600;
  font-display: swap;
  src: url("../fonts/ibm-plex-sans-latin-ext-normal.093b1b15a13c.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: italic;
  font-weight: 300 600;
  font-display: swap;
  src: url("../fonts/ibm-plex-sans-latin-italic.103427ac9875.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: italic;
  font-weight: 300 600;
  font-display: swap;
  src: url("../fonts/ibm-plex-sans-latin-ext-italic.051ff674494d.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("../fonts/jetbrains-mono-latin-normal.60467a7343db.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("../fonts/jetbrains-mono-latin-ext-normal.bbc9b4253365.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* No JetBrains Mono italic face on purpose. The prototype's Google Fonts request
   (`JetBrains+Mono:wght@400;500;600`) never asked for one, so its four italic
   mono labels -- the "coming soon" tags and the Drifters build stub -- were
   always browser-synthesized obliques. Shipping the real faces would add 42 KB
   for a rendering the design never actually had. `font-synthesis-style` stays
   at its default so they keep slanting. */

/* ---------------------------------------------------------------------------
   Theme tokens. These were computed in JS in the prototype (`renderVals()`),
   which meant the server could not know them and every load flashed the wrong
   theme. They are now plain custom properties, set from `data-theme` on <html>
   by a blocking inline script before first paint.
--------------------------------------------------------------------------- */
:root,
:root[data-theme='dark'] {
  --accent: #E3A85C;
  --bg: #120f0b;
  --bg-visual: #120f0b;
  --fg: #ece7df;
  --grain-opacity: 0.05;
  --grain-blend: overlay;
}

:root[data-theme='light'] {
  /* Darker accent: #E3A85C on cream is ~1.9:1 against the background and
     fails WCAG AA for text. #9C6A2E clears 4.5:1. */
  --accent: #9C6A2E;
  --bg: #f6f2ea;
  --bg-visual: linear-gradient(165deg, #faf7f0 0%, #f1e9da 55%, #ede4d2 100%);
  --fg: #211d18;
  --grain-opacity: 0.06;
  --grain-blend: multiply;
}

:root {
  --font-sans: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;

  /* Alpha ramps over --fg, named by percentage. */
  --fg-04: color-mix(in srgb, var(--fg) 4%, transparent);
  --fg-10: color-mix(in srgb, var(--fg) 10%, transparent);
  --fg-12: color-mix(in srgb, var(--fg) 12%, transparent);
  --fg-15: color-mix(in srgb, var(--fg) 15%, transparent);
  --fg-20: color-mix(in srgb, var(--fg) 20%, transparent);
  --fg-25: color-mix(in srgb, var(--fg) 25%, transparent);
  --fg-30: color-mix(in srgb, var(--fg) 30%, transparent);
  --fg-35: color-mix(in srgb, var(--fg) 35%, transparent);
  --fg-40: color-mix(in srgb, var(--fg) 40%, transparent);
  --fg-45: color-mix(in srgb, var(--fg) 45%, transparent);
  --fg-50: color-mix(in srgb, var(--fg) 50%, transparent);
  --fg-55: color-mix(in srgb, var(--fg) 55%, transparent);
  --fg-60: color-mix(in srgb, var(--fg) 60%, transparent);
  --fg-65: color-mix(in srgb, var(--fg) 65%, transparent);
  --fg-70: color-mix(in srgb, var(--fg) 70%, transparent);
  --fg-75: color-mix(in srgb, var(--fg) 75%, transparent);
  --fg-85: color-mix(in srgb, var(--fg) 85%, transparent);
  --fg-90: color-mix(in srgb, var(--fg) 90%, transparent);
  --accent-50: color-mix(in srgb, var(--accent) 50%, transparent);
  --accent-70: color-mix(in srgb, var(--accent) 70%, transparent);
  --accent-85: color-mix(in srgb, var(--accent) 85%, transparent);

  /* Placeholder art (portrait slot, screenshot slot, diagram slot). The
     prototype hardcodes these dark values in both themes -- they stand in for
     real images, so they do not track the theme. */
  --slot-ink: #8a7a63;
  --slot-stripes: repeating-linear-gradient(135deg, #1a1510, #1a1510 10px, #221b13 10px, #221b13 20px);

  --shell: 1160px;
  --gutter: clamp(20px, 4vw, 48px);
  --rule: 1px solid var(--fg-12);
  /* Shared height for the two nav pills. They used to size themselves -- the
     language group from its text, the theme group from a hardcoded 27px -- and
     came out 31 and 27, misaligned by 2px at the top as well. */
  --control-h: 31px;
  /* Height of the mobile bottom nav bar, safe-area included, for anything
     pinned to the bottom edge to sit above. 0 above 720px, and 0 again on
     mobile while the bar is tucked away -- see the <=720px block. */
  --bar-offset: 0px;
}

/* --- reset ---------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  min-height: 100vh;
  /* No `background-attachment: fixed` -- it forces a repaint on every scroll
     frame on mobile. The gradient already spans the body's full height. */
  background: var(--bg-visual);
  color: var(--fg);
  font-family: var(--font-sans);
  /* Real 300..600 axis is loaded; never let the browser fake a weight. */
  font-synthesis-weight: none;
  position: relative;
}
a { color: inherit; text-decoration: none; }
a:hover { color: var(--accent); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* In-page jumps glide rather than teleport. Scoped to no-preference: the
   blanket reduced-motion rule below zeroes durations, but `scroll-behavior` is
   not a duration and would keep smooth-scrolling regardless. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

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

/* --- grain ---------------------------------------------------------------- */
/* The prototype's SVG had no width/height, so as a background-image it had no
   intrinsic size and the browser rasterized feTurbulence across the whole
   viewport -- then again on every resize. Giving it 128x128 plus
   stitchTiles='stitch' rasterizes the noise once and tiles it seamlessly.
   Same look, a fraction of the paint cost. */
.grain {
  /* Absolute, not fixed: the grain scrolls with the page as in the prototype,
     and fixed + mix-blend-mode is glitchy on iOS Safari. */
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: var(--grain-opacity);
  mix-blend-mode: var(--grain-blend);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='128' height='128'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  background-repeat: repeat;
  background-size: 128px 128px;
}

/* --- layout --------------------------------------------------------------- */
.shell {
  position: relative;
  z-index: 1;
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0 var(--gutter);
  /* Footer pinned to the bottom of short pages -- About is one paragraph and
     the footer used to float mid-screen under it. Flex column rather than
     grid because the content block is several sibling sections, not one
     element. Top-level blocks space themselves with padding, so losing margin
     collapse changes nothing. svh, not dvh: with dvh the footer would jump
     every time the mobile browser chrome collapsed. */
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100svh;
}
.section { padding: clamp(48px, 8vw, 84px) 0; border-top: var(--rule); }

/* --- nav ------------------------------------------------------------------ */
.nav {
  display: flex;
  align-items: center;
  gap: clamp(18px, 3vw, 34px);
  padding: 28px 0;
  border-bottom: var(--rule);
  flex-wrap: wrap;
}
.nav__brand { font-family: var(--font-mono); font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
.nav__brand span { color: var(--accent); }
.nav__links {
  display: flex;
  align-items: center;
  /* Brand left, links and switch right. The switch is a sibling of the links
     rather than a child, so that on mobile the links can leave for the bottom
     bar while the switch stays up here. */
  margin-left: auto;
  gap: clamp(18px, 3vw, 34px);
  font-family: var(--font-mono);
  font-size: 14px;
  flex-wrap: wrap;
  white-space: nowrap;
}
.nav__link { color: var(--fg-60); }
/* Home is a tab in the mobile bar only; on desktop the brand is the way home. */
.nav__link--home { display: none; }
.nav__link:hover, .nav__link.active { color: var(--fg); }

.switch { display: flex; align-items: center; gap: 10px; }
.switch__group {
  display: flex;
  align-items: center;
  height: var(--control-h);
  font-size: 13px;
  border: 1px solid var(--fg-25);
  border-radius: 4px;
  font-family: var(--font-mono);
}
.switch__lang { padding: 6px 10px; color: var(--fg-40); }
.switch__lang:hover { color: var(--fg); }
/* Accent, not plain ink: the same "this one is selected" signal as the
   theme control beside it. */
.switch__lang[aria-current='true'] { color: var(--accent); font-weight: 600; }
.switch__sep { color: var(--fg-25); }

.theme {
  display: flex;
  align-items: stretch;
  height: var(--control-h);
  border: 1px solid var(--fg-25);
  border-radius: 4px;
}
/* A segmented control, not two loose dots. The old chips were 14px targets
   with 6px between them -- fine for a cursor, hopeless for a thumb. Now each
   button fills the whole pill height and reads as its own segment, with a
   divider between the two so it is obvious there are two targets. The dot is
   just the label inside. */
.theme__swatch {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-width: 38px;
  padding: 0 11px;
  border: 0;
  background: none;
  color: var(--fg-40);
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.15s ease, color 0.15s ease;
}
.theme__swatch + .theme__swatch { border-left: 1px solid var(--fg-25); }
/* Inner corners follow the pill's radius, so a hover or active fill does not
   poke square corners out of a rounded frame. 3px, not 4: the frame's own
   1px border eats one. */
.theme__swatch:first-child { border-radius: 3px 0 0 3px; }
.theme__swatch:last-child { border-radius: 0 3px 3px 0; }
.theme__swatch:hover { background: var(--fg-10); color: var(--fg); }
/* The global focus ring sits 3px outside the element, which here lands on
   the neighbouring segment. Draw it inside instead. */
.theme__swatch:focus-visible { outline-offset: -2px; }

.theme__dot {
  flex: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  /* The rim is a theme token rather than a fixed rgba. Both dots show in both
     themes, so the dark one needs a light rim on the dark page and the light
     one a dark rim on the cream page -- hardcoding the two left whichever
     matched the background as an invisible patch. */
  border: 1px solid var(--fg-35);
  /* Dimmed, but not to 0.35 -- that turned the cream one into a grey dot,
     which is no preview at all. The inverted fill, not the opacity, is what
     marks the active one. */
  opacity: 0.6;
  transition: opacity 0.15s ease;
}
.theme__swatch:hover .theme__dot { opacity: 1; }
.theme__swatch--dark .theme__dot { background: #120f0b; }
.theme__swatch--light .theme__dot { background: #f6f2ea; }
/* Text label is for the mobile sidebar, where there is room and a bare dot
   next to another bare dot is the least obvious control on the page. On
   desktop the dots carry it, and each button has a `title` so a hover still
   spells it out. */
.theme__label { display: none; }

/* The active segment is marked in the accent, which belongs to neither
   theme. A 10% ink tint was too faint on the dark page; an inverted ink fill
   was loud but wrong -- a cream block under "Dark" reads as a light-theme
   preview. Amber tint, amber label, amber inset border: unmistakably the
   pressed one, and it says nothing about what the theme looks like. The dot
   does that. */
:root[data-theme='dark'] .theme__swatch--dark,
:root[data-theme='light'] .theme__swatch--light {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
  cursor: default;
}
:root[data-theme='dark'] .theme__swatch--dark .theme__dot,
:root[data-theme='light'] .theme__swatch--light .theme__dot {
  opacity: 1;
  border-color: transparent;
  box-shadow: 0 0 0 1.5px var(--accent);
}

/* --- nav: mobile bottom bar ----------------------------------------------- */
/* Below 720px the link row leaves the header and becomes a bar pinned to the
   bottom of the viewport: one tap per destination, all of them under the
   thumb, none hidden behind a hamburger. The language and theme switches stay
   in the header -- they are settings, not destinations.

   The bar tucks away on a scroll down and comes back on a scroll up (see
   theme.js), so a long article gets its screen back while reading. Anything
   else pinned to the bottom edge (the back-to-top and like chips) sits on
   --bar-offset, which the hidden state zeroes, so they ride down with it. */
@media (max-width: 720px) {
  :root {
    --bar-h: 54px;
    --bar-offset: calc(var(--bar-h) + env(safe-area-inset-bottom, 0px));
  }
  :root[data-bar-hidden] { --bar-offset: 0px; }
  /* Room for the bar under the footer, so the last line is never under it. */
  body { padding-bottom: calc(var(--bar-h) + env(safe-area-inset-bottom, 0px)); }
  /* The bar's reserved strip comes off the shell's minimum, or a short page
     would always scroll by exactly the bar's height. */
  .shell { min-height: calc(100svh - var(--bar-h) - env(safe-area-inset-bottom, 0px)); }

  .nav { padding: 20px 0; gap: 10px 8px; }
  /* Beside the brand when it fits, on its own row when it does not. */
  .switch { margin-left: auto; gap: 8px; --control-h: 36px; }
  .switch__lang { padding: 6px 9px; }
  .theme__swatch { min-width: 36px; padding: 0 9px; }

  .nav__links {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 30;
    margin: 0;
    gap: 0;
    flex-wrap: nowrap;
    height: var(--bar-h);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    box-sizing: content-box;
    /* Mostly the page colour, blurred over whatever is behind it. The page
       background is a gradient, so a flat --bg slab would sit a shade off at
       the bottom of a long page; letting some of the page through hides that. */
    background: color-mix(in srgb, var(--bg) 88%, transparent);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    border-top: var(--rule);
    transform: translateY(0);
    transition: transform 0.25s ease;
  }
  :root[data-bar-hidden] .nav__links { transform: translateY(100%); }

  .nav__link--home { display: flex; }
  .nav__link {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    font-size: 13px;
    text-transform: lowercase;
    color: var(--fg-60);
    /* The active mark is a 2px line on the tab's top edge, drawn over the
       bar's own 1px rule. */
    margin-top: -1px;
    border-top: 2px solid transparent;
    -webkit-tap-highlight-color: transparent;
  }
  /* Tabs as paths -- ~/ko, ~/blog, ~/kontakt -- since the bar is a prompt,
     not an app tray. */
  .nav__link::before { content: "~/"; color: var(--fg-35); }
  .nav__link:hover { color: var(--fg); }
  .nav__link.active { color: var(--accent); border-top-color: var(--accent); }
  .nav__link.active::before { color: var(--accent-70); }
}

/* ---------------------------------------------------------------------------
   Theme change as a reveal. The default View Transitions behaviour is a
   cross-fade of the whole page; here the outgoing snapshot holds still and a
   circular clip grows on the incoming one from the swatch that was clicked,
   so the new theme is uncovered rather than faded in.

   No glow on the leading edge: a drop-shadow would trace the clip circle, but
   `clip-path` is applied *after* `filter`, so the shadow is clipped away with
   everything else outside the circle. The edge stays a clean cut.
--------------------------------------------------------------------------- */
::view-transition-old(root) {
  animation: none;
  mix-blend-mode: normal;
  z-index: 1;
}
::view-transition-new(root) {
  animation: vt-reveal 620ms cubic-bezier(0.65, 0, 0.35, 1);
  mix-blend-mode: normal;
  z-index: 2;
}
/* theme.js sets the origin and radius from the swatch that was clicked, as
   percentages of the viewport (px went astray on scaled displays -- see the
   note in theme.js); custom properties on the root inherit into the
   view-transition pseudo tree. */
@keyframes vt-reveal {
  from { clip-path: circle(0 at var(--vt-x) var(--vt-y)); }
  to { clip-path: circle(var(--vt-r) at var(--vt-x) var(--vt-y)); }
}

/* --- type ----------------------------------------------------------------- */
.eyebrow { font-family: var(--font-mono); font-size: 14px; color: var(--accent); margin-bottom: 20px; }
.h1 { font-weight: 600; font-size: clamp(28px, 3.6vw, 42px); margin: 0 0 16px; letter-spacing: -0.01em; }
.lede { font-weight: 300; font-size: 17px; color: var(--fg-60); max-width: 480px; margin: 0 0 40px; }

/* The prototype used the glyph U+258C for the terminal cursor, which is absent
   from every latin subset -- it silently fell back to a system font. A box is
   crisper, matches the accent colour, and costs nothing. */
.cursor {
  display: inline-block;
  width: 0.55em;
  height: 1em;
  margin-left: 2px;
  vertical-align: text-bottom;
  background: currentColor;
  animation: blink 1s step-end infinite;
}
@keyframes blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

/* --- contact -------------------------------------------------------------- */
.contact-grid {
  display: grid;
  grid-template-columns: minmax(280px, 1fr) minmax(260px, 340px);
  gap: 60px;
  padding: clamp(48px, 8vw, 84px) 0;
  align-items: start;
}
@media (max-width: 720px) {
  .contact-grid { grid-template-columns: 1fr; gap: 40px; }
  .contact-aside { border-left: none !important; padding-left: 0 !important; border-top: var(--rule); padding-top: 32px; }
}

.form { display: flex; flex-direction: column; gap: 22px; max-width: 440px; }
.field { border-bottom: 1px solid var(--fg-20); padding-bottom: 10px; }
.field__label { font-family: var(--font-mono); font-size: 12px; color: var(--fg-40); margin-bottom: 6px; }
.field input,
.field textarea {
  font-family: var(--font-mono);
  background: transparent;
  border: none;
  outline: none;
  color: var(--fg);
  font-size: 15px;
  width: 100%;
  resize: vertical;
}
.field input::placeholder, .field textarea::placeholder { color: var(--fg-30); }
.field:focus-within { border-bottom-color: var(--accent); }

.btn {
  align-self: flex-start;
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 15px;
  border: 1px solid var(--accent);
  background: transparent;
  color: var(--accent);
  padding: 13px 24px;
  border-radius: 4px;
  cursor: pointer;
  /* No nowrap: the case-study CTAs are a full sentence, and on a 350px
     column an unbreakable button is the widest thing on the page. */
  max-width: 100%;
  transition: background 0.15s ease, color 0.15s ease;
}
.btn:hover { background: var(--accent); color: var(--bg); }

.errors { font-family: var(--font-mono); font-size: 13px; color: #e2725b; margin: 0; padding: 0; list-style: none; }
:root[data-theme='light'] .errors { color: #a3341f; }
.field__error { margin-top: 6px; }

/* Honeypot. Off-screen rather than display:none -- some bots skip hidden
   inputs, and `visibility:hidden`/`display:none` are the first things they
   check. Screen readers skip it via aria-hidden + tabindex="-1". */
.hp {
  position: absolute !important;
  left: -9999px !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden !important;
}

.contact-aside { border-left: var(--rule); padding-left: 40px; }
.contact-aside__heading { font-family: var(--font-mono); font-size: 13px; color: var(--fg-40); margin-bottom: 20px; }
.contact-aside__links { display: flex; flex-direction: column; gap: 16px; font-family: var(--font-mono); font-size: 15px; }
.contact-aside__links a { color: var(--fg-75); }
.contact-aside__links a:hover { color: var(--accent); }

/* --- copy-to-clipboard email ---------------------------------------------- */
/* Still an <a href="mailto:"> in the markup, and JS cancels the navigation to
   copy instead. That ordering matters: with JS off, or if the Clipboard API is
   unavailable (it needs a secure context, so plain http:// on a LAN address
   has none), the link keeps working the old way rather than becoming dead
   text. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.mail { position: relative; cursor: pointer; }
.mail::after {
  content: attr(data-copied);
  position: absolute;
  bottom: calc(100% + 9px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  padding: 5px 10px;
  border: 1px solid var(--accent-50);
  border-radius: 4px;
  background: color-mix(in srgb, var(--accent) 12%, var(--bg));
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.mail.is-copied::after { opacity: 1; transform: translateX(-50%) translateY(0); }

/* --- back to top ---------------------------------------------------------- */
/* Hidden at the top of the page, where it would be pointing at where you
   already are. Revealed by the scroll position itself rather than a JS scroll
   listener -- no handler firing on every frame, and it works with JS off.

   `scroll()` needs the same @supports gate as the coop rail: unsupported, the
   rule falls back to the document timeline, plays once on load and leaves the
   button visible at the very top. Outside the gate it is always visible, which
   is merely redundant rather than broken. */
.totop {
  position: fixed;
  right: clamp(16px, 3vw, 32px);
  bottom: calc(clamp(16px, 3vw, 32px) + var(--bar-offset));
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border: 1px solid var(--fg-20);
  border-radius: 6px;
  background: color-mix(in srgb, var(--fg) 6%, var(--bg));
  color: var(--fg-55);
  font-family: var(--font-mono);
  font-size: 17px;
  line-height: 1;
  backdrop-filter: blur(6px);
  transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease, bottom 0.25s ease;
}
.totop:hover {
  color: var(--accent);
  border-color: var(--accent-50);
  transform: translateY(-2px);
}

/* Like sits above the back-to-top chip so the two don't overlap. */
.notelike {
  position: fixed;
  right: clamp(16px, 3vw, 32px);
  bottom: calc(clamp(16px, 3vw, 32px) + 52px + var(--bar-offset));
  z-index: 5;
  margin: 0;
}
.notelike__btn {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  min-width: 42px;
  padding: 8px 6px 6px;
  border: 1px solid var(--fg-20);
  border-radius: 6px;
  background: color-mix(in srgb, var(--fg) 6%, var(--bg));
  color: var(--fg-55);
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1;
  cursor: pointer;
  backdrop-filter: blur(6px);
  overflow: visible;
  transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease, bottom 0.25s ease;
}
.notelike__btn:hover {
  color: var(--accent);
  border-color: var(--accent-50);
  transform: translateY(-2px);
}
.notelike__btn:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 2px;
}
.notelike__icon {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linejoin: round;
}
.notelike[data-liked='1'] .notelike__btn {
  color: var(--accent);
  border-color: var(--accent-50);
  cursor: default;
  transform: none;
}
.notelike[data-liked='1'] .notelike__icon { fill: var(--accent); stroke: var(--accent); }
.notelike[data-liked='1'] .notelike__btn:hover { transform: none; }

/* Both chips stay fixed on phones too, bottom right above the nav bar
   (they ride on --bar-offset). They do cover the last few characters of a
   long line while scrolled to them; keeping them in one place won over
   moving them into the flow. */

.notelike.is-pop .notelike__icon {
  transform-origin: 30% 90%;
  animation: notelike-thumb 0.55s cubic-bezier(0.2, 1.6, 0.4, 1);
}
.notelike.is-pop .notelike__n {
  animation: notelike-count 0.4s ease;
}
.notelike.is-pop .notelike__btn::after {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: 8px;
  border: 1px solid var(--accent);
  pointer-events: none;
  animation: notelike-ring 0.5s ease-out forwards;
}

@keyframes notelike-thumb {
  0% { transform: scale(1) rotate(0deg); }
  32% { transform: scale(1.4) rotate(-22deg); }
  58% { transform: scale(0.9) rotate(10deg); }
  100% { transform: scale(1) rotate(0deg); }
}
@keyframes notelike-count {
  0% { transform: translateY(0); }
  45% { transform: translateY(-5px); }
  100% { transform: translateY(0); }
}
@keyframes notelike-ring {
  from { opacity: 0.85; transform: scale(0.8); }
  to { opacity: 0; transform: scale(1.5); }
}

@supports (animation-timeline: scroll()) {
  .totop {
    opacity: 0;
    animation: totop-in linear both;
    animation-timeline: scroll();
    /* Appears across the first viewport-and-a-bit of scrolling. */
    animation-range: 220px 520px;
  }
  @keyframes totop-in {
    from { opacity: 0; visibility: hidden; }
    to { opacity: 1; visibility: visible; }
  }
}

/* --- footer --------------------------------------------------------------- */
.footer {
  margin-top: auto;
  padding: 22px 0 40px;
  border-top: var(--rule);
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: space-between;
  align-items: center;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--fg-35);
}
.footer a:hover { color: var(--fg); }

/* --- footer note (home, case studies) ------------------------------------- */
.footer__note { font-family: var(--font-sans); font-style: italic; max-width: 420px; text-align: right; }

/* --- stack table (home, below the writing) -------------------------------- */
.stack { padding: clamp(44px, 6vw, 68px) 0; border-top: var(--rule); }
.stack .eyebrow { margin-bottom: 16px; }
.stack__heading { font-weight: 600; font-size: clamp(22px, 2.6vw, 28px); line-height: 1.25; max-width: 620px; margin: 0; }

/* --- hero (home) ---------------------------------------------------------- */
.hero { padding: clamp(56px, 9vw, 96px) 0 clamp(44px, 6vw, 72px); }
.hero__prompt { font-family: var(--font-mono); font-size: 15px; color: var(--accent); margin-bottom: 24px; min-height: 20px; }
/* One step down from the old 58px: the claim is a longer sentence than the
   name-and-role line it replaced, and at 58px it ran to four lines. */
.hero__heading { font-weight: 600; font-size: clamp(31px, 4.1vw, 52px); line-height: 1.16; max-width: 900px; letter-spacing: -0.01em; margin: 0; }
.hero__sub { font-family: var(--font-mono); font-size: 15px; line-height: 1.75; color: var(--fg-55); margin: 26px 0 0; max-width: 620px; }
.hero__sub a { color: var(--accent); }
.hero__sub a:hover { color: var(--fg); }

/* The claim above, its two paragraphs of proof, and the agent stack that
   shows the same thing in one glance. Was `.approach__grid`, one section
   further down, back when the hero made a different argument. */
.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(290px, 380px);
  gap: clamp(28px, 5vw, 64px);
  align-items: center;
  margin-top: clamp(34px, 4vw, 48px);
}

/* Signature: the stack as `ps` output. Closed it is a single full-width line --
   one amber owner fanning out to five layers -- so the claim lands without a
   click and without a tall column standing beside a two-line paragraph. Open
   it becomes the table, with what each layer is built with.
   Deliberately not a window like .agent-card below: a table, so the page does
   not tell the same joke twice. */
.ps {
  margin-top: 26px;
  border: 1px solid var(--fg-15);
  border-radius: 8px;
  background: color-mix(in srgb, var(--fg) 4%, var(--bg));
  font-family: var(--font-mono);
}

.ps__summary {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 13px 16px;
  font-size: 12.5px;
  /* A fixed line box, not a ratio. The chain and the `~/stack` title swap
     places on open at different font sizes, so a ratio made the summary 9px
     shorter the instant it opened -- the header visibly jolted before the
     panel had begun to expand. Both children now inherit the same line box. */
  line-height: 24px;
  cursor: pointer;
  list-style: none;
  border-radius: 8px;
}
.ps__summary::-webkit-details-marker { display: none; }
.ps__summary:hover .ps__chain-item { color: var(--fg-85); }
/* The rule is always in the box and only becomes visible on open, so the
   summary does not gain a pixel of height at the moment it expands. */
.ps__summary { border-bottom: 1px solid transparent; }
.ps[open] .ps__summary { border-bottom-color: var(--fg-10); border-radius: 8px 8px 0 0; }

/* Two drawn bars rather than a "+" / "\2212" glyph swap: `content` cannot be
   transitioned, so a swap can only ever pop. Rotating the upright bar onto the
   flat one turns the plus into a minus over the same 300ms the panel takes. */
.ps__mark {
  position: relative;
  flex: none;
  margin-left: auto;
  width: 11px;
  height: 11px;
  align-self: center;
}
.ps__mark::before,
.ps__mark::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  background: var(--accent);
  transition: transform 300ms cubic-bezier(0.65, 0, 0.35, 1);
}
.ps__mark::before {
  width: 11px;
  height: 1.5px;
  transform: translate(-50%, -50%);
}
.ps__mark::after {
  width: 1.5px;
  height: 11px;
  transform: translate(-50%, -50%);
}
.ps[open] .ps__mark::after { transform: translate(-50%, -50%) rotate(90deg); }

/* Closed, the chain carries the message; open, it would only repeat the LAYER
   column, so it gives way to the pane's title. */
.ps__title { display: none; font-size: 11px; color: var(--fg-40); }
.ps[open] .ps__title { display: block; }
.ps[open] .ps__chain { display: none; }
.ps__chain { display: block; min-width: 0; }
.ps__owner { color: var(--accent); }
/* A colon, not an arrow glyph: U+25B8 and U+2192 both fall outside the mono
   subset's unicode-range, so they drop to a fallback face and read as one more
   separator dot -- which turns "one owner, five layers" back into a six-item
   list. The colon carries the ownership and is in every face. Generated rather
   than marked up, so no inter-tag whitespace opens a gap before it. */
.ps__chain .ps__owner::after { content: ":"; color: var(--fg-30); margin-right: 8px; }

/* Separators trail their item rather than leading the next one: when the chain
   wraps on a phone, a line must not open with a stray dot. `nowrap` keeps a
   layer like "backend / API" from splitting across the break. */
.ps__chain-item { color: var(--fg-70); white-space: nowrap; transition: color 0.15s ease; }
.ps__chain-item:not(:last-child)::after {
  content: "\00b7";
  color: var(--fg-25);
  margin: 0 7px;
}

.ps__head, .ps__row {
  display: grid;
  /* LAYER is capped rather than 1fr so it does not open a void between itself
     and STACK; the slack goes to STACK the way real `ps` output packs. */
  grid-template-columns: minmax(130px, 220px) minmax(0, 1fr);
  gap: 4px 16px;
  align-items: baseline;
  padding: 0 16px;
}
.ps__head {
  font-size: 10px;
  letter-spacing: 0.09em;
  color: var(--fg-30);
  padding-top: 14px;
  padding-bottom: 4px;
}
.ps__list { list-style: none; margin: 0; padding: 0 0 12px; }
.ps__row { font-size: 12.5px; padding-top: 7px; padding-bottom: 7px; }
.ps__layer { color: var(--fg-70); }
.ps__stack { color: var(--fg-45); }

/* <details> has no open/close animation of its own -- the panel simply appears.
   `::details-content` is the handle for it, and `interpolate-size` is what
   makes `block-size: auto` a transitionable value; without it there is no
   target height to animate toward. `content-visibility` has to ride along as a
   discrete transition, otherwise the content unhides instantly and the height
   animates around already-visible rows.

   Progressive enhancement: browsers without `::details-content` ignore all of
   this and the panel snaps open, exactly as it did before.

   No stagger on the rows any more. The height reveal already uncovers them in
   sequence, and fading each one in on top of that was two animations doing one
   job. */
:root { interpolate-size: allow-keywords; }
.ps::details-content {
  block-size: 0;
  overflow: hidden;
  transition: block-size 320ms cubic-bezier(0.4, 0, 0.2, 1),
              content-visibility 320ms allow-discrete;
}
.ps[open]::details-content { block-size: auto; }

@media (prefers-reduced-motion: reduce) {
  .ps::details-content { transition: none; }
  .ps__mark::before, .ps__mark::after { transition: none; }
}

/* Narrow: STACK drops to its own line under LAYER rather than squeezing two
   columns into a phone width. */
@media (max-width: 640px) {
  .ps__head { grid-template-columns: minmax(0, 1fr); }
  .ps__head span:last-child { display: none; }
  .ps__row { grid-template-columns: minmax(0, 1fr); }
  .ps__stack { font-size: 11.5px; }
}

/* --- notes list (home teaser + /notes) ------------------------------------ */
.cs { padding: clamp(20px, 3vw, 32px) 0 clamp(28px, 4vw, 40px); border-top: var(--rule); }
.cs__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 16px;
}
.cs__head .eyebrow { margin-bottom: 0; }
.cs__all {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--accent);
  white-space: nowrap;
}
.cs-intro { padding: clamp(48px, 8vw, 80px) 0 clamp(28px, 4vw, 40px); }
.cs-intro__heading { font-weight: 600; font-size: clamp(30px, 4vw, 46px); line-height: 1.15; margin: 0 0 18px; }
.cs-intro__sub { font-family: var(--font-mono); font-size: 16px; line-height: 1.7; color: var(--fg-55); max-width: 620px; margin: 0; }
.cs-intro__curl {
  font-family: var(--font-mono);
  font-size: 14px;
  margin: 18px 0 0;
}
.cs-intro__curl a { color: var(--accent); }
.cs-intro__curl a:hover { color: var(--fg); }
.cs-list { padding-bottom: 32px; }

/* auto-fill, not auto-fit: a single latest note must not stretch full-row. */
.cs-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: 16px;
}
.cs-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 22px 20px;
  border: 1px solid var(--fg-12);
  border-radius: 10px;
  background: color-mix(in srgb, var(--fg) 3%, transparent);
  transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
}
/* The global `a:hover { color: var(--accent) }` would repaint every word in
   the card amber. Only the title should react. */
.cs-card:hover {
  color: inherit;
  border-color: var(--accent-50);
  background: color-mix(in srgb, var(--fg) 6%, transparent);
  transform: translateY(-3px);
}
.cs-card__title {
  margin: 0;
  font-weight: 500;
  font-size: 18px;
  line-height: 1.35;
  letter-spacing: -0.01em;
  transition: color 0.18s ease;
}
.cs-card:hover .cs-card__title { color: var(--accent); }
/* Pushes the tag rows to the bottom, so cards of unequal text still line their
   tags up across the row. */
.cs-card__date {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--fg-40);
  letter-spacing: 0.04em;
}
.cs-card__hook { margin: 0; flex: 1 1 auto; font-size: 14.5px; line-height: 1.6; color: var(--fg-55); }

.cs-card__tags { display: flex; flex-wrap: wrap; gap: 6px; }
.cs-card__tags--out { padding-top: 12px; border-top: 1px solid var(--fg-10); }

.tag {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.4;
  padding: 4px 8px;
  border: 1px solid var(--fg-15);
  border-radius: 4px;
  color: var(--fg-50);
  white-space: nowrap;
}
.tag--out {
  border-color: var(--accent-50);
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

/* A row of the index. Three columns: the `ls -lt` rail, the text, the still.
   The whole row is the link, so nothing inside it needs its own hit target --
   the arrow after the title is the affordance and the row background is the
   feedback. */
.cs-item {
  display: grid;
  grid-template-columns: 108px minmax(0, 1fr);
  gap: 0 30px;
  align-items: start;
  padding: 28px 14px;
  /* Same geometry as a row inside a series (16px rail + 14px padding), so
     the dates, titles, stills and separators of a lone post line up with
     the series rows above and below it. The rail line is then the only
     thing that differs. Used to be `margin: 0 -14px`, which hung the lone
     rows 30px left of everything else. */
  margin: 0 0 0 16px;
  /* No radius: the hover background is a band between two hairlines, and
     rounded corners on it looked like a card trying to escape a table. */
  border-top: 1px solid var(--fg-12);
  transition: background 0.18s ease;
}
.cs-item--has-art { grid-template-columns: 108px minmax(0, 1fr) 208px; }
a.cs-item:hover { background: var(--fg-04); }
/* No closing rule on the last row: the footer's own top rule follows a
   few lines below, and two hairlines that close together read as a mistake. */
.cs-item--last { border-bottom: 0; }

/* When it went up and how long it takes -- the two facts a reader scans a
   blog index for before committing. Mono, so the column aligns down the page. */
.cs-item__rail {
  grid-column: 1;
  padding-top: 4px;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.5;
  letter-spacing: 0.02em;
}
.cs-item__date { display: block; color: var(--fg-60); white-space: nowrap; }
.cs-item__mins { display: block; margin-top: 5px; color: var(--fg-45); white-space: nowrap; }

.cs-item__body { grid-column: 2; min-width: 0; }
.cs-item__series {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--accent-85);
  letter-spacing: 0.02em;
  margin: 0 0 8px;
}
.cs-item__title {
  font-weight: 500;
  font-size: clamp(19px, 2.2vw, 24px);
  line-height: 1.3;
  letter-spacing: -0.01em;
  margin: 0 0 10px;
  transition: color 0.18s ease;
}
a.cs-item:hover .cs-item__title { color: var(--accent); }
/* Always visible, so a touch reader gets the same affordance as a hover one. */
.cs-item__arrow {
  display: inline-block;
  margin-left: 10px;
  color: var(--accent);
  opacity: 0.5;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
a.cs-item:hover .cs-item__arrow { opacity: 1; transform: translateX(4px); }
.cs-item__hook {
  font-weight: 300;
  font-size: 15.5px;
  line-height: 1.65;
  color: var(--fg-60);
  max-width: 60ch;
  margin: 0;
}
.cs-item__tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 16px; }

/* --- series group --------------------------------------------------------- */
.cs-series { padding-top: 10px; }
.cs-series__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  /* 30px = the rail's 16 plus the row's own 14, so the label starts on the
     same vertical as the dates below it. */
  padding: 0 14px 10px 30px;
}
.cs-series__head .eyebrow { margin-bottom: 0; }
.cs-series__count { font-family: var(--font-mono); font-size: 12px; color: var(--fg-35); }
/* The rail is the whole device: it says these rows are one sequence. It
   starts as the plain hairline under the `$ series` label and warms to
   amber by the last part: the series builds towards something. Solid all
   the way, never fading out. border-image, since a border cannot be a
   gradient. */
.cs-series__body {
  padding-left: 16px;
  border-left: 1px solid transparent;
  border-image: linear-gradient(to bottom, var(--fg-15), var(--accent)) 1;
}
.cs-series__body .cs-item { margin-left: 0; }
.cs-series--last .cs-item:last-child { border-bottom: 0; }

.badge {
  font-family: var(--font-mono);
  font-size: 12px;
  border: 1px solid var(--accent-50);
  color: var(--accent);
  padding: 5px 10px;
  border-radius: 4px;
  white-space: nowrap;
}
.badge--muted { border-color: var(--fg-25); color: var(--fg-60); }

/* --- error pages ---------------------------------------------------------- */
.errorpage { padding: clamp(56px, 9vw, 96px) 0; }
.errorpage__out { font-family: var(--font-mono); font-size: 14px; color: var(--fg-45); margin: 0 0 32px; word-break: break-all; }
.errorpage__actions { display: flex; align-items: center; gap: 24px; flex-wrap: wrap; }
.errorpage__link { font-family: var(--font-mono); font-size: 15px; color: var(--fg-60); }
.errorpage__link:hover { color: var(--fg); }

/* --- how we work together (home) ------------------------------------------ */
/* Deliberately the quietest section on the page. Its job is to answer "what
   happens if I write to you", and a prospective client reading it wants
   legibility, not personality -- the hero already
   carry that. The only device is a hairline rail down the step numbers, which
   encodes the one thing that is actually true of this content: it is a
   sequence. */
.coop { padding: clamp(44px, 6vw, 68px) 0; border-top: var(--rule); }
.coop .eyebrow { margin-bottom: 16px; }
.coop__heading { font-weight: 600; font-size: clamp(22px, 2.6vw, 28px); margin: 0 0 8px; }
.coop__lede { font-family: var(--font-mono); font-size: 14px; color: var(--fg-45); margin: 0 0 28px; }

.coop__list { list-style: none; margin: 0; padding: 0; }
.coop__step {
  position: relative;
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr);
  gap: 20px;
  padding: 14px 0;
}
/* The rail runs from under one number to the next, so it stitches the steps
   together without drawing a box around them. */
.coop__step:not(:last-child)::before {
  content: "";
  position: absolute;
  left: 9px;
  top: 32px;
  bottom: 2px;
  width: 1px;
  background: var(--fg-12);
}
.coop__n { font-family: var(--font-mono); font-size: 13px; color: var(--accent); line-height: 1.7; }

/* The pipeline draws itself as you read down it: each rail segment grows from
   its number and each number warms to the accent as its step enters view.
   Scroll-driven, so the motion tracks reading position instead of firing on a
   timer -- the section is well below the fold and a timed reveal would have
   played to nobody.

   Gated on @supports: without scroll-driven timelines these same rules would
   fall back to the document timeline and play once on load, far off-screen,
   leaving the rail stuck at scaleY(0). Outside the gate the rail is simply
   drawn, which is the correct resting state. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .coop__step:not(:last-child)::before {
      transform-origin: top;
      animation: coop-rail linear both;
      animation-timeline: view();
      animation-range: entry 15% cover 25%;
    }
    .coop__n {
      animation: coop-num linear both;
      animation-timeline: view();
      animation-range: entry 15% cover 20%;
    }
  }
}
@keyframes coop-rail { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes coop-num { from { color: var(--fg-25); } to { color: var(--accent); } }
.coop__text {
  margin: 0;
  font-size: 16px;
  line-height: 1.7;
  color: var(--fg-75);
  max-width: 620px;
}
/* --- closing CTA (home) --------------------------------------------------- */
.cta { padding: clamp(48px, 8vw, 84px) 0 clamp(32px, 5vw, 48px); border-top: var(--rule); }
.cta .eyebrow { margin-bottom: 18px; }
.cta__heading { font-weight: 600; font-size: clamp(28px, 3.6vw, 42px); margin: 0 0 14px; }
.cta__sub { font-weight: 300; font-size: 17px; color: var(--fg-60); max-width: 520px; margin: 0 0 32px; }
.cta__row { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; }
.cta__link { font-family: var(--font-mono); font-size: 14px; color: var(--fg-55); white-space: nowrap; }
.cta__link:hover { color: var(--fg); }

/* --- about ---------------------------------------------------------------- */
.about {
  /* One column while there is no portrait. With one, this was
     `grid-template-columns: minmax(200px, 260px) 1fr; gap: 60px` and a
     single column under 720px. */
  padding: clamp(48px, 7vw, 80px) 0 clamp(36px, 5vw, 52px);
}
.about .eyebrow { margin-bottom: 22px; }
.about__heading { font-weight: 600; font-size: clamp(28px, 4vw, 42px); margin: 0 0 24px; }
.about__bio { font-weight: 300; font-size: 18px; line-height: 1.75; color: var(--fg-70); max-width: 640px; margin: 0; }

/* Faux terminal/file window, used for code samples. */
.window { border: 1px solid var(--fg-15); border-radius: 6px; overflow: hidden; }
.window__bar { display: flex; align-items: center; gap: 8px; padding: 10px 14px; background: var(--fg-04); border-bottom: 1px solid var(--fg-10); }
.window__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--fg-25); }
.window__name { font-family: var(--font-mono); font-size: 11px; color: var(--fg-40); margin-left: 6px; }
.window__body { padding: 22px; font-family: var(--font-mono); font-size: 13px; color: var(--fg-35); }


/* --- drifters ------------------------------------------------------------- */
.drifters__head { padding: clamp(48px, 8vw, 80px) 0 clamp(28px, 4vw, 36px); }
.drifters__head .eyebrow { margin-bottom: 22px; }
.drifters__title { font-weight: 600; font-size: clamp(44px, 7vw, 84px); line-height: 1; margin: 0 0 22px; }
.drifters__tagline { font-weight: 300; font-style: italic; font-size: 19px; line-height: 1.6; color: var(--fg-65); max-width: 640px; margin: 0; }
.drifters__play { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; padding-bottom: 56px; }

/* Image slots that stand in for screenshots/diagrams. */
.slot {
  aspect-ratio: 16 / 8;
  background: var(--slot-stripes);
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--slot-ink);
  margin-bottom: 28px;
}
.slot--tight { border-radius: 6px; margin-bottom: 0; }

.stub {
  font-family: var(--font-mono);
  font-size: 15px;
  border: 1px dashed var(--fg-30);
  color: var(--fg-45);
  padding: 13px 24px;
  border-radius: 4px;
  font-style: italic;
}

.module { padding: 32px 0; border-top: var(--rule); }
.module__h { font-weight: 600; font-size: 24px; margin: 0 0 14px; }
.module__p { font-weight: 300; font-size: 16px; line-height: 1.75; color: var(--fg-65); max-width: 640px; margin: 0; }
.module__p--accent { font-style: italic; color: var(--accent-70); }

.devlog .eyebrow { margin-bottom: 16px; }
.devlog__row { display: flex; flex-wrap: wrap; gap: 14px; align-items: baseline; font-size: 15px; font-style: italic; color: var(--accent-70); }
.devlog__date { font-family: var(--font-mono); font-size: 12px; color: var(--fg-35); font-style: normal; }

.context { padding: 32px 0 56px; border-top: var(--rule); }
.context__heading { font-family: var(--font-mono); font-size: 14px; color: var(--fg-40); margin-bottom: 16px; }
.context__links { display: flex; flex-direction: column; gap: 10px; }
.context__links a { font-size: 15px; color: var(--fg-70); }
.context__links a:hover { color: var(--accent); }

/* --- article (collidergrid) ----------------------------------------------- */
.backlink-row { padding: 20px 0 0; }
.backlink { font-family: var(--font-mono); font-size: 13px; color: var(--fg-45); }
.backlink:hover { color: var(--fg); }

.article__head { padding: 36px 0 8px; }
.article__head .eyebrow { margin-bottom: 22px; }
.article__title { font-weight: 600; font-size: clamp(28px, 4vw, 44px); line-height: 1.18; max-width: 820px; margin: 0 0 20px; }
.article__dek { font-weight: 300; font-size: 18px; line-height: 1.6; color: var(--fg-60); max-width: 680px; margin: 0 0 22px; }
.note-series {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 12px;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--accent-85);
  letter-spacing: 0.02em;
  margin: -8px 0 18px;
}
.note-series__name { color: var(--accent-85); }
/* Pills, not loose digits: "1 2" after a series name reads as noise until the
   parts look like something you can go to. */
.note-series__parts { display: inline-flex; gap: 6px; }
.note-series__parts a,
.note-series__parts [aria-current='page'] {
  display: inline-block;
  min-width: 24px;
  padding: 3px 7px;
  border: 1px solid var(--fg-15);
  border-radius: 4px;
  text-align: center;
  line-height: 1.2;
  color: var(--fg-45);
  transition: color 0.15s ease, border-color 0.15s ease;
}
.note-series__parts a:hover { color: var(--accent); border-color: var(--accent-50); }
.note-series__parts [aria-current='page'] {
  color: var(--accent);
  border-color: var(--accent-50);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
/* Date, reading time and tags are one band, not three stacked rows: they
   answer the same question -- is this worth my next seven minutes. */
.article__meta { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; font-family: var(--font-mono); font-size: 12px; color: var(--fg-45); }
.article__tags { display: inline-flex; flex-wrap: wrap; gap: 6px; margin-left: 8px; }
.article__context { font-family: var(--font-mono); font-size: 12px; color: var(--fg-40); margin-top: 10px; }

/* --- note player ---------------------------------------------------------- */
/* $ play is a command, same family as $ cat / $ copy / $ next. Inverted fill
   while playing is the on-air light; the meter is the same 1px rule the rest
   of the page is drawn with, not a rounded scrubber. Sticky so the command
   stays on the article while the body scrolls — containing block is the
   <article>, so it lets go at the pager, not the footer. */
.note-play {
  position: sticky;
  top: 0;
  z-index: 4;
  max-width: 680px;
  margin: 18px 0 0;
  padding: 12px 0 14px;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--fg-45);
  border-bottom: var(--rule);
  /* Fallback until JS paints the same --bg-visual slice the page is
     showing at this scroll position. Solid --bg is the mid-cream token and
     reads as a cooler slab once the gradient has gone warmer down the page. */
  background-color: var(--bg);
  background-repeat: no-repeat;
}
/* The meter is the rule once JS is on — keeping both stacked two hairlines. */
.note-play.is-ready {
  border-bottom: none;
  padding-bottom: 4px;
}
.note-play__row {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}
.note-play__cmd {
  flex: 0 0 auto;
  color: var(--accent);
  border: 1px solid var(--accent-50);
  border-radius: 4px;
  padding: 6px 8px;
  background: transparent;
  font: inherit;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  user-select: none;
  transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}
.note-play__cmd:hover { color: var(--fg); border-color: var(--fg-25); }
.note-play__cmd:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 2px;
}
.note-play.is-playing .note-play__cmd {
  color: var(--bg);
  background: var(--accent);
  border-color: var(--accent);
}
.note-play.is-playing .note-play__cmd:hover { color: var(--bg); }
.note-play__file {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--fg-40);
}
.note-play__time {
  margin-left: auto;
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  color: var(--fg-40);
}
.note-play__meter {
  display: none;
  position: relative;
  height: 24px;
  margin-top: 4px;
  cursor: pointer;
}
.note-play.is-ready .note-play__meter { display: block; }
.note-play__meter:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 2px;
}
.note-play__meter::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 11px;
  height: 1px;
  background: var(--fg-15);
}
.note-play__fill {
  position: absolute;
  left: 0;
  top: 11px;
  height: 1px;
  width: 0%;
  background: var(--accent);
  pointer-events: none;
}
.note-play__knob {
  position: absolute;
  top: 8px;
  left: 0%;
  width: 7px;
  height: 7px;
  margin-left: -3px;
  background: var(--accent);
  border-radius: 1px;
  pointer-events: none;
}
.note-play audio {
  position: absolute;
  width: 0;
  height: 0;
  opacity: 0;
  pointer-events: none;
}
@media (max-width: 520px) {
  .note-play__file { display: none; }
}

.metric {
  display: flex;
  align-items: baseline;
  gap: 20px;
  padding: 44px 32px;
  margin: 44px 0;
  border: 1px solid var(--fg-12);
  border-radius: 8px;
  flex-wrap: wrap;
}
.metric__from { font-weight: 600; font-size: clamp(40px, 6vw, 64px); color: var(--fg-90); }
.metric__arrow { font-family: var(--font-mono); font-size: 22px; color: var(--accent); }
.metric__to { font-weight: 600; font-size: clamp(40px, 6vw, 64px); color: var(--accent); }
.metric__label { font-family: var(--font-mono); font-size: 13px; color: var(--fg-45); flex: 1 1 180px; min-width: 160px; }

.prose { max-width: 680px; padding-bottom: 20px; }
.sec { padding: 28px 0; border-top: var(--rule); }
.sec__h { font-weight: 600; font-size: 22px; margin: 0 0 14px; }
.sec__n { color: var(--accent); font-family: var(--font-mono); font-size: 14px; margin-right: 10px; }
.sec__p { font-weight: 300; font-style: italic; font-size: 16px; line-height: 1.75; color: var(--accent-70); margin: 0; }
.sec__p + .window, .sec__p + .slot { margin-top: 20px; }

.pullquote {
  margin: 36px 0;
  padding: 28px 32px;
  border-left: 2px solid var(--accent);
  font-weight: 400;
  font-style: italic;
  font-size: 20px;
  line-height: 1.5;
  color: var(--fg-75);
}

.next-cta { padding: 36px 0 60px; border-top: var(--rule); }

.note-adj {
  display: block;
  max-width: 680px;
  margin: 8px 0 0;
  padding: 28px 0 0;
  border-top: var(--rule);
  color: inherit;
}
.note-adj + .note-adj { margin-top: 28px; }
.note-adj:last-child { margin-bottom: 64px; }
.note-adj:hover { color: inherit; }
.note-adj .eyebrow { margin-bottom: 14px; }
.note-adj__title {
  font-weight: 600;
  font-size: clamp(22px, 2.6vw, 28px);
  line-height: 1.25;
  margin: 0 0 10px;
  transition: color 0.18s ease;
}
.note-adj:hover .note-adj__title { color: var(--accent); }
.note-adj__dek {
  font-weight: 300;
  font-size: 16px;
  line-height: 1.6;
  color: var(--fg-55);
  margin: 0 0 16px;
}
.note-adj__go {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--accent);
}

/* --- case study: two-voice (flagship) ------------------------------------- */
/* A plain-language "short version" that anyone can read, and a collapsed
   "under the hood" lane where the jargon lives -- so a non-technical client
   gets the story and an engineer still gets the depth, without either wading
   through the other. */

.cs-outcome {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 12px 20px;
  padding: 22px 28px;
  margin: 40px 0;
  border: 1px solid var(--fg-12);
  border-radius: 8px;
}
.cs-outcome__from { font-weight: 300; font-size: clamp(17px, 2.2vw, 22px); color: var(--fg-65); }
.cs-outcome__arrow { font-family: var(--font-mono); font-size: 20px; color: var(--accent); }
.cs-outcome__to { font-weight: 600; font-size: clamp(19px, 2.6vw, 26px); color: var(--accent); }

.voice { padding: 8px 0 4px; }
.voice__label {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--fg-45);
  margin-bottom: 20px;
}
.voice__label::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
}

/* Readable prose: upright and higher-contrast than the muted greys used for
   labels. Long reading copy must not sit at 55% opacity. */
.readable { max-width: 680px; }
.readable p {
  font-size: 18px;
  line-height: 1.75;
  color: var(--fg-85);
  margin: 0 0 20px;
}
.readable p:last-child { margin-bottom: 0; }

/* Under the hood: collapsed by default (native <details>, no JS) so the
   non-technical reader skips it and the engineer opens it. */
.underhood {
  margin: 40px 0 8px;
  border: 1px solid var(--fg-12);
  border-radius: 8px;
  background: var(--fg-04);
}
.underhood__summary {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 18px 22px;
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--fg);
  cursor: pointer;
  list-style: none;
}
.underhood__summary::-webkit-details-marker { display: none; }
.underhood__summary::before {
  content: "+";
  width: 1ch;
  color: var(--accent);
  font-size: 16px;
}
.underhood[open] .underhood__summary::before { content: "\2212"; }
.underhood__summary:hover { color: var(--accent); }
.underhood__body { padding: 4px 22px 26px; max-width: 720px; }
.uh-context { font-size: 15px; line-height: 1.7; color: var(--fg-70); margin: 0; }
.uh-h {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--accent);
  letter-spacing: 0.02em;
  margin: 26px 0 12px;
}
.uh-list { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 12px; }
.uh-list li { font-size: 15px; line-height: 1.65; color: var(--fg-70); }
.uh-p { font-size: 15px; line-height: 1.7; color: var(--fg-70); margin: 0; }
.uh-term { font-family: var(--font-mono); font-size: 13.5px; color: var(--fg-90); }
.underhood__body .window { margin: 22px 0 6px; }
.window__code {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--fg-75);
  white-space: pre;
  overflow-x: auto;
}
.uh-note {
  margin: 26px 0 0;
  padding-top: 18px;
  border-top: 1px solid var(--fg-10);
  font-size: 14px;
  font-style: italic;
  line-height: 1.7;
  color: var(--fg-55);
}

/* --- agent stack (home hero) ---------------------------------------------- */
/* Signature: a fanned stack of faux-terminal "agent" panes, each quiet and
   mono, converging on the one accent element -- the human's decision. Purely
   decorative (aria-hidden); the paragraphs carry the meaning. */
.agent-stack { position: relative; display: flex; flex-direction: column; }
.agent-card {
  width: 84%;
  border: 1px solid var(--fg-15);
  border-radius: 8px;
  background: color-mix(in srgb, var(--fg) 6%, var(--bg));
  box-shadow: 0 10px 26px -18px rgba(0, 0, 0, 0.6);
  overflow: hidden;
  transition: transform 0.28s ease;
}
.agent-card { transform: translateX(var(--agent-x)); }
.agent-card--1 { --agent-x: 0%; z-index: 1; }
.agent-card--2 { --agent-x: 8%; margin-top: -9px; z-index: 2; }
.agent-card--3 { --agent-x: 16%; margin-top: -9px; z-index: 3; }
.agent-stack:hover .agent-card--1 { transform: translateX(calc(var(--agent-x) - 2%)) translateY(-3px); }
.agent-stack:hover .agent-card--3 { transform: translateX(calc(var(--agent-x) + 2%)) translateY(3px); }

/* A pass of the loop. The three panes kick in sequence rather than together --
   explore, then write, then review -- because that is the order they actually
   run in, and a simultaneous flash would read as a glitch rather than a
   sequence. theme.js swaps each body at the bottom of its own dip. */
.agent-card.is-rerun { animation: agent-rerun 460ms cubic-bezier(0.32, 0, 0.24, 1); }
.agent-card.is-rerun .agent-card__body { animation: agent-body 460ms ease; }
@keyframes agent-rerun {
  0%   { transform: translateX(var(--agent-x)); }
  30%  { transform: translateX(calc(var(--agent-x) - 3%)) translateY(-5px); }
  100% { transform: translateX(var(--agent-x)); }
}
@keyframes agent-body {
  0%, 100% { opacity: 1; }
  28%, 42% { opacity: 0; }
}
.agent-card__bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--fg-10);
}
.agent-card__dots { display: flex; gap: 5px; }
.agent-card__dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--fg-25); }
.agent-card__name { font-family: var(--font-mono); font-size: 11px; color: var(--fg-45); }
.agent-card__body {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--fg-60);
  padding: 12px 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.agent-card__mark { color: var(--accent); }
.agent-stack__foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 14px;
  margin-top: 16px;
  padding-right: 2%;
}
.agent-chip {
  appearance: none;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.4;
  color: var(--accent);
  border: 1px solid var(--accent-50);
  border-radius: 999px;
  padding: 5px 13px;
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.agent-chip:hover {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  border-color: var(--accent);
}
.agent-chip:active { transform: translateY(1px); }
.agent-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.agent-loop { font-family: var(--font-mono); font-size: 11px; color: var(--fg-40); transition: color 0.2s ease; }
/* Converged: the gate has nothing left to reject, so the counter -- not the
   button -- is what turns amber. The button keeps its job; pressing it again
   starts the run over. */
.agent-stack.is-done .agent-loop { color: var(--accent); }

/* --- note body (markdown) ------------------------------------------------- */
.notes-empty {
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--fg-45);
  padding: 8px 0 40px;
}
.notes-empty__cmd { color: var(--fg-35); margin-bottom: 10px; }
.notes-empty p { margin: 0; line-height: 1.6; }

/* One measure for the whole article -- 680px, what the rest of the site
   already uses for prose (.prose, .readable), and about 68 characters a line
   at 18px. Everything shares it: title, still, paragraphs, code, pager.
   Two widths were tried and thrown out. On a centred layout a wide code block
   breaks out symmetrically and reads as a device; this grid is anchored to the
   left gutter, so a wider block can only step out to the right -- which reads
   as a block that failed to line up, not as a decision. Alignment down one
   edge is worth more here than the extra room code would get. */
.article--note .article__title,
.article--note .article__dek,
.note-body { max-width: 680px; }
.note-body { padding: 24px 0 64px; }
.note-body > *:first-child { margin-top: 0; }
.note-body p {
  font-weight: 300;
  font-size: 18px;
  line-height: 1.75;
  color: var(--fg-85);
  margin: 0 0 20px;
}
.note-body p:last-child { margin-bottom: 0; }
/* A section break has to out-weigh a paragraph break, or the article reads as
   one undifferentiated column. 52px above, 16px below. */
.note-body h2 {
  font-weight: 600;
  font-size: 23px;
  line-height: 1.3;
  letter-spacing: -0.01em;
  margin: 52px 0 16px;
}
.note-body h3 {
  font-weight: 600;
  font-size: 18.5px;
  line-height: 1.35;
  margin: 36px 0 12px;
}
.note-body h2 + h3 { margin-top: 20px; }
.note-body ul,
.note-body ol {
  margin: 0 0 20px;
  padding-left: 22px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.note-body li {
  font-weight: 300;
  font-size: 18px;
  line-height: 1.75;
  color: var(--fg-85);
}
.note-body blockquote {
  margin: 28px 0;
  padding: 4px 0 4px 20px;
  border-left: 2px solid var(--accent);
  font-style: italic;
  color: var(--fg-75);
}
.note-body blockquote p:last-child { margin-bottom: 0; }
.note-body hr {
  border: 0;
  border-top: 1px solid var(--fg-12);
  margin: 36px 0;
}
.note-body a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
.note-body img { max-width: 100%; height: auto; border-radius: 8px; margin: 20px 0; }
.note-body pre {
  margin: 24px 0;
  padding: 16px 18px;
  border: 1px solid var(--fg-12);
  border-radius: 8px;
  background: var(--fg-04);
  overflow-x: auto;
  /* The default scrollbar renders light grey and shouts on a near-black
     page. Thin and drawn from the theme's own ramp instead. */
  scrollbar-width: thin;
  scrollbar-color: var(--fg-25) transparent;
}
.note-body pre::-webkit-scrollbar { height: 8px; }
.note-body pre::-webkit-scrollbar-button { display: none; }
.note-body pre::-webkit-scrollbar-track { background: transparent; }
.note-body pre::-webkit-scrollbar-thumb {
  background: var(--fg-20);
  border-radius: 4px;
}
.note-body pre::-webkit-scrollbar-thumb:hover { background: var(--fg-35); }
.note-pre { position: relative; margin: 24px 0; }
.note-body .note-pre > pre {
  margin: 0;
  padding-right: 5.75rem;
}
.note-pre__copy {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 1;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1;
  color: var(--accent);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  border: 1px solid var(--accent-50);
  border-radius: 4px;
  padding: 6px 8px;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.note-pre__copy:hover { color: var(--fg); border-color: var(--fg-25); }
.note-pre__copy:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 2px;
}
.note-pre.is-copied .note-pre__copy { color: var(--fg); border-color: var(--accent); }
.note-body pre code {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.6;
  color: var(--fg-75);
  background: none;
  padding: 0;
}
.note-body :not(pre) > code {
  font-family: var(--font-mono);
  font-size: 0.88em;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  padding: 0.1em 0.35em;
  border-radius: 3px;
}
/* catalog.py wraps every table so a wide one scrolls inside its own box
   instead of widening the page -- on a phone the layout viewport grew to
   the table's width and the bottom nav stretched with it. */
.note-table { overflow-x: auto; margin: 24px 0; -webkit-overflow-scrolling: touch; }
.note-body table {
  width: 100%;
  border-collapse: collapse;
  margin: 0;
  font-size: 15px;
}
.note-body th,
.note-body td {
  text-align: left;
  padding: 8px 12px;
  border-bottom: 1px solid var(--fg-10);
}
.note-body th { font-family: var(--font-mono); font-size: 12px; color: var(--fg-45); font-weight: 500; }

/* --- featured image (first LLM article only, preview) --------------------- */
.note-art__img { width: 100%; }
.note-art__img img,
img.note-art__img {
  width: 100%;
  height: auto;
}
img.note-art__img--dark,
picture.note-art__img--dark { display: block; }
img.note-art__img--light,
picture.note-art__img--light { display: none; }
:root[data-theme='light'] img.note-art__img--dark,
:root[data-theme='light'] picture.note-art__img--dark { display: none; }
:root[data-theme='light'] img.note-art__img--light,
:root[data-theme='light'] picture.note-art__img--light { display: block; }

.note-hero {
  max-width: 680px;
  margin: 28px 0 0;
}
.note-hero img {
  border-radius: 8px;
  border: 1px solid var(--fg-10);
}

.cs-item__thumb { grid-column: 3; }
.cs-item__thumb img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  /* Scales and net sit on the right of the still; keep them in frame. */
  object-position: 82% center;
  border-radius: 6px;
  border: 1px solid var(--fg-10);
  background: var(--fg-04);
  transition: border-color 0.18s ease;
}
a.cs-item--has-art:hover .cs-item__thumb img { border-color: var(--accent-50); }

.cs-card__art {
  margin: -22px -20px 4px;
}
/* 16:9, not the 1:1 still. Two cards across a 1160px shell are ~570px wide,
   so a square crop was ~570px of art per card -- the teaser's pictures
   outweighed the titles they were meant to introduce. The pack's desktop
   still is exactly 16:9, so this crops nothing above 720px; below it the 1:1
   file is centre-cropped, and the subjects sit centre. */
.cs-card__art img {
  aspect-ratio: 16 / 9;
  object-fit: cover;
  object-position: center;
  border-radius: 10px 10px 0 0;
}

/* Phone: one column. The still leads, then the rail collapses to a single
   `date · minutes` line, then the text. */
@media (max-width: 760px) {
  .cs-item,
  .cs-item--has-art { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .cs-item__rail,
  .cs-item__body,
  .cs-item__thumb { grid-column: 1; }
  /* The <picture> serves the 1:1 still below 720px, so the off-centre crop
     the 16:9 desktop art needs would cut it. Centre it and give it more
     height, since it is the full width of the card here. */
  .cs-item__thumb { order: -1; margin-bottom: 18px; }
  .cs-item__thumb img { aspect-ratio: 16 / 10; object-position: center; }
  .cs-item__rail { display: flex; align-items: baseline; gap: 10px; padding: 0 0 10px; }
  .cs-item__mins { margin-top: 0; }
  .cs-item__mins::before { content: "· "; color: var(--fg-25); }
  .cs-series__body { padding-left: 12px; }
  .cs-item { margin-left: 12px; }
  .cs-series__body .cs-item { margin-left: 0; }
}

@media (max-width: 820px) {
  .hero__grid { grid-template-columns: 1fr; gap: 30px; }
  .agent-stack { max-width: 400px; }
}

/* In-body anchors (e.g. the numbered decision classes a table links back to):
   leave room for the sticky player so the target is not hidden under it. */
.note-body a[id]:empty {
  scroll-margin-top: 5rem;
}
