/* ============================================================
   PassbackAI shared components.
   Lives downstream of tokens.css. Every page loads this file, so
   rules here win against browser defaults but lose to any inline
   page CSS that follows — intended during migration. Once the
   inline duplicates are removed from a page, this file owns the
   component outright. New design for a shared pattern? Comes
   here first, never inline.
   ============================================================ */

/* ------------------------------------------------------------
   Self-hosted fonts. Loaded once via this file by every static
   page. The SPA shell (index.html) keeps its own inline copy as
   well — Inter + Hebrew variants — until that surface migrates
   too; the second declaration is a no-op (same family, same src).
   ------------------------------------------------------------ */

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/inter.woff2') format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url('/fonts/jetbrains-mono.woff2') format('woff2');
}
@font-face {
  font-family: 'Noto Serif';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/fonts/noto-serif.woff2') format('woff2');
}

/* ------------------------------------------------------------
   Page baseline — static pages only. The SPA shell sets these
   itself and may diverge (it pins `overflow-y: scroll` to keep
   the scrollbar gutter reserved while React swaps modes). For
   static pages this is the floor.
   ------------------------------------------------------------ */

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
}

body {
  font-family: var(--ui-font);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overflow-y: scroll;
}

/* ------------------------------------------------------------
   Static-page topbar — sticky brand row + "Try the app" CTA.
   Used verbatim on every static HTML (blog index, blog posts,
   manifesto, privacy, skill). The SPA topbar is a
   different surface (.topbar in index.html's inline styles)
   and stays inline for now.
   ------------------------------------------------------------ */

.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--topbar-blur);
  backdrop-filter: saturate(180%) blur(8px);
  -webkit-backdrop-filter: saturate(180%) blur(8px);
  border-bottom: 1px solid var(--border);
}
.topbar-inner {
  max-width: 1100px;
  margin: 0 auto;
  padding: 10px 24px;
  display: flex;
  align-items: center;
  gap: 12px;
}
.brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 600;
  font-size: 14px;
  color: var(--ink);
  letter-spacing: -0.005em;
  text-decoration: none;
  padding: 4px 6px;
  margin: -4px -6px;
  border-radius: 6px;
  transition: background 120ms var(--ease-quiet);
}
.brand:hover { background: var(--bg-sub); }
.brand .dot {
  width: 16px;
  height: 16px;
  border-radius: 4px;
  background: linear-gradient(135deg, #37352f 0%, #5c5b57 100%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 10px;
  font-weight: 700;
}
.brand .muted {
  color: var(--muted);
  font-weight: 400;
}
@media (max-width: 640px) {
  .brand .muted { display: none; }
}
.brand-name { letter-spacing: -0.01em; }
.brand-omg, .brand-md { font-weight: 700; }
.top-actions { margin-inline-start: auto; display: flex; gap: 10px; align-items: center; }

.try-app {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  background: var(--ink);
  color: var(--text-on-emphasis);
  border-radius: 7px;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  text-decoration: none;
  transition: background 120ms var(--ease-quiet);
}
.try-app:hover { background: #000; }
[data-theme="dark"] .try-app:hover { background: var(--text-secondary); }
.try-app .arrow {
  transition: transform 140ms var(--ease-quiet);
  display: inline-block;
}
.try-app:hover .arrow { transform: translateX(2px); }

/* Phone bar inset + cluster gap — the app's exact 520px values (index.html
   keeps 10px 24px until its useIsMobile width, then drops to 6px 10px). */
@media (max-width: 520px) {
  .topbar-inner { padding: 6px 10px; gap: 6px; }
  .top-actions { gap: 6px; }
}

/* ------------------------------------------------------------
   Long-read post body. Used by every blog post + /manifesto +
   /skill + /about. The .wrap container caps the reading column;
   .eyebrow + article h1 + .byline build the post header; the
   rest is prose typography for `<article>`.
   Page-specific rules (e.g. .fail, .recipe) stay inline; only
   the shape of "an OMGfixMD long read" lives here.
   ------------------------------------------------------------ */

/* The long-read shell. .wrap provides the outer padding + full width;
   the article INSIDE it is the editorial grid that centres the
   rail+measure block. No max-width here anymore — the grid owns the
   measure, so every page lands on the identical column (a page that
   wants a different measure overrides the --measure token, not this). */
.wrap {
  padding: var(--read-pad-top) 24px var(--read-pad-bottom);
}

/* ---- The canonical long-read grid: RAIL + MEASURE -------------------
   Two tracks centred as a unit. Prose flows in the reading measure
   (column 2); the mono kicker rides up into the left rail (column 1),
   top-aligned to the H1 cap and hugging the gutter — the page's spine.
   Below --read-collapse the grid folds to a single centred column and
   the kicker returns to normal flow above the H1 (see media query). */
.wrap article {
  display: grid;
  grid-template-columns: var(--read-rail) minmax(0, var(--measure));
  column-gap: var(--read-rail-gap);
  justify-content: center;
  align-items: start;
  font-family: var(--reading-font);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink);
}
.wrap article > * { grid-column: 2; min-width: 0; }
/* Break the measure and take the full rail+text span (wide tables,
   full-bleed figures). Opt-in, never automatic. */
.wrap article > .bleed { grid-column: 1 / -1; }

/* The margin kicker. Mono, tracked, quiet — the same register as the
   post-card eyebrows, so a long-read's head reads as the same family
   as its cards. In the rail it top-aligns to the H1 cap. */
.eyebrow {
  font-family: var(--mono);
  font-size: var(--fs-kicker);
  font-weight: 500;
  letter-spacing: var(--ls-kicker);
  text-transform: uppercase;
  color: var(--muted);
  line-height: 1.4;
}
.wrap article > .eyebrow {
  grid-column: 1;
  grid-row: 1;
  justify-self: end;
  text-align: end;
  margin: 0;
  padding-top: 0.55em;   /* optical cap-height align to the H1 */
}

.wrap article h1 {
  font-family: var(--reading-font);
  font-size: var(--fs-h1);
  font-weight: 600;
  line-height: var(--lh-h1);
  letter-spacing: var(--ls-h1);
  margin: 0 0 0.42em;
  text-wrap: balance;
}
/* Flagship modifier — the ONE sanctioned way to run a louder masthead
   (the manifesto). Bumps the H1 a single step and tightens it; nothing
   else changes, so the page still reads as the same system. This is the
   "one grid, maybe two" latitude — used sparingly, never per-page px. */
.wrap.longread--flagship article h1 {
  font-size: var(--fs-display);
  line-height: var(--lh-display);
  letter-spacing: var(--ls-display);
}

.byline {
  font-family: var(--ui-font);
  font-size: var(--fs-ui-sm);
  color: var(--ink-2);
  margin: 0 0 1.7em;
}
.byline a {
  color: var(--ink-2);
  border-bottom: 1px solid var(--border);
  text-decoration: none;
}
.byline a:hover { color: var(--ink); }
.byline .sep {
  color: var(--border-strong);
  margin: 0 6px;
}

article p { margin: 0 0 1.15em; }
article p.lede {
  font-size: var(--fs-lede);
  line-height: var(--lh-lede);
  color: var(--ink);
  margin: 0 0 1em;
}
article p.lede strong { font-weight: 600; }
article h2 {
  font-family: var(--reading-font);
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: var(--lh-h2);
  letter-spacing: var(--ls-h2);
  margin: 2.2em 0 0.5em;
}
article h3 {
  font-family: var(--reading-font);
  font-size: var(--fs-h3);
  font-weight: 600;
  line-height: var(--lh-h3);
  margin: 1.6em 0 0.4em;
}
article ul,
article ol {
  margin: 0 0 1.2em;
  padding-inline-start: 1.5em;
}
article li { margin: 0.35em 0; }
.doc li { white-space: pre-line; }
article strong { font-weight: 600; }
article em { font-style: italic; }
article code {
  font-family: var(--mono);
  font-size: 0.86em;
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 1px 5px;
}
article pre {
  font-family: var(--mono);
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 14px 16px;
  overflow-x: auto;
  font-size: 14px;
  line-height: 1.55;
  margin: 0 0 1.4em;
}
article pre code {
  background: transparent;
  border: 0;
  padding: 0;
  font-size: 1em;
}
/* Auto-linked URLs inside code blocks. The `.code-link` class opts out of the
   prose highlighter-marker style (which targets a:not([class])) — a yellow
   band reads wrong over monospace. Restrained underline instead, thickening on
   hover so it still announces itself as interactive. */
article pre code a.code-link {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in oklab, var(--link-accent) 65%, var(--ink));
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
  border-radius: 2px;
}
article pre code a.code-link:hover {
  text-decoration-thickness: 2px;
}
article pre code a.code-link:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 1px;
}
article hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 2.2em 0;
}

/* Rail collapse — below --read-collapse (62rem / 992px) the left rail
   folds and the kicker returns to normal flow above the H1, so the
   reading measure recentres. Type sizes are NOT touched here: the scale
   steps down via the token override in tokens.css at 640px, so every
   long-read shrinks in lockstep. (Media queries can't read a custom
   property, so 62rem is inlined; it mirrors --read-collapse.) */
@media (max-width: 62rem) {
  .wrap article {
    grid-template-columns: minmax(0, var(--measure));
  }
  /* The rail is gone, so every child moves back to the single column —
     without this the content stays pinned to `grid-column: 2` (the desktop
     rule below) and the browser spawns an implicit second column, squeezing
     the reading measure to half-width. This line is what makes the fold real. */
  .wrap article > * { grid-column: 1; }
  .wrap article > .eyebrow {
    grid-column: 1;
    grid-row: auto;
    justify-self: start;
    text-align: start;
    padding-top: 0;
    margin-bottom: 1.5em;
  }
}

@media (max-width: 640px) {
  .wrap { padding-inline: 20px; }
  article pre {
    font-size: 13px;
    padding: 12px 14px;
    border-radius: 6px;
  }
}

/* ------------------------------------------------------------
   TL;DR box — opens long reads with a single quotable summary.
   Same copy contract on every post: short enough that an LLM
   answering "what's this post about" can quote it whole.
   ------------------------------------------------------------ */

.tldr {
  border: 1px solid var(--border);
  background: var(--bg-sub);
  border-radius: 10px;
  padding: 18px 20px;
  margin: 0 0 1.8em;
  font-family: var(--ui-font);
  font-size: 15px;
  line-height: 1.55;
  color: var(--ink);
}
.tldr-label {
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  display: block;
  margin: 0 0 8px;
}
.tldr p {
  margin: 0;
  font-family: var(--ui-font);
  font-size: 15px;
  line-height: 1.55;
}
.tldr code {
  font-family: var(--mono);
  font-size: 0.88em;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 1px 5px;
}

/* ------------------------------------------------------------
   In-post FAQ — "Before you paste." accordion on long reads.
   Identical markup + behavior as the homepage FAQ; the React
   component (HomepageFAQ.jsx) and these rules render the same
   semantic <details>/<summary> tree.
   ------------------------------------------------------------ */

/* In-post FAQ — rides the SAME rail+measure grid as the article, so its
   questions line up under the reading column rather than sitting on their
   own width. Rail stays empty (the article's kicker already titled the
   page); the FAQ's own <h2> heads the text column. */
.faq {
  display: grid;
  grid-template-columns: var(--read-rail) minmax(0, var(--measure));
  column-gap: var(--read-rail-gap);
  justify-content: center;
  margin: 56px auto 0;
  padding: 0 24px;
  font-family: var(--ui-font);
}
.faq > * { grid-column: 2; min-width: 0; }
.faq h2 {
  font-family: var(--reading-font);
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: var(--lh-h2);
  letter-spacing: var(--ls-h2);
  margin: 0 0 18px;
  color: var(--ink);
}
.faq details {
  border-top: 1px solid var(--border);
  padding: 18px 0;
}
.faq details:last-of-type { border-bottom: 1px solid var(--border); }
.faq summary {
  list-style: none;
  cursor: pointer;
  font-size: 17px;
  font-weight: 600;
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: '+';
  font-family: var(--mono);
  font-size: 20px;
  font-weight: 400;
  color: var(--muted);
}
.faq details[open] summary::after {
  content: '−';
  color: var(--ink-2);
}
.faq .answer {
  margin-top: 12px;
  font-size: 16px;
  line-height: 1.65;
  color: var(--ink-2);
}
.faq .answer p { margin: 0 0 0.8em; }
.faq .answer p:last-child { margin-bottom: 0; }
.faq .answer code {
  font-family: var(--mono);
  font-size: 0.88em;
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 1px 5px;
}

/* FAQ rail collapse — mirror the article's fold at --read-collapse. */
@media (max-width: 62rem) {
  .faq { grid-template-columns: minmax(0, var(--measure)); }
  .faq > * { grid-column: 1; } /* move content off the desktop `grid-column: 2` — see the article fold note */
}
@media (max-width: 640px) {
  .faq { padding: 0 20px; }
  .faq summary { font-size: 16px; }
}

/* ------------------------------------------------------------
   Related-reading rail. Two-column on desktop, single on mobile.
   Cards inside use .post-card (defined above).
   ------------------------------------------------------------ */

.related {
  max-width: 880px;
  margin: 56px auto 0;
  padding: 0 24px;
}
.related .label {
  font-family: var(--ui-font);
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 12px;
}
.related .cards {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}

@media (max-width: 640px) {
  .related {
    padding: 0 20px;
  }
  .related .cards {
    grid-template-columns: 1fr;
    gap: 10px;
  }
}

/* ------------------------------------------------------------
   Post card — the one card that links to a post.
   One component, one variant. Used on:
     - /blog index (as .post-card)
     - in-post "Related reading" blocks (as .post-card .post-card--compact)
     - homepage Reading Cards (React) (as .post-card)
   ------------------------------------------------------------ */

.post-card {
  display: flex;
  flex-direction: column;
  padding: 28px 28px 24px;
  background: var(--bg);
  border: 1px solid var(--edge);
  border-radius: 14px;
  text-decoration: none;
  color: var(--ink);
  transition:
    border-color 200ms var(--ease-quiet),
    transform 260ms var(--ease-considered),
    box-shadow 260ms var(--ease-considered),
    background 200ms var(--ease-quiet);
}

@media (hover: hover) and (pointer: fine) {
  .post-card:hover {
    border-color: var(--edge-strong);
    transform: translateY(-2px);
    box-shadow:
      0 1px 2px rgba(0, 0, 0, 0.03),
      0 14px 30px -20px rgba(55, 53, 47, 0.14);
  }
}

.post-card:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 3px;
}

/* Eyebrow — .post-card__eyebrow is the preferred class; the short
   .eb alias is kept so the in-post related-reading markup can stay
   terse. Format is always "CATEGORY · N MIN READ", uppercased here
   so the markup can stay natural-case (better for JSON-LD / LLMs). */
.post-card__eyebrow,
.post-card .eb {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 14px;
}

/* Title — .post-card__title is preferred. Plain <h3> inside a card
   gets the same treatment, so markup can stay lean. */
