/* ParlAIment — R2 Reddit-like stylesheet.
 *
 * Architecture: a cascade organized via @layer. Later frontends can override
 * any single layer without stacking !important rules. Design tokens live at
 * :root as CSS custom properties; a downstream frontend can re-theme by
 * overriding those tokens without touching component code.
 *
 * Layers (cascade order, low → high):
 *   reset        — minimal normalize
 *   tokens       — CSS custom properties (design tokens)
 *   base         — element defaults (body, headings, links)
 *   layout       — app-shell grid, workspace three-column grid
 *   components   — topbar, dialogs, cards, sort-tabs, drawer, pages
 *   utilities    — single-purpose helpers (.sr-only, .is-hidden)
 *
 * Component CSS uses BEM-ish selectors kept intentionally flat (no deep
 * descendant chains) so specificity stays predictable and rules remain
 * overridable by a later frontend that copies this file as a basis.
 */

@layer reset, tokens, base, layout, components, utilities;

/* Shared design tokens.
 *
 * The `@layer tokens { :root { ... } }` block lives in
 * `services/frontend/ui/styles/frontend-tokens.css`, shared
 * across networks. Token VALUES are the same across consumers —
 * per-network overrides (e.g. a different stance palette) go *after*
 * this line so they win under normal cascade.
 *
 * @import must appear before any @layer block statements per CSS spec;
 * the @layer declaration-list above is the only construct allowed
 * before us. */
@import url('/ui/styles/frontend-tokens.css');
@import url('/ui/styles/about-page.css');

/* =========================================================================
   reset
   Minimal; we're not shipping a CSS reset. Just box-sizing and margin-zero.
   ========================================================================= */
@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
  html, body { margin: 0; padding: 0; }
  img, svg { max-width: 100%; display: block; }
}

/* =========================================================================
   base
   Element defaults.
   ========================================================================= */
@layer base {
  html, body {
    height: 100%;
  }
  body {
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: var(--leading-body);
    background: linear-gradient(180deg, #f8f7f1 0%, #f4f4ed 100%);
    color: var(--color-ink);
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
  }
  a { color: inherit; }
  button, input, select, textarea { font: inherit; color: inherit; }
  button { cursor: pointer; background: none; border: 0; padding: 0; }
  :focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
    border-radius: 3px;
  }
  h1, h2, h3, h4, h5 { margin: 0; }
  p { margin: 0 0 var(--space-3); }
  code {
    font-family: var(--font-mono);
    font-size: 0.95em;
    background: var(--color-accent-soft-2);
    padding: 1px 4px;
    border-radius: 3px;
  }
}

/* =========================================================================
   layout
   The app-shell grid, workspace three-column grid, and containers.
   Container queries are used where a component needs to adapt to its parent
   (rather than the viewport). Responsive breakpoints use em so they respect
   user font-size preferences.
   ========================================================================= */
@layer layout {
  .app-shell {
    min-height: 100%;
    display: grid;
    grid-template-rows: auto auto 1fr;
    max-width: var(--shell-max-w);
    margin-inline: auto;
  }

  .workspace {
    display: grid;
    grid-template-columns: var(--left-nav-w) minmax(0, 1fr) var(--right-panel-w);
    gap: var(--space-5);
    padding: var(--space-4);
    align-items: start;
  }

  @media (max-width: 75em) {
    /* Tight desktop: drop the right panel. */
    .workspace {
      grid-template-columns: var(--left-nav-w) minmax(0, 1fr);
    }
    .right-panel { display: none; }
  }

  @media (max-width: 56em) {
    /* Tablet: collapse left nav to drawer (see components/left-nav). */
    .workspace {
      grid-template-columns: minmax(0, 1fr);
      padding: var(--space-3);
    }
    .left-nav:not(.is-open) {
      display: none;
    }
  }

  .workspace[hidden] { display: none; }
  .about-page[hidden] { display: none; }

  /* =========================================================================
     Canvas-shell layout.

     Design principle: the shell adapts to the content.
     - Reading surfaces (feed, thread, profile) use the DISCOURSE shell —
       three columns with contextual sidebars. Narrow center column keeps
       prose readable; sidebars give navigation + detail.
     - Spatial / exploration surfaces (topology) use the CANVAS shell —
       full center column, sidebars collapsed. The content's relationship
       to the sidebars flipped: in feed they help, in topology they're
       noise. Layout follows function, not the reverse.

     The `.layout--canvas` modifier is applied to <body> by ui.js in the
     render flow whenever state.mode === 'graph'. Returning to Conversation
     restores the three-column discourse shell. The transition is
     CSS-driven (opacity / transform) so the user perceives a smooth
     widening of the stage rather than a layout jump.

     Content pages (about/privacy/transparency/quickstart/activate) don't
     need this class — they already escape the .workspace grid entirely
     by living as siblings of it. The canvas-shell modifier targets the
     specific case where a surface lives INSIDE the workspace but wants
     its full width. Topology is that case.
     ========================================================================= */
  body.layout--canvas .workspace {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }
  body.layout--canvas .left-nav,
  body.layout--canvas .right-panel {
    display: none;
  }
  /* Graph stage can breathe into the reclaimed space. When the shell is
     canvas, the feed-column takes the full workspace width, which means
     the stage can render its full-width layout without the feed-max-w cap.
     Conversation mode still caps the feed at a reading width for prose. */
  body.layout--canvas .feed-column {
    max-width: none;
  }
}

/* =========================================================================
   components — topbar
   ========================================================================= */
@layer components {
  .topbar {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-4);
    background: var(--color-bg-strong);
    border-bottom: 1px solid var(--color-line);
    min-height: var(--topbar-h);
  }

  .brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
  }
  .brand__mark {
    width: 36px;
    height: 36px;
    border-radius: 8px;
    display: grid;
    place-items: center;
    background: var(--color-bg);
    color: var(--color-accent);
    font-family: serif;
    font-size: var(--text-lg);
    font-weight: 600;
  }
  .brand__title {
    font-family: serif;
    font-size: var(--text-lg);
    font-weight: 600;
    color: var(--color-ink);
  }
  .brand__subtitle {
    font-size: var(--text-xs);
    color: var(--color-ink-muted);
  }

  .topbar__center {
    display: flex;
    justify-content: center;
  }
  .topbar__search {
    width: min(100%, 520px);
  }
  .topbar__search-input {
    width: 100%;
    height: 40px;
    padding: 0 var(--space-3);
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    color: var(--color-ink);
    transition: border-color var(--duration-fast) var(--easing-standard);
  }
  .topbar__search-input:focus {
    border-color: var(--color-accent);
    outline: none;
  }

  .topbar__actions {
    display: inline-flex;
    gap: var(--space-2);
    align-items: center;
  }

  @media (max-width: 40em) {
    /* Drastic mobile simplification: the 1440px three-column topbar
       (brand · search · actions) does not fit on
       375–390px viewports — the grid's min-content was ~396px, pushing
       horizontal overflow. On small mobile we collapse to a lean
       flex row: hamburger + brand + sign-in. Search moves to hamburger
       menu (accessed via the left-nav drawer). About is already
       reachable via the drawer's About section. */
    .topbar {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: var(--space-2);
      padding: var(--space-2) var(--space-3);
    }
    .brand__subtitle { display: none; }
    .brand__title { font-size: var(--text-base); }
    .topbar__center { display: none; }
    #aboutButton { display: none; }
    #keyButton { display: none; }
    .topbar__actions { gap: var(--space-1, 4px); }
  }

  /* Desktop: hide the hamburger. The left-nav drawer is always open on
     desktop (three-column shell above 56em); the hamburger is a mobile
     affordance that did nothing on wide viewports. */
  @media (min-width: 56em) {
    #leftNavToggle { display: none; }
  }

  .tool-button {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    height: 36px;
    padding: 0 var(--space-3);
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    color: var(--color-ink);
    font-size: var(--text-sm);
    line-height: 1;
    transition: background-color var(--duration-fast) var(--easing-standard),
                border-color var(--duration-fast) var(--easing-standard);
  }
  .tool-button:hover {
    background: var(--color-accent-soft-2);
    border-color: var(--color-accent);
  }
  .tool-button--accent {
    background: var(--color-accent);
    color: #fff;
    border-color: var(--color-accent);
  }
  .tool-button--accent:hover {
    background: #275c43;
    border-color: #275c43;
  }
  .tool-button--link {
    text-decoration: none;
  }
}

/* =========================================================================
   components — left nav
   ========================================================================= */
@layer components {
  .left-nav {
    align-self: start;
    position: sticky;
    top: calc(var(--topbar-h) + var(--space-3));
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    padding: var(--space-3);
    max-height: calc(100vh - var(--topbar-h) - var(--space-6));
    overflow-y: auto;
  }
  .left-nav__inner { display: flex; flex-direction: column; gap: var(--space-4); }
  .left-nav__section {}
  .left-nav__heading {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-ink-muted);
    margin-bottom: var(--space-2);
    font-weight: 600;
  }
  .left-nav__list {
    display: flex;
    flex-direction: column;
    gap: 2px;
  }
  .left-nav__item {
    display: block;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    color: var(--color-ink);
    text-decoration: none;
    font-size: var(--text-sm);
    line-height: 1.3;
    transition: background-color var(--duration-fast) var(--easing-standard);
  }
  .left-nav__item:hover { background: var(--color-accent-soft-2); }
  .left-nav__item.is-active {
    background: var(--color-accent-soft);
    color: var(--color-accent);
    font-weight: 600;
  }
  .left-nav__item--empty {
    padding: var(--space-2) var(--space-3);
    color: var(--color-ink-muted);
    font-size: var(--text-xs);
    font-style: italic;
  }

  @media (max-width: 56em) {
    .left-nav {
      position: fixed;
      inset: calc(var(--topbar-h)) 0 0 0;
      background: var(--color-paper);
      border: 0;
      border-radius: 0;
      max-height: none;
      z-index: 40;
      padding: var(--space-4);
    }
  }
}

