/* ============================================================
   BLOG — page styles.

   Source: the two inline <style> blocks in
     mockups/landing_page/Closer Design System/ui_kits/blog/index.html
     mockups/landing_page/Closer Design System/ui_kits/blog/article.html
   lifted out and deduped into one stylesheet both pages link.

   Load AFTER site/tokens.css — every rule here consumes tokens
   (--accent-orange, --bg-panel, ...) and relies on the base reset.

   Deduping decisions (the two blocks disagreed in a few places):
   1. `.wrap` was max-width:1080px on the index and 760px on the
      article. Kept 1080px as the default and added a `.narrow`
      modifier for the 760px reading measure.
      >>> ARTICLE PAGES MUST USE <div class="wrap narrow"> <<<
   2. `.news h3` was 24px on the index and 22px on the article —
      unified at 24px.
   3. `footer` had margin-top:20px only on the index — kept for both.
   4. Cosmetic drift in the newsletter block (`.news` margin 50px vs
      44px, `.news form` flex-basis 300px vs 280px) — unified at
      44px / 300px.

   The bare `a { color; text-decoration }` / `a:hover` rules that both
   blocks repeated are omitted: site/tokens.css (base.css) already
   sets exactly those.
   ============================================================ */

/* ---------- shared chrome ---------- */

/* Nav island — mirrors the landing page's NavIsland
   (frontend/src/landing/ds/NavIsland.tsx) value for value, so the marketing
   site and the blog behave identically: a full-width bar at rest that morphs
   into a floating pill past 50px of scroll. site/nav.js toggles `.docked`;
   without JS the bar simply stays full-width.

   `position: fixed` takes the nav out of flow (the old `sticky` reserved its
   own 80px), so `body` carries that offset instead — see below. */
.nav{
  position:fixed;top:0;left:50%;transform:translateX(-50%);z-index:100;
  box-sizing:border-box;
  display:flex;justify-content:space-between;align-items:center;
  width:100%;max-width:none;height:80px;padding:0 40px;
  background:transparent;backdrop-filter:none;-webkit-backdrop-filter:none;
  border:1px solid transparent;border-bottom:1px solid var(--border-color);
  border-radius:0;box-shadow:none;
  transition:
    max-width .4s cubic-bezier(.25,1,.5,1),
    width .4s cubic-bezier(.25,1,.5,1),
    height .4s cubic-bezier(.25,1,.5,1),
    top .4s cubic-bezier(.25,1,.5,1),
    padding .4s cubic-bezier(.25,1,.5,1),
    border-radius .4s cubic-bezier(.25,1,.5,1),
    background .4s cubic-bezier(.25,1,.5,1),
    backdrop-filter .4s cubic-bezier(.25,1,.5,1),
    border-color .4s cubic-bezier(.25,1,.5,1),
    box-shadow .4s cubic-bezier(.25,1,.5,1);
}
.nav.docked{
  top:20px;width:90%;max-width:800px;height:60px;padding:0 30px;
  background-color:var(--bg-panel);
  background:color-mix(in srgb, var(--bg-panel) 95%, transparent);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
  border:1px solid var(--accent-orange);
  border-bottom:1px solid var(--accent-orange);
  border-radius:50px;box-shadow:var(--shadow-island);
}
/* Replaces the layout space the sticky bar used to occupy. */
body{padding-top:80px}
/* The nav is fixed, so a fragment jump (`/blog#memory`, used by the article's
   breadcrumb, related cards and pillar link) would park the target heading
   underneath it. Clears the docked pill's 80px bottom edge plus a little air. */
html{scroll-padding-top:96px}

.brand{font-family:var(--font-display);font-size:24px;color:var(--accent-orange);letter-spacing:4px}
.nav-links{display:flex;gap:20px;font-size:12px}
/* Orange marks the page you are on — nothing else. The Contact link used to
   carry a hardcoded orange from the design, which lit two links at once. */
.nav-links a[aria-current]{color:var(--accent-orange)}

@media (prefers-reduced-motion: reduce){
  .nav{transition:none}
}

/* Default measure = blog index. Articles add `.narrow`. */
.wrap{max-width:1080px;margin:0 auto;padding:0 20px}
.wrap.narrow{max-width:760px}

.kicker{font-size:10px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent-orange);margin-bottom:14px}

/* ---------- blog index ---------- */
/* Halved from 80/40. The lede is gone, so the header is one line of display
   type and no longer needs the airier block it was sized for. */
.head{padding:40px 0 20px;border-bottom:1px solid var(--border-color)}

/* Prose pointer to the Graph View, which lives on article pages. Reads as a
   sidebar note, not a card — no fill, one accent rule down the left. */
.graph-note{
  margin:18px 0 0;padding:0 0 0 var(--space-4);
  border-left:2px solid var(--accent-orange);
}
.graph-note h2{
  font-family:var(--font-display);font-size:22px;text-transform:uppercase;
  letter-spacing:var(--tracking-display);color:var(--text-light);margin:0 0 10px;
}
.graph-note > p{font-size:13px;line-height:var(--leading-body);color:var(--text-muted);margin:0 0 12px;max-width:62ch}

/* The legend. Overrides `article li`'s dashed rule and 14px type — this is a
   key, not prose. */
.graph-key{list-style:none;margin:var(--space-3) 0 0;padding:0}
.graph-key li{
  margin:0 0 8px;padding:0 0 0 14px;border:0;position:relative;
  font-family:var(--font-body);font-size:12px;line-height:var(--leading-snug);
  color:var(--text-muted);
}
.graph-key li::before{content:'>';position:absolute;left:0;color:var(--accent-orange)}
.graph-key b{color:var(--text-light);font-weight:400}
.h1{font-family:var(--font-display);font-size:112px;text-transform:uppercase;letter-spacing:2px;line-height:1.05;margin-bottom:8px}
/* `.lede` is gone with its markup — the index header is the h1 alone now. The
   <head> keeps its own shorter description for search and social; that copy was
   never this paragraph. */