.post-card__title,
.post-card h3 {
  font-family: var(--reading-font);
  font-size: 22px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.012em;
  color: var(--ink);
  margin: 0 0 10px;
  text-wrap: balance;
  /* The hover underline draws in the marker yellow — same --hl-fill
     as the hero's .mark band. Transparent at rest so nothing shifts
     when the color arrives. Thicker than a default underline because
     the yellow is pale by design. */
  text-decoration: underline 3px transparent;
  text-underline-offset: 3px;
  transition: text-decoration-color 260ms var(--ease-considered);
}

.post-card:hover .post-card__title,
.post-card:hover h3 {
  text-decoration-color: var(--hl-fill);
}

/* Description — .post-card__desc is preferred. Plain <p> inside a
   card inherits the same treatment. */
.post-card__desc,
.post-card p {
  font-family: var(--ui-font);
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--ink-2);
  margin: 0 0 22px;
  flex: 1;
  text-wrap: pretty;
}

.post-card__desc code,
.post-card p code {
  font-family: var(--mono);
  font-size: 0.88em;
  background: var(--bg-sub);
  border: 1px solid var(--edge);
  border-radius: 3px;
  padding: 1px 5px;
}

/* CTA — .post-card__cta is preferred. The historical .more span
   inside the in-post related-reading markup is aliased so existing
   markup still renders. Note: the ::after arrow is on the class
   variant only; .more markup has the arrow baked into its text node,
   so doubling it would look wrong. */
.post-card__cta,
.post-card .more {
  font-family: var(--ui-font);
  font-size: 13px;
  font-weight: 500;
  color: var(--ink);
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  text-decoration: none;
}

.post-card__cta::after {
  content: "→";
  color: var(--muted);
  transition:
    transform 200ms var(--ease-quiet),
    color 200ms var(--ease-quiet);
}

.post-card:hover .post-card__cta::after {
  transform: translateX(3px);
  color: var(--ink);
}

/* For legacy .more (arrow baked into the text), shift the whole span
   on hover instead — the character is part of the text node, we can't
   animate it independently. Subtle and still feels alive. */
.post-card:hover .more {
  color: var(--ink);
  transform: translateX(2px);
}
.post-card .more {
  transition:
    color 180ms var(--ease-quiet),
    transform 200ms var(--ease-quiet);
}

.post-card__meta {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: auto;
}

/* Compact — the in-post "Related reading" density. Same component,
   smaller breathing room and a slightly quieter title. */
.post-card--compact {
  padding: 20px 22px;
  border-radius: 12px;
}

.post-card--compact .post-card__title,
.post-card--compact h3 {
  font-size: 20px;
  line-height: 1.22;
  margin: 0 0 6px;
}

.post-card--compact .post-card__eyebrow,
.post-card--compact .eb {
  margin-bottom: 6px;
}

.post-card--compact .post-card__desc,
.post-card--compact p {
  font-size: 14px;
  line-height: 1.55;
  margin: 0 0 8px;
}

@media (hover: hover) and (pointer: fine) {
  .post-card--compact:hover {
    transform: translateY(-1px);
  }
}

/* Mobile — editorial cards get a hair less padding; compact cards
   stay tight. Title drops one step so two-line titles don't dominate
   a narrow viewport. */
@media (max-width: 640px) {
  .post-card {
    padding: 22px 22px 20px;
    border-radius: 12px;
  }
  .post-card__title,
  .post-card h3 {
    font-size: 20px;
  }
  .post-card--compact {
    padding: 18px 20px;
  }
  .post-card--compact .post-card__title,
  .post-card--compact h3 {
    font-size: 18px;
  }
}

/* ------------------------------------------------------------
   Body prose link — marker highlight.
   Design intent: a real highlighter ink-swipe, not a CSS underline
   pretending to be one. Three moves that separate this from the
   flat-band version:
     1. Translucent fill (color-mix with alpha) — paper shows
        through, so the mark reads as pigment absorbed into the
        page, not printed-plastic placed on top.
     2. Density variation — a two-stop gradient (slightly darker
        at the top "wet edge" where a marker tip lands, lightening
        as it bleeds down). This is how real marker ink looks when
        you swipe a highlighter across a word.
     3. The band sits *behind* the text (high on the line, covering
        the x-height), not below it. You read through the ink, the
        way a highlighter actually works.
   Animation: background-size / background-position DO animate
   (gradient-stop % don't). At rest the band is a narrow stripe
   sitting on the baseline; on hover it scales up to cover the
   x-height with a measured overshoot — the marker landing. One
   wild moment per surface, here.
   ------------------------------------------------------------ */

article a:not([class]),
.doc a:not([class]),
.tldr a:not([class]),
.post-body a:not([class]) {
  color: var(--ink);
  text-decoration: none;
  /* Translucent fill — same hue family as --hl-fill, but with alpha
     so ink absorbs into the paper. Top of the band is the "wet
     edge": ~3% darker, slightly more saturated. Bottom bleeds out
     to a softer wash. Not flat. */
  background-image: linear-gradient(
    175deg,
    color-mix(in oklab, var(--hl-fill) 82%, #b8860b) 0%,
    color-mix(in oklab, var(--hl-fill) 78%, transparent) 45%,
    color-mix(in oklab, var(--hl-fill) 64%, transparent) 100%
  );
  background-repeat: no-repeat;
  /* At rest: the band already covers the x-height so a link reads as
     a marked word, not a faint baseline underline — a bare URL in
     bold or RTL prose needs that affordance up front. Positioned at
     100% (bottom), 60% tall; floods to ~88% on hover via
     background-size. */
  background-position: 0 100%;
  background-size: 100% 60%;
  padding: 0 1px;
  border-radius: 1px;
  /* Tiny -0.5° skew gives the mark a hand-drawn feel. Applied to
     the background via a CSS custom property so text stays upright.
     Easier route: let the stripe itself stay horizontal but add a
     micro-offset at one edge via background-position. We use
     size/position — both animate natively. */
  transition:
    background-size 320ms var(--ease-overshoot),
    background-position 320ms var(--ease-overshoot),
    color 200ms var(--ease-quiet);
}

/* The byline's author link is an unclassed <a> inside the article, so it would
   otherwise pick up the prose-link marker highlight above. A byline is
   metadata, not marked prose — opt it out (needs to out-specify the
   `article a:not([class])` selector, hence the :not() here). */
.byline a:not([class]) {
  background-image: none;
  padding: 0;
  border-radius: 0;
}

article a:not([class]):hover,
.doc a:not([class]):hover,
.tldr a:not([class]):hover,
.post-body a:not([class]):hover {
  /* Band floods from the resting 60% up to 88% (covering the
     x-height and a hair of the ascenders). The overshoot easing
     makes it settle, not snap. Position stays at bottom — the mark
     grows upward from the baseline, the way a highlighter would
     flood the word. */
  background-size: 100% 88%;
  background-position: 0 98%;
}

article a:not([class]):focus-visible,
.doc a:not([class]):focus-visible,
.tldr a:not([class]):focus-visible,
.post-body a:not([class]):focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  border-radius: 2px;
}

/* ------------------------------------------------------------
   Sticky-note — one component, three uses.
   .audience-card sits in the homepage marketing grid; .verdict and
   .pullquote sit mid-prose in long reads. Same paper paint, same
   paper-shadow, same hover gesture (un-tilt + lift). Variants
   override only what differs: paper color (--note-bg), rest tilt
   (--note-tilt), hover-lift amplitude (--note-lift). The legacy
   class names stay as the variant selectors so existing markup
   keeps working without touch.
   ------------------------------------------------------------ */

.audience-card,
.verdict,
.pullquote,
.sample-note,
.shared-note {
  position: relative;
  border: 0;
  border-radius: 6px;
  background: var(--note-bg, var(--note-butter));
  transform: rotate(var(--note-tilt, -0.4deg));
  transform-origin: 50% 60%;
  box-shadow:
    0 1px 2px rgba(15, 15, 15, 0.05),
    0 4px 10px -2px rgba(15, 15, 15, 0.08),
    0 18px 32px -18px rgba(15, 15, 15, 0.18);
  color: #1a1a1a;
  transition:
    transform 240ms cubic-bezier(.2, .9, .2, 1),
    box-shadow 240ms ease;
}

/* The lift is a pointer affordance — gate it so a tap on a touch device
   doesn't leave the card stuck flat-and-lifted (sticky :hover). The resting
   tilt stays on every device; touch gets its own :active press below. */
@media (hover: hover) and (pointer: fine) {
  .audience-card:hover,
  .verdict:hover,
  .pullquote:hover,
  .sample-note:hover,
  .shared-note:hover {
    transform: rotate(0deg) translateY(var(--note-lift, -3px));
    box-shadow:
      0 2px 4px rgba(15, 15, 15, 0.06),
      0 10px 22px -4px rgba(15, 15, 15, 0.13),
      0 28px 48px -20px rgba(15, 15, 15, 0.25);
  }
}

/* Press: straighten and settle DOWN (never up) so the card feels alive on a
   tap without leaving a sticky hover. Faster than the 240ms resting ease. */
.audience-card:active,
.verdict:active,
.pullquote:active,
.sample-note:active,
.shared-note:active {
  transform: rotate(0deg) translateY(1px) scale(0.992);
  transition: transform 140ms var(--ease-quiet);
}

/* In-post .verdict — quieter sit, butter, mid-prose. */
.verdict {
  --note-bg: var(--note-butter);
  --note-tilt: -0.35deg;
  --note-lift: -2px;
  margin: 1.6em 0 1.8em;
  padding: 14px 16px 15px;
  font-family: var(--ui-font);
  font-size: 15px;
  line-height: 1.55;
}

.verdict strong {
  display: block;
  margin-bottom: 3px;
  font-family: var(--ui-font);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(15, 15, 15, 0.62);
}

/* In-post .pullquote — editorial moment, blush, stronger tilt. */
.pullquote {
  --note-bg: var(--note-blush);
  --note-tilt: 0.6deg;
  --note-lift: -3px;
  margin: 2.6em 0 2.8em;
  padding: 26px 28px 28px;
  font-family: var(--reading-font);
  font-size: 20px;
  font-weight: 500;
  line-height: 1.45;
  letter-spacing: -0.005em;
}

/* Example banner — thin strip that rides inside the sticky .topbar on an
   example surface (a live, pre-marked sample). It carries the visitor to the
   matching tool page to learn how it works and get started. Because it lives
   inside .topbar it inherits the sticky-to-top behavior; the topbar's own
   bottom border sits below it, so a top border separates it from the brand row. */
/* The banner is a container, not a single link: it orients the visitor ("this
   is a sample, your own docs are safe") and offers TWO exits — forward to the
   real tool, and (for a returning user with docs) back to their own. So the
   interactive affordance lives on the cta/back children, not the whole row. */
.example-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 9px;
  max-width: 1100px;
  margin: 0 auto;
  padding: 7px 24px;
  border-top: 1px solid var(--edge-subtle);
  font-size: 13px;
  color: var(--ink-2);
}
.example-banner-tag {
  flex: none;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #7a4f00;
  background: var(--hl-fill);
  padding: 2px 7px;
  border-radius: 5px;
}
.example-banner-cta,
.example-banner-back {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: inherit;
  font-weight: 600;
  color: var(--ink-2);
  background: none;
  border: 0;
  padding: 3px 8px;
  border-radius: 6px;
  cursor: pointer;
  text-decoration: none;
  transition: color 120ms var(--ease-quiet), background 120ms var(--ease-quiet);
}
.example-banner-back { color: var(--ink); }
.example-banner-cta:hover,
.example-banner-back:hover { color: var(--ink); background: var(--bg-sub); }
.example-banner-cta:focus-visible,
.example-banner-back:focus-visible {
  outline: 2px solid var(--ring, var(--hl-edge));
  outline-offset: -2px;
}
.example-banner-arrow {
  flex: none;
  transition: transform 140ms var(--ease-quiet);
}
.example-banner-cta:hover .example-banner-arrow { transform: translateX(2px); }
@media (max-width: 640px) {
  .example-banner { padding: 7px 16px; gap: 7px; font-size: 12px; }
  /* On a phone the banner + edits strip + sample note stack up to ~70% of the
     viewport before the document's first word. The reassurance sentence is
     restated by the sample note pinned right below, so collapse the banner to
     one row (chip + exits) — but keep the sentence in the accessibility tree:
     it's the role="status" announcement that orients a screen-reader user
     arriving on a `?sample=` link. */
  .example-banner-text {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}
@media (prefers-reduced-motion: reduce) {
  .example-banner-arrow { transition: none; }
}

/* In-app .sample-note — instructional sticky pinned to the top of the
   review column when the user clicks "Try a sample". Blush paper so it
   sits warm against the yellow marker that the .strong inside its title
   uses, and stays out of the cool/green register where nothing else on
   the site lives. Slightly stronger lift than .verdict so it reads as
   "interactive UI element," not "in-prose callout." */
.sample-note {
  --note-bg: var(--note-blush);
  --note-tilt: -0.6deg;
  --note-lift: -3px;
  margin: 0 0 36px;
  padding: 16px 44px 18px 20px;
  font-family: var(--ui-font);
  /* One-shot entrance: settles into rest tilt with a gentle overshoot
     on first paint. The single wild moment on this surface — never
     fires again, never on hover. */
  animation: sample-note-settle 520ms var(--ease-overshoot) both;
}

@keyframes sample-note-settle {
  0%   { opacity: 0; transform: rotate(0deg) translateY(-6px) scale(.985); }
  60%  { opacity: 1; }
  100% { opacity: 1; transform: rotate(var(--note-tilt)) translateY(0) scale(1); }
}

/* Homepage variant — one register quieter so the H1's promise stays the
   first read on the page: shallower tilt, and the far-throw third shadow
   layer dropped at rest. Everything else (blush paper, settle animation,
   pulsing dot, in-title mark, full hover lift) is kept — the note stays
   the page's orientation device, it just stops outranking the headline.
   Review-sample and shared-link notes keep the full treatment. */
.sample-note--home {
  --note-tilt: -0.4deg;
  box-shadow:
    0 1px 2px rgba(15, 15, 15, 0.05),
    0 4px 10px -2px rgba(15, 15, 15, 0.08);
}

/* ------------------------------------------------------------
   Homepage margin checklist (homepage overhaul PR 4) — the checklist arm's
   orientation device. .sample-note DNA (blush paper, 6px radius, paper
   shadows, settle entrance) with three real-button rows. Inline placement
   (<1180px / mobile): sits in the note slot after the Free paragraph,
   −0.8° tilt. Desktop ≥1180px: floats absolutely into the reading column's
   end-side margin (inset-inline-start mirrors it to the LEFT margin in RTL),
   −1.2° tilt. The width clamp keeps the card from ever pushing past the
   viewport edge just above the breakpoint — the page must never scroll
   horizontally (240px from ~1304px of viewport up, per the mock).
   ------------------------------------------------------------ */
.home-doc-col .sample-note.home-checklist {
  --note-tilt: -0.8deg;
  margin: 4px 0 26px;
  padding: 14px 40px 14px 16px;
}

@media (min-width: 1180px) {
  .home-doc-col .sample-note.home-checklist {
    position: absolute;
    inset-inline-start: calc(100% + 32px);
    top: 148px; /* aligned with the hero subhead's first lines */
    width: min(240px, calc(50vw - 380px - 44px));
    margin: 0;
    padding: 14px 16px;
    --note-tilt: -1.2deg;
  }
}

.home-checklist .sample-note-title {
  margin: 0 0 10px;
  font-size: 14px;
  font-weight: 600;
}

.home-checklist-rows {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Rows are REAL buttons: full-row target, quiet paper fill, hairline ink
   border. Checked: box fills marker butter + ink check stroke, row ink
   drops to 55%. NO strikethrough — strike means deletion on this page. */
.chk-row {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  min-height: 36px;
  padding: 7px 10px;
  background: rgba(255, 255, 255, 0.5);
  border: 1px solid rgba(15, 15, 15, 0.16);
  border-radius: 6px;
  font-family: var(--ui-font);
  font-size: 13px;
  font-weight: 500;
  line-height: 1.35;
  text-align: start;
  color: #1a1a1a;
  cursor: pointer;
  transition:
    background 140ms var(--ease-quiet),
    border-color 140ms var(--ease-quiet),
    color 200ms var(--ease-quiet);
}
.chk-row:hover {
  background: rgba(255, 255, 255, 0.72);
  border-color: rgba(15, 15, 15, 0.3);
}
.chk-row:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 1px;
}
.chk-row.is-done { color: rgba(15, 15, 15, 0.55); }

.chk-box {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border: 1.5px solid rgba(15, 15, 15, 0.45);
  border-radius: 4px;
  background: transparent;
  color: var(--text-on-hl);
  transition:
    background 160ms var(--ease-quiet),
    border-color 160ms var(--ease-quiet);
}
.chk-box svg { display: block; }
.chk-check-path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
.chk-row.is-done .chk-box {
  background: var(--hl-fill);
  border-color: rgba(15, 15, 15, 0.45);
}
/* The ~300ms ink check stroke — the climax beat's opening move. */
.chk-row.is-done .chk-check-path {
  animation: chk-check-stroke 300ms var(--ease-quiet) forwards;
}
@keyframes chk-check-stroke {
  to { stroke-dashoffset: 0; }
}

/* Directional arrows are decoration (aria-hidden); flip them with the
   writing direction so "points at the doc / points forward" stays logical
   in RTL (↑ is its own mirror image, so row 1 mobile is unaffected). */
.chk-arrow { display: inline-block; }
[dir="rtl"] .chk-arrow { transform: scaleX(-1); }

/* Death: the peel exit — lift off the corner and fade (rotate to +4°,
   drop 12px, 400ms). Declared after the settle animation it replaces. */
.home-checklist.is-peeling {
  animation: home-checklist-peel 400ms var(--ease-quiet) forwards;
  pointer-events: none;
}
@keyframes home-checklist-peel {
  from { opacity: 1; transform: rotate(var(--note-tilt, -0.8deg)) translateY(0); }
  to   { opacity: 0; transform: rotate(4deg) translateY(12px); }
}

/* Phones: 44px row targets (spec minimum for touch). */
@media (max-width: 520px) {
  .chk-row { min-height: 44px; font-size: 13.5px; }
}

/* Guided-selection pulse fallback — additive overlay flash over the target
   sentence's line boxes (src/utils/guided-selection.js). multiply keeps the
   text legible through the butter. */
.guided-pulse {
  position: absolute;
  pointer-events: none;
  border-radius: 3px;
  background: var(--hl-fill);
  mix-blend-mode: multiply;
  opacity: 0;
  animation: guided-pulse-flash 900ms ease-out 1;
}
@keyframes guided-pulse-flash {
  0%   { opacity: 0; }
  25%  { opacity: 0.9; }
  100% { opacity: 0; }
}

/* Climax beat, closing move: the pill's count ticks once with an emphasis
   pulse (HomeBottomPill sets .is-ticking for the beat only). */
.home-pill .preview-trigger.is-ticking .preview-trigger-count {
  display: inline-block;
  animation: count-tick 480ms var(--ease-overshoot) 1;
}
@keyframes count-tick {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.22); }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .home-checklist.is-peeling { animation: none; opacity: 0; }
  .chk-row, .chk-box { transition: none; }
  .chk-row.is-done .chk-check-path { animation: none; stroke-dashoffset: 0; }
  .guided-pulse { animation: none; opacity: 0.6; }
  .home-pill .preview-trigger.is-ticking .preview-trigger-count { animation: none; }
}