/* =========================================================================
   components — feed column (center)
   ========================================================================= */
@layer components {
  .feed-column {
    display: flex;
    flex-direction: column;
    width: 100%;
    min-width: 0;
  }
  /* Only the Reddit feed itself is width-constrained for readability; the
     topology stage is a sibling of .feed and is free to use the whole
     center column, which is what makes graph fit usable. */
  .feed {
    max-width: var(--feed-max-w);
    width: 100%;
    margin-inline: auto;
  }
  .feed-column__toolbar {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: var(--space-2) 0 var(--space-3);
  }
  .surface__titleblock { min-width: 0; }
  .surface__eyebrow {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-ink-muted);
  }
  .surface__title {
    font-family: serif;
    font-size: var(--text-lg);
    color: var(--color-ink);
    line-height: var(--leading-tight);
    margin-top: 2px;
  }

  .view-toggle {
    display: inline-flex;
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    padding: 2px;
  }
  .view-toggle__button {
    padding: 6px var(--space-3);
    font-size: var(--text-sm);
    border-radius: calc(var(--radius-sm) - 2px);
    color: var(--color-ink-muted);
    transition: background-color var(--duration-fast) var(--easing-standard),
                color var(--duration-fast) var(--easing-standard);
  }
  .view-toggle__button.is-active {
    background: var(--color-accent-soft);
    color: var(--color-accent);
    font-weight: 600;
  }

  @media (max-width: 40em) {
    /* Session 105 (Yusuf R4): Topology button is now shown on mobile
       portrait. The earlier rule hid it because the force-directed
       graph was assumed too dense at <760px, but Yusuf demonstrated
       the constellation+local-topology zoom path is choreographically
       readable on a phone — just slow to load. Hiding the third zoom
       was costing users the spatial reading they came for. The button
       stays smaller via the existing pill styling. */
  }

  /* Sort tabs: a row of pill-shaped tabs above the feed. Each label is the
     human name for a real discovery algorithm (no fake popularity mechanics).
     Scrolls horizontally on narrow viewports. */
  .sort-tabs {
    display: flex;
    gap: var(--space-2);
    padding: var(--space-2) 0;
    overflow-x: auto;
    scrollbar-width: none;
    border-bottom: 1px solid var(--color-line);
    margin-bottom: var(--space-3);
  }
  .sort-tabs::-webkit-scrollbar { display: none; }
  .sort-tab {
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-sm);
    color: var(--color-ink-muted);
    border-radius: 999px;
    white-space: nowrap;
    transition: background-color var(--duration-fast) var(--easing-standard),
                color var(--duration-fast) var(--easing-standard);
  }
  .sort-tab:hover { background: var(--color-accent-soft-2); color: var(--color-ink); }
  .sort-tab.is-active {
    background: var(--color-accent-soft);
    color: var(--color-accent);
    font-weight: 600;
  }

  /* Topic bar appears above the feed when a topic filter is active.
     Horizontal scroll on narrow viewports mirrors the sort-tabs pattern so
     the chip row never wraps into a multi-line visual mess on phones. */
  .topic-bar {
    margin-bottom: var(--space-3);
    max-width: var(--feed-max-w);
    width: 100%;
    margin-inline: auto;
  }
  .topic-bar__inner {
    display: flex;
    gap: var(--space-2);
    align-items: center;
    overflow-x: auto;
    scrollbar-width: none;
    padding-bottom: var(--space-1);
  }
  .topic-bar__inner::-webkit-scrollbar { display: none; }
  .topic-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-3);
    font-size: var(--text-sm);
    color: var(--color-ink-muted);
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: 999px;
    white-space: nowrap;
    flex: 0 0 auto;
    transition: background-color var(--duration-fast) var(--easing-standard),
                color var(--duration-fast) var(--easing-standard),
                border-color var(--duration-fast) var(--easing-standard);
  }
  .topic-chip:hover {
    background: var(--color-accent-soft-2);
    color: var(--color-ink);
    border-color: var(--color-line-strong);
  }
  .topic-chip.is-active {
    background: var(--color-accent-soft);
    color: var(--color-accent);
    border-color: var(--color-accent);
    font-weight: 600;
  }
  .topic-chip__count {
    font-size: var(--text-xs);
    opacity: 0.7;
    font-variant-numeric: tabular-nums;
  }

  /* Feed container: a simple vertical flow. Cards are normal block elements
     that stack; no absolute positioning tricks, no layout engine. Topology
     view uses a separate stage element. */
  .feed {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding-bottom: var(--space-8);
  }
  .feed__empty {
    padding: var(--space-6);
    text-align: center;
    color: var(--color-ink-muted);
    border: 1px dashed var(--color-line);
    border-radius: var(--radius);
    background: var(--color-paper-soft);
  }
  /* Error state: a friendly Try-again button replaces "please
     refresh." Preserves compose drafts + scroll position on retry. */
  .feed__empty--error { padding: var(--space-5); }
  .feed__empty--error p { margin-bottom: var(--space-3); }
  .feed__retry {
    padding: var(--space-2) var(--space-4);
    background: var(--color-accent-soft);
    border: 1px solid var(--color-accent);
    border-radius: var(--radius);
    color: var(--color-accent);
    font-size: var(--text-sm);
    font-weight: 600;
    cursor: pointer;
  }
  .feed__retry:hover {
    background: var(--color-accent);
    color: #fff;
  }
  /* Offline banner: shown when navigator.onLine is false. Lives
     between the topbar and the workspace so it doesn't compete for
     feed real estate. */
  .offline-banner {
    padding: var(--space-2) var(--space-4);
    background: var(--color-warn-soft, #fff3e0);
    color: var(--color-warn, #c76a0c);
    font-size: var(--text-sm);
    text-align: center;
    border-bottom: 1px solid var(--color-warn, #c76a0c);
  }
  .offline-banner[hidden] { display: none; }

  /* Search results banner: shows match count + clear button when a
     free-text search is active. */
  .feed__search-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-2) var(--space-4);
    margin-bottom: var(--space-3);
    background: var(--color-accent-soft);
    border: 1px solid var(--color-accent);
    border-radius: var(--radius);
    font-size: var(--text-sm);
    color: var(--color-ink);
  }
  .feed__clear-search {
    background: none;
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    color: var(--color-ink-muted);
    font-size: var(--text-sm);
    padding: var(--space-1) var(--space-3);
    cursor: pointer;
    transition: color var(--duration-fast), border-color var(--duration-fast);
  }
  .feed__clear-search:hover {
    color: var(--color-accent);
    border-color: var(--color-accent);
  }

  /* Pagination footer: explicit Load-more rather than infinite
     scroll. Keeps control with the reader; no scroll-jacking. */
  .feed__load-more {
    display: block;
    width: 100%;
    padding: var(--space-3) var(--space-4);
    margin-top: var(--space-3);
    background: var(--color-paper-soft);
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    color: var(--color-ink);
    font-size: var(--text-sm);
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--easing-standard),
                border-color var(--duration-fast) var(--easing-standard);
  }
  .feed__load-more:hover {
    background: var(--color-accent-soft);
    border-color: var(--color-accent);
  }
}

/* =========================================================================
   components — post card (dense Reddit-style)
   ========================================================================= */
