/* spaces-svc — public stylesheet
   Matches the Niche of One network (os.css / RATION v5):
   warm paper stock, ink text, Oswald display, Space Mono kickers, Lora reading.
   DISCIPLINE: red (--buy) is the MONEY DOOR ONLY — price, unlock, tip. Never décor. */

/* Self-hosted fonts (no Google CDN): woff2 under /fonts/, font-display:swap baked in.
   Latin + latin-ext split by unicode-range so only the needed subset downloads.
   Mirrors the network's /fonts/display.css + reader.css. */
/* Poppins — display/SHOUT (--disp): 500/600/700/800 */
@font-face{font-family:'Poppins';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/poppins-v24-pxiByp8kv8JHgFVrLGT9Z1JlFc-K.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:'Poppins';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/poppins-v24-pxiByp8kv8JHgFVrLGT9Z1xlFQ.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:'Poppins';font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/poppins-v24-pxiByp8kv8JHgFVrLEj6Z1JlFc-K.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:'Poppins';font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/poppins-v24-pxiByp8kv8JHgFVrLEj6Z1xlFQ.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:'Poppins';font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/poppins-v24-pxiByp8kv8JHgFVrLCz7Z1JlFc-K.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:'Poppins';font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/poppins-v24-pxiByp8kv8JHgFVrLCz7Z1xlFQ.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:'Poppins';font-style:normal;font-weight:800;font-display:swap;src:url(/fonts/poppins-v24-pxiByp8kv8JHgFVrLDD4Z1JlFc-K.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:'Poppins';font-style:normal;font-weight:800;font-display:swap;src:url(/fonts/poppins-v24-pxiByp8kv8JHgFVrLDD4Z1xlFQ.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;}
/* Figtree — variable (300..900), body/UI face (--ui) */
@font-face{font-family:'Figtree';font-style:normal;font-weight:300 900;font-display:swap;src:url(/fonts/figtree-variable-latinext.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:'Figtree';font-style:normal;font-weight:300 900;font-display:swap;src:url(/fonts/figtree-variable-latin.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;}
/* Space Mono — kickers/mono (--mono): 400/700 */
@font-face{font-family:'Space Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/spacemono-v17-i7dPIFZifjKcF5UAWdDRYE98RWq7.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:'Space Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/spacemono-v17-i7dPIFZifjKcF5UAWdDRYEF8RQ.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:'Space Mono';font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/spacemono-v17-i7dMIFZifjKcF5UAWdDRaPpZUFuaHjyV.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:'Space Mono';font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/spacemono-v17-i7dMIFZifjKcF5UAWdDRaPpZUFWaHg.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;}
/* Lora — reading voice (--read): 400/600 + italic 400 */
@font-face{font-family:'Lora';font-style:italic;font-weight:400;font-display:swap;src:url(/fonts/lora-v37-0QI8MX1D_JOuMw_hLdO6T2wV9KnW-MoFoqF2nOeZ.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:'Lora';font-style:italic;font-weight:400;font-display:swap;src:url(/fonts/lora-v37-0QI8MX1D_JOuMw_hLdO6T2wV9KnW-MoFoq92nA.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:'Lora';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/lora-v37-0QIvMX1D_JOuMwT7I-NP.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:'Lora';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/lora-v37-0QIvMX1D_JOuMwr7Iw.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:'Lora';font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/lora-v37-0QIvMX1D_JOuMwT7I-NP.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:'Lora';font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/lora-v37-0QIvMX1D_JOuMwr7Iw.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;}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  /* Canonical brand tokens now live in design-tokens.css (loaded first): --paper, --ink, --panel,
     --panel2, --buy, --on-buy, --rule-hard, --lift, --disp, --ui, --read, --mono, --s1..--s6.
     Only genuine per-property overrides stay here. */
  --muted:   #595959;   /* reader muted — press value, 7.00:1 on the white ground (was #5c5952 for cream) */
  /* --rule is ALSO declared in design-tokens.css. This copy loads second and wins,
     so the two must move together — fixing only the token file is a no-op, which
     is how the 20% survived a contrast pass. Raised 20% -> 52% (light) and
     26% -> 38% (dark) on 2026-08-07: at 20% this drew every card edge, table
     rule and form border at 1.51:1, against a 3:1 WCAG 1.4.11 non-text floor,
     and failed on all five stocks in both modes. The values are the apex's own
     measured ones. Full derivation in design-tokens.css. */
  --rule:    color-mix(in srgb, var(--ink) 52%, var(--paper));
  --yellow:  #ffe000;

  /* TWO ceilings, deliberately different. --measure is the ARTICLE line length and
     it does not move: 700px is the correct editorial measure and widening it makes
     prose worse, not better. --chrome-max is everything that is NOT prose — the
     band, the masthead, the footer, the feed grids — and it steps up with the
     viewport the way the apex's --grid-max does. Until 2026-08-08 both jobs were
     done by --measure alone, applied to <body>, so a 2560px screen rendered a
     748px column with dead grey either side on every page. Mirroring the apex's
     PATTERN, not importing its tokens: spaces-svc keeps its own token layer. */
  --measure:    700px;
  --chrome-max: 56rem;
  --gutter:     24px;
}
@media (min-width: 1500px) { :root { --chrome-max: 72rem; } }
@media (min-width: 1900px) { :root { --chrome-max: 88rem; } }

/* Dark palette. Follow the OS preference by default, but a network-wide theme choice (shared
   noo_theme cookie on .nicheof.one, applied as data-theme in the head) wins: an explicit light
   choice overrides OS-dark, and an explicit dark choice applies even on a light OS. Keeps the
   eZine in sync with the apex theme toggle instead of drifting to the OS default. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* Surfaces/ink/buy/rule-hard flip in design-tokens.css; only the reader's local overrides stay. */
    --muted: #8f8f8f;
    --rule: color-mix(in srgb, var(--ink) 38%, var(--paper));   /* was 26% = 2.16:1 */
  }
  /* .net-bar__mark moved to os-band.css — its filter INVERTED when the band stopped
     being an ink slab and started tracking the page. Leaving a copy here would have
     re-flipped it, because this file loads second. */
  :root:not([data-theme="light"]) .site-footer__dot { filter: invert(1); }
}
:root[data-theme="dark"] {
  --muted: #8f8f8f;
  --rule: color-mix(in srgb, var(--ink) 38%, var(--paper));   /* was 26% = 2.16:1 */
}
:root[data-theme="dark"] .site-footer__dot { filter: invert(1); }

html { font-size: 16px; -webkit-text-size-adjust: 100%; overflow-x: hidden; }

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--ui);
  font-size: 1rem;
  line-height: 1.6;
  min-height: 100vh;
}

/* The PAGE is full width; the COLUMNS cap themselves. <body> used to carry
   max-width:calc(--measure + --gutter*2), which capped the reading line and the
   chrome with one number — so the band, the banner and the footer each needed a
   100vw escape hatch to get back out, and a 2560px screen still rendered a 748px
   strip with dead grey either side on every route.
   .net-bar / .site-banner / .site-footer keep their existing breakouts: against a
   full-width body those now resolve to left:0/width:100vw, i.e. the same result
   by a shorter path, so they are correct either way and were left alone. */
.site-header,
.site-header--compact,
main#main {
  max-width: calc(var(--chrome-max) + var(--gutter) * 2);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
/* Everything in main falls back to the READING measure, so widening the page is a
   no-op until a surface opts in. Deliberately low specificity (0,0,2): any class
   rule below wins, which is how .feed / .mag claim the extra width. */
main > * { max-width: var(--measure); }
/* EXEMPTION, and read this before adding a child to <main>. The cap above is a
   flow-content rule, but `main > *` cannot tell flow content from an overlay. The
   reading-progress bar is position:fixed with left:0/right:0, so a max-width does
   not clip it — it RESIZES it to 700px and margin-inline:auto then centres it,
   giving a progress bar that spans the article instead of the window. It looks
   deliberate, which is why it is called out here. Any future fixed/absolute direct
   child of <main> needs the same two lines. */
main > .reading-progress { max-width: none; margin-inline: 0; }
/* Centring is NOT negotiable the same way, so it is stated at (1,0,1). <body> used
   to centre these blocks for free; a class further down this file that sets the
   `margin` shorthand (.tag-row does) would otherwise silently reset margin-inline
   to 0 and pin that block to the left edge of a now-1456px main. Only the inline
   axis is touched — every block keeps its own vertical margins. */
main#main > * { margin-inline: auto; }
/* Sanctioned local override, same shape as admin.css:111 — the band is chrome, so
   it tracks --chrome-max, not the reading measure. Only max-width/padding: the
   band's LOOK stays in os-band.css (tests/band_test.php enforces that). */
.net-bar__inner { max-width: calc(var(--chrome-max) + var(--gutter) * 2); }

/* ── Network bar (shared chrome — every space is a node on nicheof.one) ──────── */
/* Skip link — keyboard users bypass the net-bar straight to the content.
   Visually hidden until :focus (mirrors the network's os.css .skip). */
.skip {
  position: fixed; left: 10px; top: -60px; z-index: 100003;
  background: var(--ink); color: var(--paper); font-family: var(--ui); font-weight: 700;
  padding: 11px 16px; border: 2px solid var(--buy); text-decoration: none; transition: top 0.15s ease;
}
.skip:focus { top: 10px; outline: 3px solid var(--ink); outline-offset: 2px; }
main#main:focus { outline: none; }   /* programmatic focus target only — no ring on the whole main */

/* THE BAND LIVES IN os-band.css. Everything that used to sit here — .net-bar,
   __inner, __brand, __mark, __name, __cta, __right, __cta--own — was the
   inverted-ink version retired on 2026-08-07 and is now defined once, on the
   apex's --band/--on-band tokens, in that file. Do NOT restate any of it here:
   this file loads SECOND, so a stray copy silently wins and re-introduces the
   white-slab-in-dark-mode bug. tests/band_test.php fails if it comes back. */

/* Reusable kicker — Space Mono, tracked, uppercase. The zero-color tier. */
.kick, .badge, .site-header__links a, .post-list__meta, .post__byline-meta,
.paywall__label, .post__nav, .site-footer {
  font-family: var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

a { color: var(--ink); }

/* Selection = the house yellow highlighter; dark ink text stays readable on it
   in both light and dark themes (matches .post__body mark). */
::selection { background: var(--yellow, #f5d90a); color: #141414; }

/* ── Masthead ──────────────────────────────────────────────────────────────── */
.site-header {
  border-bottom: 3px solid var(--rule-hard);
  padding: var(--s5) 0 var(--s3);
  margin-bottom: var(--s5);
}
.site-header__name {
  font-family: var(--disp); font-synthesis-weight: none;
  font-weight: 800;
  /* Ceiling (--name-max) is stepped down by title length in layout.php so a long space
     name doesn't balloon; the 1.6rem floor + overflow-wrap keep even an unbroken word in bounds. */
  font-size: clamp(1.6rem, 8vw, var(--name-max, 4.4rem));
  line-height: 0.9;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  text-wrap: balance;
  overflow-wrap: anywhere;
  min-width: 0;
}
.site-header__name a { color: var(--ink); text-decoration: none; }
.site-header__name a:hover { text-decoration: underline; }
/* Broadsheet dateline — per-handle furniture (folio + fleuron hashed from the handle) so every
   space reads hand-set, not provisioned. Space Mono caps between hairline rules; fleuron in ink.
   Kept at the 11.5px floor, solid colour, full opacity. */
.site-header__dateline {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding: 7px 0;
  margin-top: 14px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
}
.site-header__dateline .sh-fleuron { color: var(--ink); font-size: 1rem; margin-right: 2px; }
.site-header__bio {
  font-family: var(--ui);
  font-size: 0.9375rem;
  color: var(--muted);
  margin-top: 10px;
  max-width: 52ch;
  line-height: 1.5;
}
/* Link-in-bio block: the member's links as tappable button pills (shop, socials, current work),
   with the network's fill-on-hover + press-down craft. They update whenever the member edits their
   links in the dashboard — a dynamic links section, not decoration. */
.site-header__links { display: flex; flex-wrap: wrap; gap: 8px 10px; margin-top: 18px; }
.site-header__links a {
  font-family: var(--ui);
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--ink);
  text-decoration: none;
  border: 2px solid var(--ink);
  border-radius: var(--btn-radius, 999px);
  padding: 7px 14px;
  background: var(--panel);
  transition: transform .1s ease, box-shadow .1s ease, background .15s ease, color .15s ease;
}
.site-header__links a:hover { background: var(--ink); color: var(--paper); }
.site-header__links a:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--rule-hard); }
@media (max-width: 600px) { .site-header__links a { font-size: 0.86rem; padding: 9px 15px; } }

/* ── Compact masthead (every route except space home + archive) ──────────────
   The full masthead is 300-500px of identity — banner, 96px avatar, up-to-4.4rem
   nameplate, dateline, bio, link pills, pinned nav — and it used to repeat above
   every post, so the reader scrolled the whole thing to reach the first sentence.
   This is the same header reduced to one ~60px line: who, and where else to go.
   The space name drops to the MONO face on purpose. The display face is the
   nameplate, and a nameplate is a front-page object; repeating it small on an
   interior page makes it furniture. Mono reads as a running head, which is what
   this is. See layout.php for why Follow is not here. */
.site-header--compact {
  /* Aligned to the ARTICLE column, not to --chrome-max. Two reasons, and the
     second is not cosmetic: (1) on a reading page the chrome sitting directly
     over the 700px measure reads as one column instead of a wide bar above a
     narrow one; (2) the TOC rail below is position:fixed in the left margin,
     positioned off the measure — a chrome-max header reaches left into exactly
     that margin and lands on top of it at >=1240px. The full masthead keeps the
     wide ceiling because the feed underneath it is wide. */
  max-width: calc(var(--measure) + var(--gutter) * 2);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px 18px;
  border-bottom: 1px solid var(--rule);
  padding: 12px 0;
  margin-bottom: var(--s4);
}
.site-header__compact-id {
  display: inline-flex; align-items: center; gap: 10px;
  min-width: 0; text-decoration: none; color: var(--ink);
}
.site-header__compact-avatar {
  flex: none; width: 36px; height: 36px; border-radius: 50%;
  object-fit: cover; border: 2px solid var(--rule-hard); background: var(--panel);
}
.site-header__compact-name {
  font-family: var(--mono);
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.site-header__compact-id:hover .site-header__compact-name { text-decoration: underline; }
/* Reuses the .site-header__links pill styling; only the 18px top margin, which is
   for a stacked masthead, has to go in a single-row strip. */
.site-header__compact-nav { margin-top: 0; gap: 6px 8px; }
.site-header__compact-nav a { font-size: 0.75rem; padding: 5px 11px; border-width: 2px; }
@media (max-width: 600px) {
  .site-header__compact-nav a { font-size: 0.78rem; padding: 6px 12px; }
}

/* ── Space banner + avatar (Track: personalize — curated layout presets) ─────
   body.layout-{standard|banner|minimal} is the ONLY hook; three fixed looks,
   no free-form CSS. Graceful when a member has no banner/avatar: the header
   renders exactly as the pre-personalize plain text masthead. ────────────── */
.site-banner {
  position: relative; left: 50%; width: 100vw; margin-left: -50vw;
  border-bottom: 1px solid var(--rule);
  overflow: hidden;
}
.site-banner img { display: block; width: 100%; height: 100%; object-fit: cover; }

.site-header__top { display: flex; align-items: center; gap: 16px; }
.site-header__avatar {
  flex: none; width: 56px; height: 56px; border-radius: 50%; overflow: hidden;
  border: 2px solid var(--rule-hard); background: var(--panel);
}
.site-header__avatar img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* standard: the avatar straddles the banner's bottom edge (magazine nameplate), but the
   TITLE sits fully below the banner so display type never bleeds onto the image. Only the
   avatar is lifted, with a transform (visual only) so its straddle is independent of how
   many lines the title wraps to. */
body.layout-standard .site-banner { height: 210px; margin-bottom: 0; }
body.layout-standard .site-header { padding-top: 14px; margin-top: 0; position: relative; }
body.layout-standard .site-header__top { align-items: flex-end; gap: 18px; }
body.layout-standard .site-header__avatar {
  width: 96px; height: 96px; border: 4px solid var(--paper);
  box-shadow: 0 0 0 1px var(--rule-hard); border-radius: 8px;
  transform: translateY(-46px);   /* lift the avatar up to straddle the banner edge */
}
body.layout-standard .site-header__name { padding-bottom: 6px; }
/* no banner: nothing to straddle, so drop the lift and keep an honest top margin */
body.layout-standard:not(:has(.site-banner)) .site-header { padding-top: var(--s5); }
body.layout-standard:not(:has(.site-banner)) .site-header__avatar { transform: none; }

/* banner: big hero, avatar + name centered, straddling the bottom edge */
body.layout-banner .site-banner { height: 320px; }
body.layout-banner .site-header { text-align: center; }
body.layout-banner .site-header__top {
  flex-direction: column; align-items: center; margin-top: -54px;
}
body.layout-banner .site-header__avatar {
  width: 108px; height: 108px; border: 4px solid var(--paper);
  box-shadow: 0 0 0 1px var(--rule-hard); margin-bottom: 14px;
}
body.layout-banner .site-header__bio { margin-left: auto; margin-right: auto; }
body.layout-banner .site-header__links { justify-content: center; }

/* minimal: no banner (suppressed server-side too), tight vertical rhythm */
body.layout-minimal .site-header { padding: 22px 0 12px; margin-bottom: 20px; }
body.layout-minimal .site-header__avatar { width: 40px; height: 40px; }

/* ── Magazine (space home — featured/pinned, up to 5) ────────────────────────
   Lead = first pinned post (big cover + title). mag-grid = next up-to-4 pinned,
   smaller cards. Graceful with no cover_url: text-only, divided by a top rule
   instead of an image. Covers are always object-fit:cover at a fixed 16:9 box
   with a 1px var(--rule) border — never crop-jump, never distort. ───────────── */
.mag { margin-bottom: 44px; }

.mag-lead { margin-bottom: 8px; }
.mag-lead__cover {
  display: block;
  margin-bottom: 18px;
  border: 1px solid var(--rule);
}
.mag-lead__cover img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}
.mag-lead__title {
  font-family: var(--disp); font-synthesis-weight: none;
  font-weight: 800;
  font-size: clamp(2.3rem, 5vw, 3rem);
  line-height: 0.98;
  text-transform: uppercase;
  letter-spacing: -0.02em;
}
.mag-lead__title a { color: var(--ink); text-decoration: none; }
.mag-lead__title a:hover { text-decoration: underline; }
.mag-lead__excerpt {
  font-family: var(--read);
  font-size: 1.0625rem;
  color: var(--ink);
  margin-top: 12px;
  max-width: 62ch;
  line-height: 1.55;
}
.mag-lead__meta {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: 14px;
}

.mag-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 28px;
  margin-top: 34px;
}
/* The PINNED cards carry the same zine card as the feed below them. They used to be
   a hairline top rule and nothing else, which was fine while the chronological feed
   was flat text rows — both were quiet, the lead was loud, and the hierarchy read
   correctly. The moment the feed became cards, the writer's own FEATURED posts were
   the plainest objects on the page and the newest post outranked them. Boxing these
   too puts the order back: loud lead, cards, cards. */