/* In-app .shared-note — the line the sharer wrote, pinned to the top of the
   review column when a recipient opens a shared link. Butter paper (the warm
   marker-yellow register) so it reads as a personal sticky from another person.
   Tilts the opposite way from the sample note. Reuses the sample-note eyebrow /
   dot / close sub-elements. */
.shared-note {
  --note-bg: var(--note-butter);
  --note-tilt: 0.6deg;
  --note-lift: -3px;
  margin: 0 0 36px;
  padding: 16px 44px 18px 20px;
  font-family: var(--ui-font);
  animation: sample-note-settle 520ms var(--ease-overshoot) both;
}

/* Sharer's name in the eyebrow slot — opts out of the eyebrow's uppercase /
   wide-tracking label register so a real name reads as a name. */
.shared-note-from {
  text-transform: none;
  letter-spacing: 0.005em;
  font-size: 12.5px;
  color: var(--ink);
}

/* Phones: the notes ride above the document on an already chrome-heavy
   viewport — tighten their padding and bottom margin so the document's first
   line lands higher. Right padding stays clear of the absolute close button. */
@media (max-width: 640px) {
  .sample-note,
  .shared-note {
    padding: 13px 40px 14px 16px;
    margin-bottom: 24px;
  }
}

.shared-note-body {
  margin: 0;
  font-family: var(--ui-font);
  font-size: 15.5px;
  font-weight: 500;
  line-height: 1.5;
  color: var(--ink);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* In-app .share-welcome — a quiet provenance byline above a shared doc, NOT a
   banner. No border, no fill, no full-width pill: just a small muted line that
   says who the doc came from, sitting like a byline over the title. It's the
   always-on companion to the sidebar "shared by" badge. The first time a
   recipient opens a shared doc it also carries a subtle "What is this?" text
   link; after that the link drops and only the byline stays. */
.share-welcome {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 10px;
  margin: 0 0 18px;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--ui-font);
  font-size: 12px;
  line-height: 1.4;
  color: var(--muted);
  animation: share-welcome-in var(--dur-enter) var(--ease-quiet) both;
}

@keyframes share-welcome-in {
  from { opacity: 0; transform: translateY(-3px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Reduced motion — keep the fade for comprehension, drop the slide. */
@media (prefers-reduced-motion: reduce) {
  .share-welcome { animation-name: share-welcome-fade; }
  @keyframes share-welcome-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
}

.share-welcome-text { text-align: start; }

/* Leading person marker — the same quiet bust as the sidebar "shared by" badge,
   so provenance reads consistently in both places. */
.share-welcome-icon {
  display: inline-flex;
  align-items: center;
  flex: none;
  color: var(--muted);
  opacity: 0.8;
}
.share-welcome-icon svg { display: block; }

/* The sharer's name — lifted a notch out of the muted line so it reads as a
   person, not boilerplate. dir="auto" in the markup keeps a Hebrew name laid
   out correctly inside the English byline. The addressee (`· for <name> to
   review`) wears the same class; on narrow screens both names truncate
   (inline-block + ellipsis) so the byline stays one line — the Send dialog's
   destination line is the full-name surface. */
.share-welcome-from {
  color: var(--ink-2);
  font-weight: 600;
}
@media (max-width: 480px) {
  .share-welcome-from {
    display: inline-block;
    max-width: 14ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
  }
}

/* First-time explainer link — a subtle text link, never a chip. A hairline
   underline that warms to full ink on hover; the marker-yellow chip was too
   loud for a byline. */
.share-welcome-link {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: var(--ink-2);
  font-weight: 500;
  text-decoration: underline;
  text-decoration-color: var(--border-strong);
  text-underline-offset: 3px;
  transition: color 120ms var(--ease-quiet), text-decoration-color 120ms var(--ease-quiet);
}
.share-welcome-link:hover { color: var(--ink); text-decoration-color: var(--ink); }
.share-welcome-link:focus-visible {
  outline: none;
  color: var(--ink);
  text-decoration-color: var(--ink);
  border-radius: 3px;
  box-shadow: 0 0 0 2px var(--ring);
}
.share-welcome-link svg { display: block; flex: none; opacity: 0.7; }

.sample-note-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--ui-font);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(15, 15, 15, 0.62);
  margin: 0 0 8px;
}

.sample-note-dot {
  position: relative;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #f5c518;
  flex: none;
  box-shadow: 0 0 0 0 rgba(245, 197, 24, 0.55);
  animation: sample-dot-pulse 1800ms cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

.sample-note-title {
  margin: 0 0 6px;
  font-family: var(--ui-font);
  font-size: 15.5px;
  font-weight: 500;
  line-height: 1.45;
  color: var(--ink);
}
.sample-note-title strong {
  font-weight: 600;
  background: var(--hl-fill);
  padding: 1px 4px;
  border-radius: 2px;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/* Editorial highlight mark — the page's signature brand-yellow gesture, emitted
   by markdown.jsx for ==text==. Reset the UA <mark> default (its own yellow +
   black) to the brand token; box-decoration-break keeps the fill wrapping
   cleanly across line breaks on mobile. Scoped to .doc so it only paints
   rendered document marks, never anything else that might use <mark>. */
.doc mark {
  background: var(--hl-fill);
  /* NOT --ink: the fill stays yellow in dark while --ink flips to off-white
     (≈1.5:1). --text-on-hl is the non-flipping dark ink for on-yellow text. */
  color: var(--text-on-hl);
  padding: 0 3px;
  border-radius: 2px;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/* Ink stamp — a rubber-stamped label pressed onto the page at a slight angle.
   THE YELLOW LAW: on the home doc, full-strength butter means exactly one
   thing — "a live comment sits here". The ==Free== trust mark is decoration,
   not a comment, so it trades the butter fill for this ink-outline stamp.
   Applied by restyling the renderer's own <mark> output, scoped under
   .home-doc-col — the parser stays untouched (==...== still emits <mark>)
   and user documents keep the butter mark. :not(.hl) keeps the selector off
   live-comment spans; a highlight painted INSIDE the stamp keeps full butter
   via the .hl rules. var(--ink) flips with the theme, so the stamp inverts
   to off-white-on-dark for free. */
.ink-stamp,
.home-doc-col .doc mark:not(.hl) {
  background: transparent;
  color: var(--ink);
  border: 1.5px solid var(--ink);
  border-radius: 3px;
  padding: 0 7px 1px;
  font-family: var(--ui-font);
  font-size: 12.5px;
  font-weight: 700;
  line-height: 1.5;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  display: inline-block;
  transform: rotate(-2deg);
  vertical-align: 1px;
}

/* Homepage "Free" trust strip — the one .doc paragraph carrying a ==mark==
   (rendered as the ink stamp above). Scoped to .home-doc so it only touches
   the marketing hero, never a user document that happens to use the highlight
   syntax. Two intents:
   (1) Asymmetric rhythm — pull the strip UP toward the subhead it qualifies
       (tighter margin-top) while keeping normal separation BELOW from the H2.
   (2) Quiet supporting clause — the line sits in --ink-2 so only the stamped
       "FREE" carries emphasis; the stamp itself re-asserts --ink (rule above).
   :has(mark) is the existing in-codebase idiom for "the element that contains";
   no per-line class is needed on the rendered DOM. */
.home-doc .doc p:has(mark) {
  margin-top: -0.35em;
  margin-bottom: 1.6em;
  color: var(--ink-2);
}

.sample-note-sub {
  margin: 0;
  font-family: var(--ui-font);
  font-size: 13px;
  line-height: 1.55;
  color: var(--ink-2);
}
.sample-note-sub strong {
  color: var(--ink);
  font-weight: 600;
}

/* Dismiss control — top-right "×". Sized to the optical balance of the
   eyebrow row, not boxy. Hover darkens the ink, no background change;
   the sticky note's own un-tilt would be loud here, so we suppress it
   by keeping the paper still while the × responds. */
.sample-note-close {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: transparent;
  border-radius: 4px;
  color: rgba(15, 15, 15, 0.42);
  cursor: pointer;
  transition: color 140ms var(--ease-quiet), background 140ms var(--ease-quiet);
}
.sample-note-close:hover {
  color: rgba(15, 15, 15, 0.78);
  background: rgba(15, 15, 15, 0.06);
}
.sample-note-close:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
  color: rgba(15, 15, 15, 0.78);
}
.sample-note-close svg { display: block; }

@media (max-width: 720px) {
  .pullquote { font-size: 18px; padding: 22px 22px 24px; }
  .verdict { padding: 13px 15px 14px; }
  .sample-note,
  .shared-note {
    padding: 14px 40px 15px 18px;
    margin-bottom: 28px;
  }
  .sample-note-title { font-size: 14.5px; }
  .sample-note-sub { font-size: 12.5px; line-height: 1.5; }
  .shared-note-body { font-size: 14.5px; }
}

/* Phones: the sticky's second paragraph pushes the document's H1 below the
   fold. Eyebrow + title carry the message; the elaboration is desktop air. */
@media (max-width: 520px) {
  .sample-note-sub { display: none; }
}

/* ------------------------------------------------------------
   Page footer — single source. Two quiet rows: byline | nav,
   privacy tag | bug report. No pill, no underlined prose.
   <footer class="page-foot">
     <div class="page-foot-row">
       <span>Built by <a>Elad</a> in a fit of &ldquo;not that one, the other one.&rdquo;</span>
       <nav class="foot-nav">…links…</nav>
     </div>
     <div class="page-foot-row page-foot-row--quiet">
       <span>your document never leaves your browser</span>
       <a class="foot-nav-bug">Report a bug</a>
     </div>
   </footer>
   ------------------------------------------------------------ */

.page-foot {
  max-width: 880px;
  margin: 56px auto 0;
  padding: 22px 24px 40px;
  border-top: 1px solid var(--border);
  font-family: var(--ui-font);
  font-size: 13px;
  color: var(--muted);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.page-foot a {
  color: var(--ink-2);
  text-decoration: none;
  transition: color 140ms var(--ease-quiet);
}

.page-foot a:hover { color: var(--ink); }

.page-foot-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 18px;
  align-items: center;
  justify-content: space-between;
}

.page-foot-row--quiet {
  font-size: 12.5px;
  opacity: 0.78;
}

.foot-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
}

/* Hidden under the workspace — a marketing footer beneath the review surface
   is the wrong note. Only the homepage runs the React app today, but the
   rule lives here so any future React page inherits it. */
body:has(.app.mode-review) .page-foot { display: none; }

@media (max-width: 720px) {
  .page-foot {
    padding: 20px 20px 32px;
  }
  .page-foot-row {
    align-items: flex-start;
  }
  .foot-nav { gap: 14px; }
}

/* ------------------------------------------------------------
   Reduced motion — every transition in this file collapses to 0.
   Color changes still apply (they're the semantic feedback); only
   the time-based motion is cut.
   ------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .post-card,
  .post-card__title,
  .post-card__cta::after,
  .audience-card,
  .verdict,
  .pullquote,
  .sample-note,
  article a:not([class]),
  .doc a:not([class]),
  .tldr a:not([class]),
  .post-body a:not([class]) {
    transition: none;
  }
  .audience-card,
  .verdict,
  .pullquote,
  .sample-note {
    transform: none;
    animation: none;
  }
  .audience-card:hover,
  .verdict:hover,
  .pullquote:hover,
  .sample-note:hover {
    transform: translateY(-1px);
  }
  .sample-note-dot {
    animation: none;
  }
  .post-card:hover {
    transform: none;
  }
  .post-card:hover .post-card__cta::after {
    transform: none;
  }
}

/* ============================================================
   BootLoader — the brief "fetching your routed/shared doc" beat shown in the
   review column while /r/<id> resolves (runRoutedBoot) or a #s= token decodes,
   instead of flashing the marketing homepage. Centered, RTL-agnostic, zero
   layout shift on swap-out. A ring, not a feature: it earns its appearance via a
   150ms delay (a sub-150ms resolve unmounts it first → no flash) and vanishes
   the instant the doc mounts.
   ============================================================ */
.boot-loader {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  min-height: 40vh;
  /* Delay-then-appear: invisible for the first 150ms, then a quiet 200ms fade.
     A fast resolve unmounts this before the delay elapses → the user sees a
     clean swap to the document, no spinner flash. */
  opacity: 0;
  animation: boot-loader-appear 200ms var(--ease-quiet) 150ms forwards;
}

.boot-loader-ring {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid var(--border);          /* faint full track */
  border-top-color: var(--accent-deep);     /* the moving arc */
  animation: boot-loader-spin 720ms linear infinite;
}

.boot-loader-label {
  font-family: var(--ui-font);
  font-size: 13px;
  color: var(--muted);
}

@keyframes boot-loader-appear {
  to { opacity: 1; }
}

@keyframes boot-loader-spin {
  to { transform: rotate(360deg); }   /* transform only — GPU, no layout */
}

/* Reduced motion: appear instantly, never spin. The ring stays a static
   two-tone arc that still reads as an indicator, with a calm opacity breathe
   instead of rotation. Present and instant — never a frozen spin, never nothing. */
@media (prefers-reduced-motion: reduce) {
  .boot-loader {
    opacity: 1;
    animation: none;
  }
  .boot-loader-ring {
    animation: boot-loader-breathe 1400ms var(--ease-quiet) infinite;
  }
}

@keyframes boot-loader-breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.45; }
}