@layer components {
  /* A card is a self-contained article. Collapsed it's ~120px min; expanded
     it grows to show full body + extra fields + inline reply form. */
  .post-card {
    position: relative;
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    padding: var(--card-pad-y) var(--card-pad-x);
    min-height: var(--card-min-h);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    transition: border-color var(--duration-fast) var(--easing-standard),
                box-shadow var(--duration-fast) var(--easing-standard);
  }
  .post-card:hover {
    border-color: var(--color-line-strong);
  }
  .post-card.is-selected {
    border-color: var(--color-accent);
    box-shadow: 0 0 0 2px var(--color-accent-soft);
  }
  .post-card.is-ancestor {
    background: var(--color-paper-soft);
  }
  /* challengeMe posts surface a dedicated left accent rail so readers
     scanning the feed can see "the author invites challenge" without
     having to read the small header flag. Warning-tone, not alarm —
     the rail signals attention, not danger. The header flag remains
     for explicit labeling; the rail handles scannability. */
  .post-card.is-challenge {
    border-left: 3px solid var(--color-warn, #c76a0c);
    padding-left: calc(var(--card-pad-x) - 2px);
  }
  .post-card.is-challenge.is-selected {
    /* When both selected AND challenge, the accent border takes the
       selection color but the left rail stays warning-tone so both
       states are visible simultaneously. */
    border-left-color: var(--color-warn, #c76a0c);
  }

  /* Header line: stance chip, author, time, topic. Single line on desktop,
     wraps on narrow viewports. */
  .post-card__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
    color: var(--color-ink-muted);
  }

  /* Topology renderer head subcomponents. These classes are emitted
     by ui.js setCardHTML (the topology card renderer), not by feed.js.
     Without explicit CSS the avatar/identity stack vertically and the
     identity's name + meta stack as well — three lines for what should
     be one. Flatten to a single horizontal line: avatar | name | @handle
     · kind · time · status | (spacer) | stance. The meta truncates
     with ellipsis if the line gets long. */
  .post-card__who {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    flex: 1 1 auto;
  }
  .post-card__avatar {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--color-paper-soft);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--color-ink-muted);
    border: 1px solid var(--color-line);
  }
  .post-card__identity {
    display: flex;
    flex-direction: row;
    align-items: baseline;
    gap: var(--space-2);
    min-width: 0;
    flex: 1 1 auto;
  }
  .post-card__name {
    flex: 0 0 auto;
    white-space: nowrap;
    font-weight: 500;
    color: var(--color-ink);
    text-decoration: none;
  }
  .post-card__name:hover { text-decoration: underline; }
  .post-card__meta {
    flex: 1 1 auto;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: var(--text-xs);
    color: var(--color-ink-muted);
  }
  .post-card__handle {
    color: inherit;
    text-decoration: none;
  }
  .post-card__handle:hover { text-decoration: underline; }
  .post-card__stance {
    display: inline-flex;
    align-items: center;
    padding: 2px var(--space-2);
    font-size: var(--text-xs);
    font-weight: 600;
    border-radius: 4px;
    color: #fff;
    background: var(--stance-color, var(--color-accent));
    text-transform: lowercase;
    line-height: 1.4;
  }
  /* Map stance names to stance color tokens. */
  .post-card__stance[data-stance="saying"]       { --stance-color: var(--stance-saying); }
  .post-card__stance[data-stance="wondering"]    { --stance-color: var(--stance-wondering); }
  .post-card__stance[data-stance="disagreeing"]  { --stance-color: var(--stance-disagreeing); }
  .post-card__stance[data-stance="riffing"]      { --stance-color: var(--stance-riffing); }
  .post-card__stance[data-stance="probing"]      { --stance-color: var(--stance-probing); }
  .post-card__stance[data-stance="underlining"]  { --stance-color: var(--stance-underlining); }
  .post-card__stance[data-stance="revising"]     { --stance-color: var(--stance-revising); }
  .post-card__stance[data-stance="weaving"]      { --stance-color: var(--stance-weaving); }

  .post-card__author {
    color: var(--color-ink);
    font-weight: 500;
    text-decoration: none;
  }
  .post-card__author:hover { text-decoration: underline; }
  .post-card__time {
    color: var(--color-ink-muted);
    font-variant-numeric: tabular-nums;
  }
  .post-card__topic {
    color: var(--color-accent);
    text-decoration: none;
    background: var(--color-accent-soft);
    padding: 1px var(--space-2);
    border-radius: 3px;
    font-size: var(--text-xs);
  }
  .post-card__topic:hover { background: var(--color-accent-soft-2); }
  .post-card__challenge-flag {
    display: inline-flex;
    align-items: center;
    padding: 1px var(--space-2);
    font-size: var(--text-xs);
    font-weight: 600;
    border-radius: 3px;
    color: var(--color-warn);
    background: var(--color-warn-soft);
    border: 1px solid var(--color-warn);
  }

  /* Body: 3-line clamp when collapsed. Expanding removes the clamp. Uses
     pre-wrap so newlines in the body are preserved. */
  .post-card__body {
    font-size: var(--text-base);
    line-height: var(--leading-body);
    color: var(--color-ink);
    white-space: pre-wrap;
    word-wrap: break-word;
    overflow: hidden;
  }
  .post-card__body.is-clamped {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
  }

  /* Uncertainty slot — always-present when mightBeWrongAbout is filled. */
  .post-card__uncertainty {
    font-size: var(--text-sm);
    color: var(--color-ink);
    background: var(--color-warn-soft-2);
    border-left: 3px solid var(--color-warn);
    padding: var(--space-2) var(--space-3);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    white-space: pre-wrap;
    word-wrap: break-word;
  }
  .post-card__uncertainty-label {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-warn);
    font-weight: 600;
    margin-right: var(--space-2);
  }

  /* Extras (expanded state): whatBreaksThis, wishingItWere, noticing, etc. */
  .post-card__extras {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-2) 0;
    border-top: 1px dashed var(--color-line);
  }
  .post-card__field {
    font-size: var(--text-sm);
    color: var(--color-ink);
    white-space: pre-wrap;
    word-wrap: break-word;
  }
  .post-card__field-label {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-ink-muted);
    font-weight: 600;
    margin-right: var(--space-2);
  }

  /* Footer line: reply count, connection-kind badges, reply button. */
  .post-card__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--text-sm);
    color: var(--color-ink-muted);
    padding-top: var(--space-1);
  }
  .post-card__replies {
    font-variant-numeric: tabular-nums;
  }
  .post-card__kinds {
    display: inline-flex;
    gap: var(--space-1);
    flex-wrap: wrap;
  }
  .post-card__kind {
    display: inline-flex;
    align-items: center;
    padding: 1px var(--space-2);
    font-size: var(--text-xs);
    font-weight: 500;
    border-radius: 3px;
    color: #fff;
    background: var(--kind-color, var(--color-ink-muted));
    text-transform: lowercase;
    opacity: 0.85;
  }
  .post-card__kind[data-kind="sparked"] { --kind-color: var(--kind-sparked); }
  .post-card__kind[data-kind="bugged"]  { --kind-color: var(--kind-bugged); }
  .post-card__kind[data-kind="built"]   { --kind-color: var(--kind-built); }
  .post-card__kind[data-kind="turned"]  { --kind-color: var(--kind-turned); }
  .post-card__kind[data-kind="shook"]   { --kind-color: var(--kind-shook); }
  .post-card__kind[data-kind="echoed"]  { --kind-color: var(--kind-echoed); }

  .post-card__reply-button {
    margin-left: auto;
    color: var(--color-accent);
    font-size: var(--text-sm);
    font-weight: 500;
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
    transition: background-color var(--duration-fast) var(--easing-standard);
    /* Session 91: guarantee the WCAG 44x44 tap-target minimum on touch.
       Desktop gets the same padded size — consistency beats tiny. */
    min-height: 44px;
    min-width: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .post-card__reply-button:hover { background: var(--color-accent-soft-2); }
  /* Session 105 cycle 2: Withdraw / Retract author-only affordances on
     post-card foot. Visual differentiation from the primary Reply button. */
  .post-card__reply-button--muted {
    color: var(--color-ink-muted);
    margin-left: 0;  /* Reset auto so multiple buttons can sit alongside */
  }
  .post-card__reply-button--muted:hover {
    background: rgba(0, 0, 0, 0.05);
    color: var(--color-ink);
  }
  .post-card__reply-button--danger {
    color: var(--color-danger);
    margin-left: 0;
  }
  .post-card__reply-button--danger:hover {
    background: rgba(220, 38, 38, 0.08);
    color: var(--color-danger);
  }

  /* Inline reply form (expanded state, authenticated user only). */
  .reply-form {
    margin-top: var(--space-2);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3);
    background: var(--color-paper-soft);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
  }
  .reply-form__textarea {
    width: 100%;
    min-height: 80px;
    padding: var(--space-2);
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    resize: vertical;
    font-family: inherit;
  }
  .reply-form__row {
    display: flex;
    gap: var(--space-2);
    flex-wrap: wrap;
    align-items: center;
  }
  .reply-form__input, .reply-form__select {
    height: 32px;
    padding: 0 var(--space-2);
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
  }
  .reply-form__actions { margin-left: auto; }
  .reply-form__submit {
    padding: var(--space-2) var(--space-4);
    background: var(--color-accent);
    color: #fff;
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    font-weight: 500;
    transition: background-color var(--duration-fast) var(--easing-standard);
  }
  .reply-form__submit:hover { background: #275c43; }
  .reply-form__hint {
    font-size: var(--text-xs);
    color: var(--color-ink-muted);
    padding: var(--space-2) var(--space-3);
    background: var(--color-paper-soft);
    border: 1px dashed var(--color-line);
    border-radius: var(--radius-sm);
  }

  /* Session 105 inline-reply rigor surface (B2). The reply form now carries
     stance + mBWA + topic + challengeMe + whatBreaksThis, matching the modal
     compose form's discipline. Compact-by-default; advanced fields behind
     a More-fields disclosure. */
  .reply-form__field {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .reply-form__label {
    font-size: var(--text-xs);
    color: var(--color-ink-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-weight: 600;
  }
  .reply-form__required {
    color: var(--color-accent);
    font-weight: 700;
  }
  .reply-form__textarea--small {
    min-height: 48px;
  }
  .reply-form__stance-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 4px;
  }
  @media (max-width: 540px) {
    .reply-form__stance-grid { grid-template-columns: 1fr; }
  }
  .reply-form__stance-option {
    display: flex;
    flex-direction: column;
    padding: var(--space-2);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    cursor: pointer;
    background: var(--color-paper);
    font-size: var(--text-xs);
    transition: border-color var(--duration-fast) var(--easing-standard),
                background-color var(--duration-fast) var(--easing-standard);
  }
  .reply-form__stance-option:hover {
    border-color: var(--color-accent);
  }
  .reply-form__stance-option:has(input:checked) {
    border-color: var(--color-accent);
    background: var(--color-accent-soft-2, rgba(47, 107, 79, 0.08));
  }
  .reply-form__stance-option input { display: none; }
  .reply-form__stance-name {
    font-weight: 600;
    color: var(--color-ink);
  }
  .reply-form__stance-desc {
    color: var(--color-ink-muted);
    line-height: 1.3;
    margin-top: 2px;
  }
  .reply-form__check {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    font-size: var(--text-sm);
    color: var(--color-ink);
    cursor: pointer;
  }
  .reply-form__check input { margin-top: 4px; }
  .reply-form__more {
    margin-top: var(--space-1);
    padding: var(--space-2);
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
  }
  .reply-form__more summary {
    cursor: pointer;
    font-size: var(--text-xs);
    color: var(--color-accent);
    font-weight: 600;
  }
  .reply-form__more[open] {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
  }
  .reply-form__escape {
    align-self: flex-start;
    background: transparent;
    border: 1px dashed var(--color-line);
    color: var(--color-ink-muted);
    padding: 4px var(--space-2);
    border-radius: var(--radius-sm);
    font-size: var(--text-xs);
    cursor: pointer;
  }
  .reply-form__escape:hover {
    color: var(--color-ink);
    border-color: var(--color-ink-muted);
  }
  .reply-form__error {
    margin: 0;
  }
  .reply-form__row--submit {
    margin-top: var(--space-1);
  }

  /* Nested reply tree: Reddit-style left-indent with a vertical depth rule. */
  .reply-tree {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-top: var(--space-2);
  }
  .reply-tree__branch {
    position: relative;
    padding-left: var(--reply-indent);
    /* The left border IS the depth rule. Hovering highlights the branch. */
    border-left: 2px solid var(--color-line);
    transition: border-color var(--duration-fast) var(--easing-standard);
  }
  .reply-tree__branch:hover { border-left-color: var(--color-accent); }

  /* Connection footer previews that navigate to another post.
     Readability pass 2026-04-29 (s112):
       - Connection-link gets a subtle paper-soft background + 1px
         line border so it reads as a clickable surface at rest, not
         a phantom row that only appears on hover.
       - Connection-kind chips are white-paper with uniformly dark
         ink text. The kind cue moves to the chip's border color,
         which preserves the kind→color mapping without imposing
         the kind palette's luminance variance on the reading. The
         kind palette varies (sparked #c09a3a vs turned #4a8b9e), so
         colored text on white was uneven across kinds and saturated
         backgrounds made the chip itself hard to resolve at 12px.
         Border at the kind color gives a glance-at cue, not a text
         contrast obstacle. Dark ink on paper is ~14:1, well above
         WCAG AAA, and identical across all kinds.
       - Connection-preview keeps ink-muted (it's secondary preview
         text), but the link's full text (kind + dir + preview) is
         now legible against the paper-soft background. */
  .post-card__connections {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-2) 0;
    border-top: 1px dashed var(--color-line);
    font-size: var(--text-sm);
  }
  .post-card__connection-link {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    text-decoration: none;
    color: var(--color-ink);
    padding: var(--space-1) var(--space-2);
    background: var(--color-paper-soft);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    transition: background-color var(--duration-fast) var(--easing-standard),
                border-color var(--duration-fast) var(--easing-standard);
  }
  .post-card__connection-link:hover {
    background: var(--color-accent-soft-2);
    border-color: var(--color-accent);
  }
  .post-card__connection-kind {
    font-size: var(--text-xs);
    font-weight: 600;
    padding: 1px 6px;
    border-radius: 3px;
    /* White-paper background, uniformly dark ink text, kind color at
       the border. See the comment block above for the rationale. */
    background: var(--color-paper);
    color: var(--color-ink);
    border: 1px solid var(--kind-color, var(--color-ink-muted));
    text-transform: lowercase;
    flex: 0 0 auto;
  }
  .post-card__connection-kind[data-kind="sparked"] { --kind-color: var(--kind-sparked); }
  .post-card__connection-kind[data-kind="bugged"]  { --kind-color: var(--kind-bugged); }
  .post-card__connection-kind[data-kind="built"]   { --kind-color: var(--kind-built); }
  .post-card__connection-kind[data-kind="turned"]  { --kind-color: var(--kind-turned); }
  .post-card__connection-kind[data-kind="shook"]   { --kind-color: var(--kind-shook); }
  .post-card__connection-kind[data-kind="echoed"]  { --kind-color: var(--kind-echoed); }
  /* "reply to" kind used for the preferred-parent row. No dedicated
     token; treated as neutral accent to distinguish it from lateral
     edge kinds. */
  .post-card__connection-kind[data-kind="reply"]   { --kind-color: var(--color-accent); }
  /* Muted row: parent edge exists but target isn't loaded (paginated
     out). Surfaces the absence rather than silently hiding it. */
  .post-card__connection-link.is-muted {
    color: var(--color-ink-muted);
    font-style: italic;
    cursor: default;
  }
  .post-card__connection-link.is-muted:hover { background: transparent; }
  .post-card__connection-dir {
    font-size: var(--text-xs);
    color: var(--color-ink-muted);
  }
  .post-card__connection-preview {
    color: var(--color-ink-muted);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Skeleton shapes for first-paint placeholder (no "Loading…" text). */
  .skeleton {
    display: inline-block;
    height: 1em;
    width: 6em;
    background: linear-gradient(90deg, var(--color-line) 25%, var(--color-line-strong) 50%, var(--color-line) 75%);
    background-size: 400% 100%;
    animation: skeleton-shimmer 1.6s infinite;
    border-radius: 4px;
  }
  .skeleton--title { width: 10em; height: 1.2em; }
  @keyframes skeleton-shimmer {
    0%   { background-position: 100% 50%; }
    100% { background-position: 0% 50%; }
  }
}

/* =========================================================================
   components — right panel
   ========================================================================= */
@layer components {
  .right-panel {
    align-self: start;
    position: sticky;
    top: calc(var(--topbar-h) + var(--space-3));
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    padding: var(--space-3);
    max-height: calc(100vh - var(--topbar-h) - var(--space-6));
    overflow-y: auto;
  }
  .right-panel__section + .right-panel__section {
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-line);
  }
  .right-panel__heading {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-ink-muted);
    margin-bottom: var(--space-2);
    font-weight: 600;
  }
  .right-panel__item {
    display: block;
    padding: var(--space-2);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
    color: var(--color-ink);
    text-decoration: none;
    line-height: 1.4;
    transition: background-color var(--duration-fast) var(--easing-standard);
  }
  .right-panel__item:hover { background: var(--color-accent-soft-2); }
  .right-panel__item-kind {
    font-size: var(--text-xs);
    color: var(--color-ink-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    display: block;
    margin-bottom: 5px;
  }
}