.mag-card {
  display: flex;
  flex-direction: column;
  border: 2px solid var(--ink);
  background: var(--panel);
  box-shadow: var(--lift);
  padding: 0 0 16px;
  transition: transform .15s ease, box-shadow .15s ease;
}
.mag-card:hover { transform: translate(-2px, -2px); box-shadow: var(--lift-hover); }
@media (prefers-reduced-motion: reduce) {
  .mag-card { transition: none; }
  .mag-card:hover { transform: none; }
}
.mag-card__title, .mag-card__meta { padding-inline: 16px; }
/* No cover: the title would otherwise sit flush against the top border. */
.mag-card > .mag-card__title:first-child { padding-top: 16px; }
.mag-card__cover {
  display: block;
  margin-bottom: 14px;
  border: 0;
  border-bottom: 2px solid var(--ink);
}
.mag-card__cover img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}
.mag-card__title {
  font-family: var(--disp); font-synthesis-weight: none;
  font-weight: 600;
  font-size: 1.1875rem;
  line-height: 1.15;
  text-transform: uppercase;
  letter-spacing: -0.01em;
}
.mag-card__title a { color: var(--ink); text-decoration: none; }
.mag-card__title a:hover { text-decoration: underline; }
.mag-card__meta {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: 8px;
}

/* ── Feed (space home — chronological, below the magazine) ───────────────────── */
.feed__kicker {
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  border-bottom: 1px solid var(--rule);
  padding-bottom: 10px;
  margin-bottom: 4px;
}

/* Desktop: 2-col grid for the secondary featured cards */
@media (min-width: 601px) {
  .mag-grid { grid-template-columns: repeat(2, 1fr); gap: 30px 26px; }
}

/* ── Post list (home) — Medium-style rows: text left, square cover right ────
   .post-list__item is a flex row (text block + optional 112px thumb) with a
   clean divider between items. Falls back to a plain text row when a post
   has no cover_url. Stacks to thumb-below on mobile (see @media below). ──── */
.post-list { list-style: none; }
.post-list__item {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  border-bottom: 1px solid var(--rule);
  padding: 26px 0;
}
.post-list__item:first-child { border-top: 1px solid var(--rule); }
.post-list__text { flex: 1 1 auto; min-width: 0; }
.post-list__thumb {
  display: block;
  flex: none;
  width: 112px;
  height: 112px;
  border: 1px solid var(--rule);
}
.post-list__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.post-list__title {
  font-family: var(--disp); font-synthesis-weight: none;
  font-weight: 600;
  font-size: 1.3rem;
  line-height: 1.12;
  text-transform: uppercase;
  letter-spacing: -0.01em;
}
.post-list__title a { color: var(--ink); text-decoration: none; }
.post-list__title a:hover { text-decoration: underline; }
.post-list__excerpt {
  font-family: var(--read);
  font-size: 1rem;
  color: var(--ink);
  margin-top: 8px;
  line-height: 1.5;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.post-list__meta { font-size: 0.72rem; color: var(--muted); margin-top: 10px; }

/* ── The feed, as CARDS ──────────────────────────────────────────────────────
   Until 2026-08-08 a space visually downgraded the moment you scrolled past the
   five pinned slots: .mag-lead/.mag-card had the zine language (2px ink rule +
   offset --lift shadow) and the chronological feed below it was flat text rows.
   The design already existed; the feed just did not use it. So this restyles the
   SAME markup — no template change — into the same card family, and only inside
   .feed. That scoping is load-bearing: .post-list is also used by post.php's
   "Related"/"More from" strips (inside a 700px article, where rows are correct)
   and by discover.php, and neither should become a grid.
   Hover uses --lift-hover, which design-tokens.css:121 has defined and nothing
   has used since. Same tactile "pick this one up" as the apex's own cards. */
.feed { max-width: var(--chrome-max); }
/* Feed head: kicker left, sort right, sharing the kicker's own bottom rule so the
   toggle reads as part of the section label rather than as a floating control. */
.feed__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 14px; flex-wrap: wrap;
  border-bottom: 1px solid var(--rule); padding-bottom: 10px; margin-bottom: 4px;
}
.feed__head .feed__kicker { border-bottom: 0; padding-bottom: 0; margin-bottom: 0; }
.feed__sort {
  font-family: var(--mono); font-size: max(0.7rem, 11.5px);
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted);
  display: flex; align-items: center; gap: 7px;
}
.feed__sort a { color: var(--muted); text-decoration: none; border-bottom: 1px solid var(--rule); }
.feed__sort a:hover { color: var(--ink); border-bottom-color: var(--ink); }
/* The active sort is a <span>, not a link. Ink so it reads as the current state. */
.feed__sort [aria-current] { color: var(--ink); font-weight: 700; }
.feed .post-list { display: grid; grid-template-columns: 1fr; gap: 26px; margin-top: 22px; }
/* The third column lands at 1500px, NOT at 1400: that is the width where
   --chrome-max steps 56rem -> 72rem. Splitting a container into more columns
   before the container itself grows just makes each card narrower. Keep these two
   breakpoints married — if --chrome-max's steps move, move these with them. */