/* `.tools` / `.search` / `.chip` are GONE, with their markup. The index carried
   its own search box plus five cluster chips — a second filter UI over the same
   content-index.json the rail already filters, and the two could disagree about
   what "matches" means. The rail is now the single filter on every blog page,
   and its folder tree does the chips' job. */
.cluster{padding:50px 0;border-bottom:1px solid var(--border-color)}
.cluster h2{font-family:var(--font-display);font-size:34px;text-transform:uppercase;letter-spacing:2px;color:var(--accent-orange);margin-bottom:8px}
.cluster .sub{font-size:12.5px;color:var(--text-muted);margin-bottom:26px;max-width:620px;line-height:1.6}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;align-items:stretch}
.card{display:flex;flex-direction:column;background:var(--bg-panel);border:1px solid var(--border-color);padding:24px 26px}
.card.pillar{border-color:var(--accent-orange);box-shadow:var(--shadow-glow);grid-column:1/-1}
.card h3{font-family:var(--font-display);font-size:22px;text-transform:uppercase;letter-spacing:1.5px;line-height:1.15;margin-bottom:12px}
.card.pillar h3{font-size:32px}
.card p{font-size:12.5px;color:var(--text-muted);line-height:1.7;flex-grow:1}
.meta{display:flex;gap:14px;margin-top:18px;padding-top:14px;border-top:1px dashed var(--border-color);font-size:10px;color:var(--text-faint)}
.stub{color:var(--text-faint);font-style:italic}
.hidden{display:none}

/* ---------- article ---------- */
.crumbs{display:flex;flex-wrap:wrap;gap:8px;font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--text-muted);padding-top:60px}
.crumbs a{color:var(--text-muted)}
.crumbs .sep{color:var(--accent-orange)}
header.post{padding:20px 0 28px;border-bottom:1px solid var(--border-color)}
h1{font-family:var(--font-display);font-size:52px;text-transform:uppercase;letter-spacing:2px;line-height:1.05;margin-bottom:24px}
.byline{display:flex;flex-wrap:wrap;gap:18px;align-items:center;font-size:11px;color:var(--text-muted)}
.byline .who{color:var(--text-light)}
.byline .upd{color:var(--accent-orange)}
.answer{position:relative;background:var(--bg-panel);border:1px solid var(--accent-orange);box-shadow:var(--shadow-glow);padding:30px 28px 26px;margin:36px 0}
.answer .bar{position:absolute;top:0;left:0;width:100%;box-sizing:border-box;padding:5px 15px;border-bottom:1px solid var(--border-color);font-size:10px;letter-spacing:1px;color:var(--accent-orange);display:flex;justify-content:space-between}
.answer p{margin-top:14px;font-size:15px;line-height:1.65;color:var(--text-light)}
article h2{font-family:var(--font-display);font-size:32px;text-transform:uppercase;letter-spacing:2px;margin:44px 0 16px}
article h3{font-family:var(--font-display);font-size:24px;text-transform:uppercase;letter-spacing:2px;margin:30px 0 12px}
article p{font-size:14px;line-height:1.75;color:var(--text-muted);margin-bottom:16px}
article ul,article ol{margin:0 0 18px 0;padding-left:0;list-style:none}
article li{font-size:14px;line-height:1.75;color:var(--text-muted);border-bottom:1px dashed var(--border-color);padding-bottom:10px;margin-bottom:10px}
article li:last-child{border-bottom:none}
article li b{color:var(--accent-orange);font-weight:400}
.note{color:var(--text-faint);font-style:italic}
.takeaways{background:var(--bg-panel);border:1px solid var(--border-color);padding:24px 26px;margin:36px 0}
.takeaways h2{font-size:24px;margin:0 0 16px}
.cta{background:var(--bg-panel);border:1px solid var(--border-color);border-left:1px solid var(--accent-orange);padding:24px 26px;display:flex;gap:24px;align-items:center;flex-wrap:wrap;margin:36px 0}
.cta h3{margin:0 0 8px;font-size:24px}
.cta p{margin:0;font-size:12.5px}
.cta a{background:var(--btn-bg);color:var(--btn-text);font-size:12px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;padding:12px 20px;white-space:nowrap}
.faq{margin:44px 0}
.faq details{border:1px solid var(--border-color);background:var(--bg-panel);margin-bottom:10px}
.faq summary{padding:20px;cursor:pointer;font-size:13px;font-weight:700;color:var(--text-light);list-style:none;display:flex;justify-content:space-between;gap:20px}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{content:"+";color:var(--accent-orange);font-size:18px;line-height:1}
.faq details[open] summary::after{content:"\2212"}
.faq .a{padding:0 20px 20px;font-size:12.5px;color:var(--text-muted);line-height:1.7}
/* FAQ answers are a lead sentence plus key points. The list rules above are
   scoped to `article`, and <section class="faq"> is a SIBLING of </article>, so
   none of them reach here — without these the bullets inherit only the global
   `* { margin:0; padding:0 }` reset and run together as one block with discs
   hanging outside the panel. Markers match the site's terminal idiom rather
   than a default disc. The answer markup cannot carry classes: it has to stay
   byte-identical to the FAQPage JSON-LD, which permits only a small HTML
   subset, so everything here is driven off bare tags. */
.faq .a p{margin:0 0 10px}
.faq .a ul{margin:0;padding-left:0;list-style:none}
.faq .a li{position:relative;padding-left:16px;margin-bottom:6px}
.faq .a li::before{content:">";position:absolute;left:0;color:var(--accent-orange)}
.faq .a li:last-child{margin-bottom:0}
.faq .a a{color:var(--accent-orange)}
.author{background:var(--bg-panel);border:1px solid var(--border-color);padding:24px 26px;margin:36px 0}
.author .lab{font-size:10px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent-orange);margin-bottom:12px}
.author .name{font-family:var(--font-display);font-size:26px;text-transform:uppercase;letter-spacing:2px;color:var(--text-faint)}
.related{margin:44px 0}
.related h2{font-size:28px;margin-bottom:20px}
.rgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px}
.rgrid a{display:block;background:var(--bg-panel);border:1px solid var(--border-color);padding:20px 22px;font-size:13px;line-height:1.55;color:var(--text-light)}
.rgrid .k{font-size:10px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent-orange);margin-bottom:10px}
.pillarlink{display:inline-block;margin-top:18px;font-size:12px;color:var(--accent-orange);border-bottom:1px dashed var(--accent-orange)}