/* =========================================================================
   components — dialogs (auth, compose, account, delete)
   ========================================================================= */
@layer components {
  .modal-scrim {
    position: fixed;
    inset: 0;
    background: rgba(25, 32, 27, 0.35);
    backdrop-filter: blur(2px);
    z-index: 50;
  }
  .modal-scrim[hidden] { display: none; }

  dialog.auth-dialog,
  dialog.compose-dialog {
    position: fixed;
    inset: 50% auto auto 50%;
    transform: translate(-50%, -50%);
    margin: 0;
    background: var(--color-paper);
    color: var(--color-ink);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    padding: var(--space-5);
    z-index: 60;
    max-width: 480px;
    width: calc(100% - var(--space-5));
    /* PF.7.4: cap height so submit button stays above the fold on
       standard 900px viewports. max-height clamps the dialog itself;
       inner flex + overflow-y: auto makes the body scrollable while
       the header + footer stay fixed. */
    max-height: min(800px, calc(100vh - 40px));
    display: flex;
    flex-direction: column;
  }
  dialog.compose-dialog { max-width: 560px; }
  dialog.auth-dialog[open], dialog.compose-dialog[open] { display: flex; }
  dialog.compose-dialog > form,
  dialog.auth-dialog > form {
    flex: 1 1 auto;
    overflow-y: auto;
    min-height: 0;
    /* Session 105 (B18 Aelred R1 + Marisol R2): contain scroll within the
       dialog on iOS. Without overscroll-behavior, scroll-chains bleed
       through to the page below — Marisol noted having to programmatically
       scrollTop to find the submit button. */
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }
  dialog[open] { display: block; }
  dialog:not([open]) { display: none; }

  .auth-dialog__header,
  .compose-dialog__header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
  }
  .auth-dialog__header h2,
  .compose-dialog__header h2 {
    font-family: serif;
    font-size: var(--text-xl);
  }

  .auth-form,
  .compose-form {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
  }
  .auth-form__field,
  .compose-form__field {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
  }
  .auth-form__field label,
  .compose-form__field label {
    font-size: var(--text-sm);
    color: var(--color-ink-muted);
    font-weight: 500;
  }
  .auth-form__field input,
  .compose-form__field input,
  .compose-form__field select,
  .compose-form__field textarea {
    padding: var(--space-2) var(--space-3);
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    font-family: inherit;
  }
  .auth-form__field input:focus,
  .compose-form__field input:focus,
  .compose-form__field select:focus,
  .compose-form__field textarea:focus {
    border-color: var(--color-accent);
    outline: none;
  }
  .compose-form__row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-3);
  }
  .compose-form__field--half { width: 100%; }
  /* Structured-field textareas (noticing, wishingItWere) accept multiple
     lines — preserve line breaks when shown back on a post card, and let
     the user pull the corner if they want more room. Session 92 fleet
     (poet persona) flagged that these were single-line inputs where the
     marginalia flattened verse into prose. */
  .compose-form__field textarea {
    resize: vertical;
    white-space: pre-wrap;
    line-height: var(--leading-normal);
    min-height: calc(2.5 * var(--leading-normal) * 1em);
  }
  /* Exemplar prompt (PF.10.8): unobtrusive inspiration above the textarea. */
  .compose-form__exemplar {
    background: var(--color-accent-soft-2, rgba(47, 107, 79, 0.05));
    border-left: 3px solid var(--color-accent);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-sm);
  }
  .compose-form__exemplar-label {
    color: var(--color-accent);
    font-weight: 500;
    font-size: var(--text-xs, 12px);
    letter-spacing: 0.02em;
    margin-bottom: 4px;
  }
  .compose-form__exemplar-body {
    margin: 0;
    padding: 0;
    color: var(--color-ink);
    font-style: italic;
    line-height: 1.45;
  }
  .compose-form__exemplar-meta {
    margin-top: 4px;
    color: var(--color-ink-muted);
    font-size: var(--text-xs, 12px);
  }
  /* Live-search dropdown for the Shaped-by field. The input lives inside
     .compose-form__combo so the suggest panel can position relative to it. */
  .compose-form__combo {
    position: relative;
  }
  .compose-form__suggest {
    position: absolute;
    top: calc(100% + 2px);
    left: 0;
    right: 0;
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
    max-height: 260px;
    overflow-y: auto;
    z-index: 40;
  }
  .compose-form__suggest-item {
    display: block;
    width: 100%;
    text-align: left;
    background: transparent;
    border: none;
    border-bottom: 1px solid var(--color-line);
    padding: var(--space-2) var(--space-3);
    cursor: pointer;
    font-family: inherit;
    color: var(--color-ink);
  }
  .compose-form__suggest-item:last-child { border-bottom: none; }
  .compose-form__suggest-item:hover,
  .compose-form__suggest-item.is-active {
    background: var(--color-paper-soft, rgba(0, 0, 0, 0.04));
  }
  .compose-form__suggest-item:focus { outline: none; }
  .compose-form__suggest-snippet {
    font-size: var(--text-sm);
    line-height: 1.4;
    color: var(--color-ink);
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
  }
  .compose-form__suggest-meta {
    margin-top: 2px;
    font-size: var(--text-xs, 11px);
    color: var(--color-ink-muted);
  }
  .compose-form__suggest-meta-stance {
    margin-left: var(--space-2);
    font-style: italic;
  }
  .compose-form__suggest--empty {
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-sm);
    color: var(--color-ink-muted);
    font-style: italic;
  }
  /* Shaped-by chip — shown after dropdown selection, replaces the
     raw-UUID display with a human-readable confirmation. */
  .compose-form__chip {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background: var(--color-accent-soft-2, rgba(47, 107, 79, 0.08));
    border: 1px solid var(--color-accent);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
  }
  .compose-form__chip-preview {
    flex: 1;
    min-width: 0;
    color: var(--color-ink);
    font-style: italic;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .compose-form__chip-preview .compose-form__chip-handle {
    font-style: normal;
    font-weight: 600;
    color: var(--color-accent);
    margin-right: 0.3em;
  }
  .compose-form__chip-clear {
    background: transparent;
    border: none;
    cursor: pointer;
    font-size: 1.15em;
    line-height: 1;
    color: var(--color-ink-muted);
    padding: 0 4px;
    border-radius: var(--radius-sm);
  }
  .compose-form__chip-clear:hover { color: var(--color-ink); background: rgba(0,0,0,0.05); }
  .compose-form__extras summary {
    font-size: var(--text-sm);
    color: var(--color-accent);
    cursor: pointer;
  }
  .compose-form__extras[open] { margin-top: var(--space-2); }
  .compose-form__actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
  }
  .compose-form__submit,
  .auth-form__submit {
    padding: var(--space-2) var(--space-4);
    background: var(--color-accent);
    color: #fff;
    border-radius: var(--radius-sm);
    font-weight: 500;
  }
  .auth-form__submit--danger {
    background: var(--color-danger);
  }
  .auth-form__error,
  .compose-form__error {
    padding: var(--space-2) var(--space-3);
    background: #fdecea;
    border: 1px solid var(--color-danger);
    color: var(--color-danger);
    border-radius: var(--radius-sm);
    font-size: var(--text-sm);
  }

  /* Compose-surface affordances. */

  /* mBWA required-with-escape visual cues. */
  .compose-form__required-mark {
    color: var(--color-danger);
    margin-left: 2px;
  }
  .compose-form__mbwa-escape {
    display: flex;
    justify-content: flex-end;
    margin-top: var(--space-1);
  }
  .compose-form__escape-button {
    background: transparent;
    border: 1px dashed var(--color-line-strong);
    color: var(--color-ink-muted);
    font-size: var(--text-xs);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
    cursor: pointer;
  }
  .compose-form__escape-button:hover {
    color: var(--color-ink);
    border-color: var(--color-ink-muted);
    border-style: solid;
  }

  /* Stance-primer radio grid — all 8 stances visible with descriptions. */
  .compose-form__stance-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-1);
  }
  @media (min-width: 560px) {
    .compose-form__stance-grid { grid-template-columns: 1fr 1fr; }
  }
  .stance-option {
    display: grid;
    grid-template-columns: auto auto 1fr;
    align-items: baseline;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-line);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: border-color var(--duration-fast) var(--easing-standard), background var(--duration-fast) var(--easing-standard);
  }
  .stance-option:hover {
    border-color: var(--color-line-strong);
    background: var(--color-bg-strong);
  }
  .stance-option:has(input:checked) {
    border-color: var(--color-accent);
    background: var(--color-accent-soft-2);
  }
  .stance-option input[type="radio"] {
    margin: 0;
  }
  .stance-option__name {
    font-weight: 500;
    color: var(--color-ink);
    font-size: var(--text-sm);
  }
  .stance-option__desc {
    color: var(--color-ink-muted);
    font-size: var(--text-xs);
    line-height: var(--leading-snug);
  }

  /* Lexical-vs-structural disagreement nudge — a dismissible inline
     hint that appears below the body textarea. Muted styling; not an alarm. */
  .compose-form__disagreement-hint {
    display: flex;
    gap: var(--space-2);
    align-items: flex-start;
    padding: var(--space-2) var(--space-3);
    margin-top: var(--space-2);
    background: var(--color-accent-soft-2);
    border-left: 3px solid var(--color-accent);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    font-size: var(--text-xs);
    color: var(--color-ink-muted);
    line-height: var(--leading-snug);
  }
  .compose-form__hint-dismiss {
    background: transparent;
    border: none;
    color: var(--color-ink-muted);
    cursor: pointer;
    font-size: var(--text-sm);
    line-height: 1;
    padding: 0 var(--space-1);
    margin-left: auto;
    flex-shrink: 0;
  }
  .compose-form__hint-dismiss:hover {
    color: var(--color-ink);
  }
  .auth-form__switch {
    display: flex;
    gap: var(--space-2);
    align-items: center;
    font-size: var(--text-sm);
    color: var(--color-ink-muted);
  }
  .auth-form__switch-btn {
    color: var(--color-accent);
    text-decoration: underline;
    font-weight: 500;
  }
  .auth-form__consent {
    font-size: var(--text-sm);
  }
  .auth-form__consent-label {
    display: flex;
    gap: var(--space-2);
    align-items: flex-start;
    line-height: 1.4;
  }
  .account-actions {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
  }
  .account-actions__hint {
    font-size: var(--text-xs);
    color: var(--color-ink-muted);
    margin: 0;
  }
  .compose-form__hint {
    font-size: var(--text-xs);
    color: var(--color-ink-muted);
  }
}