@media (min-width: 601px)  { .feed .post-list { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1500px) { .feed .post-list { grid-template-columns: repeat(3, 1fr); } }
/* 'novel' is read front-to-back — router.php:211 already flips it to oldest-first.
   Columns would scramble that reading order, so it stays one column at every
   width. The second selector is the admin appearance preview, which simulates the
   format on <html> instead of <body>; both are listed so the preview and a real
   page cannot disagree about column count. */
body[data-format="novel"] .feed .post-list,
html[data-preview-format="novel"] .feed .post-list { grid-template-columns: 1fr; }

.feed .post-list__item {
  display: flex;
  flex-direction: column;
  gap: 0;
  border: 2px solid var(--ink);
  background: var(--panel);
  box-shadow: var(--lift);
  padding: 0;
  transition: transform .15s ease, box-shadow .15s ease;
}
.feed .post-list__item:hover { transform: translate(-2px, -2px); box-shadow: var(--lift-hover); }
@media (prefers-reduced-motion: reduce) {
  .feed .post-list__item { transition: none; }
  .feed .post-list__item:hover { transform: none; }
}
/* The cover is LAST in the DOM (it trails the text in the row layout). order:-1
   lifts it to the top of the card without touching space.php — the reading order
   for a screen reader stays title-first, which is the better order anyway. */
.feed .post-list__thumb {
  order: -1;
  width: 100%; height: auto;
  border: 0; border-bottom: 2px solid var(--ink);
}
.feed .post-list__thumb img { width: 100%; height: auto; aspect-ratio: 16 / 9; }
.feed .post-list__text { padding: 16px 18px 18px; }
.feed .post-list__excerpt { -webkit-line-clamp: 3; }

/* The magazine block is chrome-wide too, or the pinned grid stays a 700px column
   inside a 1400px page. The LEAD splits into cover|text on a wide screen instead
   of growing into a billboard — its excerpt is still capped at 62ch above. */
.mag { max-width: var(--chrome-max); }
@media (min-width: 1000px) {
  /* The cover spans every row; the text stacks top-aligned in column 2. Row 3 is
     1fr purely so the slack from a tall cover lands BELOW the meta line instead
     of being shared out between title, excerpt and meta as row padding. */
  .mag-lead {
    display: grid;
    grid-template-columns: 58% 1fr;
    grid-template-rows: auto auto 1fr;
    column-gap: 30px;
    align-items: start;
  }
  .mag-lead__cover { grid-column: 1; grid-row: 1 / -1; margin-bottom: 0; }
  .mag-lead > :not(.mag-lead__cover) { grid-column: 2; }
  /* No cover: nothing to sit beside, so fall back to the single-column hero. */
  .mag-lead:not(:has(.mag-lead__cover)) { display: block; }
}
@media (min-width: 1500px) { .mag-grid { grid-template-columns: repeat(3, 1fr); } }

/* ── Tags — the category nav (organizational, NOT money — ink, never red) ────
   .tag-row  = quiet chip row under the space-home header (only renders when a
   published post carries a tag). .post__tags = same chips under a post. Both
   link to /t/{tag}. font-size floors at 11.5px (network readability floor). */
/* Chrome-wide, like the .mag and .feed it sits between. It was the one block in
   main still capped at --measure, which centred it inside a 1456px column and left
   it visibly inset from the masthead above and the feed below — invisible while the
   row was long enough to fill the measure anyway, obvious the moment P1-6 capped it
   at 8 chips. Found by looking at the deployed page, not the diff. Again. */
.tag-row { display: flex; flex-wrap: wrap; gap: 8px; margin: -14px 0 30px; max-width: var(--chrome-max); }
.post__tags { display: flex; flex-wrap: wrap; gap: 8px; margin: 30px 0 4px; }
.tag-chip {
  display: inline-block;
  font-family: var(--mono);
  font-size: max(0.75rem, 11.5px);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  border: 1px solid var(--ink);
  padding: 4px 10px;
}
.tag-chip:hover { background: var(--ink); color: var(--paper); }
.tag-page__title { font-size: max(0.75rem, 11.5px); color: var(--ink); }
/* Tag overflow (P1-6). <details> is display:block by default, which would drop the
   "+N more" onto its own line and defeat the point of capping the row. Making the
   collapsed state inline-flex keeps it in the chip run; the OPEN state goes back to
   a wrapping flex so the revealed chips lay out like the ones above them. */
.tag-row__more { display: inline-flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.tag-row__more > summary { list-style: none; cursor: pointer; }
.tag-row__more > summary::-webkit-details-marker { display: none; }
.tag-chip--more { background: var(--ink); color: var(--paper); }
.tag-chip--more:hover { background: var(--paper); color: var(--ink); }
.tag-row__more[open] > summary { margin-right: 0; }

/* The honest activity line (P1-7). Real numbers only — post count, when the space
   started, when it was last touched — in the mono/kicker tier under the dateline.
   A credibility cue, not a badge and not a CTA, so it carries no colour: a reader
   deciding whether a space is alive should not have to guess from a wall of posts
   with no dates on them. Absent entirely when there is nothing true to say. */
.site-header__activity {
  font-family: var(--mono);
  font-size: max(0.68rem, 11.5px);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: 8px;
}

/* ── Price badge (money → red) ─────────────────────────────────────────────── */
.badge {
  display: inline-block;
  font-size: 0.72rem;
  font-weight: 700;
  padding: 2px 7px;
  vertical-align: 0.14em;
  margin-left: 8px;
  letter-spacing: 0.06em;
}
.badge--gated { background: var(--buy); color: var(--on-buy); }
/* Discovery paid indicator — INK chip, never red. Discovery has no money door
   of its own (red is reserved for the actual checkout on the origin post); this
   only flags that the piece is paid, then links out. Floored at 11.5px. */
.badge--paid {
  background: transparent; color: var(--ink);
  border: 1px solid var(--ink);
  font-size: max(0.625rem, 11.5px);
}

/* ── Discovery (discover.{apex}) — network-wide "a real find" ─────────────────
   Reuses the crafted .post-list rows; only the masthead + author link are new.
   Ink/accent only — Discovery surfaces work, it never sells, so no red chrome. */
.discover__masthead {
  border-bottom: 3px solid var(--rule-hard);
  padding: var(--s5) 0 var(--s3);
  margin-bottom: var(--s4);
}
.discover__kicker {
  font-family: var(--mono);
  font-size: max(0.75rem, 11.5px);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 12px;
}
.discover__title {
  font-family: var(--disp); font-synthesis-weight: none;
  font-weight: 800;
  font-size: clamp(2.6rem, 7vw, 3.6rem);
  line-height: 0.92;
  letter-spacing: -0.02em;
  text-transform: uppercase;
}
.discover__lede {
  font-family: var(--read);
  font-size: 1.0625rem;
  color: var(--muted);
  margin-top: 14px;
  max-width: 54ch;
  line-height: 1.5;
}
/* The position stated on the surface (B2 of UIUX-integration-2026-08-03): no scoreboard,
   no tracking, and the tally made structurally unreachable rather than merely hidden. */
.discover__ethos {
  font-family: var(--mono);
  font-size: max(0.78rem, 11.5px);
  color: var(--muted);
  margin-top: 10px;
  max-width: 54ch;
  line-height: 1.5;
}
/* Author credit on each card — ink link, never red. Sits inside .post-list__meta
   so it inherits the mono/uppercase/tracked meta styling. */
.discover__by { color: var(--ink); text-decoration: none; font-weight: 700; }
.discover__by:hover { text-decoration: underline; text-underline-offset: 2px; }
.discover__empty-cta { margin-top: 14px; }
.discover__empty-cta a { color: var(--ink); text-decoration: underline; text-underline-offset: 2px; }
.discover__empty-cta a:hover { opacity: 0.7; }

/* ── Article ───────────────────────────────────────────────────────────────── */
.post__cover, .post__body img.post__cover {
  width: 100%; height: auto; max-height: 480px; object-fit: cover;
  display: block; margin: 0 0 32px; border: 1px solid var(--rule);
}
.post__header { margin-bottom: 8px; }
.post__title {
  font-family: var(--disp2); font-synthesis-weight: none;
  font-weight: 700;
  font-size: clamp(2.1rem, 4.6vw, 3.05rem);
  line-height: 1.08;
  letter-spacing: -0.01em;
  text-transform: none;
  text-wrap: balance;
  margin-bottom: 16px;
}
.post__subtitle {
  font-family: var(--read);
  font-weight: 400;
  font-style: normal;
  font-size: 1.375rem;
  line-height: 1.45;
  color: var(--muted);
  margin-bottom: 26px;
}

/* Byline row — Medium's signature: monogram avatar + name + "N min read · date" */
.post__byline {
  display: flex;
  align-items: center;
  gap: 12px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--rule);
}
.post__avatar {
  flex: none;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  overflow: hidden;
  border: 2px solid var(--rule-hard);
  background: var(--panel);
}
.post__avatar img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* Monogram fallback only — a member without an avatar. Matches .author-card__avatar--mono. */
.post__avatar--mono {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
  font-family: var(--ui);
  font-weight: 700;
  font-size: 1.0625rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
.post__byline-text { flex: 1 1 auto; min-width: 0; }
.post__byline-name {
  font-family: var(--ui);
  font-weight: 600;
  font-size: 0.9375rem;
  color: var(--ink);
  line-height: 1.3;
}
.post__byline-meta {
  font-size: 0.72rem;
  color: var(--muted);
  margin-top: 3px;
}
.post__byline-badge { flex: none; margin-left: auto; }

/* Co-author byline (Collaboration Phase A) — attribution only, never money → never red. */
.post__byline-name .post__author { color: var(--ink); }
.post__byline-name a.post__author { text-decoration: none; }
.post__byline-name a.post__author:hover { text-decoration: underline; text-underline-offset: 2px; }
.post__byline-sep { color: var(--muted); font-weight: 400; }

/* Inline paywall teaser — the free above-the-marker portion, fading into the
   paper just before the paywall block to signal there's more below. */
.post__body--teaser { position: relative; margin-bottom: 0; }
.post__body--teaser::after {
  content: ""; display: block; height: 2.6em; margin-top: -2.6em;
  background: linear-gradient(to bottom, transparent, var(--paper));
  pointer-events: none;
}

/* Draft preview flag — a quiet note, never money → never red. */
.preview-flag {
  font-family: var(--ui); font-size: 0.75rem; font-weight: 600;
  border: 1px dashed var(--rule-hard); padding: 8px 12px;
  margin-bottom: var(--s3); color: var(--muted);
}

/* Cross-post indicator on the feed — a piece co-authored on another space,
   linking out to its origin. Muted, never red. */
.post-list__cross { color: var(--muted); font-size: 0.85em; }

/* Body prose — Lora, the reading voice. Medium-scale: ~21px/1.62, airy paragraphs. */
.post__body { font-family: var(--read); font-size: 1.3rem; line-height: 1.62; }
/* --disp2, NOT --disp. The owner sign-off rule three lines down resets these to
   sentence case, and Anton is poster caps — the apex says so in console.css:195
   while deliberately picking Poppins for its one sentence-case display line. A
   caps face set in sentence case is the exact mistake that note exists to prevent. */
.post__body h1, .post__body h2, .post__body h3 {
  font-family: var(--disp2); font-synthesis-weight: none; font-weight: 600; line-height: 1.15;
  text-transform: uppercase; letter-spacing: -0.01em; margin: 44px 0 14px;
}
/* OWNER SIGN-OFF (single, easily-reverted rule): mid-article headings read in
   sentence case, not the ALL-CAPS used for furniture/kickers. Delete this one
   rule to restore uppercase in-body headings. */
.post__body h1, .post__body h2, .post__body h3 { text-transform: none; }
.post__body h1 { font-size: 1.8rem; }
.post__body h2 { font-size: 1.45rem; }
.post__body h3 { font-size: 1.2rem; }
.post__body p { margin-bottom: 1.4em; text-wrap: pretty; }
.post__body a { color: var(--ink); text-decoration: underline; text-underline-offset: 2px; text-decoration-thickness: 1px; }
.post__body a:hover { opacity: 0.7; }
.post__body strong { font-weight: 600; }
.post__body em { font-style: italic; }
.post__body blockquote {
  border-left: 3px solid var(--ink); margin: 30px 0; padding: 2px 0 2px 22px;
  color: var(--muted); font-style: italic;
}
.post__body blockquote p:last-child { margin-bottom: 0; }
.post__body code { font-family: var(--mono); font-size: 0.8em; background: var(--panel2); padding: 1px 5px; }
.post__body pre { background: var(--ink); color: var(--paper); padding: 20px; overflow-x: auto; margin: 30px 0; font-size: 0.85rem; }
.post__body pre code { background: transparent; padding: 0; color: inherit; }
.post__body ul, .post__body ol { padding-left: 28px; margin-bottom: 1.4em; }
.post__body li { margin-bottom: 9px; }
.post__body hr { border: none; border-top: 2px solid var(--rule); margin: 46px 0; }
.post__body img { max-width: 100%; height: auto; display: block; margin: 30px 0; }
.post__body mark { background: var(--yellow); color: #141414; padding: 0 3px; }

/* ── Inline body figures (Markdown ![alt](url) images) ─────────────────────────
   figure + italic Lora caption in --muted. Width tiers reuse the .net-bar
   full-bleed breakout: --wide centers a bounded band on the viewport, --full
   spans edge to edge. Default (--regular) stays inside the reading measure. */
.post-fig { margin: 34px 0; }
.post-fig img { width: 100%; max-width: 100%; height: auto; display: block; margin: 0; border: 1px solid var(--rule); }
.post-fig figcaption {
  font-family: var(--read); font-style: italic; font-size: 0.9375rem;
  color: var(--muted); line-height: 1.5; margin-top: 10px; text-align: center;
}
.post-fig--wide {
  position: relative; left: 50%;
  width: min(100vw, 1000px); margin-left: calc(min(100vw, 1000px) / -2);
}
.post-fig--full { position: relative; left: 50%; width: 100vw; margin-left: -50vw; }
.post-fig--full img { border-left: none; border-right: none; }
/* Keep the caption readable — re-constrain it to the reading measure when the
   figure itself has broken out of the column. */
.post-fig--wide figcaption, .post-fig--full figcaption {
  max-width: var(--measure); margin-left: auto; margin-right: auto; padding: 0 var(--gutter);
}

/* ── Block media embeds (YouTube/Vimeo/Spotify/SoundCloud/Bandcamp iframes) ────
   Same vertical rhythm as .post-fig. Video hosts get a responsive 16:9 box via
   padding-top; audio hosts get a fixed player height. */
.post-embed { margin: 34px 0; }
.post-embed iframe { display: block; width: 100%; max-width: 100%; border: 1px solid var(--rule); }

/* Responsive 16:9 for video (YouTube, Vimeo). */
.post-embed--video { position: relative; width: 100%; padding-top: 56.25%; }
.post-embed--video iframe { position: absolute; inset: 0; height: 100%; border: 0; }

/* Fixed height for audio players (Spotify, SoundCloud, Bandcamp). */
.post-embed--audio iframe { height: 166px; }
.post-embed--audio-tall iframe { height: 352px; }

/* ── Bookmark cards (link previews) ────────────────────────────────────────────
   A lone non-embed URL whose OG metadata the cron (src/resolve_bookmarks.php)
   resolved into url_cards renders as this card — see _md_bookmark_card() in
   render.php (pure DB read, zero network). Same border + --lift + panel as
   .post-fig / .author-card so a preview sits in the post's craft language, NOT
   the money-door red. Nodes: figure.bookmark-card > a.bmc-link >
   (img.bmc-img?) + span.bmc-host + span.bmc-title + span.bmc-desc. */
.bookmark-card { margin: 34px 0; }
.bookmark-card .bmc-link {
  display: grid; grid-template-columns: 1fr; align-items: start;
  border: 2px solid var(--ink); background: var(--panel); box-shadow: var(--lift);
  text-decoration: none; color: var(--ink); overflow: hidden;
}
.bookmark-card .bmc-link:hover { background: var(--panel2); }
.bookmark-card .bmc-img {
  display: block; width: 100%; height: 180px; object-fit: cover;
  background: var(--panel2); border-bottom: 2px solid var(--ink);
}
.bookmark-card .bmc-host {
  font-family: var(--mono); font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted);
  padding: 16px 18px 0;
}
.bookmark-card .bmc-title {
  font-family: var(--disp2); font-synthesis-weight: none; font-weight: 700; font-size: 1.15rem; line-height: 1.2;
  letter-spacing: -0.01em; color: var(--ink); padding: 5px 18px 0;
}
.bookmark-card .bmc-desc {
  font-family: var(--read); font-size: 0.95rem; line-height: 1.5; color: var(--muted);
  padding: 8px 18px 18px;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
/* Image left on wider viewports. :has() lets a broken/absent image collapse the
   column cleanly — onerror="this.remove()" drops the <img>, the selector stops
   matching, and the grid reverts to a single text column. */
@media (min-width: 560px) {
  .bookmark-card .bmc-link:has(.bmc-img) { grid-template-columns: 220px 1fr; }
  .bookmark-card .bmc-link:has(.bmc-img) .bmc-img {
    grid-column: 1; grid-row: 1 / span 3; height: 100%; min-height: 150px;
    border-bottom: none; border-right: 2px solid var(--ink);
  }
  .bookmark-card .bmc-link:has(.bmc-img) .bmc-host,
  .bookmark-card .bmc-link:has(.bmc-img) .bmc-title,
  .bookmark-card .bmc-link:has(.bmc-img) .bmc-desc { grid-column: 2; }
  .bookmark-card .bmc-link:has(.bmc-img) .bmc-host { padding-top: 18px; }
}

/* Gumroad button (their JS restyles, but give it a sane base) */
.post__body a.gumroad-button {
  display: inline-block; text-decoration: none; color: var(--on-buy);
  background: var(--buy); font-family: var(--ui); font-weight: 700;
  padding: 11px 22px; margin: 4px 0;
}
.post__body a.gumroad-button:hover { color: var(--on-buy); opacity: 0.9; }

/* ── Download PDF — ghost button (NOT money, so ink not red) ────────────────── */
.post__pdf-link { margin: 28px 0 4px; }
.post__pdf-link a {
  display: inline-block;
  font-family: var(--ui); font-size: 0.8125rem; font-weight: 600;
  color: var(--ink); text-decoration: none;
  border: 1.5px solid var(--ink); padding: 8px 16px;
}
.post__pdf-link a:hover { background: var(--ink); color: var(--paper); }

/* ── Paywall (money → red) ─────────────────────────────────────────────────── */
.paywall { border: 2px solid var(--ink); background: var(--panel); box-shadow: var(--lift); margin-top: var(--s5); }
.paywall__inner { padding: var(--s4) var(--s3); text-align: center; }
.paywall__label { font-size: 0.72rem; font-weight: 700; color: var(--muted); margin-bottom: 10px; }
.paywall__price { font-family: var(--disp2); font-synthesis-weight: none; font-size: 2.6rem; font-weight: 700; color: var(--buy); line-height: 1; margin-bottom: 10px; }
.paywall__desc { font-family: var(--read); font-size: 1rem; color: var(--ink); margin-bottom: 24px; }
.paywall__form { display: flex; flex-direction: column; align-items: center; gap: 14px; max-width: 340px; margin: 0 auto; }
.paywall__email-label { font-family: var(--ui); font-size: 0.8125rem; font-weight: 600; text-align: left; width: 100%; }
.paywall__email-note { font-weight: 400; color: var(--muted); display: block; font-size: 0.75rem; margin-top: 3px; }
.paywall__email-input, .tip-widget__email-input, .tip-widget__custom {
  border: 1.5px solid var(--ink); padding: 10px 12px; font-family: var(--ui);
  font-size: 0.9375rem; outline: none; background: var(--paper); color: var(--ink);
}
.paywall__email-input { width: 100%; }
.paywall__email-input:focus, .tip-widget__email-input:focus, .tip-widget__custom:focus { border-color: var(--ink); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ink) 28%, transparent); }
.paywall__notice { font-family: var(--ui); font-size: 0.75rem; text-transform: none; letter-spacing: 0; color: var(--muted); margin-top: 16px; }

/* ── Money button ──────────────────────────────────────────────────────────── */
.btn-unlock {
  display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box;
  background: var(--buy); color: var(--on-buy); border: 2px solid var(--buy);
  font-family: var(--ui); font-size: 0.86rem; font-weight: 700; letter-spacing: normal;
  text-transform: none; border-radius: var(--btn-radius, 3px);
  min-height: 44px; padding: 13px 28px; cursor: pointer; width: 100%;
}
.btn-unlock:hover { opacity: 0.92; }
.btn-unlock:active { opacity: 1; }

/* ── Free-Stories member pool (NOT money — ink, never --buy) ────────────────── */
.paywall__comp { margin-top: 22px; padding-top: 20px; border-top: 1px solid var(--rule); }
.btn-comp {
  display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box;
  background: transparent; color: var(--ink);
  border: 2px solid var(--ink); border-radius: var(--btn-radius, 3px); font-family: var(--ui); font-size: 0.9375rem;
  font-weight: 700; letter-spacing: normal; min-height: 44px; padding: 11px 26px; cursor: pointer;
  width: 100%; margin-top: 10px;
}
.btn-comp:hover { background: var(--ink); color: var(--paper); }
.btn-comp:active { opacity: 0.85; }

/* ── Spark — members-only appreciation tap (NOT money, so ink not red) ─────── */
.spark-wrap { margin: 30px 0 4px; }
.spark-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px; box-sizing: border-box;
  font-family: var(--ui); font-size: 0.8125rem; font-weight: 700;
  background: transparent; color: var(--ink);
  border: 2px solid var(--ink); border-radius: var(--btn-radius, 3px); min-height: 36px; padding: 8px 16px; cursor: pointer;
}
.spark-btn:hover { background: var(--panel2); }
.spark-btn:disabled { opacity: 0.6; cursor: default; }
/* Sparked = subtle filled state — ink, never --buy (red is the money door). */
.spark-btn.is-sparked { background: var(--ink); color: var(--paper); }
.spark-btn.is-sparked:hover { opacity: 0.9; }