/* ---------- newsletter + footer (shared) ---------- */
.news{display:flex;gap:20px;align-items:center;flex-wrap:wrap;border:1px dashed var(--border-color);padding:26px;margin:44px 0}
.news h3{font-family:var(--font-display);font-size:24px;text-transform:uppercase;letter-spacing:2px}
.news form{display:flex;gap:10px;flex:1 1 300px}
.news input{flex:1;min-width:0;background:var(--bg-dark);border:1px solid var(--border-color);padding:12px 14px;font-family:var(--font-body);font-size:12px;color:var(--text-light)}
.news button{background:transparent;color:var(--text-light);border:1px solid var(--border-color);padding:12px 18px;font-family:var(--font-body);font-size:11px;font-weight:700;text-transform:uppercase;cursor:pointer}
/* The footer is a permanently-black band: `background:#000` is a literal, and
   --black stays #000000 in both palettes. So its ink has to be pinned to the
   dark values too — otherwise light mode flips --text-light to near-black and
   the links render at 1.17:1 on black, i.e. invisible. Same fix VideoStage uses
   for the video bands. Renders identically in dark mode; light only. */
footer{
  --text-light:#f0f0f0;--text-muted:#8b949e;--text-faint:#555555;
  --border-color:#2d333b;--accent-orange:#FF6600;
  border-top:1px solid var(--border-color);padding:40px 20px;text-align:center;background:#000;margin-top:20px
}
footer a{color:var(--text-light)}
footer p{font-size:10px;color:var(--text-faint)}

/* ============================================================
   EXPLORER — the post tree, on EVERY blog page including /blog.

   Markup is server-rendered (see site/explorer.js for the DOM
   contract); this only dresses it. Mostly a design-system port:
   VT323 uppercase + --tracking-display for the Explorer heading
   and the folder headings, Space Mono for links, --accent-orange
   for active / markers / focus, --bg-panel + --border-color for
   the surface.

   ONE DELIBERATE DEPARTURE: the search control is a rounded pill
   (border-radius:999px), not --radius-none. The design system's
   control is square and the docked nav pill was until now the
   only rounded thing on the site. The reference screenshot this
   chrome is drawn from has a pill, and that was the explicit
   instruction — so it is a decision, not drift. Do not "fix" it
   back to --radius-none without asking.

   PROGRESSIVE-ENHANCEMENT CONTRACT. Every rule below renders the
   tree OPEN. `.collapsed` (folders), `.explorer-tree-collapsed`
   (the whole tree, from the Explorer heading) and
   `.explorer-collapsed` (the small-screen disclosure) are set by
   explorer.js and by nothing else, so with JS off no folder's
   contents are hidden and every link is present and clickable.
   Do not move a collapse into an initial state here.

   LAYOUT, >= 1024px. The rail and the reading column are centred
   AS ONE UNIT rather than the column being centred in the
   leftover space. Two reasons: the article's measure stays
   byte-identical to what it is today at every width (the padding
   trick below adds the rail to both max-width and padding-left,
   so the content box is 720px before and after), and the reader's
   eye keeps a fixed relationship to the nav, which is centred on
   the same axis. It is also what Quartz/Docusaurus do. Cost: the
   text block sits --exp-rail/2 right of the viewport centre.

   TWO MEASURES, TWO SUMS. Articles are `.wrap.narrow` (760px
   block, 720px measure); /blog is a plain `.wrap` (1080px block,
   1040px measure) and now carries the same rail. The narrow
   arithmetic does NOT transfer — 1080 + rail overflows every
   viewport the narrow one fits in — so the index gets its own
   `left`, derived below.

   The aside is `position: fixed`, so it does not care whether
   the build emits it as a sibling of the wrap or inside it (it
   emits it INSIDE, as the first child, on both page types), and
   it gets its own scroll container for free — the tree is 19
   items and will grow past a short viewport.

   SPECIFICITY. The width cascade below is deliberately flat: the
   nested-in-`.wrap` override is written `:where(.wrap) .explorer`
   so it weighs (0,1,0), exactly like the bare `.explorer` rules
   before and after it. Every width therefore resolves by SOURCE
   ORDER alone — base, then nested, then the >= 1024px rail. A
   plain `.wrap .explorer` would be (0,2,0) and would beat the
   rail rule inside the media query, leaving the fixed aside at
   `width:100%` of the viewport, i.e. covering the article.
   The one (0,2,0) selector that IS allowed —
   `.wrap:not(.narrow) .explorer` — sets `left` and nothing else.
   No width, no max-width; keep it that way.

   NO HORIZONTAL OVERFLOW — the arithmetic. `.wrap` is
   `margin:0 auto` with `padding:0 20px` (15px below 480px), so
   its CONTENT width is the measure, and the nested aside is
   `width:100%` of exactly that box. The aside can therefore
   never be wider than the page column it sits above.

     >= 1024px, ARTICLES  (superseded at >= 1440px by the RIGHT
                RAIL block below, which adds a third column and
                moves this aside's `left` with the wider block)
                block = 760 + rail, padding-left = 20 + rail
                content = (760+rail) - (20+rail) - 20 = 720  [1]
       1024: block 1000 <= 1024, gutters 12px each.
             aside left = 50% - 500px = 12px, width 208px,
             right edge 220px; text starts at 272px.
       1280: rail steps to 300 -> block 1060, gutters 110px,
             aside 260px wide.
       1440: block 1060, gutters 190px.
       `left: 50%` on a fixed element resolves against the initial
       containing block. The reset zeroes html/body margins and
       body adds no horizontal padding, so the ICB and the body
       content box are both documentElement.clientWidth — a 15px
       scrollbar narrows BOTH, moving the aside and the column
       together, and [1] still holds.

     >= 1024px, INDEX (re-derived, not copied)
                block = min(vw, 1080 + rail)
                padding-left = 20 + rail
                content = block - (20+rail) - 20             [2]
       1080 + rail is 1320/1380, i.e. WIDER than the viewport at
       1024 and 1280, so the block is viewport-capped there and
       `50% - (1080+rail)/2` goes negative. Hence
       `left: max(20px, ...)` — a bare calc() would park the rail
       off-screen left. Writing L for the block's left edge
       (L = max(0, (vw - 1080 - rail)/2)):
         aside spans  [max(20,L), max(20,L) + rail - gap]
         text starts  L + 20 + rail
       so the clearance is >= gap for every vw, and never
       negative (the rail cannot slide under the cards).
       1024: rail 240 gap 32 -> block 1024, content 744.
             aside 20..228, text at 260. Cards: 744 fits two
             260px minmax columns + 16px gap (536), not three.
       1280: rail 300 gap 40 -> block 1280, content 940.
             aside 20..280, text at 320.
       1440: block 1380, L=30 -> content 1040, i.e. the SAME
             1040px measure the index has today. aside 30..290,
             text at 350.
       1920: L=270 -> aside 270..530, text at 590.

     < 1024px  the aside is a plain block in the flow, width:100%
               of the wrap's content box. Identical on both page
               types; the index's block is capped at 1080 and the
               article's at 760, and every width here is smaller
               than either.
       768:  wrap content 728 (index) / 720 (article) -> aside
             the same.
       375:  wrap gutter 20 -> content 335 -> aside 335.
       320:  wrap gutter 15 -> content 290 -> aside 290.
             Inside that 290: 2px border -> 288; tools padding
             14px x2 -> row 260, minus the 28px theme toggle and
             an 8px gap -> input 224, of which 32px is the
             magnifier inset -> 178px of typing room.
             Tree: `.content` margin-left 12 + 1px guide line +
             padding 14px/2px -> link box 259, link padding
             16px/6px -> 237px of text. The longest unbreakable
             token in any post title is ~13ch of Space Mono at
             12px (0.6em advance) = 94px, and links carry
             `overflow-wrap:anywhere`, so nothing can push past
             237px. Folder headings wrap rather than overflow.
       The standalone (not-in-a-wrap) fallback is
       `width:calc(100% - 40px)`, which is narrower than the
       viewport at every size, so it cannot overflow either.
   ============================================================ */