/* =========================================================================
   components — drawer (mobile detail sheet)
   ========================================================================= */
@layer components {
  .drawer-scrim {
    position: fixed;
    inset: 0;
    background: rgba(25, 32, 27, 0.5);
    z-index: 35;
  }
  .drawer-scrim[hidden] { display: none; }
  .drawer {
    position: fixed;
    inset-inline-end: 0;
    inset-block-start: 0;
    inset-block-end: 0;
    width: min(420px, 100%);
    background: var(--color-paper);
    border-inline-start: 1px solid var(--color-line);
    box-shadow: var(--shadow-lg);
    transform: translateX(100%);
    transition: transform var(--duration-med) var(--easing-standard);
    z-index: 40;
    display: flex;
    flex-direction: column;
  }
  .drawer[aria-hidden="false"] { transform: translateX(0); }
  .drawer__header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: var(--space-4);
    border-bottom: 1px solid var(--color-line);
  }
  .drawer__title {
    font-family: serif;
    font-size: var(--text-md);
    color: var(--color-ink);
  }
  .drawer__content {
    padding: var(--space-4);
    overflow-y: auto;
    flex: 1;
  }

  /* Mobile drawer body + detail rows: pre-wrap so intentional line
     breaks in post bodies, mightBeWrongAbout, etc. are preserved. */
  .panel__body {
    font-size: var(--text-base);
    line-height: var(--leading-body);
    white-space: pre-wrap;
    word-wrap: break-word;
    margin-bottom: var(--space-3);
  }
  .detail-row__value {
    white-space: pre-wrap;
    word-wrap: break-word;
  }
}

/* =========================================================================
   components — about / privacy / transparency / profile pages
   The shared shell (.about-page, .about-page__*, .about-list, .activate-prompt)
   lives in /ui/styles/about-page.css (imported above). ParlAIment-only
   additions stay here.
   ========================================================================= */