/* ── Tip widget ────────────────────────────────────────────────────────────── */
.tip-widget { border: 2px solid var(--ink); background: var(--panel); box-shadow: var(--lift); margin-top: var(--s5); }
.tip-widget__inner { padding: var(--s3); text-align: center; }
.tip-widget__label { font-family: var(--ui); font-size: 0.9375rem; color: var(--ink); margin-bottom: 16px; }
.tip-widget__amounts { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 8px; margin-bottom: 16px; }
.tip-amt {
  font-family: var(--ui); font-size: 0.9375rem; font-weight: 700;
  background: var(--paper); border: 1.5px solid var(--ink); color: var(--ink);
  padding: 9px 18px; cursor: pointer;
}
.tip-amt:hover { background: var(--panel2); }
.tip-amt.is-active { background: var(--buy); border-color: var(--buy); color: var(--on-buy); }
.tip-widget__custom { width: 108px; }
.tip-widget__email-label { font-family: var(--ui); font-size: 0.8125rem; font-weight: 600; display: block; text-align: left; max-width: 320px; margin: 0 auto 6px; }
.tip-widget__email-note { font-weight: 400; color: var(--muted); }
.tip-widget__email-input { width: 100%; max-width: 320px; margin: 0 auto 16px; display: block; }
.tip-widget .btn-unlock { max-width: 320px; margin: 0 auto; }