/* Spec §6. Deliberately unscoped: it is the one class the filter
   layer toggles, on entries and on emptied folders alike. */
.filtered-out{display:none}

/* No fill and no outline. As a bordered panel it read as another card
   competing with the article cards; the rail is now just its contents sitting
   on the page grid. The border box is kept (transparent) rather than removed so
   the 1px never changes the column's width — see the [1]/[2]/[3] sums above. */
.explorer{
  display:flex;flex-direction:column;min-width:0;
  width:calc(100% - 40px);max-width:720px;margin:32px auto;
  background:transparent;
  border:1px solid transparent;border-radius:var(--radius-none);
  font-family:var(--font-body);
}
/* The build nests the aside inside <div class="wrap narrow">, which
   already pays the gutter — don't pay it twice. `:where()` keeps this
   at (0,1,0) so the >= 1024px rail rule can still win on source
   order; see the SPECIFICITY note above. */
:where(.wrap) .explorer{width:100%;max-width:none}

/* `article li` sets a dashed rule and 14px type on every list item.
   The aside is a sibling of <article>, but it may not always be —
   make the tree immune rather than dependent on where it lands. */
.explorer li{list-style:none;margin:0;padding:0;border:0;font-size:inherit;line-height:inherit;color:inherit}

/* JS-injected wrapper: the disclosure target below 1024px. The flex
   chain is written so it behaves identically when it is absent
   (JS off), where .explorer-tools/.explorer-tree are direct
   children of .explorer. */
.explorer-body{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}

/* ---- tools ------------------------------------------------- */
/* Search pill + theme toggle on one row. No bottom rule: the
   Explorer heading below is the divider now. */
.explorer-tools{flex:0 0 auto;display:flex;align-items:center;gap:8px;padding:14px}

.explorer-search-wrap{position:relative;display:flex;flex:1 1 auto;min-width:0}
/* Inset INSIDE the pill, not a prefix beside it. `pointer-events:none`
   so a click on the glyph still lands in the field. */
.explorer-search-icon{
  position:absolute;left:12px;top:50%;transform:translateY(-50%);
  color:var(--text-faint);pointer-events:none;
}
.explorer-search{
  display:block;width:100%;min-width:0;
  background:var(--bg-dark);
  border:1px solid var(--border-color);
  /* The pill. See the DELIBERATE DEPARTURE note at the top of this
     block before changing it to --radius-none. */
  border-radius:999px;
  /* Left padding clears the 14px glyph at its 12px inset. */
  padding:9px 14px 9px 34px;
  /* 1.25x --text-xs (12px). The rail's type scale is set here rather than on
     the tokens, which the article body shares. */
  font-family:var(--font-body);font-size:15px;
  color:var(--text-light);
}
.explorer-search::placeholder{color:var(--text-faint)}
/* Keep the border tint (that is the design) and put a real ring on
   top — WCAG 2.4.7, and the tint alone is invisible to anyone who
   cannot separate #2d333b from #FF6600 and disappears entirely in
   forced-colors mode. */
.explorer-search:focus{border-color:var(--accent-orange);outline:2px solid var(--focus-ring);outline-offset:2px}

/* Theme toggle. Behaviour, icon swap, aria-pressed and the label all
   belong to site/theme.js — this only sizes and colours it. The SVG
   is stroke:currentColor both before and after the swap, so the
   colour here covers the moon and the sun alike. Square-sized so the
   28px box in the 320px arithmetic above stays true. */
.theme-toggle{
  flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
  width:28px;height:28px;
  background:transparent;border:0;border-radius:var(--radius-none);
  color:var(--text-muted);cursor:pointer;
  transition:var(--transition-hover);
}
.theme-toggle:hover{color:var(--accent-orange)}
.theme-icon{display:inline-flex;line-height:0}
.theme-icon svg{display:block}