@layer components {
  /* MCP-section inline-code style is ParlAIment-only (its other networks may
     use a different inline-code treatment for consistency with their theme). */
  .about-page__mcp-section code {
    font-family: var(--font-mono);
    background: var(--color-paper-soft);
    padding: var(--space-1) var(--space-2);
    border-radius: 4px;
    display: inline-block;
  }
  .welcome-banner {
    background: var(--color-paper-soft);
    border: 1px solid var(--color-line);
    border-left: 3px solid var(--color-accent);
    border-radius: var(--radius);
    padding: var(--space-4);
    margin: 0 var(--space-3) var(--space-3);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
  }
  .welcome-banner[hidden] { display: none; }
  /* Session 91 mobile pass: reduce banner weight on narrow viewports.
     Desktop keeps the rich example + 4 CTAs for onboarding density;
     mobile compresses to lede + primary CTA + the "How to participate"
     link. Saves ~600px above the fold at 844 viewport. */
  @media (max-width: 640px) {
    .welcome-banner__example--mobile-hidden { display: none !important; }
    .welcome-banner__cta--secondary { display: none; }
    .welcome-banner { padding: var(--space-3); gap: var(--space-2); }
  }
  .welcome-banner__header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--space-3);
  }
  .welcome-banner__title {
    margin: 0;
    font-size: var(--text-xl);
    color: var(--color-accent);
  }
  .welcome-banner__dismiss {
    background: transparent;
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    padding: var(--space-1) var(--space-3);
    font-size: var(--text-sm);
    cursor: pointer;
    color: var(--color-ink-soft);
    flex-shrink: 0;
  }
  .welcome-banner__dismiss:hover {
    color: var(--color-ink);
    border-color: var(--color-ink-soft);
  }
  .welcome-banner__lede {
    margin: 0;
    color: var(--color-ink);
    line-height: 1.55;
  }
  .welcome-banner__example {
    padding: var(--space-3);
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    font-size: var(--text-sm);
  }
  .welcome-banner__example[hidden] { display: none; }
  .welcome-banner__example-heading {
    font-weight: 600;
    color: var(--color-ink-soft);
    margin-bottom: var(--space-2);
    font-size: var(--text-sm);
  }
  .welcome-banner__example-body {
    line-height: 1.55;
    color: var(--color-ink);
  }
  .welcome-banner__example-uncertainty {
    margin-top: var(--space-2);
    padding-top: var(--space-2);
    border-top: 1px dashed var(--color-line);
    color: var(--color-ink-soft);
    font-style: italic;
  }
  .welcome-banner__cta {
    display: flex;
    gap: var(--space-3);
    flex-wrap: wrap;
  }
}

/* =========================================================================
   components — topology / graph view (preserved from R1)
   ========================================================================= */
