/* ---------------------------------------------------------------------------
 * toc-extras.css — post table-of-contents sidebar
 *
 * WHY THIS FILE EXISTS
 * The theme ships compiled assets only (no vite config, no src/), so Tailwind
 * cannot be re-run to emit new utilities. Everything the TOC layout needs
 * beyond what is already in built/app.css is hand-written here.
 *
 * Three jobs:
 *   1. xl: variants of the utilities toc.hbs / post.hbs now use. The bundle
 *      only carries the lg: forms, and the sidebar must not appear until
 *      1280px (below that the content column and the rail collide).
 *   2. Raise the layout container. .ghost-content is a CSS grid whose main
 *      track is fixed at the site's --content-width (1000px live). The
 *      theme's max-w-wide caps the row at 1024px, so article + rail cannot
 *      both fit at any viewport until the cap is lifted.
 *   3. Bound the scroller. .toc-list already has overflow-y:scroll but is
 *      capped at 80vh, which on a 800px-tall laptop puts its lower half below
 *      the fold where it cannot be reached. Cap it to the space actually
 *      visible under the sticky header instead.
 *
 * Colors come from the theme's own token layer (--color-border,
 * --color-typography-tone, --ghost-accent-color …), which is redefined under
 * [data-color-scheme=dark|system], so dark mode needs no separate rules here.
 *
 * NOTE: `prefers-reduced-motion` appears ZERO times in built/app.css, so the
 * block at the bottom of this file is the whole site's reduced-motion handling.
 * ------------------------------------------------------------------------- */

/* Height the sticky rail has to work with: viewport minus the sticky header
 * (92px when present, set by toc-extras.js) minus the "Table of Contents"
 * label and a little breathing room at the bottom. */
:root {
  --nb-toc-top: 20px;
  /* Clears the site's fixed email-capture bar (76px tall, measured live at
   * 1440x900) plus a margin. At 5rem the list ended 30px inside the bar and
   * three entries were unreachable. Costs ~48px of visible list. */
  --nb-toc-gutter: 8rem;

  /* The site accent --ghost-accent-color is #03afef, which measures 2.51:1 on
   * white: it fails SC 1.4.3 for the active-entry text (needs 4.5:1) and
   * SC 1.4.11 for the focus ring (needs 3:1). Dark mode is fine at 6.95:1.
   * These are the theme-tuned brand values from novelbits-design DESIGN.md
   * §1, which exists because the original teal has this exact problem:
   * #0E7C99 measures 4.82:1 on white, #1FB6D7 measures 7.24:1 on #1a1a1a.
   * Scoped to the rail - this does not restyle the rest of the site. */
  --nb-toc-accent: #0E7C99;
}

[data-color-scheme="dark"],
[data-color-scheme="system"] {
  --nb-toc-accent: #1FB6D7;
}

/* --- 1. xl: utilities the compiled bundle does not carry ----------------- */

@media (min-width: 80rem) {
  .xl\:flex-row      { flex-direction: row; }
  .xl\:flex-1        { flex: 1; }
  .xl\:block         { display: block; }
  .xl\:border-none   { border-style: none; }
  .xl\:justify-start { justify-content: flex-start; }
  .xl\:cursor-text   { cursor: text; }
  .xl\:px-2\.5       { padding-inline: 0.625rem; }
  .xl\:py-1          { padding-block: 0.25rem; }
  .xl\:invisible     { visibility: hidden; }
  .xl\:text-\[0\.8rem\]  { font-size: 0.8rem; }
  .xl\:text-\[0\.95rem\] { font-size: 0.95rem; }
  .xl\:w-\[250px\]   { width: 250px; }
  .xl\:hover\:bg-bgr:hover { background-color: var(--color-background); }
}

/* --- 2. The layout row --------------------------------------------------- */