/* ---- Explorer heading -------------------------------------- */
/* Collapses the whole tree. Server-rendered aria-expanded="true"
   with the '⌄' glyph; explorer.js swaps in '>' when it shuts. */
.explorer-heading{
  display:flex;align-items:center;gap:8px;width:100%;
  background:transparent;border:0;border-radius:var(--radius-none);
  padding:4px 14px 10px;cursor:pointer;text-align:left;
  font-family:var(--font-display);font-size:25px;text-transform:uppercase;
  letter-spacing:var(--tracking-display);line-height:var(--leading-snug);
  color:var(--text-light);
  transition:var(--transition-hover);
}
.explorer-heading:hover{color:var(--accent-orange)}
.explorer-heading-icon{flex:0 0 auto;font-size:17.5px;color:var(--accent-orange)}
.explorer.explorer-tree-collapsed .explorer-tree,
.explorer.explorer-tree-collapsed .explorer-empty{display:none}

/* Count announcements for the polite live region. Clipped, not
   display:none — a hidden live region is never announced. */
.explorer-status{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}

.explorer-empty{flex:0 0 auto;padding:14px;font-size:15px;color:var(--text-faint);font-style:italic}

/* ---- tree -------------------------------------------------- */
.explorer-tree{
  flex:1 1 auto;min-height:0;overflow-y:auto;
  /* Keeps a flick inside the rail from scrolling the article. */
  overscroll-behavior:contain;
  list-style:none;margin:0;padding:8px 0 12px;
  scrollbar-width:thin;scrollbar-color:var(--border-color) transparent;
}
/* Applied before the saved state lands, removed on the next frame,
   so folders do not animate shut on first paint. */
.explorer-tree.no-transition,.explorer-tree.no-transition *{transition:none !important}

/* Two controls now: the chevron collapses, the label filters. They read as one
   row, so the gap between them is closed and only the label takes the width. */
.explorer .folder-container{display:flex;align-items:baseline}
.explorer .folder-toggle{
  flex:0 0 auto;
  background:transparent;border:0;border-radius:var(--radius-none);
  padding:9px 4px 9px 12px;cursor:pointer;line-height:var(--leading-snug);
  color:var(--accent-orange);
  transition:var(--transition-hover);
}
.explorer .folder-title{
  display:flex;align-items:baseline;gap:6px;
  flex:1 1 auto;min-width:0;
  background:transparent;border:0;border-radius:var(--radius-none);
  padding:9px 12px 9px 2px;cursor:pointer;text-align:left;
  font-family:var(--font-display);font-size:20px;text-transform:uppercase;
  letter-spacing:var(--tracking-display);line-height:var(--leading-snug);
  color:var(--text-light);
  transition:var(--transition-hover);
}
.explorer .folder-title:hover,.explorer .folder-toggle:hover{color:var(--accent-orange)}
/* The pillar you filtered to. aria-pressed IS the state — styling off it keeps
   the visual and the accessible answer from ever disagreeing. */
.explorer .folder-title[aria-pressed='true']{color:var(--accent-orange)}

/* The way back to everything. Hidden until a pillar is active (the `hidden`
   attribute does that), so it costs nothing in the default view. */
.explorer-clear{
  flex:0 0 auto;margin:0 14px 8px;padding:7px 10px;
  background:var(--accent-orange-soft);
  border:1px solid var(--accent-orange);border-radius:var(--radius-none);
  cursor:pointer;text-align:left;
  font-family:var(--font-body);font-size:15px;
  color:var(--accent-orange);
  transition:var(--transition-hover);
}
.explorer-clear[hidden]{display:none}
.explorer-clear:hover{background:var(--accent-orange);color:var(--bg-dark)}
/* No rotate. The reference chrome reads '>' shut and '⌄' open —
   two different marks, not one turned 90 degrees — so explorer.js
   swaps the glyph and this only places it. Server-rendered content
   is '⌄', matching the server-rendered aria-expanded="true". */
.explorer .folder-icon{
  flex:0 0 auto;display:inline-block;
  color:var(--accent-orange);
}

/* grid-template-rows 1fr -> 0fr animates to the content's real
   height without JS having to measure it. */
.explorer .folder-outer{
  display:grid;grid-template-rows:1fr;
  transition:grid-template-rows var(--dur-accordion) var(--ease-island);
}
.explorer .folder.collapsed > .folder-outer{grid-template-rows:0fr}
/* A 0fr row still leaves its links in the tab order, so visibility
   has to follow — delayed on the way out so it does not blink off
   before the collapse finishes, immediate on the way in. */
.explorer .folder-outer > .content{visibility:visible;transition:visibility 0s}
.explorer .folder.collapsed > .folder-outer > .content{visibility:hidden;transition:visibility 0s linear var(--dur-accordion)}

/* The 1px guide line running down a folder's children is this
   border, hung off a 12px margin so it lands under the folder's
   chevron. It collapses with the row when the folder shuts, since
   it belongs to the element the 1fr -> 0fr grid is animating. */
.explorer .content{
  list-style:none;margin:0 0 0 12px;
  border-left:1px solid var(--border-color);
  /* Right/bottom padding is outline room: the box clips at its
     padding edge, and focus rings sit 2px outside the link. */
  padding:2px 2px 8px 14px;
  overflow:hidden;min-height:0;
}
.explorer .content a{
  display:block;position:relative;
  padding:6px 6px 6px 16px;
  font-family:var(--font-body);font-size:15px;
  line-height:var(--leading-snug);
  color:var(--text-muted);
  overflow-wrap:anywhere;
  transition:var(--transition-hover);
}
/* Bullet, not the site's terminal '>' marker: '>' is now the SHUT
   chevron, and reusing it on every leaf made a folder's children
   read as a column of collapsed folders. */
.explorer .content a::before{content:"\2022";position:absolute;left:4px;color:var(--accent-orange);opacity:.4}
.explorer .content a:hover{color:var(--text-light)}
.explorer .content a:hover::before{opacity:1}
.explorer .content a.active{color:var(--accent-orange)}
.explorer .content a.active::before{opacity:1}