/* ============================================================
   Dark mode overrides — shared components.

   These selectors flip surfaces, shadows, and chips that use raw
   values for legitimate reasons (semantic chips, opacity-based
   shadows, photographic dim). Token-bound rules elsewhere flip
   automatically via the [data-theme="dark"] block in tokens.css.
   ============================================================ */

/* ---- Topbar ----
   The blur background is var(--topbar-blur) — already flips. Keep
   the rule here as a no-op cue for future maintainers. */

/* ---- Post-card hover shadow ----
   Light shadow (rgba(55,53,47,0.14)) becomes invisible on dark.
   Use stronger black with lower offset; the surface lift does
   most of the work. */
[data-theme="dark"] .post-card {
  background: var(--surface-2);
}
[data-theme="dark"] .post-card:hover {
  background: var(--surface-3);
  border-color: var(--edge-strong);
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.45),
    0 14px 30px -20px rgba(0, 0, 0, 0.55);
}

/* ---- Sticky notes (.audience-card / .verdict / .pullquote / .sample-note) ----
   Paper stays paper — we don't flip the cream/blush/sage hex.
   Two things happen in dark mode:
     1. Dim the whole element so the paper doesn't blast as a
        headlight against the near-black canvas.
     2. Re-pin --ink, --ink-2, --muted to literal dark values
        WITHIN the scope of the note. Children that read
        var(--ink) for text would otherwise resolve to the dark
        theme's off-white — invisible on the still-light paper. */
[data-theme="dark"] .audience-card,
[data-theme="dark"] .verdict,
[data-theme="dark"] .pullquote,
[data-theme="dark"] .sample-note,
[data-theme="dark"] .shared-note {
  filter: brightness(0.88) contrast(1.04);
  --ink:            #1a1815;
  --ink-2:          #4a463f;
  --muted:          #676259;
  --text-primary:   #1a1815;
  --text-secondary: #4a463f;
  --text-muted:     #676259;
}

/* ---- Photographs / screenshots ----
   Single blanket rule. Opt out with [data-no-dim] / .brand-asset. */
[data-theme="dark"] img:not([data-no-dim]):not(.brand-asset):not(.no-dim) {
  filter: var(--img-filter);
}

/* ---- Selection ---- */
[data-theme="dark"] ::selection {
  background: rgba(252, 211, 77, 0.32);
  color: var(--text-primary);
}

/* NOTE deliberately no `html { transition: background-color/color }` here:
   only the root canvas would tween while every surface/border/chip flips
   instantly — a visible 200ms tear on the dark↔light boundary. The app
   crossfades the WHOLE page via a `theme` view transition (theme.js);
   static pages flip instantly, which reads cleaner than a torn fade. */

/* ============================================================
   .skill-cta — prominent download pill for SKILL.md files,
   used on the launch-note blog page (and any future skill
   article that needs to break the inline-link-buried-in-prose
   pattern). Mirrors the .launch-banner-cta-primary vocabulary
   so the article CTA reads as the same call as the homepage
   banner: filled ink pill, marker-yellow .md glyph, hover lift.
   Sits as its own block — never inline inside a paragraph.

   .skill-cta-row pairs the pill with a quiet meta line
   (version · license · size) on the right; on mobile they
   stack with the pill above the meta.
   ============================================================ */
.skill-cta-row {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  margin: 28px 0 36px;
}
.skill-cta {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 14px 22px 14px 18px;
  background: var(--ink);
  color: var(--text-on-emphasis);
  font-family: var(--ui-font);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.005em;
  text-decoration: none;
  border-radius: 999px;
  line-height: 1;
  box-shadow:
    0 1px 2px rgba(15, 15, 15, 0.18),
    0 6px 16px -8px rgba(15, 15, 15, 0.35);
  transition:
    background-color 160ms var(--ease-quiet),
    transform 200ms var(--ease-considered),
    box-shadow 200ms var(--ease-considered);
}
@media (hover: hover) and (pointer: fine) {
  .skill-cta:hover {
    background: #1f1d18;
    transform: translateY(-1px);
    box-shadow:
      0 2px 3px rgba(15, 15, 15, 0.22),
      0 12px 22px -10px rgba(15, 15, 15, 0.45);
  }
}
.skill-cta:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 3px;
}
/* Press registers on touch too (no lift to undo there), so scale instead of
   only resetting the hover lift. */
.skill-cta:active { transform: translateY(0) scale(0.97); }
.skill-cta-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  color: var(--hl-fill);
  flex-shrink: 0;
}
.skill-cta-glyph svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.skill-cta-arrow {
  display: inline-block;
  transition: transform 200ms var(--ease-considered);
}
.skill-cta:hover .skill-cta-arrow { transform: translateX(2px); }
.skill-cta-meta {
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: 0.02em;
  color: var(--muted);
}
[data-theme="dark"] .skill-cta:hover {
  background: #ffffff;
  box-shadow:
    0 2px 3px rgba(0, 0, 0, 0.45),
    0 12px 22px -10px rgba(0, 0, 0, 0.55);
}
@media (prefers-reduced-motion: reduce) {
  .skill-cta, .skill-cta-arrow { transition: none; }
}
@media (max-width: 600px) {
  .skill-cta-row { flex-direction: column; align-items: flex-start; gap: 10px; }
  .skill-cta { font-size: 14.5px; padding: 13px 20px 13px 16px; }
}

/* ============================================================================
   Shell pill — the ONE dark floating bottom bar, shared by the app and the
   static content pages (nav-shell-v2 Step 4).
   ----------------------------------------------------------------------------
   The translucent "liquid-glass" pill, centered + floating in the thumb zone,
   is the single bottom-bar treatment in the app. The CONTAINER lives here so the
   app's review panel (.bottom-panel, index.html) reads it from one home (the
   content pages' bottom bar is retired — reading pages carry no floating pill;
   their Paste CTA rides the topbar). App-specific
   review internals (copy/share/preview controls, the docked review-column
   recentering geometry) stay inline in index.html; this is just the shell.

   The pill ALWAYS clears the iOS home indicator: bottom is the flat inset plus
   env(safe-area-inset-bottom) (the pre-Step-4 flat `bottom: 18px` ignored the
   inset — fixed here as we formalize the bottom zone). The rise-in motion knob
   is the named --pill-rise (cmo-motion tunes it in one place); reduced-motion
   drops it via the guard below. --pill-inset is the flat lift off the edge.
   ============================================================================ */