@media (min-width: 80rem) {
  /* max-w-wide is 1024px, which is narrower than the live content column plus
   * a rail. Widen only this row; every other max-w-wide user is untouched. */
  [data-post-content] {
    max-width: min(1400px, 100% - 2.5rem);
  }

  /* Fixed basis so the rail cannot be squeezed by the article's grid, which
   * has a fixed main track and will otherwise win every flex negotiation.
   * 250px sits inside the 220-280px rail range in DESIGN.md §3. */
  [data-post-content] > [data-toc] {
    flex: 0 0 250px;
    max-width: 250px;
  }

  /* Flex items default to min-width:auto, so the article refuses to shrink
   * below the min-content width of its widest unbreakable child (code blocks,
   * tables). Measured without this: the article held 1024px and squeezed the
   * rail to 139px. The grid's own main track is min(--content-width, 100% -
   * 2*gap) and shrinks willingly; it is the flex floor that does not. */
  [data-post-content] > .ghost-content {
    min-width: 0;
  }

  /* One source of truth for the offset. toc.hbs also carries top-[92px]/top-4
   * from @custom.sticky_header; those stay as the no-CSS fallback, but the
   * value that wins is the one toc-extras.js measured from the actual header,
   * which is also what tocbot uses for its scroll-spy. */
  [data-post-content] > [data-toc] > div {
    top: var(--nb-toc-top);
  }
}

/* --- 3. The scroller ----------------------------------------------------- */

/* .toc-list already sets overflow-y:scroll and hides its scrollbar in
 * built/app.css. Only the bound is wrong: 80vh ignores the sticky header, so
 * on a 1440x800 laptop the last third of a 37-entry list sits below the fold
 * and the active entry can be unreachable. */
/* ONLY the outer list scrolls. tocbot nests <ul class="toc-list"> inside
 * itself, and built/app.css sets `.toc-list { max-height:80vh; overflow-y:
 * scroll }` on the bare class, which turns every sublist into its own scroll
 * container. Combined with overscroll containment that made a wheel over an
 * expanded subsection *swallow* the event instead of chaining to the TOC -
 * reported as the list "getting stuck". Sublists are unwound below. */
[data-toc] [data-toc-content] > .toc-list {
  max-height: calc(100vh - var(--nb-toc-top) - var(--nb-toc-gutter));
  /* iOS Safari's 100vh excludes the collapsible URL bar, so the vh value
   * alone puts the end of the list below the fold - the exact failure this
   * section exists to fix. vh stays as the fallback for older engines. */
  max-height: calc(100dvh - var(--nb-toc-top) - var(--nb-toc-gutter));
  overflow-y: auto;
  scroll-behavior: smooth;
  /* Tab-scrolling a link into view lands it flush against the edge, inside
   * the mask's 2rem transparent band, so the focused item would be faded. */
  scroll-padding-block: 2rem;
  /* Chaining is the DEFAULT here; containment is re-applied conditionally
   * below. See the block after this rule for why it cannot be unconditional. */
  overscroll-behavior: auto;
}

/* --- 3a. Containment, but only when the list can actually scroll ---------
 *
 * BUG, reported 2026-08-28: with the cursor over the TOC, the wheel did
 * nothing at all - the page would not move, with a slight jitter in a list
 * far too short to scroll.
 *
 * Cause: `overflow-y: auto` makes this element a scroll container even when
 * its content fits exactly, and `overscroll-behavior: contain` then blocks
 * chaining to the page. So on any post whose TOC fits - measured live on
 * uart-console-vs-rtt-logging-power: 10 entries, scrollHeight 393 ==
 * clientHeight 393, zero overflow - the element could neither scroll itself
 * nor pass the event on. The wheel was simply dead over the whole rail.
 * This was NOT mobile-only; it reproduced at 1801px wide.
 *
 * Fix: contain only when there is somewhere to scroll. `[data-fade]` is
 * exactly that signal - toc-extras.js `readFade()` returns null, and
 * `applyFade()` removes the attribute, whenever scrollHeight - clientHeight
 * <= 2. It is recomputed in sync() on scroll and on resize, before any early
 * return, so it stays correct at every width.
 *
 * Gated to the rail as well. At >=1280px the rail is a narrow column beside
 * the article, so stopping at its end is reasonable and the reader can move
 * the cursor off it. Below that the bar spans the full content width and
 * there is nowhere else to put the cursor, so it always chains. */