/* ── Subscribe (reader email capture — free, so ink not red; --buy is money-only) ── */
.subscribe { border: 2px solid var(--ink); background: var(--panel); box-shadow: var(--lift); margin-top: var(--s5); }
.subscribe__inner { padding: var(--s4) var(--s3); text-align: center; }
.subscribe__label { font-family: var(--disp2); font-synthesis-weight: none; font-weight: 700; font-size: 1.15rem; color: var(--ink); margin-bottom: var(--s2); letter-spacing: -0.01em; }
.subscribe__form { display: flex; flex-direction: column; align-items: center; gap: 12px; max-width: 320px; margin: 0 auto; }
.subscribe__email-label { font-family: var(--ui); font-size: 0.8125rem; font-weight: 600; text-align: left; width: 100%; }
.subscribe__email-input { width: 100%; border: 1.5px solid var(--ink); padding: 10px 12px; font-family: var(--ui); font-size: 0.9375rem; outline: none; background: var(--paper); color: var(--ink); }
.subscribe__email-input:focus { border-color: var(--ink); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ink) 28%, transparent); }
.subscribe__submit {
  display: block; width: 100%; background: var(--ink); color: var(--paper); border: none;
  font-family: var(--disp); font-synthesis-weight: none; font-size: 0.8rem; font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; border-radius: var(--btn-radius, 999px);
  padding: 13px 22px; cursor: pointer;
}
.subscribe__submit:hover { opacity: 0.88; }
.subscribe__notice { font-family: var(--ui); font-size: 0.75rem; text-transform: none; letter-spacing: 0; color: var(--muted); margin-top: 12px; min-height: 1em; }
.subscribe__privacy { font-family: var(--ui); font-size: 0.72rem; text-transform: none; letter-spacing: 0; color: var(--muted); margin-top: 14px; line-height: 1.5; }

/* ── "Written by" author end card (premium finish, NOT money → ink, never red) ── */
.author-card {
  border: 2px solid var(--ink); background: var(--panel); box-shadow: var(--lift);
  margin-top: var(--s5); padding: var(--s3);
}
.author-card__head { display: flex; align-items: center; gap: 14px; }
.author-card__avatar {
  flex: none; width: 56px; height: 56px; border-radius: 50%; overflow: hidden;
  border: 2px solid var(--rule-hard); background: var(--panel);
}
.author-card__avatar img { display: block; width: 100%; height: 100%; object-fit: cover; }
.author-card__avatar--mono {
  display: flex; align-items: center; justify-content: center;
  background: var(--ink); color: var(--paper); border-color: var(--ink);
  font-family: var(--ui); font-weight: 700; font-size: 1.375rem;
}
.author-card__id { flex: 1 1 auto; min-width: 0; }
.author-card__kicker {
  display: block; font-family: var(--mono); font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted);
}
.author-card__name {
  display: block; font-family: var(--disp2); font-synthesis-weight: none; font-weight: 700; font-size: 1.35rem;
  line-height: 1.1; letter-spacing: -0.01em; color: var(--ink); margin-top: 2px;
}
.author-card__follow {
  flex: none; align-self: center;
  font-family: var(--mono); font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink); text-decoration: none;
  border: 1.5px solid var(--ink); padding: 7px 14px;
}
.author-card__follow:hover { background: var(--ink); color: var(--paper); }
.author-card__bio {
  font-family: var(--read); font-size: 1rem; line-height: 1.55;
  color: var(--ink); margin-top: 16px; max-width: 60ch;
}

/* ── Recommended reading (blogroll — curation, NOT money → ink, never red) ─────
   Same craft language as .author-card / .subscribe: 2px ink border + panel fill +
   --lift offset shadow. Light/dark + mobile all ride the shared vars/query. ──── */
.blogroll {
  border: 2px solid var(--ink); background: var(--panel); box-shadow: var(--lift);
  margin-top: var(--s5); padding: var(--s3);
}
.blogroll__kicker {
  font-family: var(--mono); font-size: max(0.72rem, 11.5px); font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted);
  margin-bottom: var(--s2);
}
.blogroll__list { list-style: none; margin: 0; padding: 0; }
.blogroll__item {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px 18px;
  padding: 14px 0; border-top: 1px solid var(--rule);
}
.blogroll__item:first-child { border-top: 0; padding-top: 4px; }
.blogroll__text { flex: 1 1 auto; min-width: 0; }
.blogroll__space {
  font-family: var(--disp2); font-synthesis-weight: none; font-weight: 700; font-size: 1.1rem; line-height: 1.15;
  color: var(--ink); text-decoration: none;
}
.blogroll__space:hover { text-decoration: underline; text-underline-offset: 3px; }
.blogroll__note {
  font-family: var(--read); font-size: 0.95rem; line-height: 1.5;
  color: var(--muted); margin-top: 4px; max-width: 60ch;
}
.blogroll__follow {
  flex: none; align-self: center;
  font-family: var(--mono); font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink); text-decoration: none;
  border: 1.5px solid var(--ink); padding: 7px 14px; white-space: nowrap;
}
.blogroll__follow:hover { background: var(--ink); color: var(--paper); }
@media (max-width: 600px) {
  .blogroll__item { flex-wrap: wrap; }
  .blogroll__follow { align-self: flex-start; }
}

/* ── Nav / footer ──────────────────────────────────────────────────────────── */
.post__nav { margin-top: 52px; padding-top: 18px; border-top: 1px solid var(--rule); font-size: 0.72rem; }
.post__nav a { color: var(--muted); text-decoration: none; }
.post__nav a:hover { color: var(--ink); text-decoration: underline; }
.empty-state { font-family: var(--ui); font-size: 1rem; color: var(--muted); padding: 56px 0; text-align: center; }
.not-found { text-align: center; padding: 80px 0; }
.not-found__code { font-family: var(--disp2); font-synthesis-weight: none; font-size: 5.5rem; font-weight: 700; color: var(--ink); line-height: 1; }
.not-found__msg { font-family: var(--read); font-size: 1.15rem; color: var(--ink); margin-top: 14px; }
.not-found__note { font-family: var(--ui); font-size: 0.875rem; color: var(--muted); margin-top: 14px; }
.not-found__note a { color: var(--ink); text-decoration: underline; text-underline-offset: 2px; }
.not-found__note a:hover { opacity: 0.7; }
.not-found__links { display: flex; flex-wrap: wrap; justify-content: center; gap: 14px 26px; margin-top: 26px; }
.not-found__back { display: inline-block; font-family: var(--mono); font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink); text-decoration: none; border-bottom: 2px solid var(--ink); padding-bottom: 2px; }
.not-found__back:hover { opacity: 0.65; }
.site-footer { position: relative; left: 50%; width: 100vw; margin-left: -50vw; background: var(--panel2); border-top: 3px solid var(--rule); margin-top: var(--s6); padding: var(--s4) var(--gutter) var(--s5); font-size: 0.72rem; color: var(--muted); }
/* Chrome, not prose — tracks --chrome-max with the band and the masthead. */
.site-footer > * { max-width: var(--chrome-max); margin-left: auto; margin-right: auto; }
.site-footer a { color: var(--muted); text-decoration: underline; }
.site-footer a:hover { color: var(--ink); }
.site-footer__brand { display: inline-flex; align-items: center; gap: 9px; font-family: var(--disp2); font-synthesis-weight: none; font-weight: 800; font-size: 1.05rem; letter-spacing: 0; text-transform: none; color: var(--ink); text-decoration: none; margin-bottom: 10px; }
.site-footer__brand:hover { text-decoration: underline; }
.site-footer__dot { width: 18px; height: 18px; flex: none; background: url('/assets/jf.png') center/contain no-repeat; }
.site-footer > span { display: block; text-transform: none; letter-spacing: 0; font-family: var(--ui); font-size: 0.75rem; }

/* ── Responsive ────────────────────────────────────────────────────────────── */

/* Tablet: ease the banner presets down from full desktop scale before the
   phone breakpoint takes over — a 320px hero banner is fine at 1200px wide
   but overpowers a 768px screen. */
@media (max-width: 900px) and (min-width: 601px) {
  body.layout-standard .site-banner { height: 130px; }
  body.layout-banner .site-banner { height: 250px; }
  body.layout-banner .site-header__avatar { width: 92px; height: 92px; }
}

@media (max-width: 600px) {
  :root { --gutter: 16px; }
  html { font-size: 15px; }
  .site-header { padding: 28px 0 14px; }
  /* Stack avatar above the title so the nameplate gets full width instead of being
     squeezed into a narrow column beside the avatar (which forced ugly mid-word breaks).
     No straddle lift here — the avatar leads the stack, fully below the banner. */
  body.layout-standard .site-header__top { flex-direction: column; align-items: flex-start; gap: 12px; }
  body.layout-standard .site-header__avatar { transform: none; }
  .site-header__name { font-size: clamp(1.7rem, 9vw, var(--name-max, 2.8rem)); }
  .post__title { font-size: 2rem; line-height: 1.12; }
  .post__subtitle { font-size: 1.125rem; }
  .post-list__title { font-size: 1.25rem; }
  .post-list__item { flex-direction: column; gap: 14px; }
  .post-list__thumb { width: 100%; height: auto; aspect-ratio: 1 / 1; }
  .post__body { font-size: 1.18rem; line-height: 1.58; }
  .author-card__head { flex-wrap: wrap; }
  .author-card__follow { margin-top: 4px; }
  .paywall__inner { padding: 24px 18px; }
  .paywall__price { font-size: 2.1rem; }
  .mag-lead__title { font-size: 1.7rem; }
  .mag-grid { gap: 22px; }
  .discover__masthead { padding: 28px 0 16px; }
  .discover__title { font-size: 2.2rem; }
  body.layout-standard .site-banner { height: 110px; }
  body.layout-banner .site-banner { height: 200px; }
  body.layout-banner .site-header__avatar { width: 84px; height: 84px; }

  /* The band's phone rules moved to os-band.css and CHANGED SHAPE there. This
     block let the bar wrap to two rows; the band is sticky now, and a two-row
     sticky bar eats a phone viewport. os-band.css sheds the divider, then the
     search label, then the secondary CTA instead — it stays one row. */

  /* Tappable ghost-button on mobile (was ~35px tall). */
  .post__pdf-link a { padding: 12px 18px; }
}

/* Follow — a reading relationship, not a status. Ink outline, never red. */
.follow-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 36px; margin-left: 14px; padding: 6px 14px; border: 2px solid var(--ink); border-radius: var(--btn-radius, 3px); font-family: var(--ui); font-size: var(--min); font-weight: 700; letter-spacing: normal; text-transform: none; color: var(--ink); text-decoration: none; vertical-align: middle; }

/* Coarse-pointer (touch) bump: 44px comfortable tap target. The band's own
   controls get theirs in os-band.css; this one covers Follow, which is the
   member's control and lives in the masthead. */
@media (pointer: coarse) {
  .follow-btn { min-height: 44px; }
}
.follow-btn:hover { background: var(--ink); color: var(--paper); }
.follow-btn.is-following { background: var(--ink); color: var(--paper); pointer-events: none; }

/* ── Reading-progress bar (calm reading) ─────────────────────────────────────
   A thin line pinned to the very top that fills as the article scrolls. INK,
   never red — it tracks reading, it is not a money door. Pure CSS via a scroll
   timeline where supported; the tiny fallback JS in post.php drives it otherwise;
   with neither it stays scaleX(0) (invisible) and nothing breaks. Removed wholesale
   under prefers-reduced-motion. */
.reading-progress {
  position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 200;
  background: transparent; pointer-events: none;
}
.reading-progress__bar {
  display: block; height: 100%; width: 100%;
  background: var(--ink); transform: scaleX(0); transform-origin: 0 50%;
}
@supports (animation-timeline: scroll()) {
  .reading-progress__bar {
    animation: reading-progress-fill linear both;
    animation-timeline: scroll(root block);
  }
}
@keyframes reading-progress-fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@media (prefers-reduced-motion: reduce) { .reading-progress { display: none; } }

/* ── Heading anchors + Contents TOC ──────────────────────────────────────────
   Anchor: a subtle '#' that fades in on heading hover — INK, never red (an
   affordance, not a money door). It's aria-hidden + tabindex=-1 in the markup,
   so screen readers and keyboard tabbing skip it. TOC: a slim fixed rail in the
   left margin on wide screens, a collapsible <details> at the top otherwise.
   Pure anchor links — fully functional with zero JS (native jump + scroll-margin);
   JS only adds the active-section highlight. Floors at the 11.5px readability min. */

/* Land an anchored heading clear of the top chrome, never jammed to the edge. */
.post__body h1, .post__body h2, .post__body h3 { scroll-margin-top: 84px; }