/* ---- small-screen disclosure ------------------------------- */
.explorer-toggle{
  display:flex;align-items:baseline;gap:8px;width:100%;
  background:transparent;border:0;border-radius:var(--radius-none);
  padding:14px;cursor:pointer;text-align:left;
  font-family:var(--font-display);font-size:22.5px;text-transform:uppercase;
  letter-spacing:var(--tracking-display);line-height:var(--leading-snug);
  color:var(--text-light);
  transition:var(--transition-hover);
}
.explorer-toggle:hover{color:var(--accent-orange)}
.explorer:not(.explorer-collapsed) .explorer-toggle{border-bottom:1px solid var(--border-color)}

/* ---- focus ------------------------------------------------- */
.explorer .folder-title:focus-visible,
.explorer .folder-toggle:focus-visible,
.explorer-clear:focus-visible,
.explorer .content a:focus-visible,
.explorer-heading:focus-visible,
.theme-toggle:focus-visible,
.explorer-toggle:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}

@media (prefers-reduced-motion: reduce){
  .explorer .folder-outer{transition:none}
  .explorer .folder.collapsed > .folder-outer > .content{transition-delay:0s}
}

/* ---- < 1024px: block in the flow, collapsed behind a toggle -- */
@media (max-width: 1023.98px){
  .explorer.explorer-collapsed .explorer-body{display:none}
  /* Expanded, the tree must not bury the article under 19 rows. */
  .explorer-tree{max-height:60vh}
}

/* ---- >= 1024px: fixed rail beside the reading column --------- */
@media (min-width: 1024px){
  /* Layout locals for this component, not design tokens — the
     token set has no rail width and this file must not invent one
     in tokens.css. */
  :root{--exp-rail:240px;--exp-gap:32px}

  .wrap.narrow{
    max-width:calc(760px + var(--exp-rail));
    padding-left:calc(20px + var(--exp-rail));
  }
  /* The index. Same trick, its own sum — see TWO MEASURES above.
     Both stay 20px on the right, so [1] and [2] hold together. */
  .wrap:not(.narrow){
    max-width:calc(1080px + var(--exp-rail));
    padding-left:calc(20px + var(--exp-rail));
  }
  /* Belt and braces for a `.wrap` page with no explorer.
     `:not(:has())` is NOT forgiving, so in a browser without
     `:has()` these rules are dropped and the shifted layout —
     the case that actually ships — is what survives. */
  body:not(:has(.explorer)) .wrap.narrow{max-width:760px;padding-left:20px}
  body:not(:has(.explorer)) .wrap:not(.narrow){max-width:1080px;padding-left:20px}

  .explorer{
    position:fixed;z-index:10;
    /* Starts level with the first content block instead of up under the nav,
       then rises to the nav offset as you scroll past the header — explorer.js
       writes --rail-top each frame. The fallback is the docked offset, so with
       JS off (or before first paint) the rail sits where it always did. */
    top:var(--rail-top, calc(var(--nav-height) + var(--space-4)));
    left:calc(50% - (760px + var(--exp-rail)) / 2);
    width:calc(var(--exp-rail) - var(--exp-gap));
    max-width:none;
    /* explorer.js narrows this as the footer comes into view, so the fixed
       rail stops short of it instead of overlapping. Falls back to the
       viewport sum when JS has not run. */
    max-height:var(--rail-max-h, calc(100vh - var(--rail-top, calc(var(--nav-height) + var(--space-4))) - var(--space-4)));
    margin:0;
  }
  /* The index's block is 320px wider, so its left edge is further
     left — and 1080 + rail exceeds the viewport at 1024 and 1280,
     where the bare calc() goes negative and would push the rail
     off-screen. max() pins it to the page gutter instead.
     ONLY `left` here: this selector is (0,2,0) and would beat the
     width above. See the SPECIFICITY note at the top. */
  .wrap:not(.narrow) .explorer{
    left:max(20px, calc(50% - (1080px + var(--exp-rail)) / 2));
  }
  .explorer-toggle{display:none}
}
@media (min-width: 1280px){
  :root{--exp-rail:300px;--exp-gap:40px}
}

/* ============================================================
   RIGHT RAIL — Graph View + Table of Contents (articles only)

   THIRD COLUMN, >= 1440px ONLY. Three columns do not fit at 1280:
   760 + 300 + 260 = 1320 > 1280, and the only way to make them fit
   is shrinking the EXISTING left rail at 1280-1439 — a visible
   change to shipped layout nobody asked for. At 1440 the sum is
   760 + 300 + 300 = 1360 and every current breakpoint is untouched.

   The 720px measure is preserved the same way the left rail does
   it — widen the block by the rail and pad by the same amount:
     block   = 760 + exp-rail + toc-rail
     padding = (20 + exp-rail) left, (20 + toc-rail) right
     content = (760+e+t) - (20+e) - (20+t) = 720          [3]
   1440: block 1360 <= 1440, gutters 40px each.
   1710: block 1360, centred, gutters 175px each.
   ============================================================ */