@media (min-width: 80rem) {
  [data-toc] [data-toc-content] > .toc-list[data-fade] {
    overscroll-behavior: contain;
  }
}

/* Undo the bundle's blanket rule for every nested list. */
[data-toc] .toc-list .toc-list {
  max-height: none;
  overflow: visible;
  overscroll-behavior: auto;
}

/* The bundle hides the scrollbar entirely (scrollbar-width:none plus a
 * ::-webkit-scrollbar{display:none}), so a list of 37 entries looked like a
 * list of 12 with no way to tell. Give the one real scroller a thin, quiet
 * scrollbar; the edge fades stay as the peripheral cue. */
[data-toc] [data-toc-content] > .toc-list {
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--color-typography-tone) 40%, transparent) transparent;
}
[data-toc] [data-toc-content] > .toc-list::-webkit-scrollbar {
  display: block;
  width: 6px;
}
[data-toc] [data-toc-content] > .toc-list::-webkit-scrollbar-track {
  background: transparent;
}
[data-toc] [data-toc-content] > .toc-list::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--color-typography-tone) 40%, transparent);
  border-radius: 3px;
}
[data-toc] [data-toc-content] > .toc-list::-webkit-scrollbar-thumb:hover {
  background: var(--color-typography-tone);
}

/* A 250px rail cannot break an unbroken identifier (CONFIG_BT_CTLR_…, a URL)
 * and its overflow-x is visible, so without this it spills sideways. */
[data-toc] .toc-list-item a {
  overflow-wrap: anywhere;
}

/* The theme hides the scrollbar, so a list that scrolls looks identical to one
 * that doesn't. Fade the clipped edge to say "there is more". Ungated: the
 * list is height-capped below 1280px too, where the affordance matters most. */