.shell-pill {
  /* Motion + geometry knobs (cmo-motion / eng-a11y tune here). */
  --pill-rise: 280ms;
  --pill-inset: 18px;
  /* The one standard content-row height shared by every state the pill can
     morph into (empty CTA / populated buttons / toast message) — without it,
     each state's height falls out independently from its own padding/font
     metrics and the whole "one continuous object" illusion breaks the moment
     the silhouette's HEIGHT visibly shifts (only its width should breathe). */
  --bar-ctrl-h: 32px;
  /* The review pill's start-edge padding varies by state (see
     .bottom-panel.is-empty in index.html, which widens it to give the CTA
     text room) — `--pill-pad-base` is the CONSTANT reference value (this
     padding, unchanged) and `--pill-pad-start` is whatever padding is
     ACTUALLY in effect right now. `.dir-btn` derives its margin from the gap
     between the two, so its corner-fit stays correct in every state instead
     of being hand-tuned for just one of them. */
  --pill-pad-base: 8px;
  --pill-pad-start: 8px;

  position: fixed;
  bottom: calc(var(--pill-inset) + env(safe-area-inset-bottom, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 30;
  background: rgba(31, 30, 28, 0.72);
  -webkit-backdrop-filter: saturate(180%) blur(22px);
  backdrop-filter: saturate(180%) blur(22px);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 999px;
  padding: 8px;
  display: flex;
  align-items: center;
  gap: 8px;
  width: max-content;
  max-width: calc(100vw - 20px);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.08) inset,
    0 0 0 1px rgba(0, 0, 0, 0.04),
    0 12px 32px -12px rgba(15, 15, 15, 0.45),
    0 4px 10px -4px rgba(15, 15, 15, 0.25);
  font-family: var(--ui-font);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: -0.005em;
}
/* Fallback when backdrop-filter is unsupported — a nearly opaque ink. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .shell-pill { background: rgba(31, 30, 28, 0.96); }
}
/* Entry — slide up + fade in (centered, so the transform carries the -50% X). */
@keyframes shell-pill-rise {
  from { opacity: 0; transform: translate(-50%, 8px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
@media (prefers-reduced-motion: no-preference) {
  .shell-pill { animation: shell-pill-rise var(--pill-rise) var(--ease-considered) both; }
}

/* The app docs panel wrapper — a flex child of the rail's flex column: it must
   TAKE the available height and let its inner scroll region (the doc list)
   scroll, so the footer stays pinned at the foot of the capped sheet instead of
   being pushed past its bottom edge. */
.rail-docs-body { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }

/* ─── Floating About / Contact menu (AboutMenu) ──────────────────────────────
   The ☰ surface that replaced the retired docked rail's About tab. Desktop: a
   popover in the header's own language (surface-4, 1px --edge, radius 14, the
   switcher-popover shadow), hanging from the ☰ — positioned by inline style from
   the trigger's rect, so it lives at the app root (fixed-overlay invariant) yet
   anchors to the button. Mobile: the SAME content as a bottom sheet (rounded top,
   --sheet-rise in / --sheet-fall out, --scrim backdrop). Motion (cmo-motion):
   popover scale(.98)+fade from top-inline-start ~160ms --ease-considered; sheet
   rides the shared sheet tokens; reduced-motion collapses both to opacity only. */
.about-pop {
  position: fixed; z-index: 50;
  width: 308px; max-width: 88vw; max-height: min(70vh, 520px);
  overflow-y: auto;
  padding: 8px;
  background: var(--surface-4);
  border: 1px solid var(--edge);
  border-radius: 14px;
  box-shadow: 0 1px 2px rgba(15, 15, 15, 0.06), 0 12px 32px -10px rgba(15, 15, 15, 0.18);
  transform-origin: top left; /* top-inline-start; the app chrome is LTR */
  animation: about-pop-in var(--dur-quick) var(--ease-considered) both;
}
[data-theme="dark"] .about-pop {
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 16px 40px -8px rgba(0, 0, 0, 0.7);
}
.about-pop.is-exiting { animation: about-pop-out var(--dur-fast) var(--ease-considered) both; }
@keyframes about-pop-in { from { opacity: 0; transform: scale(0.98); } to { opacity: 1; transform: scale(1); } }
@keyframes about-pop-out { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.98); } }

/* The doors zone — [Last document][Home] — leads the menu. The rows are
   buttons in the app and anchors on the static pages, so restate the button
   reset for both. Doors keep the bold, full-ink dress; the editorial rows
   below drop to 500/--ink-2 — the weight hierarchy that lets the eye rank
   zones instead of reading nine identical rows. */
.am-last,
.am-home {
  width: 100%;
  border: 0; background: transparent; cursor: pointer;
  text-align: start;
  font-weight: 600; color: var(--ink);
}
/* The hidden shell must actually hide: .am-link's display:flex (author) beats
   the UA's [hidden] rule, so restate it. */
.am-last[hidden] { display: none; }
/* Two-line Last-document row: fixed label over the dynamic "Created …" pulse.
   The sub-line inherits the row's ellipsis discipline; tabular digits keep
   the time from wobbling per-minute. */
.am-last .am-link-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.am-last .am-link-sub {
  font-family: var(--ui-font); font-size: 11px; font-weight: 400;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.am-last .am-link-sub:empty { display: none; }
.am-home { margin-block-start: 2px; }

/* ONE seam construction fences every zone — 6px air / hairline / 6px air,
   full-bleed through the container's padding (the line belongs to the
   surface, not the row). Replaces the four bespoke hairline geometries that
   made the old menu jitter. Zone openers: the links nav (after the doors),
   each subsequent link group, the app-only Settings row, and Theme when it
   opens the controls zone itself (static pages, no Settings). */
.am-links,
.am-group + .am-group,
.am-settings,
.am-theme { position: relative; margin-block-start: 13px; }
.am-links::before,
.am-group + .am-group::before,
.am-settings::before,
.am-theme::before {
  content: "";
  position: absolute; inset-inline: -8px; inset-block-start: -7px;
  border-block-start: 1px solid var(--edge-subtle);
}
.about-sheet .am-links::before,
.about-sheet .am-group + .am-group::before,
.about-sheet .am-settings::before,
.about-sheet .am-theme::before { inset-inline: -12px; }

/* The quiet you-are-here — the row for the page being read (aria-current).
   A resting fill one step calmer than hover; never a loud accent. */
.am-link.is-current { background: var(--chip-bg, var(--bg-sub)); color: var(--ink); }

/* APP-ONLY Settings row (the menu-row face of SettingsLauncher) — opens the
   controls zone above Theme; Theme keeps its own seam below it (founder call:
   a hairline between Settings and Theme too). Absent on the static twin (a
   content page has no settings) — a sanctioned host difference. */
.am-settings {
  width: 100%;
  border: 0; background: transparent; cursor: pointer;
  text-align: start;
}

.am-links { display: flex; flex-direction: column; }
.am-group { display: flex; flex-direction: column; gap: 2px; }
.am-link {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 10px; border-radius: 8px;
  text-decoration: none; color: var(--ink-2);
  font-family: var(--ui-font); font-size: 13px; font-weight: 500;
  transition: background 120ms var(--ease-quiet), color 120ms var(--ease-quiet);
}
.am-link:hover { background: var(--chip-bg, var(--bg-sub)); color: var(--ink); }
.am-link:active { background: var(--chip-bg-hover, var(--bg-sub)); }
.am-link:focus-visible { outline: 2px solid var(--ring, var(--border-strong)); outline-offset: -2px; color: var(--ink); }
.am-link-ico { display: inline-flex; color: var(--muted); flex: 0 0 auto; transition: color 120ms var(--ease-quiet); }
.am-link:hover .am-link-ico,
.am-link.is-current .am-link-ico { color: var(--ink-2); }
.am-link-ico svg { width: 16px; height: 16px; }
.am-link-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Trailing ↗ on the one external row — the visible twin of the aria new-tab
   cue, so sighted users also learn the row leaves the site. */
.am-link-ext { display: inline-flex; margin-inline-start: auto; color: var(--muted); flex: 0 0 auto; }

/* Personal Contact footer — the colophon band: a recessed paper strip that
   ENDS the menu instead of reading as one more row. --surface-3 sits one
   elevation step below the --surface-4 popover in BOTH themes. */
.am-foot {
  margin: 12px -8px -8px;
  padding: 11px 18px 12px;
  background: var(--surface-3);
  border-block-start: 1px solid var(--edge-subtle);
  border-radius: 0 0 13px 13px;
}
.am-foot-note { margin: 0; font-family: var(--ui-font); font-size: 11.5px; line-height: 1.55; color: var(--muted); }
.am-foot-note strong { color: var(--ink-2); font-weight: 600; }
.am-foot-link {
  color: var(--ink-2); text-decoration: none;
  border-block-end: 1px solid var(--edge-strong); white-space: nowrap;
  transition: color 120ms var(--ease-quiet), border-color 120ms var(--ease-quiet);
}
.am-foot-link:hover { color: var(--ink); border-block-end-color: var(--muted); }
.am-foot-link:focus-visible { outline: 2px solid var(--ring, var(--border-strong)); outline-offset: 2px; border-radius: 2px; }

/* Mobile bottom sheet — the same content, a separate door from the docs pill. */
.am-backdrop {
  position: fixed; inset: 0; z-index: 90;
  background: var(--scrim);
  animation: am-scrim-in var(--sheet-rise) var(--ease-quiet) both;
}
.am-backdrop.is-exiting { animation: am-scrim-out var(--sheet-fall) var(--ease-quiet) both; }
.about-sheet {
  position: fixed; inset-inline: 0; inset-block-end: 0; z-index: 91;
  background: var(--surface-4);
  border-start-start-radius: 20px; border-start-end-radius: 20px;
  box-shadow: 0 -10px 40px rgba(15, 15, 15, 0.18);
  padding: 6px 12px calc(14px + env(safe-area-inset-bottom));
  animation: about-sheet-in var(--sheet-rise) var(--sheet-ease-rise, var(--ease-considered)) both;
}
.about-sheet.is-exiting { animation: about-sheet-out var(--sheet-fall) var(--sheet-ease-fall) both; }
.am-grab { width: 40px; height: 4px; border-radius: 2px; background: var(--edge-strong); margin: 8px auto 10px; }
.about-sheet .am-link { padding: 12px 10px; font-size: 15px; }
.about-sheet .am-link-ico svg { width: 18px; height: 18px; }
/* The colophon band runs to the sheet's bottom edge (swallowing the sheet's
   own safe-area padding). */
.about-sheet .am-foot {
  margin: 12px -12px calc(-14px - env(safe-area-inset-bottom));
  padding: 13px 22px calc(16px + env(safe-area-inset-bottom));
  border-radius: 0;
}
@keyframes about-sheet-in { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes about-sheet-out { from { transform: translateY(0); } to { transform: translateY(100%); } }
@keyframes am-scrim-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes am-scrim-out { from { opacity: 1; } to { opacity: 0; } }

/* Settings opened from the menu SUSPENDS the menu — no sheet-over-sheet: the
   sheet/backdrop play their exit and HOLD offscreen (fill-mode both) while the
   Settings dialog rises in their place. The menu stays mounted (it hosts the
   dialog's portal — see AboutMenu.jsx) and unmounts for real once the dialog
   closes, so no re-entrance ever plays. */
.about-sheet.is-suspended {
  animation: about-sheet-out var(--sheet-fall) var(--sheet-ease-fall) both;
  pointer-events: none;
}
.am-backdrop.is-suspended {
  animation: am-scrim-out var(--sheet-fall) ease both;
  pointer-events: none;
}
/* The desktop popover has no exit choreography — it just yields to the modal. */
.about-pop.is-suspended { visibility: hidden; }

/* Reduced motion — no scale, no slide: opacity only (both surfaces). */
@media (prefers-reduced-motion: reduce) {
  .about-pop, .about-pop.is-exiting { animation: about-opacity-in var(--dur-fast) var(--ease-quiet) both; }
  .about-sheet, .about-sheet.is-exiting { animation: about-opacity-in var(--sheet-fall) var(--ease-quiet) both; }
  @keyframes about-opacity-in { from { opacity: 0; } to { opacity: 1; } }
  /* Suspended = dismissed: hide outright (the RM rules above would otherwise
     re-apply the opacity-in entrance and leave the sheet visible). */
  .about-sheet.is-suspended,
  .am-backdrop.is-suspended { animation: none; visibility: hidden; }
}

/* Static content pages host the SAME menu on one server-emitted node
   (#site-about-menu.site-about, hidden until /js/static-menu.js opens it):
   the sheet presentation under 900px, the popover below the topbar ☰ at
   >=900px — the exact app presentation. The grab handle is sheet-only chrome
   (React mounts it only on mobile; the static node always carries it, so CSS
   scopes it to the sheet presentation). The static topbar's ☰ reuses the app's
   .hamburger metrics; the button-reset the app gets from its inline .btn.ghost
   is restated here (static pages don't load the app's inline styles). */
.site-topbar .hamburger {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  color: var(--ink-2);
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-quiet), color var(--dur-fast) var(--ease-quiet), border-color var(--dur-fast) var(--ease-quiet);
}
.site-topbar .hamburger:hover { background: var(--chip-bg, var(--bg-sub)); color: var(--ink); }
.site-topbar .hamburger:focus-visible { outline: 2px solid var(--ring, var(--border-strong)); outline-offset: 2px; }

/* One glyph geometry across BOTH bars: every stroked bar glyph is 18px at 1.75
   stroke. The shield's inline SVG attrs stay 13×13 as the no-CSS fallback;
   this rule is what actually renders (the app states the same values in its
   inline styles — equal on purpose, so the bars can't disagree). */
.topbar .privacy-pill.compact .privacy-shield { width: 18px; height: 18px; stroke-width: 1.75; }

/* Touch scale for the static bar (≤520px, the same width the app's
   useIsMobile flips at): 44px icon hits for the ☰ + shield, the 40px labeled
   Paste chip (the app's `.top-actions .btn` phone metrics). The pill compacts
   HERE, responsively — the emitted markup is the full shield+label pill, this
   is the static twin of the app handing <PrivacyPill compact /> on mobile.
   (The app's phone bar drops the brand + shield entirely to make room for the
   centered doc title; a content page has no doc title, so the brand stays —
   it's the page's identity — and the shield stays down to 390px.) */
@media (max-width: 520px) {
  .site-topbar .hamburger { width: 44px; height: 44px; }
  .site-topbar .privacy-pill { width: 44px; height: 44px; padding: 0; justify-content: center; gap: 0; cursor: pointer; }
  .site-topbar .privacy-pill .privacy-label { display: none; }
  .site-topbar .privacy-pill .privacy-shield { width: 18px; height: 18px; stroke-width: 1.75; }
  .site-paste-cta { min-height: 40px; padding: 5px 12px; font-size: 13px; }
  .site-paste-cta .btn-icon { width: 18px; height: 18px; }
}
/* Tighter phones: the full row (☰ 44 + brand ~115 + shield 44 + labeled CTA
   ~154 + gaps and insets) needs ~395px; below 390 the CTA starts eating the
   bar's END inset. The shield comes off first (the app's phone bar already
   made that call — "fewer icons beats more doors", #682) so the CTA keeps its
   verb; the privacy promise stays one tap away as the ☰ menu's Privacy link. */
@media (max-width: 389px) {
  .site-topbar .privacy-pill { display: none; }
}
/* 320px-class phones: ☰ 44 + brand ~115 + insets leave ~123px for the CTA,
   under its labeled+glyph 146px. The glyph goes, the verb stays — the inverse
   of the app's icon-only collapse, because on a content page the CTA verb is
   the conversion (and the anchor's accessible name). */
@media (max-width: 340px) {
  .site-paste-cta .btn-icon { display: none; }
}
/* Desktop parity for the pill's promise popover: the app's full pill opens on
   hover/focus (React wires mouseenter/focus/blur); the static pill mirrors
   that in CSS on hover-capable devices. The click toggle (static-menu.js)
   stays — it's the only opener on touch. */
@media (hover: hover) {
  .site-topbar .privacy-pill:hover .privacy-popover,
  .site-topbar .privacy-pill:focus .privacy-popover {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
  }
}
.site-about .am-grab { display: none; }
.site-about.about-sheet .am-grab { display: block; }
/* The menu container takes the OPEN-focus seed (tabindex -1) so no control
   wears a ring on a touch open — and the container itself never draws one. */
.about-pop:focus,
.about-sheet:focus,
.site-about:focus { outline: none; }

/* Segmented radiogroup (.dir-toggle app-only / .theme-toggle everywhere) —
   PROMOTED from index.html's inline styles: the theme segment now rides the ☰
   About menu on EVERY surface (app + static content pages), so the control has
   ONE home. Icon-only visually; labels stay in the DOM for AT. */
/* Direction override (Auto / RTL / LTR): a 3-state segmented radiogroup.
   Only renders when the document contains RTL content — otherwise the
   toggle is meaningless. The default 'auto' keeps per-block detection;
   RTL/LTR pin every block (and the table element) to one direction. */
.dir-toggle,
.theme-toggle {
  display: inline-flex;
  align-items: center;
  padding: 2px;
  /* Ink-tinted track (was a hard-coded #ececea + border): the fill defines
     the shape, and it theme-flips instead of going invisible on dark. */
  background: color-mix(in oklab, var(--ink) 8%, transparent);
  border: 0;
  border-radius: 7px;
  gap: 0;
}
.dir-toggle-opt,
.theme-toggle-opt {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--ink-2);
  padding: 5px 7px;
  border-radius: 5px;
  cursor: pointer;
  transition: background 120ms var(--ease-quiet), color 120ms var(--ease-quiet), box-shadow 120ms var(--ease-quiet);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 30px;
}
/* Visible label is icon-only; the text label stays in the DOM for
   screen readers and Playwright role queries (aria-label also set). */
.dir-toggle-label,
.theme-toggle-label {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip: rect(0 0 0 0); overflow: hidden;
  white-space: nowrap;
}
.dir-toggle-icon,
.theme-toggle-icon { opacity: 0.85; flex-shrink: 0; }
.dir-toggle-opt:hover:not(.is-active),
.theme-toggle-opt:hover:not(.is-active) {
  color: var(--ink);
  background: rgba(255, 255, 255, 0.55);
}
.dir-toggle-opt:hover:not(.is-active) .dir-toggle-icon,
.theme-toggle-opt:hover:not(.is-active) .theme-toggle-icon { opacity: 0.95; }
.dir-toggle-opt:focus-visible,
.theme-toggle-opt:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 1px;
}
.dir-toggle-opt.is-active,
.theme-toggle-opt.is-active {
  background: var(--bg);
  color: var(--ink);
  font-weight: 600;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.10), 0 0 0 0.5px rgba(0, 0, 0, 0.04);
}
.dir-toggle-opt.is-active .dir-toggle-icon,
.theme-toggle-opt.is-active .theme-toggle-icon { opacity: 1; }

/* Dark: the track lifts via white-overlay (--surface-2 RECESSED below the
   popover — the inverse of elevation-lightens). */
[data-theme="dark"] .dir-toggle,
[data-theme="dark"] .theme-toggle { background: rgba(255, 255, 255, 0.07); }
[data-theme="dark"] .dir-toggle-opt:hover:not(.is-active),
[data-theme="dark"] .theme-toggle-opt:hover:not(.is-active) {
  background: rgba(255, 255, 255, 0.06);
}
[data-theme="dark"] .dir-toggle-opt.is-active,
[data-theme="dark"] .theme-toggle-opt.is-active {
  /* Was --surface-4 == the popover's own color, i.e. an invisible selection. */
  background: rgba(255, 255, 255, 0.10);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.30), 0 0 0 0.5px rgba(255, 255, 255, 0.10);
}
[data-theme="dark"] .dir-toggle-opt:focus-visible,
[data-theme="dark"] .theme-toggle-opt:focus-visible {
  outline-color: var(--focus-ring, var(--hl-fill));
}

/* The ☰ menu's Theme row — a micro-eyebrow label + the segmented control,
   sitting between the links and the Contact footer on both renderers. The
   zone seam (::before) and margin live with the shared seam rules above. */
.am-theme {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 6px 10px 2px;
}
.am-theme-label {
  font-family: var(--ui-font); font-size: 11px; font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--muted);
}

/* Amber pill chip — the warm "Paste an answer" primary the APP's home bottom
   pill renders (HomeOverviewScreen's .shell-pill chip): marker-yellow fill with
   brown ink, the one located spot of brand color on the ink pill. (The static
   content pages' bottom bar that also wore it is retired; their Paste CTA is
   the topbar .site-paste-cta below.) */
.site-bar-cta {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 40px;
  padding: 0 16px; border-radius: 999px;
  background: var(--hl-fill); color: #7a4f02;
  box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.35);
  text-decoration: none;
  font-family: var(--ui-font); font-size: 13px; font-weight: 700;
  letter-spacing: -0.005em;
  white-space: nowrap;
  transition: filter 120ms var(--ease-quiet), transform 240ms var(--ease-overshoot);
}
.site-bar-cta:hover { filter: brightness(1.04); color: #7a4f02; }
.site-bar-cta:active { transform: scale(0.96); }
.site-bar-cta:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.35), 0 0 0 2px rgba(255, 255, 255, 0.5);
}
@media (prefers-reduced-motion: reduce) {
  .site-bar-cta { transition: none; }
  .site-bar-cta:active { transform: none; }
}