/* The hover '#' affordance. */
.heading-anchor {
  margin-left: 0.4em; color: var(--muted); text-decoration: none;
  font-family: var(--mono); font-weight: 400;
  opacity: 0; transition: opacity .12s ease;
}
.post__body h1:hover .heading-anchor,
.post__body h2:hover .heading-anchor,
.post__body h3:hover .heading-anchor { opacity: .5; }
.heading-anchor:hover { opacity: 1; color: var(--ink); }
/* Touch / no-hover pointers can't reveal-on-hover, so keep it faintly present. */
@media (hover: none) { .heading-anchor { opacity: .3; } }

/* Contents nav — default (mobile / tablet): collapsible card at the top. */
.post-toc { margin: 0 0 var(--s4); }
.post-toc__wrap { border: 1.5px solid var(--rule); background: var(--panel); }
.post-toc__summary {
  font-family: var(--mono); font-size: max(0.72rem, 11.5px); font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted);
  padding: 12px 16px; cursor: pointer; list-style: none;
}
.post-toc__summary::-webkit-details-marker { display: none; }
.post-toc__summary::after { content: '+'; float: right; font-family: var(--mono); font-weight: 400; }
.post-toc__wrap[open] .post-toc__summary::after { content: '\2013'; }
.post-toc__list { list-style: none; margin: 0; padding: 4px 16px 14px; font-family: var(--ui); }
.post-toc__item { margin: 0; }
.post-toc__link {
  display: block; padding: 5px 0 5px 12px; margin-left: -1px;
  font-size: max(0.82rem, 11.5px); line-height: 1.3;
  color: var(--muted); text-decoration: none;
  border-left: 2px solid transparent;
}
.post-toc__item--h3 .post-toc__link { padding-left: 26px; }
.post-toc__link:hover { color: var(--ink); }
.post-toc__link.is-active { color: var(--ink); border-left-color: var(--ink); font-weight: 600; }

/* Wide screens: lift Contents into the left margin as a slim fixed rail, left of
   the centred reading column. The <details> is forced open (summary hidden) so it
   reads as a plain rail — it's rendered with the `open` attribute, so no JS is
   needed to keep it expanded. */
@media (min-width: 1240px) {
  .post-toc {
    position: fixed; top: 96px; margin: 0; z-index: 50;
    left: max(var(--gutter), calc((100vw - var(--measure) - var(--gutter) * 2) / 2 - 228px));
    width: 200px; max-height: calc(100vh - 140px); overflow-y: auto;
  }
  .post-toc__wrap { border: none; background: transparent; }
  .post-toc__summary { display: none; }
  .post-toc__list { padding: 0; border-left: 1px solid var(--rule); }
  .post-toc__link { padding-left: 14px; }
  .post-toc__item--h3 .post-toc__link { padding-left: 28px; }
}

/* Smooth-scroll to anchors — progressive: only when a Contents nav is present and
   the reader hasn't asked for reduced motion. Unsupported (:has) or no-JS → the
   native anchor jump still works; this only softens the landing. */
@media (prefers-reduced-motion: no-preference) {
  html:has(.post-toc) { scroll-behavior: smooth; }
}

/* ── Prev / Next within the space (post foot) ────────────────────────────────
   The chronologically adjacent posts by this writer — a calm two-up. Same craft
   language as .author-card (2px ink border + --lift + panel). Ink, never red:
   navigation, not a money door. Server-rendered links — fully functional no-JS. */
.post-neighbors {
  display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: var(--s5);
}
.post-neighbors__link {
  display: flex; flex-direction: column; gap: 6px;
  border: 2px solid var(--ink); background: var(--panel); box-shadow: var(--lift);
  padding: 16px 18px; text-decoration: none; color: var(--ink);
  transition: background .15s ease;
}
.post-neighbors__link:hover { background: var(--panel2); }
.post-neighbors__link--next { text-align: right; }
.post-neighbors__link--empty { border: none; background: none; box-shadow: none; }
.post-neighbors__dir {
  font-family: var(--mono); font-size: max(0.72rem, 11.5px); font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted);
}
.post-neighbors__title {
  font-family: var(--disp2); font-synthesis-weight: none; font-weight: 600; font-size: 1.05rem; line-height: 1.15;
  letter-spacing: -0.01em; color: var(--ink);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* ── "More from {writer}" strip — reuses .post-list rows; chronological, not
   ranked. Only the kicker is new (mirrors .feed__kicker). Ink, never red. */
.more-from { margin-top: var(--s5); }
.more-from__kicker {
  font-family: var(--mono); font-size: 0.75rem; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted);
  border-bottom: 1px solid var(--rule); padding-bottom: 10px; margin-bottom: 4px;
}

/* ── Reader-controlled Follow block (the anti-feed) ──────────────────────────
   The reader chooses HOW the space reaches them — RSS, Email, or The Underground.
   Card in the house craft language (2px ink border + --lift + panel), matching
   .author-card / .subscribe. Ink, never red: following is a reading relationship,
   not a money door. Choices floor at 11.5px. */
.follow-block { border: 2px solid var(--ink); background: var(--panel); box-shadow: var(--lift); margin-top: var(--s5); }
.follow-block__inner { padding: var(--s4) var(--s3); }
.follow-block__kicker {
  font-family: var(--disp2); font-synthesis-weight: none; font-weight: 700; font-size: 1.15rem;
  letter-spacing: -0.01em; color: var(--ink); margin-bottom: 8px;
}
.follow-block__lede {
  font-family: var(--read); font-size: 0.95rem; line-height: 1.5;
  color: var(--muted); margin-bottom: var(--s3); max-width: 54ch;
}
.follow-block__choices { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.follow-block__choice {
  display: flex; flex-direction: column; gap: 4px;
  border: 1.5px solid var(--ink); background: var(--paper);
  padding: 14px 16px; text-decoration: none; color: var(--ink);
  transition: transform .1s ease, box-shadow .1s ease, background .15s ease, color .15s ease;
}
.follow-block__choice:hover { background: var(--ink); color: var(--paper); }
.follow-block__choice:hover .follow-block__choice-note { color: var(--paper); }
.follow-block__choice:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--rule-hard); }
.follow-block__choice-name {
  font-family: var(--mono); font-size: max(0.78rem, 11.5px); font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase;
}
.follow-block__choice-note {
  font-family: var(--ui); font-size: max(0.72rem, 11.5px); color: var(--muted); line-height: 1.35;
}

/* Mobile: single-column stacks for the two-up neighbours and the follow choices. */
@media (max-width: 600px) {
  .post-neighbors { grid-template-columns: 1fr; }
  .post-neighbors__link--next { text-align: left; }
  .post-neighbors__link--empty { display: none; }
  .follow-block__choices { grid-template-columns: 1fr; }
}

/* ── Cmd+K search: net-bar trigger + overlay ──────────────────────────────────
   ACCENT DISCIPLINE: search is INK, never --buy. Red stays the money door only.
   The trigger sits in the dark net-bar (paper-on-ink); the panel is paper stock
   with the zine offset --lift, Space Mono labels, and the 11.5px readability floor. */

/* The TRIGGER moved to os-band.css with the rest of the band. It had to: every
   colour in it was var(--paper), which is only correct on an inverted ink slab.
   On a band that tracks the page, paper-on-band is paper-on-almost-paper — an
   invisible chip. The replacement uses --on-band, so it is legible in both
   themes on all five stocks (asserted in tests/band_test.php). Only the OVERLAY
   below stays here: the dialog is paper stock and belongs to the reader. */

/* Overlay — native <dialog> */
.nf-search {
  width: min(640px, calc(100vw - 28px));
  max-height: min(70vh, 620px);
  margin: 12vh auto auto;              /* sit high, spotlight-style */
  padding: 0; border: 2px solid var(--ink);
  background: var(--panel); color: var(--ink);
  box-shadow: var(--lift);
  overflow: hidden;                    /* results list owns the scroll */
  display: flex; flex-direction: column;
}
/* A SCRIM DARKENS. It must not be keyed on --ink: in dark mode --ink resolves to
   pure white, so `color-mix(--ink 42%, transparent)` painted a WHITE wash over an
   all-black page and blew out every reader who opened search after dark. That is
   the same defect as the inverted band retired on 2026-08-07 (os-band.css), from
   the same cause — a token that flips reused where a fixed value was meant.
   Black at a fixed alpha in both themes: in light it dims the page, in dark it is
   nearly a no-op and the dialog's own paper ground and border carry the
   separation, which is the correct outcome either way. Found 2026-08-08 while
   building the Space drawer, which had copied this line. */
.nf-search::backdrop { background: rgb(0 0 0 / .55); backdrop-filter: blur(1.5px); }
.nf-search[open] { animation: nfSearchIn .12s ease-out; }
@keyframes nfSearchIn { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .nf-search[open] { animation: none; } }

.nf-search__form { margin: 0; }
.nf-search__bar {
  display: flex; align-items: center; gap: 10px;
  padding: 14px 16px; border-bottom: 2px solid var(--ink); background: var(--paper);
}
.nf-search__bar-icon { flex: none; color: var(--muted); }
.nf-search__input {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--ui); font-size: max(1rem, 16px);   /* 16px min: no iOS zoom-on-focus */
  color: var(--ink); background: transparent; border: 0; outline: 0; padding: 2px 0;
}
.nf-search__input::placeholder { color: var(--muted); }
.nf-search__esc {
  flex: none; font-family: var(--mono); font-size: 0.62rem; font-weight: 700;
  color: var(--muted); border: 1px solid var(--rule); border-radius: 2px; padding: 2px 5px;
}

.nf-search__results { list-style: none; margin: 0; padding: 4px; overflow-y: auto; }
.nf-search__row { margin: 0; }
.nf-search__hit {
  display: block; text-decoration: none; color: var(--ink);
  padding: 10px 12px; border-radius: 2px; border: 1px solid transparent;
}
/* keyboard/mouse highlight — INK wash, not red.
   The wash is DECORATION, not the indicator: at 9% it is 1.19:1 against the
   panel, and it used to be the only cue because `outline: none` removed the
   browser's ring and the border resolved to --rule (1.27:1 against the wash).
   A keyboard user arrowing through results could not see which one was
   selected. Raising --rule to 52% took that border to 2.87:1 on newsprint —
   better, still under the 3:1 WCAG 1.4.11 floor, and only 'press' cleared it.
   So the indicator is now a 2px INK outline, which is ~15:1 on every stock in
   both modes and cannot be undone by a member's palette choice. inset offset
   keeps it inside the dialog's `overflow: hidden` and shifts no layout. */
.nf-search__hit.is-active, .nf-search__hit:focus-visible {
  background: color-mix(in srgb, var(--ink) 9%, var(--panel));
  border-color: var(--ink);
  outline: 2px solid var(--ink); outline-offset: -2px;
}
.nf-search__hit-title {
  display: block; font-family: var(--disp2); font-synthesis-weight: none; font-weight: 700;
  font-size: max(0.98rem, 15px); line-height: 1.25; letter-spacing: -0.01em;
}
.nf-search__hit-excerpt {
  display: block; margin-top: 3px;
  font-family: var(--ui); font-size: max(0.82rem, 11.5px); color: var(--muted); line-height: 1.4;
  overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.nf-search__hit-meta {
  display: block; margin-top: 5px;
  font-family: var(--mono); font-size: max(0.66rem, 11.5px); letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--muted);
}
/* Gated badge — a lock affordance, NOT the money door. Neutral ink chip, never red. */
.nf-search__badge {
  display: inline-block; vertical-align: 2px;
  font-family: var(--mono); font-size: max(0.6rem, 11.5px); font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--paper); background: var(--ink);
  border-radius: 2px; padding: 1px 5px; margin-left: 2px;
}

.nf-search__note {
  padding: 12px 16px; border-top: 1px solid var(--rule);
  font-family: var(--mono); font-size: max(0.68rem, 11.5px); letter-spacing: 0.04em;
  color: var(--muted); text-transform: uppercase;
}
.nf-search__note[hidden] { display: none; }
.nf-search__keys { opacity: .8; }

@media (max-width: 560px) {
  .nf-search { width: calc(100vw - 16px); margin-top: 6vh; max-height: 82vh; }
  .nf-search__keys { display: none; }
}

/* ── Reader home / library — you.{apex} ──────────────────────────────────────
   A NETWORK page (no member masthead). Reuses the paper/ink tokens, the mono
   labels, the --lift zine offset-shadow and the 11.5px readability floor.
   Accent is INK — this page has no money door of its own, so it carries NO red
   (--buy) anywhere. */