[data-toc] [data-toc-content] > .toc-list[data-fade="bottom"] {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2rem), transparent);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 2rem), transparent);
}
[data-toc] [data-toc-content] > .toc-list[data-fade="top"] {
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 2rem);
          mask-image: linear-gradient(to bottom, transparent, #000 2rem);
}
[data-toc] [data-toc-content] > .toc-list[data-fade="both"] {
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 2rem, #000 calc(100% - 2rem), transparent);
          mask-image: linear-gradient(to bottom, transparent, #000 2rem, #000 calc(100% - 2rem), transparent);
}

/* --- 4. Controls --------------------------------------------------------- */

/* built/app.css has zero :focus-visible declarations, so without this the
 * Contents toggle - the only control below 1280px - has no visible focus
 * state at all, and the rail's links have none either. */
[data-toc] button:focus-visible,
[data-toc] .toc-list-item a:focus-visible {
  outline: 2px solid var(--nb-toc-accent);
  border-radius: 8px;
}

/* .toc-list sets overflow-y:scroll, which computes overflow-x to auto, so a
 * positive offset is clipped at the left and right edges. Inset instead. */
[data-toc] .toc-list-item a:focus-visible { outline-offset: -2px; }
[data-toc] button:focus-visible { outline-offset: 2px; }

[data-toc] button,
[data-toc] .toc-list-item a {
  touch-action: manipulation;
}

/* Tailwind preflight zeroes -webkit-tap-highlight-color, and the theme gives
 * these links a :hover rule only, which sticks after a tap on touch. */
/* Overrides .is-active-li>a and .toc-list-item a:hover from built/app.css,
 * both of which use the failing raw accent. */
[data-toc] .is-active-li > a,
[data-toc] .toc-list-item a:hover {
  color: var(--nb-toc-accent);
}

@media (hover: none) {
  [data-toc] .toc-list-item a:hover { color: inherit; }
  [data-toc] .toc-list-item a:active { color: var(--nb-toc-accent); }
}

/* --- 5. Below 1280: the collapsible Contents bar -------------------------- */

/* `not all and (min-width: 80rem)` rather than max-width: 79.99rem: the two
 * would leave a 0.16px band uncovered, reachable at fractional zoom, where the
 * rail loses both its layout and its sticky fallback. */
@media not all and (min-width: 80rem) {
  /* The theme puts position:sticky on the aside's inner div. In the stacked
   * layout that div's parent is a flex item only as tall as the bar itself, so
   * the sticky box has no room to travel and the bar simply scrolls away
   * (measured at 1024px: the bar sat 1247px above the viewport mid-article).
   * Moving sticky up to the aside gives it the whole section to move through. */
  [data-toc] {
    position: sticky;
    top: var(--nb-toc-top);
    z-index: 20;
    align-self: flex-start;
    background-color: var(--color-background);
  }
  [data-toc] > div { position: static; }

  /* The bundle's only anchor allowance is
   *   header.sticky~main .ghost-content h2..h5 { scroll-margin-top: 90px }
   * which clears the site header but not this bar, so every TOC jump below
   * 1280px would land its heading behind the bar. The extra class in the
   * selector beats the bundle's (0,2,3). */
  header.sticky ~ main [data-post-content] .ghost-content :is(h2, h3, h4, h5) {
    scroll-margin-top: calc(var(--nb-toc-top) + 3.5rem);
  }
}

/* The <768px hide-on-scroll was CUT on 2026-08-28 after producing two of the
 * three bugs in this feature, for the sake of reclaiming a 52px bar:
 *
 *   1. Tapping the bar open added ~624px above the reading position; the
 *      browser's scroll anchoring compensated by jumping scrollY forward by
 *      the same amount, which the handler read as a scroll down and used to
 *      hide the panel the reader had just opened.
 *   2. Hiding while the bar was still in its own flow slot (not yet pinned)
 *      translated it out of a gap that was still on screen, leaving 108px of
 *      blank page between the hero and the article. Reported from a phone.
 *
 * The bar is now simply sticky at every width below 1280px. If hide-on-scroll
 * is ever reinstated, both failure modes above have to be handled: gate on
 * "actually pinned", and rebase the scroll baseline across the toggle. */

/* --- 6. Motion ----------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  /* default.hbs sets <html class="scroll-smooth">, so every TOC click animates
   * a full-page scroll. Guarding only .toc-list would miss the motion the TOC
   * actually causes. */
  html.scroll-smooth { scroll-behavior: auto; }
  [data-toc] [data-toc-content] > .toc-list { scroll-behavior: auto; }
}

/* --- 7. The cookie FAB shares the rail's column -------------------------- */

/* NOT OUR COMPONENT. .nb-cookie-prefs-fab is defined in Ghost's site-wide
 * code injection (an inline <style> plus an inline script), not in this repo -
 * grep the theme for it and you will find nothing. It is pinned bottom-left,
 * which was empty space until the TOC rail moved in underneath it; measured
 * live, it covered three entries.
 *
 * Moved here rather than in the code injection so the whole change stays in
 * one artifact and reverts with it. If the FAB is ever repositioned at the
 * source, delete this block. `body` is only needed to out-specify the
 * injected `.nb-cookie-prefs-fab` rule, which loads after this file. */
body .nb-cookie-prefs-fab {
  left: auto;
  /* NOT 16px: the theme's "Back to top" button is also fixed at right:40px and
   * is 45px wide, so it occupies 40-85px from the right edge at the same
   * bottom:120px. 100px clears it by 15px. Both are right-anchored, so the gap
   * holds at every viewport width. */
  right: 100px;
}

/* --- 8. Suppressed state ------------------------------------------------- */

/* Hidden until toc-extras.js has run and decided the post earns a TOC. The
 * aside is empty markup until tocbot fills it, so revealing it only on
 * success costs nothing and avoids ~108px of layout shift on short posts,
 * where the bar would otherwise render and then disappear. */
[data-toc] { display: none; }
[data-toc][data-toc-ready] { display: block; }