@media (min-width: 1440px){
  :root{--toc-rail:300px;--toc-gap:40px}

  .wrap.narrow{
    max-width:calc(760px + var(--exp-rail) + var(--toc-rail));
    padding-right:calc(20px + var(--toc-rail));
  }
  /* The left rail is positioned off the BLOCK's left edge, so widening the
     block by a third column has to move it too. Missed once and the rail sat
     90px over the prose — the article's first word disappeared behind it. */
  .wrap.narrow ~ .explorer,.explorer{
    left:max(20px, calc(50% - (760px + var(--exp-rail) + var(--toc-rail)) / 2));
  }
  /* The index has no right rail, so it keeps the two-column sum. */
  body:has(.wrap:not(.narrow)) .explorer{
    left:max(20px, calc(50% - (1080px + var(--exp-rail)) / 2));
  }
  body:not(:has(.side)) .explorer{
    left:max(20px, calc(50% - (760px + var(--exp-rail)) / 2));
  }
  /* Same belt-and-braces as .explorer: `:not(:has())` is not
     forgiving, so a browser without `:has()` drops this and keeps
     the shifted layout that actually ships. */
  body:not(:has(.side)) .wrap.narrow{
    max-width:calc(760px + var(--exp-rail));
    padding-right:20px;
  }

  /* Mirror of .explorer, anchored to the block's right edge. */
  .side{
    position:fixed;z-index:10;
    /* Starts level with the first content block instead of up under the nav,
       then rises to the nav offset as you scroll past the header — explorer.js
       writes --rail-top each frame. The fallback is the docked offset, so with
       JS off (or before first paint) the rail sits where it always did. */
    top:var(--rail-top, calc(var(--nav-height) + var(--space-4)));
    left:auto;
    right:max(20px, calc(50% - (760px + var(--exp-rail) + var(--toc-rail)) / 2));
    width:calc(var(--toc-rail) - var(--toc-gap));
    /* explorer.js narrows this as the footer comes into view, so the fixed
       rail stops short of it instead of overlapping. Falls back to the
       viewport sum when JS has not run. */
    max-height:var(--rail-max-h, calc(100vh - var(--rail-top, calc(var(--nav-height) + var(--space-4))) - var(--space-4)));
    display:flex;flex-direction:column;gap:var(--space-5);
    overflow-y:auto;overscroll-behavior:contain;
    scrollbar-width:thin;scrollbar-color:var(--border-color) transparent;
  }
}

/* Below 1440 the graph is dropped outright — it is decorative, it
   is the expensive half, and it is unusable at rail width. The TOC
   stays: jumping sections is MORE useful on a phone, not less. It
   becomes a full-width disclosure above the article, shut by
   default so it never pushes the prose off the first screen. */
@media (max-width: 1439.98px){
  .side{display:block;margin:0 0 var(--space-5)}
  .graph{display:none}
  .toc-list{display:none}
  .toc:not(.toc-collapsed) .toc-list{display:block}
}
/* >= 1440 the rail is always open; only the button collapses it. */
@media (min-width: 1440px){
  .toc.toc-collapsed .toc-list{display:none}
}

.side-h{
  font-family:var(--font-display);font-size:20px;text-transform:uppercase;
  letter-spacing:var(--tracking-display);color:var(--text-light);
  margin:0 0 var(--space-3);
}

/* ---- graph ------------------------------------------------- */
.graph-box{
  position:relative;
  aspect-ratio:1/1;width:100%;
  border:1px solid var(--border-color);border-radius:8px;
  background:var(--bg-panel);
  overflow:hidden;
}
.graph-svg{display:block;width:100%;height:100%}
.graph-expand{
  position:absolute;top:8px;right:8px;z-index:1;
  display:flex;align-items:center;justify-content:center;
  width:26px;height:26px;padding:0;
  background:transparent;border:0;border-radius:4px;
  color:var(--text-muted);cursor:pointer;
  transition:var(--transition-hover);
}
.graph-expand:hover{color:var(--accent-orange)}
.graph-expand:focus-visible,.toc-title:focus-visible,.toc-list a:focus-visible{
  outline:2px solid var(--focus-ring);outline-offset:2px;
}
/* Graph nodes are real DOM (that is why this is SVG, not canvas),
   so they take focus like anything else.

   No outline ring on any state. Chrome draws a two-tone blue/white ring
   around a focused <g>, which sat awkwardly over a 4px dot. The node turning
   solid orange IS the indicator instead — it changes fill AND stroke, so it
   stays visible in forced-colors and is a far bigger change than a hairline
   ring at this scale. Keyboard focus and hover share it deliberately: with
   only ~23 nodes there is nothing to disambiguate between them. */
.graph-node{cursor:pointer}
.graph-node:focus{outline:none}
.graph-node:focus-visible{outline:none}
.graph-node:hover circle,
.graph-node:focus-visible circle{fill:var(--accent-orange);stroke:var(--accent-orange)}
.graph-node:hover text,
.graph-node:focus-visible text{fill:var(--accent-orange)}

/* Global graph, opened from .graph-expand. */
.graph-modal{
  position:fixed;inset:0;z-index:100;
  display:flex;align-items:center;justify-content:center;
  padding:var(--space-5);
  background:rgba(0,0,0,.72);
}
.graph-modal[hidden]{display:none}
.graph-modal-box{
  position:relative;
  width:min(900px,92vw);height:min(720px,84vh);
  border:1px solid var(--border-color);border-radius:8px;
  background:var(--bg-panel);
}
.graph-modal-box .graph-svg{border-radius:8px}

/* ---- table of contents ------------------------------------- */
.toc-title{
  display:flex;align-items:baseline;gap:8px;
  width:100%;padding:0;margin:0 0 var(--space-3);
  background:transparent;border:0;cursor:pointer;text-align:left;
  font-family:var(--font-display);font-size:20px;text-transform:uppercase;
  letter-spacing:var(--tracking-display);color:var(--text-light);
  transition:var(--transition-hover);
}
.toc-title:hover{color:var(--accent-orange)}
.toc-icon{font-size:14px;color:var(--text-muted)}

.toc-list{list-style:none;margin:0;padding:0}
.toc-item{margin:0}
.toc-item a{
  display:block;padding:5px 0;
  font-family:var(--font-body);font-size:var(--text-xs);
  line-height:var(--leading-snug);color:var(--text-faint);
  transition:var(--transition-hover);
}
.toc-item a:hover{color:var(--accent-orange)}
/* h3 sits under its h2. Indent only — no bullet, no rule; the
   reference chrome carries the hierarchy on inset alone. */
.toc-item.depth-3 a{padding-left:var(--space-3)}
/* Set by site/toc.js's scroll-spy on the section you are reading.
   `in-view` is the reference markup's class name — do not rename,
   toc.js keys off it. */
.toc-item a.in-view{color:var(--text-light)}
.toc-item.depth-2 a.in-view{color:var(--accent-orange)}