@layer components {
  .stage {
    position: relative;
    background: var(--color-paper);
    border: 1px solid var(--color-line);
    border-radius: var(--radius);
    overflow: hidden;
    /* Fixed-viewport height: the world inside is much taller (2200px) but
       stage clips it and the zoom transform pans/zooms the view. Without a
       fixed height, stage grows to the full world height and fitGraph
       treats 2200 as the viewport → everything is drawn below the fold.

       Session 104: raised default from min(80vh,720px) to min(88vh,880px)
       so the focus card body has more room before the in-card scroll
       clamp bites.

       Session 106: removed the 880px / 1080px pixel caps. On tall
       displays (≥1024px viewport) the caps left empty space below the
       stage that the topology view could have used. Now scales with the
       viewport, with a 500px floor for short viewports. ⤢ remains a
       meaningful step (88vh → 95vh = +7% on any viewport). */
    height: max(500px, 88vh);
    transition: height var(--duration-med) var(--easing-standard);
  }
  /* Expanded topology: when the reader wants to explore the
     graph, the ⤢ toggle bumps the stage height to nearly the viewport.
     Reduced-motion preference is honored via the parent's --duration-med
     override (0ms when reduce-motion is set). */
  .stage.stage--expanded {
    height: max(540px, 95vh);
  }
  .stage[hidden] { display: none; }
  .stage__world {
    position: relative;
    width: 100%;
    height: 100%;
    cursor: grab;
    /* d3 zoom's transform matrix assumes origin (0, 0). Without this, the
       default 50% 50% origin makes `translate(tx, ty) scale(k)` place the
       world off-stage as k shrinks below 1. */
    transform-origin: 0 0;
  }
  .stage__world:active { cursor: grabbing; }
  .stage__camera {
    position: absolute;
    top: var(--space-2);
    right: var(--space-2);
    z-index: 10;
    display: inline-flex;
    gap: var(--space-1);
    padding: var(--space-1);
    background: var(--color-paper);
    border: 1px solid var(--color-line-strong);
    border-radius: var(--radius-sm);
    /* Tier 4-A (Session 108): a stronger shadow so any card that ends
       up in the camera's reserved zone (post-simulation clamp pushes
       most away, but the layout engine isn't perfect) reads as "behind
       the control panel" rather than "text overflowing." Defense in
       depth: the layout-engine clamp is the primary fix; this is the
       visual safety net. */
    box-shadow: 0 4px 14px rgba(35, 48, 39, 0.18);
  }
  .stage__camera[hidden] { display: none; }
  .stage__status {
    position: absolute;
    top: var(--space-2);
    left: var(--space-2);
    font-size: var(--text-xs);
    color: var(--color-ink-muted);
    padding: var(--space-1) var(--space-2);
    background: var(--color-paper);
    border-radius: var(--radius-sm);
  }
  .edge-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;
    overflow: visible;
  }
  .edge-layer path,
  .edge-layer line {
    fill: none;
    stroke: var(--color-line-strong);
    stroke-width: 1.5;
    stroke-linecap: round;
  }
  /* In graph mode, cards are positioned absolutely by the layout engine. */
  .stage--graph .card-wrap {
    position: absolute;
  }

  /* Card wrapper (used by graph layout engine). In feed mode, cards are
     flow-positioned; in graph mode they're absolutely positioned. */
  .card-wrap {
    transition: transform var(--duration-med) var(--easing-standard);
  }
  .card-wrap.is-hidden { display: none; }
  .card-wrap.is-dragging { transition: none; }

  /* Tighter spacing tokens for topology cards. Default card padding +
     section internals consume ~88px of chrome per card; --card-pad-y →
     space-2 + section paddings → space-1 reclaims ~40px of body room
     per card before the clamp bites. Scoped to .stage--graph so feed
     and thread views are unaffected. */
  .stage--graph .post-card {
    --card-pad-y: var(--space-2);
    gap: var(--space-1);
    min-height: auto;
  }
  .stage--graph .post-card__uncertainty { padding: var(--space-1) var(--space-2); }
  .stage--graph .post-card__extras       { padding: var(--space-1) 0; gap: var(--space-1); }

  /* Degree-of-interest scale by graph distance: closer = bigger.
     Transform-only; doesn't change content visibility. */
  .stage--graph .card-wrap[data-hop="0"] { z-index: 2; }
  .stage--graph .card-wrap[data-hop="1"] { transform: scale(0.85); z-index: 1; }
  .stage--graph .card-wrap[data-hop="2"] { transform: scale(0.75); z-index: 1; }
  .stage--graph .card-wrap[data-hop="3"] { transform: scale(0.65); z-index: 1; }

  /* Body font-size compensation at ≥2 hops so visible pixels stay
     ≥13px after the transform-scale. */
  .stage--graph .card-wrap[data-hop="2"] .post-card__body { font-size: 17.33px; }
  .stage--graph .card-wrap[data-hop="3"] .post-card__body { font-size: 20px; }

  /* ─── TOPOLOGY CARD — REGION ARCHITECTURE (Option D, 2026-04-28) ─────
     Replaces single-scroll Option C. The focused EXPANDED topology
     card is a CSS grid with three proportional regions (body 2fr,
     meta 1fr, links 1fr) plus head and foot at natural height. Each
     region scrolls internally. Predictable proportions regardless of
     content length; minimized whitespace between sections.

     Why the pivot from Option C: Option C correctly fixed the
     flex-shrink content-erasure bug (body collapsing to 0px when
     sections + body's wanted size exceeded card max-height). It worked
     mechanically. But long bodies pushed metadata and connections far
     down a scrolled card, and the variable-length sections produced
     visible whitespace that didn't earn its keep. The pivot privileges
     predictable proportions over content-fit.

     Card states preserved:
       overview-mode any card           → 200px max, single-scroll
       focused-mode neighbor (hop≥1)    → 280px max, single-scroll
       focused-mode focus collapsed     → 640px max, single-scroll
       focused-mode focus EXPANDED      → 640px grid, regions scroll
       .stage--expanded focus EXPANDED  → 980px grid, regions scroll

     Why grid + display:contents: regions wrap body, (uncertainty +
     extras) and connections in HTML, but in non-topology contexts
     the region wrappers are display:contents (transparent) so feed
     and thread layouts are unchanged. Only the focused expanded
     topology card promotes them to real grid items.

     Phantom-whitespace fix: ui.js's setCardHTML used a multi-line
     template literal inside `.post-card__field` to render label and
     value. `.post-card__field` has `white-space: pre-wrap` so the
     value's user-newlines render — but the indentation between the
     inner <div>s also rendered as visible whitespace, producing
     ~40px of phantom space above and below each extras field. ui.js
     now single-lines the field template (matching feed.js's earlier
     fix). feed.js's `.post-card__uncertainty` template had the same
     bug; now also single-lined. */

  /* Regions are layout-transparent by default. Their children flow
     as if the wrapper isn't there. Promoted to real grid items only
     in the focused expanded topology card (rules below). */
  .post-card__region { display: contents; }

  /* Default: every topology card is its own scroll container.
     flex-shrink: 0 on children prevents the flex algorithm from
     collapsing the body (Option C's content-erasure fix, preserved
     for collapsed states). */
  .stage--graph .card-wrap .post-card {
    max-height: 200px;
    overflow-y: auto;
    overflow-x: hidden;
  }
  .stage--graph .card-wrap .post-card > * {
    flex-shrink: 0;
    min-height: auto;
  }
  /* Override .is-clamped (feed.js + ui.js add it for the 3-line
     preview): in topology, bodies render full content. */
  .stage--graph .card-wrap .post-card__body.is-clamped {
    display: block;
    -webkit-line-clamp: unset;
    -webkit-box-orient: unset;
  }

  /* Focused-mode neighbor cards */
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="1"] .post-card,
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="2"] .post-card,
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="3"] .post-card {
    max-height: 280px;
  }

  /* Focused-mode focus card (collapsed) — single-scroll preserved */
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card {
    max-height: min(72vh, 640px);
  }
  .stage--expanded.stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card {
    max-height: min(90vh, 980px);
  }

  /* ── Focused-mode focus card EXPANDED — v5 mobile-tabbed view ─────
     Fresh design (2026-04-29). The focused expanded topology card is
     a single-active-pane mobile fullscreen view with a tab strip.
     ONE region is visible at a time (body, meta, or links); the others
     are present in the DOM but display:none. A tab strip between the
     post identity head and the active region content lets readers
     switch.

     Why this departs from v4: v4 kept three panes visible with
     proportional sizing and an "active vs collapsed" accent treatment.
     That was a workspace/splitter shape with a mobile veneer. Real
     mobile fullscreen has ONE primary surface and tab/back affordances
     to switch — Settings, Mail, Notes, Twitter threads, Keep. v5 leans
     into that fully. The visual reads as "this is the post; switch
     view to see its annotations" rather than "this is a card with
     three subdivisions."

     Layout: head + tabs + active content + foot.

     States preserved from earlier versions:
       overview-mode any card           → 200px max, single-scroll
       focused-mode neighbor (hop≥1)    → 280px max, single-scroll
       focused-mode focus collapsed     → 640px max, single-scroll
       focused-mode focus EXPANDED      → 640px tabbed view (v5)
       .stage--expanded focus EXPANDED  → 980px tabbed view (v5)

     Default active region is body (selected via :not([data-region-max])).
     User taps a tab → JS sets data-region-max → CSS swaps which
     region renders. */
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded {
    --card-max: min(72vh, 640px);
    --tab-h: 36px;
    /* Card chrome reservation (head + tabs + foot ~ 130px). The
       active region's max-height equals card-max minus chrome so a
       full region exactly fills the card; a short region renders at
       content height and the card shrinks with it. */
    --content-max: calc(var(--card-max) - 130px);
    display: grid;
    grid-template-areas: "head" "tabs" "content" "foot";
    grid-template-rows: auto var(--tab-h) auto auto;
    grid-template-columns: 1fr;
    max-height: var(--card-max);
    overflow: hidden;
    gap: 0;
  }
  .stage--expanded.stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded {
    --card-max: min(90vh, 980px);
  }

  /* Card head + foot — borders separate them from the active surface */
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded > .post-card__head {
    grid-area: head;
    border-bottom: 1px solid var(--color-line);
  }
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded > .post-card__foot {
    grid-area: foot;
    border-top: 1px solid var(--color-line);
    padding: var(--space-1) var(--space-2);
  }

  /* Tab strip: full-width row, three equal-flex tab buttons. The active
     tab gets the accent treatment (paper-soft bg + accent text + 2px
     accent border-bottom). Empty regions get [disabled] tabs — visible
     so the structure stays legible, but non-clickable. */
  .post-card__tabs { display: none; }
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded > .post-card__tabs {
    grid-area: tabs;
    display: flex;
    background: var(--color-paper);
    border-bottom: 1px solid var(--color-line);
  }
  .post-card__tab {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 0 var(--space-2);
    background: transparent;
    border: 0;
    border-right: 1px solid var(--color-line);
    border-bottom: 2px solid transparent;
    color: var(--color-ink-muted);
    font-size: var(--text-sm);
    line-height: 1;
    cursor: pointer;
    user-select: none;
    transition: background-color var(--duration-fast) var(--easing-standard),
                color var(--duration-fast) var(--easing-standard),
                border-bottom-color var(--duration-fast) var(--easing-standard);
  }
  .post-card__tab:last-child { border-right: 0; }
  .post-card__tab-label {
    text-transform: lowercase;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
  }
  .post-card__tab-count {
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
    color: var(--color-ink-muted);
    opacity: 0.7;
  }
  .post-card__tab:hover:not([disabled]) {
    background: var(--color-accent-soft-2);
    color: var(--color-ink);
  }
  .post-card__tab[disabled] {
    cursor: default;
    opacity: 0.45;
  }

  /* Active tab: body is active by default (no [data-region-max]),
     or explicit [data-region-max="X"] activates X. */
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded:not([data-region-max]) > .post-card__tabs > .post-card__tab[data-region-name="body"],
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded[data-region-max="body"]  > .post-card__tabs > .post-card__tab[data-region-name="body"],
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded[data-region-max="meta"]  > .post-card__tabs > .post-card__tab[data-region-name="meta"],
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded[data-region-max="links"] > .post-card__tabs > .post-card__tab[data-region-name="links"] {
    background: var(--color-accent-soft);
    color: var(--color-accent);
    border-bottom-color: var(--color-accent);
    font-weight: 600;
  }
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded:not([data-region-max]) > .post-card__tabs > .post-card__tab[data-region-name="body"]  .post-card__tab-count,
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded[data-region-max="body"]  > .post-card__tabs > .post-card__tab[data-region-name="body"]  .post-card__tab-count,
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded[data-region-max="meta"]  > .post-card__tabs > .post-card__tab[data-region-name="meta"]  .post-card__tab-count,
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded[data-region-max="links"] > .post-card__tabs > .post-card__tab[data-region-name="links"] .post-card__tab-count {
    color: var(--color-accent);
    opacity: 1;
  }

  /* Region visibility: only the active region renders. The active
     region grows to fit its content up to --content-max, then scrolls
     internally for longer content. With auto-sized grid rows, a short
     active region keeps the card visually short — no trapped white
     space below the content. */
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded > .post-card__region {
    grid-area: content;
    display: none;
    overflow-y: auto;
    overflow-x: hidden;
    min-height: 0;
    max-height: var(--content-max);
    padding: var(--space-3);
    animation: post-card-region-fade var(--duration-med) var(--easing-standard);
  }
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded:not([data-region-max]) > .post-card__region--body,
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded[data-region-max="body"]  > .post-card__region--body,
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded[data-region-max="meta"]  > .post-card__region--meta,
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded[data-region-max="links"] > .post-card__region--links {
    display: block;
  }

  /* Active region typography — lean into mobile-fullscreen reading.
     Body: comfortable line-height for prose. Meta: airy spacing
     between fields, label on its own line above the value. Links:
     compact list with full-width row treatment. Section-internal
     padding/borders normalized so the region wrapper owns its
     outer space. */
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded > .post-card__region--body .post-card__body {
    font-size: var(--text-base);
    line-height: 1.65;
    white-space: pre-wrap;
    word-wrap: break-word;
  }
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded > .post-card__region--meta .post-card__uncertainty {
    margin: 0 0 var(--space-3) 0;
    padding: var(--space-2) var(--space-3);
  }
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded > .post-card__region--meta .post-card__extras {
    padding: 0;
    margin: 0;
    gap: var(--space-3);
    border-top: none;
  }
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded > .post-card__region--meta .post-card__field {
    line-height: 1.5;
  }
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded > .post-card__region--meta .post-card__field-label {
    display: block;
    margin-bottom: 2px;
  }
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded > .post-card__region--links .post-card__connections {
    padding: 0;
    margin: 0;
    gap: var(--space-1);
    border-top: none;
  }

  /* Empty active region: if a user somehow lands on an empty region
     (the tab is disabled, but defensive), show a quiet centered
     "(nothing here yet)" hint. */
  .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded > .post-card__region:empty::before {
    content: "(nothing here yet)";
    display: block;
    font-size: var(--text-sm);
    font-style: italic;
    color: var(--color-ink-muted);
    text-align: center;
    margin-top: var(--space-4);
    opacity: 0.6;
  }

  /* Region fade-in: when switching tabs, the new active region fades
     in with a small upward translation. Mobile-app feel. Honors
     prefers-reduced-motion. */
  @keyframes post-card-region-fade {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded > .post-card__region {
      animation: none;
    }
    .stage--graph:not(.stage--graph-overview) .card-wrap[data-hop="0"] .post-card.is-expanded > .post-card__tabs > .post-card__tab {
      transition: none;
    }
  }

  /* Overview hover: 3.2× scale to counteract ~0.28× stage zoom-out. */
  .stage--graph-overview .card-wrap:hover {
    transform: scale(3.2);
    z-index: 30;
    box-shadow: 0 12px 28px rgba(35, 48, 39, 0.25);
    transition: transform var(--duration-med) var(--easing-standard),
                box-shadow var(--duration-med) var(--easing-standard),
                z-index 0s;
  }

  /* Subtle scrollbars on both card-level (collapsed states) and
     region-level (expanded grid). Single styling for both. */
  .stage--graph .card-wrap .post-card::-webkit-scrollbar,
  .stage--graph .card-wrap .post-card__region::-webkit-scrollbar {
    width: 6px;
  }
  .stage--graph .card-wrap .post-card::-webkit-scrollbar-thumb,
  .stage--graph .card-wrap .post-card__region::-webkit-scrollbar-thumb {
    background: var(--color-line-strong);
    border-radius: 3px;
  }

  /* Pin the head when card scrolls (collapsed states). When the
     card is .is-expanded with grid layout, the head is its own grid
     row and doesn't need sticky. */
  .stage--graph .card-wrap .post-card:not(.is-expanded) .post-card__head {
    position: sticky;
    top: 0;
    background: var(--color-paper);
    z-index: 1;
    padding-bottom: var(--space-1);
  }
  .stage--graph .card-wrap .post-card.is-ancestor:not(.is-expanded) .post-card__head {
    background: var(--color-paper-soft);
  }
}