.lib { max-width: var(--measure); margin: 0 auto; padding: 0 var(--gutter); }

.lib__masthead {
  border-bottom: 3px solid var(--rule-hard);
  padding: var(--s5) 0 var(--s3);
  margin-bottom: var(--s4);
}
.lib__kicker {
  font-family: var(--mono); font-size: max(0.75rem, 11.5px); font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted);
  margin-bottom: 12px;
}
.lib__title {
  font-family: var(--disp); font-synthesis-weight: none; font-weight: 800;
  font-size: clamp(2.2rem, 6vw, 3.2rem); line-height: 0.94;
  letter-spacing: -0.02em; text-transform: uppercase;
}

/* Signed-in identity + sign-out, on the masthead */
.lib__whoami {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px;
  margin-top: 16px;
}
.lib__email {
  font-family: var(--mono); font-size: max(0.72rem, 11.5px);
  letter-spacing: 0.04em; color: var(--muted);
}
.lib__email strong { color: var(--ink); font-weight: 700; }
.lib__signout { margin: 0; }
.lib__signout-btn {
  background: transparent; color: var(--ink); border: 1.5px solid var(--ink);
  font-family: var(--mono); font-size: max(0.66rem, 11.5px); font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase;
  border-radius: var(--btn-radius, 999px); padding: 5px 12px; cursor: pointer;
}
.lib__signout-btn:hover { background: var(--ink); color: var(--paper); }

/* Panel (sign-in / expired states) */
.lib__panel {
  border: 2px solid var(--rule-hard); box-shadow: var(--lift);
  background: var(--panel); padding: var(--s4) var(--s3);
  max-width: 460px; margin: 0 auto var(--s5);
}
.lib__lede {
  font-family: var(--read); font-size: 1.0625rem; line-height: 1.55;
  color: var(--ink); margin-bottom: 22px;
}
.lib__form { display: flex; flex-direction: column; gap: 14px; }
.lib__label {
  font-family: var(--ui); font-size: 0.8125rem; font-weight: 600; color: var(--ink);
}
.lib__label-note { font-weight: 400; color: var(--muted); display: block; font-size: 0.75rem; margin-top: 3px; }
.lib__input {
  border: 1.5px solid var(--ink); padding: 11px 12px; font-family: var(--ui);
  font-size: 0.9375rem; outline: none; background: var(--paper); color: var(--ink); width: 100%;
}
.lib__input:focus { box-shadow: 0 0 0 2px color-mix(in srgb, var(--ink) 28%, transparent); }
/* Sign-in is an INK action, never the red money door */
.lib__submit {
  display: inline-block; background: var(--ink); color: var(--paper); border: none;
  font-family: var(--disp); font-synthesis-weight: none; font-size: 0.86rem; font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; border-radius: var(--btn-radius, 999px); padding: 13px 24px; cursor: pointer; width: 100%;
}
.lib__submit:hover { opacity: 0.9; }
.lib__submit:active { opacity: 1; }
.lib__fineprint {
  font-family: var(--ui); font-size: max(0.75rem, 11.5px); color: var(--muted); margin-top: 16px;
}
.lib__fineprint a, .lib__empty-cta a { color: var(--ink); text-decoration: underline; text-underline-offset: 2px; }

/* Library list — crafted rows, mono metadata */
.lib__list { list-style: none; margin: 0; padding: 0; }
.lib__item {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
  gap: 6px 18px; padding: 18px 0; border-bottom: 1px solid var(--rule);
}
.lib__item:first-child { border-top: 1px solid var(--rule); }
.lib__item-link { text-decoration: none; color: var(--ink); flex: 1 1 60%; min-width: 0; }
.lib__item-title {
  display: block; font-family: var(--disp2); font-synthesis-weight: none; font-weight: 700;
  font-size: clamp(1.15rem, 2.6vw, 1.4rem); line-height: 1.15;
}
.lib__item-link:hover .lib__item-title { text-decoration: underline; text-underline-offset: 3px; }
.lib__item-space {
  display: block; margin-top: 4px;
  font-family: var(--mono); font-size: max(0.68rem, 11.5px); font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted);
}
.lib__item-when {
  flex: none; font-family: var(--mono); font-size: max(0.66rem, 11.5px);
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); white-space: nowrap;
}

/* Empty state — designed, not a bare line */
.lib__empty {
  border: 2px dashed var(--rule-hard); background: var(--panel);
  padding: var(--s5) var(--s3); text-align: center; max-width: 460px; margin: 0 auto var(--s5);
}
.lib__empty-head {
  font-family: var(--disp); font-synthesis-weight: none; font-weight: 800; font-size: 1.5rem;
  text-transform: uppercase; letter-spacing: -0.01em; margin-bottom: 10px;
}
.lib__empty-sub { font-family: var(--read); font-size: 1rem; line-height: 1.55; color: var(--muted); margin-bottom: 18px; }
.lib__empty-cta { font-family: var(--ui); font-size: var(--min); font-weight: 700; letter-spacing: normal; text-transform: none; }

/* Library sections (Purchased / Saved), the add-a-link form + per-card remove.
   INK, never red — the library is a reading surface, not a money door. */
.lib__section { margin: 0 0 var(--s4); }
.lib__section-title {
  font-family: var(--disp); font-synthesis-weight: none; font-weight: 800; font-size: 1.15rem;
  text-transform: uppercase; letter-spacing: 0.03em; color: var(--ink);
  margin: 0 0 var(--s2); padding-bottom: 8px; border-bottom: 2px solid var(--ink);
}
.lib__section-empty {
  font-family: var(--read); font-size: 1rem; line-height: 1.55; color: var(--muted); margin: 0 0 18px;
}
/* Saved rows carry an optional thumbnail + a remove control; center-align them. */
.lib__item--saved { align-items: center; }
.lib__item-thumb {
  flex: none; width: 56px; height: 56px; object-fit: cover;
  border: 1px solid var(--rule); background: var(--panel);
}
.lib__remove { margin: 0; flex: none; }
.lib__remove-btn {
  background: transparent; color: var(--muted); border: 2px solid var(--rule-hard);
  font-family: var(--ui); font-size: var(--min); font-weight: 700;
  letter-spacing: normal; text-transform: none;
  border-radius: var(--btn-radius, 3px); min-height: 36px; padding: 5px 10px; cursor: pointer;
}
.lib__remove-btn:hover { border-color: var(--ink); color: var(--ink); }
/* Add-a-link: inline row of url + optional title + submit, wrapping on narrow. */
.lib__addlink {
  display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end;
  margin-top: var(--s3); padding-top: var(--s3); border-top: 1px solid var(--rule);
}
.lib__addlink .lib__label { flex-basis: 100%; }
.lib__addlink .lib__input { flex: 1 1 220px; width: auto; }
.lib__addlink .lib__submit { flex: none; width: auto; }

@media (max-width: 560px) {
  .lib__masthead { padding: var(--s4) 0 var(--s3); }
  .lib__panel, .lib__empty { padding: var(--s3) var(--s2); }
  .lib__item-when { flex-basis: 100%; }
  .lib__addlink .lib__input, .lib__addlink .lib__submit { flex-basis: 100%; width: 100%; }
}

/* ── Series ───────────────────────────────────────────────────────────────────
   The on-post series strip (post.php) + the public series index (/c/{slug},
   collection.php). Reuses the same tokens as .post-neighbors / .post-list —
   INK, never red: series navigation is a reading relationship, not a money door.
   The only red on the series pages is the gated price BADGE (.badge--gated),
   which is the shared money-door token. Fully functional with zero JS. */

/* On-post strip: prev · "Part N of {Series}" (opens the index) · next */
.series-strip {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: stretch; gap: 8px;
  margin: var(--s3) 0 var(--s4);
  border: 2px solid var(--ink); background: var(--panel); box-shadow: var(--lift);
}
.series-strip__side {
  display: flex; align-items: center; gap: 6px; padding: 12px 14px;
  font-family: var(--mono); font-size: max(0.72rem, 11.5px); font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink);
  text-decoration: none; white-space: nowrap;
}
.series-strip__side--next { justify-content: flex-end; }
.series-strip__side:hover { background: var(--panel2); }
.series-strip__side--empty { pointer-events: none; }
.series-strip__mid {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 10px 16px; text-align: center; text-decoration: none; color: var(--ink);
  border-left: 1px solid var(--rule); border-right: 1px solid var(--rule);
}
.series-strip__mid:hover { background: var(--panel2); }
.series-strip__part {
  font-family: var(--mono); font-size: max(0.72rem, 11.5px); font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted);
}
.series-strip__name {
  font-family: var(--disp2); font-synthesis-weight: none; font-weight: 700; font-size: 0.98rem; line-height: 1.1;
  letter-spacing: -0.01em; color: var(--ink);
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;
}
@media (max-width: 560px) {
  .series-strip { grid-template-columns: 1fr; }
  .series-strip__side { justify-content: center; }
  .series-strip__mid { border-left: none; border-right: none;
    border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); order: -1; }
}

/* Public series index page (collection.php) */
.series-index__head { margin: var(--s4) 0 var(--s3); }
.series-index__kicker {
  font-family: var(--mono); font-size: max(0.72rem, 11.5px); font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); margin-bottom: 6px;
}
.series-index__title {
  font-family: var(--disp2); font-synthesis-weight: none; font-weight: 800; font-size: clamp(1.7rem, 5vw, 2.6rem);
  line-height: 1.05; letter-spacing: -0.02em; color: var(--ink);
}
.series-index__blurb {
  font-family: var(--read); font-size: 1.06rem; line-height: 1.55; color: var(--ink);
  margin-top: 12px; max-width: var(--measure);
}
.series-index__by {
  font-family: var(--mono); font-size: max(0.72rem, 11.5px); color: var(--muted);
  margin-top: 10px; text-transform: uppercase; letter-spacing: 0.06em;
}
.series-list { list-style: none; margin-top: var(--s3); }
.series-list__item {
  display: flex; align-items: flex-start; gap: 14px;
  padding: 18px 0; border-bottom: 1px solid var(--rule);
}
.series-list__item:first-child { border-top: 1px solid var(--rule); }
.series-list__num {
  flex: none; width: 34px; height: 34px; display: grid; place-items: center;
  border: 2px solid var(--ink); background: var(--panel);
  font-family: var(--disp2); font-synthesis-weight: none; font-weight: 800; font-size: 1rem; color: var(--ink);
}
.series-list__text { flex: 1 1 auto; min-width: 0; }
.series-list__title {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  font-family: var(--disp2); font-synthesis-weight: none; font-weight: 700; font-size: 1.2rem; line-height: 1.2;
  letter-spacing: -0.01em;
}
.series-list__title a { color: var(--ink); text-decoration: none; }
.series-list__title a:hover { text-decoration: underline; }
.series-list__excerpt {
  font-family: var(--read); font-size: 0.98rem; line-height: 1.5; color: var(--ink);
  margin-top: 6px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.series-list__meta { font-size: 0.72rem; color: var(--muted); margin-top: 10px;
  font-family: var(--mono); letter-spacing: 0.04em; }
.series-list__meta-part { text-transform: uppercase; }
.series-list__thumb {
  flex: none; width: 112px; height: 84px; overflow: hidden; border: 1px solid var(--rule);
}
.series-list__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
@media (max-width: 560px) {
  .series-list__thumb { width: 84px; height: 64px; }
  .series-list__num { width: 28px; height: 28px; font-size: 0.86rem; }
}

/* ── The Space drawer (band slot 1) ──────────────────────────────────────────
   This space's own navigation: pages, tags, archive, then the way back out to the
   network. The apex's button in the same position opens the network sidenav; this
   one opens the room, which is the whole "same button, local drawer" line in the
   OS BAND CONTRACT.

   Left-anchored, because the toggle is on the left and a panel that flies in from
   the opposite side of the screen from the control that opened it is a small lie
   about cause and effect. z-index sits above the band (60) so the panel is not
   sliced by its own trigger. */
.space-drawer {
  position: fixed; inset: 0; z-index: 100;
  /* Fixed black, NOT color-mix on --ink — see the .nf-search::backdrop note. */
  background: rgb(0 0 0 / .55);
}
.space-drawer[hidden] { display: none; }
.space-drawer__panel {
  position: absolute; inset-block: 0; left: 0;
  width: min(330px, 86vw);
  overflow-y: auto; overscroll-behavior: contain;
  background: var(--paper);
  border-right: 2px solid var(--ink);
  padding: 18px var(--gutter) var(--s4);
}
.space-drawer__head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding-bottom: 12px; margin-bottom: 6px; border-bottom: 2px solid var(--ink);
}
.space-drawer__kicker {
  font-family: var(--mono); font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.space-drawer__close {
  flex: none; width: 32px; height: 32px; padding: 0; line-height: 1;
  background: none; border: 0; color: var(--ink); font-size: 1.5rem; cursor: pointer;
}
.space-drawer__close:hover { opacity: 0.6; }
.space-drawer__close:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.space-drawer__sec { margin-top: 20px; }
.space-drawer__lbl {
  font-family: var(--mono); font-size: 0.68rem; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted);
  margin-bottom: 8px;
}
/* Rows, not pills. The compact header already uses pills for the same links, and
   a stacked list of pills reads as a control panel rather than a table of
   contents. 44px touch floor, the network's own. */