/* ============================================================
   RESPONSIVE

   The UI kit these styles came from shipped no media queries at
   all, so everything below is additive and MUST stay inert at
   >= 1024px — at that width the pages have to render exactly as
   the design system produced them. Every rule here therefore
   lives inside a `max-width` query below 1024px. Nothing is a
   `clamp()`, so "inert above the breakpoint" is checkable by
   reading the query, not by reasoning about saturation points.

   This block must stay LAST in the file: the docked-pill
   overrides are `.nav.docked` (0,2,0), the same specificity as
   the base rule they replace, so source order is what decides.

   Breakpoints mirror src/landing/landing.css, chosen from where
   the layout actually breaks rather than from device names:

     767.98px — the fixed 80px bar holds the CLOSER wordmark plus
                FIVE links. At the design sizes that row needs
                ~501px (see the arithmetic below), and the
                display headings are set at 52-56px, which no
                phone measure can take. Shrink both.
     479.98px — shrinking is no longer enough: the nav becomes a
                wrapping, centred, auto-height bar and the
                headings take a second step down.

   Nav width arithmetic. Both faces are monospace at 0.6em
   advance (Space Mono body, Courier New as the VT323 fallback —
   VT323 itself is ~0.5em, i.e. narrower, so 0.6em is the
   pessimistic case). letter-spacing adds once per character.
   Links: Features(8) Pricing(7) Contact(7) Blog(4) Log in(6) =
   32 characters over 5 items, 4 gaps.

     AT REST, no query (>= 768px)
       brand   6ch x 24px x 0.6 + 6 x 4px ls = 110.4
       links   32ch x 12px x 0.6 + 4 x 20px  = 310.4
       padding 2 x 40                        =  80.0
       total                                 = 500.8  <= 768 OK
       (at 375px this is a 126px overflow — the reported bug)

     <= 767.98px
       brand   6ch x 20px x 0.6 + 6 x 2px ls =  84.0
       links   32ch x 11px x 0.6 + 4 x 12px  = 259.2
       padding 2 x 15                        =  30.0
       total                                 = 373.2  <= 480 OK

     <= 479.98px (wrapping, centred, column-gap 12px)
       brand    6ch x 18px x 0.6 + 6 x 1px   =  70.8
       links   32ch x 11px x 0.6 + 4 x 10px  = 251.2
       padding 2 x 10                        =  20.0
       375px: 70.8 + 12 + 251.2 = 334.0 <= 355 -> one row
       320px: 334.0 > 300 -> wraps; widest row is the link row
              at 251.2 <= 300 -> two rows, no overflow
       Docked pill, width:90% with 15px gutters: the content box
       is 288 - 30 = 258px at 320 and 337.5 - 30 = 307.5px at
       375. Both are narrower than the 334px single-row width, so
       the pill wraps to two rows at both sizes — and neither
       overflows, because the wider of the two rows is the link
       row at 251.2px.

   Height, because body{padding-top:80px} compensates for the
   fixed bar: wrapped, the tallest case is
     8 + 18x1.6 + 4 + 11x1.6 + 8 = 66.4px  (two rows)
     ... + 4 + 11x1.6            = 88.0px  (three rows, < 320px)
   Both clear 80px, but the three-row case does not clear it by
   much, so the offset is raised to 96px at this breakpoint
   rather than left to chance. scroll-padding-top follows it (the
   docked pill sits at top:20px, so 20 + 88 = 108 is the deepest
   a fragment target can be hidden).
   ============================================================ */

/* ---- Tablet and below ------------------------------------- */
@media (max-width: 767.98px){
  .nav{padding:0 var(--space-3)}
  /* The docked pill's own padding has to come down with it, or the
     90%-wide island is narrower than the bar it replaces yet keeps
     30px gutters. */
  .nav.docked{padding:0 var(--space-4)}
  .brand{font-size:20px;letter-spacing:2px}
  .nav-links{gap:12px;font-size:11px}

  /* 52-56px display type cannot wrap inside a phone measure:
     "conversation?" alone is 13ch x 52 x 0.6 + 26 = 432px against
     280px of usable width at 320. */
  .h1{font-size:80px}
  h1{font-size:36px}
  .cluster h2{font-size:26px}
  article h2{font-size:26px}
  article h3{font-size:21px}
  .related h2{font-size:24px}
  .card.pillar h3{font-size:26px}
  /* The FAQ heading sets font-size inline, so it needs the weight. */
  .faq h2{font-size:26px !important}

  .head{padding:28px 0 16px}
  .cluster{padding:36px 0}
  .card{padding:20px 18px}
  .cta{padding:20px 18px;gap:16px}
  .answer{padding:30px 18px 22px}
  .takeaways,.author{padding:20px 18px}
}

/* ---- Small phones ----------------------------------------- */
@media (max-width: 479.98px){
  /* Two centred rows. No link is dropped — "Log in" is the only
     sign-in entry point on the static pages. `height:auto` replaces
     the fixed 80px/60px, and the pill needs the same treatment at
     its own (higher) specificity. */
  .nav{
    height:auto;min-height:56px;
    flex-wrap:wrap;justify-content:center;
    gap:4px 12px;
    padding:8px var(--space-2);
  }
  .nav.docked{
    height:auto;min-height:56px;
    padding:8px var(--space-3);
  }
  .nav-links{flex-wrap:wrap;justify-content:center;gap:6px 10px}
  .brand{font-size:18px;letter-spacing:1px}

  /* Track the taller wrapped bar so content cannot slide under it. */
  body{padding-top:96px}
  html{scroll-padding-top:112px}

  .h1{font-size:64px}
  h1{font-size:28px}
  .cluster h2{font-size:24px}
  article h2{font-size:22px}
  article h3{font-size:19px}
  .related h2{font-size:22px}
  .card.pillar h3{font-size:22px}
  .card h3{font-size:20px}
  .faq h2{font-size:22px !important}

  .wrap{padding:0 var(--space-3)}
  .crumbs{padding-top:40px}
  .faq summary{padding:16px;gap:12px}
  .faq .a{padding:0 16px 16px}
}