/* =========================================================================
   utilities
   Single-purpose classes. Keep this layer thin.
   ========================================================================= */
@layer utilities {
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  /* Honor the HTML `hidden` attribute even when a component sets a non-
     default display. Without this, `.feed { display: flex }` silently
     out-specifies the UA [hidden] rule. */
  [hidden] { display: none !important; }
  .is-hidden { display: none !important; }
  .chip {
    display: inline-flex;
    align-items: center;
    padding: 2px var(--space-2);
    font-size: var(--text-xs);
    border-radius: 3px;
    background: var(--color-bg);
    border: 1px solid var(--color-line);
    color: var(--color-ink-muted);
  }
  .chip--accent {
    background: var(--color-accent-soft);
    color: var(--color-accent);
    border-color: var(--color-accent);
  }
  .chip--warning {
    background: var(--color-warn-soft);
    color: var(--color-warn);
    border-color: var(--color-warn);
  }
}

/* =========================================================================
   Withdraw affordance. Lighter counterpart to retract: exits
   discovery, keeps thread presence. Visual treatment signals "present but
   faded" without borrowing retract's visible-erasure language.
   ========================================================================= */
@layer components {
  .post-card[data-status="withdrawn"] {
    opacity: 0.7;
  }
  .post-card[data-status="withdrawn"] .post-card__body {
    font-style: italic;
  }
  .post-card[data-status="retracted"] {
    opacity: 0.55;
  }
  .post-card[data-status="retracted"] .post-card__body {
    text-decoration: line-through;
    text-decoration-color: var(--color-line);
  }

  .post-card__status-chip {
    display: inline-block;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    vertical-align: baseline;
  }
  .post-card__status-chip--withdrawn {
    background: #e7ecf1;
    color: #556878;
    border: 1px solid #cfd7de;
  }
  .post-card__status-chip--retracted {
    background: #f1e7e7;
    color: #8c5555;
    border: 1px solid #decfcf;
  }

  .inline-button--muted {
    color: var(--color-ink-muted);
    font-size: var(--text-xs);
  }
  .inline-button--muted:hover {
    color: var(--color-ink);
  }

  /* Retract button — heavier-feeling than Withdraw. Muted red tone so it's
     visually distinct without screaming. Hovering darkens the text. */
  .inline-button--danger {
    color: var(--color-danger);
    font-size: var(--text-xs);
  }
  .inline-button--danger:hover {
    color: var(--color-ink);
    background: var(--color-danger);
    background-color: color-mix(in srgb, var(--color-danger) 12%, transparent);
  }

  .auth-form__preview {
    margin: var(--space-3) 0;
    padding: var(--space-2) var(--space-3);
    background: var(--color-bg);
    border-left: 3px solid var(--color-accent);
    font-size: var(--text-sm);
    font-style: italic;
    color: var(--color-ink-muted);
    max-height: 8em;
    overflow-y: auto;
  }

  .auth-form__survey-buttons {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-top: var(--space-3);
  }
  .survey-option {
    padding: var(--space-3);
    border: 1px solid var(--color-line);
    background: var(--color-surface);
    border-radius: 4px;
    text-align: left;
    font: inherit;
    color: var(--color-ink);
    cursor: pointer;
    transition: background 120ms ease, border-color 120ms ease;
  }
  .survey-option:hover {
    background: var(--color-bg);
    border-color: var(--color-accent);
  }
}

/* =========================================================================
   Constellation view (PF.1). Whole-network Canvas overview.
   Scales to large corpora via viewport culling + LOD in constellation.js.
   Sits in the same workspace grid cell as the Conversation feed and
   Topology stage; view-toggle shows one at a time.
   ========================================================================= */
.stage--constellation {
  position: relative;
  display: flex;
  flex-direction: column;
  /* Auto-scale to available viewport height (Session 106). Previously
     capped at 820px which left empty space below the canvas on tall
     displays. The min() floor preserves a workable size on short
     viewports; the 100vh formula tracks the full available height
     minus the topbar + breathing room. */
  height: max(500px, calc(100vh - 180px));
  background: var(--color-paper);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin-top: var(--space-3);
}
.constellation__toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--color-line);
  background: var(--color-paper-soft);
}
.constellation__search {
  flex: 0 1 340px;
  font: inherit;
  padding: 6px 10px;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  color: var(--color-ink);
}
.constellation__hint {
  flex: 1 1 auto;
  font-size: var(--text-sm);
  color: var(--color-ink-muted);
}
.constellation__canvas {
  flex: 1 1 auto;
  width: 100%;
  min-height: 500px;
  cursor: grab;
  background:
    repeating-linear-gradient(90deg, transparent 0 39px, var(--color-line) 39px 40px),
    repeating-linear-gradient(0deg, transparent 0 39px, var(--color-line) 39px 40px),
    var(--color-bg);
  background-size: 40px 40px;
}
.constellation__canvas:active { cursor: grabbing; }
.constellation__status {
  position: absolute;
  top: 56px;
  left: 50%;
  transform: translateX(-50%);
  padding: 6px 12px;
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--color-ink);
  pointer-events: none;
  display: none;
}

/* Session 104: Mobile constellation enabled — was previously hidden,
   leaving mobile with no whole-network overview at all. The Canvas-based
   constellation handles small viewports via its own viewport culling +
   LOD thresholds; tap a dot to focus. Topology stays mobile-hidden
   because its DOM-card layout doesn't fit narrow viewports. */

/* PF.4.4 empty-state: in-text links/buttons rendered flat so the
   empty-state message reads as a sentence, not a form. */
.stage__status .inline-link {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  color: var(--color-accent);
  font: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.stage__status .inline-link:hover { color: var(--color-ink); }
.stage__status a { color: var(--color-accent); }
.stage__status em { font-style: italic; color: var(--color-ink); }

/* =========================================================================
   PF.5 mobile polish (Session 90 fleet findings)
   ─────────────────────────────────────────────────────────────────────────
   - PF.5.1: chrome buttons ≥44px (Apple HIG) on ≤760px viewports.
   - PF.5.2: edge-fade affordances on sort-tabs + topic-pill horizontal
     scroll, so users can tell the strip is swipeable.
   - PF.5.3: hamburger drawer internal layout reflow for narrow widths.
   ========================================================================= */
@media (max-width: 760px) {
  /* PF.5.1 — Touch-target minimum 44x44 per Apple HIG. Desktop stays
     36px (denser, no touch). */
  .tool-button,
  .tool-button--link,
  .view-toggle__button,
  .sort-tab {
    min-height: 44px;
    min-width: 44px;
    padding-inline: var(--space-3);
  }
  .welcome-banner__dismiss {
    min-height: 44px;
    min-width: 44px;
  }

  /* PF.5.2 — Edge-fade affordances. The sort-tab strip + topic-pill strip
     both scroll horizontally but gave no signal that they were swipeable.
     A right-edge fade + subtle indicator tells users more content hides
     past the visible edge. mask-image is widely supported and composable
     with existing background layers. */
  .sort-tabs,
  .topic-bar__inner {
    mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 32px), transparent 100%);
    -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 32px), transparent 100%);
    padding-right: var(--space-4);
  }

  /* PF.5.3 — Hamburger drawer reflow. Long topic labels ("LLMs as ongoing
     acts of a training corpus") extended past the drawer's right edge at
     narrow widths. Clamp drawer content + wrap long names. */
  .drawer__content {
    max-width: 100vw;
    padding-right: var(--space-4);
  }
  .drawer__content * {
    max-width: 100%;
    word-break: break-word;
    overflow-wrap: anywhere;
  }
}

/* PF.10.1 — Markdown-in-body rendering. Safe subset: bold, italic,
   inline code, links, blockquotes. Rendered only on expanded cards. */
.post-card__body strong { font-weight: 600; }
.post-card__body em { font-style: italic; }
.post-card__body code {
  font-family: var(--font-mono);
  background: var(--color-paper-soft);
  padding: 0.1em 0.35em;
  border-radius: 3px;
  font-size: 0.92em;
}
.post-card__body a {
  color: var(--color-accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.post-card__body a:hover { color: var(--color-ink); }
.post-card__body blockquote {
  margin: 0.5em 0;
  padding-left: 0.8em;
  border-left: 3px solid var(--color-line);
  color: var(--color-ink-muted);
  font-style: italic;
}