.space-drawer__sec > a {
  display: block; padding: 9px 0; min-height: 44px;
  font-family: var(--ui); font-size: 0.95rem; font-weight: 600;
  color: var(--ink); text-decoration: none;
  border-bottom: 1px solid var(--rule);
}
.space-drawer__sec > a:hover { text-decoration: underline; }
.space-drawer__sec > a:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
.space-drawer__tags { display: flex; flex-wrap: wrap; gap: 8px; }
/* The network section is the way OUT, so it is quieter than the room's own nav —
   present, never competing with the writer's pages. */
.space-drawer__sec--net > a { font-weight: 500; color: var(--muted); }
.space-drawer__sec--net > a:hover { color: var(--ink); }
@media (prefers-reduced-motion: no-preference) {
  .space-drawer__panel { animation: space-drawer-in .16s ease-out; }
  @keyframes space-drawer-in { from { transform: translateX(-14px); opacity: .6; } }
}

/* NOTE FOR THE NEXT PERSON APPENDING TO THIS FILE: do NOT append at EOF.
   tests/palette_test.php defines the "preset section" as everything from the
   first `html[data-space-` selector to the end of the file, and forbids
   position/z-index/transform/opacity/display in it — a preset that could float a
   transparent layer over the Unlock or Tip control is the threat. Anything added
   after the presets inherits that ban and fails the build. New rules go ABOVE
   this line; the appearance presets stay last. */

/* ═══════════════════════════════════════════════════════════════════════════
   SPACE APPEARANCE PRESETS — curated, authored here, selected by a whitelisted key
   ═══════════════════════════════════════════════════════════════════════════
   Every rule below is scoped to a data-space-* attribute on <html> that ONLY
   templates/layout.php emits, and ONLY after re-whitelisting the member's stored
   key against src/space_theme.php. No member-supplied byte reaches this file, and
   no member can add a rule to it.

   ZERO VISUAL CHANGE CONTRACT. The defaults — stock 'newsprint', mark 'ink', type
   'press' — emit NO attribute, so not one selector below can match a space that
   has not opted in. 'newsprint' has no block at all because its palette IS
   design-tokens.css's palette; there is nothing to override. That is why this
   whole section can ship to production with zero diff on every existing space.

   STANDING PROHIBITION: no per-member CSS URL, ever. The per-member value rides
   inside the Host-keyed HTML, bound to the same cache key as the content it
   themes. A /assets/theme.css?u={username} route would be path-keyed with the
   tenant in a query string — cacheable, tenant-scoped only by convention, and one
   misconfigured "ignore query string" rule away from serving member A's theme on
   member B's domain. If a CSS endpoint ever becomes necessary it must be
   /assets/theme/{key}.css with {key} from the allowlist: path-keyed,
   tenant-independent, N files for N presets.

   WHAT A PRESET BLOCK MAY DECLARE: colour, type and spacing tokens only. No
   position, z-index, transform, opacity, pointer-events, content or url() — a
   token that reached those would let a preset float a transparent layer over the
   Unlock or Tip control. tests/palette_test.php asserts this section declares
   none of them, and that --min (the 11.5px readability floor) appears nowhere in
   it: the floor is a house guarantee, not a preference.

   ── THE SPECIFICITY TRAP (read before editing a stock) ────────────────────────
   Dark mode on a space is NOT local. layout.php runs a pre-paint script that
   reads the network-wide noo_theme cookie and stamps data-theme on <html>, and
   TWO house rules flip the palette — in design-tokens.css AND again in this file:
       @media (prefers-color-scheme:dark) { :root:not([data-theme="light"]) {…} }   (0,2,0)
       :root[data-theme="dark"] {…}                                                (0,2,0)
   Both are (0,2,0), NOT (0,1,1): ":root" is a pseudo-CLASS, so it scores in the
   same column as an attribute selector, not the element column. Get that wrong
   and the rest of the reasoning inverts.
   A stock block written only as html[data-space-stock="x"] is (0,1,1) — which
   LOSES to both house rules on the middle column. The failure is not that a
   light palette leaks into dark; it is that the member's stock is IGNORED
   entirely the moment the reader is in dark mode. Half-themed either way.
   Hence three blocks per stock, every time, even when a value equals the house
   value: base (0,1,1) beats :root (0,1,0); the media query with
   :not([data-theme="light"]) (0,2,1) beats (0,2,0); and [data-theme="dark"]
   (0,2,1) beats (0,2,0). Each wins on the element column with no !important
   anywhere. The reader's explicit light choice still wins, because neither dark
   selector matches when data-theme="light".
   Verified in a real engine, not just derived: all four theme states
   (OS-light, OS-dark, cookie=light, cookie=dark) return the intended palette.
   Do not restate --rule or --lift: design-tokens.css declares them in terms of
   --ink/--paper/--rule-hard, so they re-resolve on their own.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── STOCK: newsprint — warm cream, the house look until 2026-08-07 ────────────
   This block exists BECAUSE the default moved to press. layout.php:86 stamps
   data-space-stock only for a non-default stock, so on the day newsprint stopped
   being the default it started being stamped — and without these three rules the
   37 members holding it would have silently been repainted white. Its values are
   character-for-character what design-tokens.css used to declare as the base, so
   those members see no change whatsoever. Do not "simplify" this away. */
html[data-space-stock="newsprint"] { --paper:#f5f0e6; --ink:#1a1a1a; --muted:#5c5952; --rule-hard:#1a1a1a; }
@media (prefers-color-scheme:dark) {
  html[data-space-stock="newsprint"]:not([data-theme="light"]) { --paper:#0d0d0d; --ink:#f3f2ee; --muted:#9a988f; --rule-hard:#f3f2ee; }
}
html[data-space-stock="newsprint"][data-theme="dark"] { --paper:#0d0d0d; --ink:#f3f2ee; --muted:#9a988f; --rule-hard:#f3f2ee; }

/* ── STOCK: bone — cooler, less yellow, cleaner ─────────────────────────────── */
html[data-space-stock="bone"] { --paper:#faf7f2; --ink:#16161a; --muted:#5c5952; --rule-hard:#16161a; }
@media (prefers-color-scheme:dark) {
  html[data-space-stock="bone"]:not([data-theme="light"]) { --paper:#0d0d0d; --ink:#f3f2ee; --muted:#9a988f; --rule-hard:#f3f2ee; }
}
html[data-space-stock="bone"][data-theme="dark"] { --paper:#0d0d0d; --ink:#f3f2ee; --muted:#9a988f; --rule-hard:#f3f2ee; }

/* ── STOCK: slate — dark-leaning, cool ─────────────────────────────────────── */
html[data-space-stock="slate"] { --paper:#f5f0e6; --ink:#1a1a1a; --muted:#5c5952; --rule-hard:#1a1a1a; }
@media (prefers-color-scheme:dark) {
  html[data-space-stock="slate"]:not([data-theme="light"]) { --paper:#14161a; --ink:#e8e6e0; --muted:#9a9c9f; --rule-hard:#e8e6e0; }
}
html[data-space-stock="slate"][data-theme="dark"] { --paper:#14161a; --ink:#e8e6e0; --muted:#9a9c9f; --rule-hard:#e8e6e0; }

/* ── STOCK: oxide — dark-leaning, warm ─────────────────────────────────────── */
html[data-space-stock="oxide"] { --paper:#f5f0e6; --ink:#1a1a1a; --muted:#5c5952; --rule-hard:#1a1a1a; }
@media (prefers-color-scheme:dark) {
  html[data-space-stock="oxide"]:not([data-theme="light"]) { --paper:#17120f; --ink:#f0e9df; --muted:#a19488; --rule-hard:#f0e9df; }
}
html[data-space-stock="oxide"][data-theme="dark"] { --paper:#17120f; --ink:#f0e9df; --muted:#a19488; --rule-hard:#f0e9df; }

/* ── STOCK: press — photocopier maximum contrast, the zine extreme ────────────
   NO BLOCK, deliberately. press became SPACE_STOCK_DEFAULT on 2026-08-07, and
   the default stock is the one whose values live in design-tokens.css as the
   base — layout.php:86 never stamps data-space-stock for it, so a block here
   would be dead CSS that no selector can reach. palette_test.php asserts this
   absence. Its palette is in design-tokens.css; edit it there. */

/* ── TYPE: the faces ────────────────────────────────────────────────────────
   Reassigns --disp/--ui/--read across the four ALREADY self-hosted families
   (see the @font-face block at the top of this file). Zero extra network
   requests: every face named here is already downloaded by every space. No
   dark variant — faces do not change with the reader's theme. Size,
   line-height and measure are untouched and not themeable.
   'press' (Poppins/Figtree/Lora) is the default and has no block. ──────────── */
html[data-space-type="plain"]    { --disp: var(--ui);   --read: var(--ui); }
html[data-space-type="book"]     { --disp: var(--read); }
html[data-space-type="terminal"] { --disp: var(--mono); --read: var(--ui); }

/* ── MARK: the one colour that is not ink ───────────────────────────────────
   The hex arrives from ONE inline <style id="nf-mark"> emitted by layout.php
   from the SPACE_MARKS constant — never from this file, never from the member.
   The declaration below is a FALLBACK only: it keeps every rule in this section
   valid if the inline block is ever absent, degrading to today's ink rather
   than to an undefined custom property (which would silently inherit).

   RED RATION IS ENFORCED BY SELECTOR SCOPE HERE, not by convention. --mark
   colours furniture, tag chips, link underlines and the focus ring — and
   NOTHING ELSE. --buy stays the money door: no rule below touches .btn, .badge,
   .paywall, .tip-widget, .spark-btn, .follow-btn, .subscribe or any price,
   unlock, tip or destructive control. palette_test.php asserts that.
   The net-bar is network chrome, identical on every space, and deliberately
   does not take a member's mark. ─────────────────────────────────────────── */
html[data-space-mark] { --mark: var(--ink); }

/* furniture — the broadsheet dateline and the feed kicker rule */
html[data-space-mark] .site-header__dateline { border-top-color: var(--mark); border-bottom-color: var(--mark); }
html[data-space-mark] .site-header__dateline .sh-fleuron { color: var(--mark); }
html[data-space-mark] .feed__kicker { border-bottom-color: var(--mark); }

/* tags — the category nav. Organisational, never money. Hover inverts to
   paper-on-mark, which is the same ratio as mark-on-paper and therefore
   carries the same AA guarantee from the 80-combo matrix. */
html[data-space-mark] .tag-chip { color: var(--mark); border-color: var(--mark); }
html[data-space-mark] .tag-chip:hover { background: var(--mark); color: var(--paper); }

/* links — the hover underline only. The link text stays ink (15:1+) so reading
   contrast never depends on the member's choice; the mark marks the gesture. */
html[data-space-mark] .post__body a:hover { text-decoration-color: var(--mark); }

/* focus — WCAG 2.2 SC 1.4.11 wants 3:1 against what is adjacent; the weakest
   mark/paper pair in the matrix is 6.15:1, so every swatch clears it. */
html[data-space-mark] .skip:focus { outline-color: var(--mark); }