/* Topbar Paste CTA — the one conversion affordance on a content page, riding
   the sticky brand bar's .top-actions END edge. The static twin of the app
   Topbar's .paste-new-btn ("the topbar's one filled CTA. An ink pill" —
   index.html), value for value: the same warm charcoal, the same clipboard
   glyph, the same paper-on-ink inversion in dark. Only the ⌘V keycap is
   omitted — the shortcut doesn't work on a static page. */
.site-paste-cta {
  display: inline-flex; align-items: center; gap: 6px;
  background: #1f1e1c;
  border: 1px solid transparent;
  color: #fff;
  border-radius: 999px;
  padding: 8px 16px;
  font-family: var(--ui-font); font-size: 13.5px; font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-quiet), color var(--dur-fast) var(--ease-quiet), border-color var(--dur-fast) var(--ease-quiet);
}
.site-paste-cta:hover { background: #2b2924; color: #fff; }
.site-paste-cta .btn-icon { flex: none; }
.site-paste-cta:focus-visible {
  outline: 2px solid var(--ring, var(--border-strong)); outline-offset: 2px;
}
[data-theme="dark"] .site-paste-cta {
  background: var(--text-primary);
  color: var(--text-on-emphasis);
}
[data-theme="dark"] .site-paste-cta:hover { background: #ffffff; }

/* Dark theme (the file's mechanism is [data-theme="dark"]). On a near-black page
   the light scrim, the faint grab-handle, and the light open-sheet shadow all
   wash out — restore contrast for each. */
[data-theme="dark"] .doc-nav-handle::before { background: rgba(255, 255, 255, 0.22); }

/* ============================================================================
   App nav-shell (.doc-nav) — SHELL-LEVEL rules, single home
   ----------------------------------------------------------------------------
   The SPA's rail/drawer SHELL: the off-canvas drawer + backdrop + dock rail,
   the brand row (Zone A), the primary nav links (Zone B), the close/dock chrome,
   the primary Paste affordance (.doc-nav-new / .is-primary), the Learn gateway,
   and the footer frame. These are the structural / geometry / choreography rules
   the unified bottom-bar + up-sheet shell shares across the app AND the static
   content pages, so they live in ONE place — here — and the SPA consumes them
   instead of inline <style> in index.html. This is what keeps the app rail and
   the retired static rail from drifting (later steps
   reconcile the two onto this single shell).

   APP-ONLY payload rules stay inline in index.html, because static pages never
   emit that markup: the interactive doc-row STATES (.doc-nav-row.is-active /
   .is-menu-open / .is-renaming / .is-deleting, the shared-by badge), context
   menus (.doc-nav-menu*), rename/pin (.doc-nav-rename / .doc-nav-pin), the
   count/trust strip (.doc-nav-countbar*) and the rail toolbar
   (.doc-nav-toolbar / .rail-tool), plus the review-column recentering geometry
   (.app.is-docked .bottom-panel / .preview-popover / .doc-toc) and the
   doc-switch view-transition on the foot. The boundary test: "would the static
   bottom bar / up-sheet ever use this rule?" — yes → shell (here); no → inline.

   PROMOTED HERE (read-only, both rails render it): the doc-row DISPLAY rules
   (.doc-nav-list / -group* / -empty* / -row / -row-btn / -icon / -row-main /
   -titlerow / -title / -meta / -time / -dot / -row-count / -row-intext) AND the
   full-text search FIELD (.doc-nav-search* / .doc-nav-results-summary /
   .doc-nav-search-echo) — the retired static shelf (static-docs.js, gone in
   #668) rendered the SAME rows and search field from this one home; today only
   the app rail reads these rules.

   NOTE: index.html loads this file (line ~36) BEFORE its inline <style>, so any
   app-only inline rule of equal-or-higher specificity still wins the cascade.
   Geometry token --dock-w lives in tokens.css. Moved here with ZERO visual
   change in Step 1 of the nav-shell-v2 staged path (docs/STATIC_SHELL_PLAN.md).
   ============================================================================ */
  .hamburger {
    width: 34px;
    height: 34px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--ink-2);
    border-radius: 8px;
    flex: 0 0 auto;
  }
  .hamburger:hover { color: var(--ink); }

  .doc-nav-backdrop {
    position: fixed; inset: 0; z-index: 90;
    background: rgba(15, 15, 15, 0.32);
    animation: fade-in 140ms ease-out;
  }
  .doc-nav {
    position: fixed;
    top: 0; left: 0; bottom: 0;
    z-index: 91;
    width: 300px;
    max-width: 86vw;
    background: var(--surface-4);
    border-right: 1px solid var(--border);
    box-shadow: 10px 0 40px rgba(15, 15, 15, 0.18);
    display: flex;
    flex-direction: column;
    outline: none;
    animation: doc-nav-in 240ms var(--ease-considered) both;
    /* A row's long-press opens our context menu. Suppress native text selection
       and the iOS callout for the whole drawer: scoping this to a single row
       button still lets iOS walk up to a selectable ancestor and "select all"
       on a press that's a touch too short, leaking the OS selection toolbar. */
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
  }
  @keyframes doc-nav-in {
    from { transform: translateX(-100%); }
    to   { transform: translateX(0); }
  }
  /* Legacy side-slide exit — retained as the .doc-nav baseline (and so the
     nav-shell parity test still finds the rule + its keyframes). The mobile
     overlay now exits as an UP-SHEET (.doc-nav:not(.is-docked).is-exiting,
     below) which overrides this, and the docked rail never enters .is-exiting —
     so in practice nothing plays this anymore. The live exit timing is
     --sheet-fall (220ms), tracked by the useExitTransition buffer in app.jsx. */
  .doc-nav.is-exiting {
    animation: doc-nav-out 180ms var(--ease-quiet) both;
  }
  @keyframes doc-nav-out {
    from { transform: translateX(0); }
    to   { transform: translateX(-100%); }
  }
  .doc-nav-backdrop.is-exiting {
    animation: backdrop-out 180ms ease-out both;
    pointer-events: none;
  }
  @keyframes backdrop-out {
    from { opacity: 1; }
    to   { opacity: 0; }
  }
  /* ---- Mobile / small-tablet up-sheet (the !docked overlay, <900px) ----
     nav-shell-v2 final slice: BELOW the dock breakpoint the app's docs drawer no
     longer slides in from the LEFT — it RISES from the bottom bar as a sheet, the
     same axis / motion / feel as the retired static content sheet had.
     Scoped to :not(.is-docked) AND <900px on purpose: the DESKTOP docked rail
     (.doc-nav.is-docked) is byte-identical, and a WIDE non-docked overlay — what
     you get after UNDOCKING on a >=900 screen — keeps the base left side-drawer
     (.doc-nav above), so it enters/exits horizontally instead of falling away as a
     jarring bottom-sheet. (Before this gate the sheet rules applied at every width,
     so undocking a wide rail animated it straight down.) This block re-anchors only
     the mobile overlay: bottom-anchored, full inline width, capped height (the page
     shows behind the top edge), top radii, an up-slide. Motion resolves the shared
     --sheet-* tokens (tokens.css) so this sheet and the content sheet can't drift.
     The sheet stays a flex column, so the brand + Paste tile pin at the top,
     .doc-nav-list scrolls internally, and the count/foot pin at the bottom. */
  @media (max-width: 899px) {
    .doc-nav:not(.is-docked) {
      top: auto; left: 0; right: 0; bottom: 0;
      width: auto;
      max-width: none;
      /* Capped so a tall sheet (up to ~20 docs) still reveals the scrim above its
         top edge and never becomes a full-screen takeover. */
      max-height: min(88vh, 640px);
      border-right: none;
      border-top: 1px solid var(--border);
      border-top-left-radius: 16px;
      border-top-right-radius: 16px;
      box-shadow: 0 -10px 40px rgba(15, 15, 15, 0.18);
      /* Replace the side-slide entrance with the up-slide rise, on the shared
         token timing. The exit override rides .is-exiting below. */
      animation: doc-nav-sheet-in var(--sheet-rise) var(--sheet-ease-rise) both;
      padding-bottom: env(safe-area-inset-bottom);
    }
    /* Exit — the overlay sheet falls back down, on --sheet-fall (the app's exit
       duration). The useExitTransition buffer in app.jsx must stay >= --sheet-fall
       so the unmount never clips this slide-out. */
    .doc-nav:not(.is-docked).is-exiting {
      animation: doc-nav-sheet-out var(--sheet-fall) var(--sheet-ease-fall) both;
    }
  }
  @keyframes doc-nav-sheet-in {
    from { transform: translateY(100%); }
    to   { transform: translateY(0); }
  }
  @keyframes doc-nav-sheet-out {
    from { transform: translateY(0); }
    to   { transform: translateY(100%); }
  }
  /* Grab-handle — the swipe-down-to-close affordance at the sheet's top edge.
     Decorative (aria-hidden);
     touch-action:none + a generous transparent hit-band let the drag handler own
     the gesture without the browser claiming it for scroll. The top margin suits
     the app's brand-row context; both topstrip overrides clear it to 0 when the
     handle sits in the flex-centered mobile top strip. Hidden on the desktop
     dock, which has no swipe gesture. */
  /* A roomy transparent hit-band (the React drag handler binds to the element)
     with the VISIBLE bar painted as a crisp, thin pseudo-element pill — so it
     reads as a grab-handle, never the fat grey oval the old padding +
     background-clip trick produced. */
  .doc-nav-handle {
    flex: 0 0 auto;
    align-self: center;
    width: 56px; height: 18px;
    margin: 6px 0 0;
    padding: 0;
    background: transparent;
    cursor: grab;
    touch-action: none;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .doc-nav-handle::before {
    content: "";
    width: 32px; height: 4px;
    border-radius: 2px;
    background: var(--edge-strong);
  }
  /* Mobile sheet top strip — the slim (~44px) chrome the MOBILE up-sheet wears
     in place of the brand row: ONLY the centered grab-handle + the inline-end ✕.
     Hidden on the docked
     rail (it owns the brand row) and on a WIDER overlay (the >=900 / non-mobile
     overlay keeps the brand row, which carries the dock toggle). The mobile
     hide/show split is driven by the same 900 breakpoint the dock uses below. */
  .doc-nav-topstrip {
    flex: 0 0 auto;
    position: relative;
    display: flex; align-items: center; justify-content: center;
    min-height: 44px;
    padding: 0 8px;
  }
  .doc-nav-topstrip .doc-nav-handle {
    margin: 0;
  }
  /* Compound selector (not just `.doc-nav-topstrip-close`) so this 44px size
     wins over the base `.doc-nav-close` 28px rule regardless of source order —
     same specificity would otherwise let source order decide. 44px matches the
     ~44px touch-target minimum; the strip's own min-height (above) already
     reserves that much vertical room, so the button fills it edge-to-edge
     instead of floating a small hit-area inside a bigger one. */
  .doc-nav-close.doc-nav-topstrip-close {
    position: absolute; inset-inline-end: 8px; top: 50%;
    transform: translateY(-50%);
    margin-inline-start: 0;
    width: 44px; height: 44px;
  }
  /* Default (wider overlay / docked): the brand row is the top; the mobile strip
     is hidden. Below 900 the rail is never docked, so the overlay there is the
     true mobile sheet — drop its brand row and show the strip instead. */
  .doc-nav-topstrip { display: none; }
  @media (max-width: 899px) {
    .doc-nav:not(.is-docked) .doc-nav-topstrip { display: flex; }
    .doc-nav:not(.is-docked) .doc-nav-brand { display: none; }
  }
  /* Sticky header + foot + internal scroll for the capped sheet form. The sheet
     is a flex column; the brand + Paste tile sit above the list and the
     count/foot sit below it, so they pin at the sheet's top/bottom while the
     docs list is the lone scroll region. .doc-nav-list already declares
     `flex: 1 1 auto; overflow-y: auto` (inline, app-only) — but in a
     max-height-capped column a flex item won't shrink past its content without
     `min-height: 0`, which would push the foot out of the sheet once the list
     grows (up to ~20 docs). Add it ONLY for the overlay sheet; the docked rail
     spans the full viewport height (bottom:0, no cap) and is left byte-identical. */
  .doc-nav:not(.is-docked) .doc-nav-list { min-height: 0; }
  @media (prefers-reduced-motion: reduce) {
    .doc-nav, .doc-nav-backdrop,
    .doc-nav.is-exiting, .doc-nav-backdrop.is-exiting,
    /* The up-sheet overrides carry higher specificity than the bare .doc-nav
       above, so neutralize them explicitly — the sheet appears in place, no
       transform. */
    .doc-nav:not(.is-docked), .doc-nav:not(.is-docked).is-exiting { animation: none; }
  }
  /* Zone A — rail identity: the wordmark home link, with the dock/close
     controls riding the right of the same row. */
  .doc-nav-brand {
    display: flex;
    align-items: center;
    gap: 8px;
    /* Grid: content-left 16px, controls' rectangle insets 8px from the right —
       the same two rails every zone below shares. */
    padding: 12px 8px 8px 16px;
  }
  .doc-nav-logo {
    margin-inline-end: auto;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    /* margin pulls the hover rectangle's left edge to the shared 8px inset while
       the inner padding keeps the dot itself on the 16px content line. */
    margin-left: -8px;
    padding: 4px 8px;
    border: none;
    background: transparent;
    border-radius: 7px;
    cursor: pointer;
    font-family: var(--ui-font);
    font-weight: 600;
    font-size: 14px;
    letter-spacing: -0.005em;
    color: var(--ink);
    transition: background 120ms var(--ease-quiet);
  }
  .doc-nav-logo:hover { background: var(--bg-sub); }
  .doc-nav-logo:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
  .doc-nav-logo .dot {
    width: 16px; height: 16px;
    border-radius: 4px;
    background: linear-gradient(135deg, #37352f 0%, #5c5b57 100%);
    display: inline-flex; align-items: center; justify-content: center;
    color: #fff; font-size: 10px; font-weight: 700;
  }
  .doc-nav-logo .brand-omg, .doc-nav-logo .brand-md { font-weight: 700; }
  /* The logo owns the auto-margin now, so the trailing control sits flush. */
  .doc-nav-brand .doc-nav-close { margin-inline-start: 0; }

  .doc-nav-close {
    margin-inline-start: auto;
    width: 28px; height: 28px;
    display: inline-flex; align-items: center; justify-content: center;
    border: none; background: transparent;
    color: var(--ink-2);
    border-radius: 7px;
    cursor: pointer;
    transition: background 120ms var(--ease-quiet), color 120ms var(--ease-quiet);
  }
  .doc-nav-close:hover { background: var(--chip-bg); color: var(--ink); }
  .doc-nav-close:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }

  /* The Paste tile — the rail's one filled affordance. */
  .doc-nav-new {
    display: flex; align-items: center; gap: 10px;
    /* Same 8px edge inset as the doc rows below it. The rows have no border, so
       to land the clipboard tile on the identical 16px content line the 1px
       dashed border is absorbed out of the inner padding (7px + 1px border =
       the rows' 8px). */
    margin: 10px 8px 6px;
    padding: 7px;
    border: 1px dashed var(--border-strong);
    background: var(--bg-sub);
    color: var(--ink);
    border-radius: 9px;
    font-family: var(--ui-font);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    text-decoration: none;
    transition: background 120ms var(--ease-quiet), border-color 120ms var(--ease-quiet);
  }
  .doc-nav-new:hover { background: var(--chip-bg); border-color: var(--ink-2); }
  .doc-nav-new:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
  /* The marker-yellow tile is the one located spot of brand color in an
     otherwise monochrome rail: the eye lands on it without the button having to
     shout, so the dashed "drop a fresh paste here" affordance can stay. Brown
     on butter keeps the glyph legible at this size in both themes. */
  .doc-nav-new-ico {
    flex: 0 0 auto;
    display: inline-flex; align-items: center; justify-content: center;
    /* Match the 30px/9px type-squircle on the document rows below, so the
       clipboard tile reads as the head of the same icon column. */
    width: 30px; height: 30px;
    border-radius: 9px;
    /* Softer amber than the saturated --hl-fill — the tile was reading "too
       yellow"; the dashed amber edge below carries the brand signal instead. */
    background: color-mix(in oklab, var(--hl-fill) 38%, var(--surface-1));
    color: #7a4f02;
    box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.22);
  }
  /* Keycaps trail the label to the row's end, turning the shortcut into part of
     the affordance — the same chiclet treatment as the bubble chips, theme-aware
     so it inverts cleanly in dark mode without a per-key override. */
  .doc-nav-new-kbd {
    margin-inline-start: auto;
    display: inline-flex;
    align-items: center;
    gap: 2px;
  }
  .doc-nav-new-kbd kbd {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    background: var(--bg);
    color: var(--ink-2);
    border: 1px solid var(--border);
    border-bottom-width: 1.5px;
    border-radius: 3px;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 10.5px;
    font-weight: 600;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    transition: border-color 120ms var(--ease-quiet), color 120ms var(--ease-quiet);
  }
  .doc-nav-new:hover .doc-nav-new-kbd kbd {
    border-color: var(--border-strong);
    color: var(--ink);
  }
  /* Touch (phones, tablets): no physical keyboard, so the shortcut hint is
     meaningless — drop it and let the label own the row. Mirrors how the bubble
     chips and add-btn hide their keycaps under (hover: none). */
  @media (hover: none) {
    .doc-nav-new-kbd { display: none; }
  }

  /* T4 — the primary action, promoted directly under the brand. Rather than a
     solid-ink CTA (too coarse for the founder's eye), the primary reads as a
     warm DROP ZONE: the legacy dashed clipboard tile, dialled UP — taller, a
     faint --hl-fill-soft wash, a brand-amber dashed edge, and a soft resting
     ring + lift. It out-ranks the 20 flat monochrome rows below through size,
     warmth, and elevation, not through a heavy fill. The base .doc-nav-new
     already carries the dashed border + yellow clipboard squircle; .is-primary
     only amplifies it, so the icon/keycap base rules are inherited (the old
     overrides that turned it black are removed). Gap above >= gap below so the
     tile reads as its own band, not part of the brand row. */
  .doc-nav-new.is-primary {
    margin: 16px 8px 10px;
    padding: 12px 13px;
    border: 1px dashed color-mix(in oklab, var(--hl-edge) 42%, var(--edge-strong));
    border-radius: 11px;
    /* Just a hint of warmth, not a yellow fill (founder: still too yellow). The
       dashed amber edge + the squircle keep it unmistakably the Paste drop-zone;
       the fill barely tints. */
    background: color-mix(in oklab, var(--hl-fill-soft) 32%, var(--surface-1));
    color: var(--ink);
    font-size: 13.5px;
    font-weight: 700;
    box-shadow:
      0 0 0 1px color-mix(in oklab, var(--hl-edge) 22%, transparent),
      var(--shadow-small);
    transition: background 120ms var(--ease-quiet), border-color 120ms var(--ease-quiet), box-shadow 120ms var(--ease-quiet), transform 80ms var(--ease-quiet);
  }
  .doc-nav-new.is-primary:hover {
    background: color-mix(in oklab, var(--hl-fill-soft) 52%, var(--surface-1));
    border-color: color-mix(in oklab, var(--hl-edge) 70%, var(--edge-strong));
    box-shadow:
      0 0 0 1px color-mix(in oklab, var(--hl-edge) 40%, transparent),
      var(--shadow-small);
  }
  .doc-nav-new.is-primary:active { transform: scale(0.99); }
  @media (prefers-reduced-motion: reduce) {
    .doc-nav-new.is-primary { transition: background 120ms var(--ease-quiet), border-color 120ms var(--ease-quiet); }
    .doc-nav-new.is-primary:active { transform: none; }
  }

  /* Dark: warm near-black tile, brand-amber dashed edge, a restrained --hl-fill
     wash (dark --hl-fill-soft is a translucent amber that's too hot as a full
     fill). The clipboard squircle + keycap inherit the dark base automatically. */
  [data-theme="dark"] .doc-nav-new.is-primary {
    background: color-mix(in oklab, var(--hl-fill) 14%, var(--bg-sub));
    border-color: color-mix(in oklab, var(--hl-edge) 60%, var(--edge-strong));
  }
  [data-theme="dark"] .doc-nav-new.is-primary:hover {
    background: color-mix(in oklab, var(--hl-fill) 26%, var(--bg-sub));
    border-color: var(--hl-edge);
  }
  /* Dark: the 38%-amber squircle mix lands near-black, so the brown glyph
     (#7a4f02) sinks into its own chip. Flip the pairing — a slightly dimmer
     amber wash with the marker-yellow glyph on top — same brand signal,
     legible on the dark tile. */
  [data-theme="dark"] .doc-nav-new-ico {
    background: color-mix(in oklab, var(--hl-fill) 22%, var(--surface-2));
    color: var(--hl-fill);
    box-shadow: inset 0 0 0 1px rgba(252, 211, 77, 0.30);
  }


  /* ---- Doc rows (PROMOTED from index.html inline <style>) -------------------
     The grouped document-row display — list, group headers, the empty state, one
     row (icon + title + meta/time/count). PROMOTED here when the retired static
     content-page shelf (static-docs.js, gone in #668) shared them; today only
     the app rail (DocNavigator, rows are <button>s) renders these rows.
     Only the READ-ONLY display rules live here; the app-only interactive row
     STATES (is-active focus-dim, is-menu-open, inline rename, deferred-delete,
     the shared-by badge) stay inline in index.html (the static shelf never uses
     them). See tests/unit/nav-shell-css-home.test.js for the boundary guard. */
  .doc-nav-list {
    list-style: none;
    margin: 0;
    padding: 4px 8px 8px;
    overflow-y: auto;
    flex: 1 1 auto;
  }
  /* A bucket: its section header plus the nested role="group" list of rows.
     The nested list carries no list styling of its own — the rows keep their
     own row vocabulary; this wrapper just scopes the group for a screen reader
     and lets the header + rows sit as one block. */
  .doc-nav-group-li { list-style: none; }
  .doc-nav-sublist { list-style: none; margin: 0; padding: 0; }
  /* Section header — the same small, uppercase, muted label treatment the rail
     already uses (cf. the n/max counter and the PRIVATE pill), so groups read as
     quiet structure, not a new visual language. The trailing count is a faint
     mono digit; the screen-reader count rides the group's aria label instead. */
  .doc-nav-group {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 0;
    /* Logical props so RTL mirrors: block = top/bottom, inline = start/end. */
    padding-block: 10px 3px;
    padding-inline: 10px;
    font-family: var(--ui-font);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--muted);
  }
  /* The first group hugs the top of the list — no extra lead above "Pinned". */
  .doc-nav-group-li:first-child .doc-nav-group { padding-block-start: 4px; }
  .doc-nav-group-label { flex: 0 1 auto; }
  .doc-nav-empty {
    padding: 18px 12px;
    font-family: var(--ui-font);
    font-size: 12.5px;
    color: var(--muted);
    line-height: 1.5;
  }
  .doc-nav-empty-lead {
    margin: 0;
  }
  /* A warm, quiet "learn more" tail under the onboarding note — the
     understand-it affordance beside the do-it ones (Examples below). Reuses the
     rail's row vocabulary: pill on hover, marker-yellow warmth, no new palette.
     Gentle fade so it settles in after the note rather than competing with it. */
  .doc-nav-empty-learn {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    margin-top: 10px;
    padding: 6px 9px;
    margin-inline-start: -9px;
    border-radius: 8px;
    text-decoration: none;
    font-size: 12.5px;
    font-weight: 500;
    color: var(--ink-2);
    transition: background 120ms var(--ease-quiet), color 120ms var(--ease-quiet);
    animation: doc-nav-empty-learn-in 320ms var(--ease-quiet) both;
  }
  .doc-nav-empty-learn-ico {
    display: inline-flex;
    color: var(--muted);
    transition: color 120ms var(--ease-quiet);
  }
  .doc-nav-empty-learn:hover {
    background: var(--hl-fill);
    color: var(--text-on-hl); /* on-yellow text never follows the flipping --ink */
  }
  .doc-nav-empty-learn:hover .doc-nav-empty-learn-ico { color: var(--text-on-hl); }
  .doc-nav-empty-learn:focus-visible {
    outline: 2px solid var(--ring, var(--border-strong));
    outline-offset: 2px;
  }
  @keyframes doc-nav-empty-learn-in {
    from { opacity: 0; transform: translateY(3px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .doc-nav-empty-learn { animation: none; }
  }
  .doc-nav-row {
    position: relative;
    display: flex;
    align-items: center;
    border-radius: 8px;
    transition: opacity 180ms var(--ease-quiet), background 120ms var(--ease-quiet);
  }
  /* Focus mode: at rest the inactive docs recede so the rail reads as a calm
     stack with one lit row (the active doc). Pointing at the rail brings the
     whole list back to full strength. The active row never dims — it's your
     place. Touch devices have no hover, so they stay full-strength always
     (handled by the (hover: hover) gate below). */
  @media (hover: hover) {
    .doc-nav:not(:hover) .doc-nav-row:not(.is-active) { opacity: 0.55; }
  }
  .doc-nav-row:hover { background: var(--bg-sub); }
  /* The active doc is signalled by the focus-mode dimming above — the rail dims
     every row except the active one on rest, so the lit row reads as "you are
     here" (the old top-left badge dot + its doc-active view-transition were
     retired; the switcher popover carries its own explicit active cue). */
  .doc-nav-row-btn {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 10px;
    /* 8px inner padding + the list's 8px side padding = icon on the shared 16px
       content line, matching Paste new directly above. */
    padding: 8px;
    border: none;
    background: transparent;
    text-align: start;
    cursor: pointer;
    /* Shared by the app's <button> rows and the static shelf's <a> rows; the
       anchor form must not show link underline/colour — the title/meta own those. */
    text-decoration: none;
    color: inherit;
  }
  /* Keyboard focus on a row uses the app's brand ring, inset so it traces the
     row's rounded rect instead of the browser's default square blue outline.
     :focus-visible only — a plain click (pointer) leaves no ring. */
  .doc-nav-row-btn:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: -2px;
    border-radius: 8px;
  }
  /* Type glyph — a small, muted inline mark that says "doc" vs
     "questionnaire" at a glance. The leading 30px type TILE was retired in the
     menu-simplification pass: a row is now a single rhythm of "small glyph +
     one line of text", so the glyph is a ~22px muted column that reads as text,
     not a grid of boxes. The inner SVG is sized to ~15px so the mark stays
     quiet beside the title. Width is kept so titles align down the column. */
  .doc-nav-icon {
    flex: 0 0 auto;
    width: 22px;
    height: 22px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--muted);
    transition: color 120ms var(--ease-quiet);
  }
  .doc-nav-icon svg { width: 15px; height: 15px; }
  .doc-nav-row:hover .doc-nav-icon { color: var(--ink-2); }
  .doc-nav-row.is-active .doc-nav-icon {
    color: var(--ink);
  }
  .doc-nav-row-main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
  }
  /* Title + optional "in text" badge on one line. The title still truncates
     (min-width:0 lets it shrink); the badge holds its size at the end. */
  .doc-nav-row-titlerow {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
  }
  .doc-nav-row-title {
    font-family: var(--ui-font);
    font-size: 13px;
    font-weight: 500;
    color: var(--ink);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* Mobile up-sheet (touch): enlarge the row for thumbs and calm. Bigger tap
     targets (~54px), a larger title, a hair-bigger type tile, and more air
     between rows — the sheet reads as a confident app surface, not a shrunk
     desktop sidebar. Scoped to the non-docked overlay below the dock breakpoint
     so the docked desktop rail (mouse) stays at its tighter density. */
  @media (max-width: 899px) {
    .doc-nav:not(.is-docked) .doc-nav-row-btn,
    .doc-nav:not(.is-docked) .doc-nav-row-edit,
    .doc-nav:not(.is-docked) .doc-nav-row-undo {
      padding: 11px 10px;
      gap: 12px;
    }
    .doc-nav:not(.is-docked) .doc-nav-icon { width: 32px; height: 32px; }
    .doc-nav:not(.is-docked) .doc-nav-row-title { font-size: 15px; }
    /* The search field was cramped next to the enlarged touch rows — give it a
       comfortable height + type on the mobile sheet.
       The input MUST be ≥16px: tapping the magnifier focuses it immediately, and
       iOS Safari auto-zooms the whole page when a focused input is smaller —
       which read as the sheet "zooming weirdly" on every search tap. */
    .doc-nav:not(.is-docked) .doc-nav-search { margin: 6px 8px 4px; }
    .doc-nav:not(.is-docked) .doc-nav-search-input { padding: 12px 0; font-size: 16px; }
  }
  /* Body-only search hit marker: a quiet inline label, NOT a chip. It tells the
     user why a title with no visible query match still surfaced. It stays
     subordinate to the 13px title and deliberately drops the uppercase/tracking
     register the "{n} MATCHES" summary owns, so the two don't rhyme. No box,
     border, or fill: a bordered chip read as a control (it echoed the Ctrl/V
     key caps) and its hairline border failed 3:1 in dark. Colour is
     --text-secondary, not the warm accent — warm gold on white is only ~2.4:1
     (fails AA for this small text); --text-secondary clears AA in both themes
     (6.8:1 light / 5.5:1 dark). The visible "in text" word is aria-hidden; SRs
     hear the fuller sr-only phrase instead. */
  .doc-nav-row-intext {
    flex: 0 0 auto;
    font-family: var(--ui-font);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.01em;
    text-transform: lowercase;
    color: var(--text-secondary);
    line-height: 1.4;
    white-space: nowrap;
  }
  /* Per-row meta (timestamp + comment/answer count + "shared by" badge) is
     HIDDEN AT REST and revealed only on :hover and on the active row
     (founder's call — the Pinned/Today/Earlier group tags already give the
     time context, so a row reads as a calm single line of "glyph + title").
     CRITICAL (eng-a11y): the meta MUST stay in the row's ACCESSIBLE NAME even
     while hidden — so it is clipped with the project's .visually-hidden
     technique (in the DOM, still computed into the <button>/<a> name), NOT
     display:none and NOT aria-hidden. Hover/active lift it back into a normal
     inline line. The "in text" search marker lives in the titlerow (always
     visible while searching), so it is untouched by this. */
  .doc-nav-row-meta {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0,0,0,0);
    white-space: nowrap; border: 0;
  }
  /* Revealed ONLY on the active row — a stable, non-hover state, so moving the
     pointer down the list never changes a row's height (the hover-reveal caused
     the whole list to JUMP; founder's call: kill the reflow, the group tags carry
     the time context anyway). Non-active rows keep the meta clipped (still in the
     accessible name for SR). */
  .doc-nav-row.is-active .doc-nav-row-meta {
    position: static;
    width: auto; height: auto;
    margin: 0; overflow: visible; clip: auto;
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
  }
  .doc-nav-row-time {
    font-family: var(--mono);
    font-size: 10px;
    color: var(--muted);
    white-space: nowrap;
  }
  .doc-nav-dot {
    color: var(--border-strong);
    font-size: 10px;
    line-height: 1;
  }
  .doc-nav-row-count {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-family: var(--mono);
    font-size: 10px;
    color: var(--muted);
    white-space: nowrap;
  }
  .doc-nav-row-count svg { display: block; opacity: 0.85; }

  /* Responses-waiting badge — a creator's OWN browser share got feedback back.
     Lives in the always-visible titlerow (see DocNavRow) so it reads at a glance
     on ANY shelf row, not only the active one — that's the point of the return
     surface. A compact count pill: warm --accent-deep marks it as "new", distinct
     from the grey time/count, over a faint accent tint. --accent-deep clears AA as
     text in both themes (#b35900 light / #f6b56b dark); the bare count is
     language-neutral and the full "N responses came back." sentence is the
     accessible name. A status marker, not a CTA. */
  .doc-nav-row-responses {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 16px;
    height: 15px;
    padding: 0 5px;
    border-radius: 999px;
    font-family: var(--ui-font);
    font-size: 10px;
    font-weight: 700;
    line-height: 1;
    color: var(--accent-deep);
    background: color-mix(in oklab, var(--accent-deep) 14%, transparent);
    white-space: nowrap;
  }

  /* The foot search bar — the wrapper the search field docks into at the bottom
     of the Documents panel (disclosed via the footer magnifier); the retired
     static content rail shared it until #668. It draws
     the hairline that fences the field off from the list above and owns the
     balanced breathing room top/bottom, so the field never reads as choked
     against the list or the footer cluster below. The field keeps its own inset
     margins; this only adds the rule + vertical air. */
  .doc-nav-searchbar {
    flex: 0 0 auto;
    border-top: 1px solid var(--border);
    padding: 6px 0 8px;
  }

  /* Full-text search field — PROMOTED from index.html's inline <style> so the
     retired static content-page shelf rendered an identical field. Sits directly
     under the Paste CTA, above the groups. A quiet inset row matching the rail's
     chip vocabulary; the leading magnifier and trailing clear button flank a
     borderless input. */
  .doc-nav-search {
    position: relative;
    display: flex;
    align-items: center;
    gap: 7px;
    margin: 4px 12px 2px;
    padding: 0 8px;
    border: 1px solid var(--border);
    border-radius: 9px;
    background: var(--bg-sub);
    transition: border-color 120ms var(--ease-quiet), box-shadow 120ms var(--ease-quiet);
  }
  .doc-nav-search:focus-within {
    border-color: var(--ring, var(--border-strong));
    box-shadow: 0 0 0 2px color-mix(in oklab, var(--ring, var(--border-strong)) 22%, transparent);
  }
  .doc-nav-search-ico {
    display: inline-flex;
    flex: 0 0 auto;
    color: var(--muted);
  }
  .doc-nav-search-input {
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    /* Kill iOS Safari's native search-field chrome (it paints its OWN magnifier
       inside type=search, which doubled up with our .doc-nav-search-ico). */
    -webkit-appearance: none;
    appearance: none;
    background: transparent;
    padding: 8px 0;
    font-family: var(--ui-font);
    font-size: 13px;
    color: var(--ink);
  }
  .doc-nav-search-input::placeholder { color: var(--muted); }
  .doc-nav-search-input:focus { outline: none; }
  /* Strip the native search "×" — we draw our own (consistent + a real ≥44px
     touch target on mobile). */
  .doc-nav-search-input::-webkit-search-decoration,
  .doc-nav-search-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
  .doc-nav-search-clear {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    margin-inline-end: -4px;
    padding: 0;
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
    transition: background 120ms var(--ease-quiet), color 120ms var(--ease-quiet);
  }
  .doc-nav-search-clear:hover { background: var(--chip-bg); color: var(--ink); }
  .doc-nav-search-clear:focus-visible { outline: 2px solid var(--ring, var(--border-strong)); outline-offset: 2px; }
  /* Static rail: the clear button is always in the DOM, so hide it while the
     field is empty (the app instead conditionally mounts it — same visual). */
  .doc-nav-search.is-empty .doc-nav-search-clear { display: none; }
  /* On touch, grow the hit area to ≥44px without enlarging the drawn glyph. */
  @media (pointer: coarse) {
    .doc-nav-search-clear { width: 44px; height: 44px; margin-inline-end: -10px; }
  }
  @media (prefers-reduced-motion: reduce) {
    .doc-nav-search-clear { transition: none; }
  }
  /* The flat results summary doubles as the aria-live region. Quiet metadata,
     matching the group-header treatment but full-width (no count digit). It is
     a PERSISTENT node (sibling of the list) whose text changes across query
     states; `is-quiet` collapses the visible box on empty/no-match without
     removing the node, so the live region survives and still announces. */
  .doc-nav-results-summary {
    padding: 8px 10px 3px;
    font-family: var(--ui-font);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--muted);
  }
  /* Visually collapse (no height/padding) but keep the node mounted + in the
     a11y tree so the SR still hears its text. NOT display:none — that would
     drop it from the accessibility tree and silence the announcement. */
  .doc-nav-results-summary.is-quiet {
    padding: 0;
    height: 0;
    overflow: hidden;
  }
  /* The echoed query in the no-match line stays inline and can't blow the row —
     it's already clamped to TITLE_MAX chars in JS; this keeps it from wrapping
     oddly and bidi-isolates a pasted RTL fragment. */
  .doc-nav-search-echo {
    unicode-bidi: isolate;
    word-break: break-word;
  }

  /* The editorial side is now the About MODE of the .mode-switch (its shared,
     cross-rail rules live in the .mode-switch block above, so the app and content
     menus look + behave identically). The switch flips Documents↔About in place;
     the About panel's "All posts →" row is the crawlable bridge to /blog. */

  /* Footer frame — the settings/trust strip sits inside this. The .is-cold
     promotion and the doc-switch view-transition on the foot are APP-ONLY and
     stay inline in index.html. */
  .doc-nav-foot {
    border-top: 1px solid var(--edge-subtle);
    padding: 8px;
  }

  /* ── Compact footer SHAPE — the count/trust strip, PROMOTED ─────────────────
     The app's DocCountFooter (.doc-nav-countbar) renders the rail's foot strip:
     the PRIVATE pill on the START edge + the settings cluster (.doc-nav-toolbar)
     on the END. App-only internals
     (the count text/num, the info-dot explainer popover .doc-nav-countbar-meta /
     -num / -info / -pop, and the `+ .doc-nav-foot` sibling reset) stay inline in
     index.html — the static footer is countless + has no info dot, so it never
     emits them. Guard: tests/unit/nav-shell-css-home.test.js. */
  .doc-nav-countbar {
    position: relative;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 9px 8px 16px 16px;
    border-top: 1px solid var(--edge-subtle);
  }
  .doc-nav-countbar.is-bar {
    padding: 8px 8px 12px 16px;
    gap: 8px;
  }
  /* The privacy promise anchors the START edge of the foot strip; the count
     (when shown) sits just after it. */
  .doc-nav-countbar-trust-start {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
  }
  .doc-nav-toolbar {
    display: flex;
    align-items: center;
    gap: 1px;
  }
  /* The settings cluster owns the END edge whenever the start has an anchor
     (the trust promise OR the count). It falls back to the START only when both
     are absent (mobile sheet + countless), so it never floats over a void. */
  .doc-nav-countbar.has-trust .doc-nav-toolbar,
  .doc-nav-countbar:not(.is-countless) .doc-nav-toolbar { margin-inline-start: auto; }
  .doc-nav-countbar.is-countless:not(.has-trust) .doc-nav-toolbar { margin-inline-start: 0; }

  /* ── PRIVATE pill + its promise popover, PROMOTED ──────────────────────────
     The app's <PrivacyPill> trust badge. PROMOTED so the static topbar renders
     the IDENTICAL pill (build-content.js emits the twin; the popover copy is
     pinned equal by tests/unit/privacy-pill-parity.test.jsx). BOTH surfaces
     open the popover on click via an .is-open class — React in the app,
     static-menu.js on the content pages. */
  .privacy-pill {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: var(--ui-font);
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: #047857;
    padding: 4px 6px;
    background: transparent;
    border: none;
    border-radius: 999px;
    user-select: none;
    cursor: default;
  }
  .privacy-pill.compact { cursor: pointer; }
  .privacy-pill:focus-visible { outline: 2px solid #059669; outline-offset: 2px; }
  .privacy-pill.compact { padding: 5px; gap: 0; }
  .privacy-pill.compact .privacy-label { display: none; }
  .privacy-popover {
    position: absolute;
    top: calc(100% + 7px);
    right: 0;
    width: 268px;
    max-width: min(268px, calc(100vw - 24px));
    padding: 15px 16px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 9px;
    box-shadow: var(--shadow-medium);
    letter-spacing: normal;
    text-transform: none;
    opacity: 0;
    transform: translateY(-4px);
    transform-origin: top right;
    pointer-events: none;
    transition: opacity 150ms var(--ease-quiet), transform 150ms var(--ease-quiet);
    z-index: 60;
  }
  /* Invisible bridge across the trigger→popover gap so the mouse can cross in. */
  .privacy-popover::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: -7px;
    height: 7px;
  }
  /* Headline-led popover (founder review + polish pass): the green head is an
     EYEBROW — uppercase + tracked, echoing the pill's own label — so it reads as
     an intentional accent, not a bold green sentence. It is the popover's single
     green moment; the lede is ink, the note muted. */
  .privacy-popover-headline {
    margin: 0 0 6px;
    font-family: var(--ui-font);
    font-size: 11px;
    font-weight: 650;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    line-height: 1.2;
    color: #047857;
  }
  [data-theme="dark"] .privacy-popover-headline { color: #6ee7b7; }
  .privacy-popover-lede {
    margin: 0;
    font-family: var(--reading-font);
    font-size: 15px;
    font-weight: 500;
    line-height: 1.4;
    letter-spacing: -0.01em;
    color: var(--ink);
  }
  .privacy-popover-note {
    margin: 10px 0 0;
    font-family: var(--ui-font);
    font-size: 11.5px;
    font-weight: 400;
    line-height: 1.5;
    color: var(--muted);
  }
  .privacy-popover-note em { font-style: italic; color: var(--ink-2); }
  .privacy-pill.is-open .privacy-popover {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
  }
  /* Portaled to <body> on mobile (escapes the backdrop-filtered topbar's
     containing block); positioned by inline fixed coords, shown by this class
     since it's no longer a child of .privacy-pill.is-open. */
  .privacy-popover.is-floating {
    opacity: 1;
    transform: none;
    pointer-events: auto;
    z-index: 200;
  }
  @media (prefers-reduced-motion: reduce) {
    .privacy-popover { transition: opacity 150ms var(--ease-quiet); transform: none; }
    .privacy-pill.is-open .privacy-popover { transform: none; }
  }
  .privacy-pill .privacy-shield { color: #059669; flex: none; }
  .privacy-pill .shield-body { opacity: 0.85; }
  /* ENCRYPTED state — a padlock that rhymes with the shield (same 24 viewBox,
     2px round strokes, recessed body at 0.85). Same green: green is the one
     constant that means "safe / yours"; the SHAPE change carries "now it's on a
     server, sealed." The lock is STATIC on purpose — local is a *living*
     verification (the looping check), encrypted-at-rest is a *settled* fact, so
     no loop, and nothing to gate behind prefers-reduced-motion. */
  .privacy-pill .privacy-lock { color: #059669; }
  .privacy-pill .privacy-lock .lock-body { opacity: 0.85; }
  [data-theme="dark"] .privacy-pill.is-encrypted { color: #6ee7b7; }
  [data-theme="dark"] .privacy-pill .privacy-lock { color: #6ee7b7; }
  .privacy-pill .shield-check {
    stroke-dasharray: 10;
    stroke-dashoffset: 0;
    transform-origin: center;
    animation: shield-verify 6.5s ease-in-out infinite;
  }
  @keyframes shield-verify {
    0%, 62%, 100% { stroke-dashoffset: 0; }
    72%          { stroke-dashoffset: 10; }
    88%          { stroke-dashoffset: 0; }
  }
  @media (prefers-reduced-motion: reduce) {
    .privacy-pill .shield-check { animation: none; }
  }
  /* At the foot of the docked rail the pill sits at the bottom, so its popover
     opens UPWARD and tucks back inside the rail's START edge — the same override
     the app applies at .doc-nav-countbar-trust-start. PROMOTED so the static
     rail's foot pill opens upward too. */
  .doc-nav-countbar-trust-start .privacy-pill .privacy-popover {
    top: auto;
    bottom: calc(100% + 9px);
    left: 0;
    right: auto;
    transform-origin: bottom left;
    transform: translateY(4px);
  }
  .doc-nav-countbar-trust-start .privacy-pill .privacy-popover::before {
    top: auto;
    bottom: -9px;
    left: 0;
    right: 0;
    width: auto;
    height: 9px;
  }
  .doc-nav-countbar-trust-start .privacy-pill.is-open .privacy-popover { transform: translateY(0); }
  @media (prefers-reduced-motion: reduce) {
    .doc-nav-countbar-trust-start .privacy-pill .privacy-popover,
    .doc-nav-countbar-trust-start .privacy-pill.is-open .privacy-popover { transform: none; }
  }
  /* Static pages: static-menu.js toggles the SAME .is-open class the app uses
     (click-open parity; the old hover-only opener is retired). */
  /* Dark theme — the green reads too dark on a near-black rail; lift it, and
     re-tone the popover surface. (App-only index.html carries a twin for the SPA;
     this copy is what the static content pages — which don't load that inline
     <style> — consume.) */
  [data-theme="dark"] .privacy-pill { color: #6ee7b7; }
  [data-theme="dark"] .privacy-pill .privacy-shield { color: #6ee7b7; }
  [data-theme="dark"] .privacy-pill:focus-visible { outline-color: #6ee7b7; }
  [data-theme="dark"] .privacy-popover {
    background: var(--surface-3);
    border-color: var(--edge);
    box-shadow: var(--shadow-medium);
  }
