/* ============================================================
   PSAN THREAD SYSTEM — thread.css
   Styles the discussion pages (feed, thread, submit, inbox, mod)
   and every component in components/thread/*.js, against the DOM
   those files actually emit (CONTRACT §11.4 plus the page markup).

   LOOK
   Alden, read as a document: every page sits on the white canvas
   in one reading column, items are separated by fog hairlines or
   whitespace (one separator per surface, never two), and words do
   the work pills used to — kind, space, pinned and locked are
   graphite labels. Boundaries stay only where they carry meaning:
   form fields, the bell's popover, dialogs and focus rings. Ink and
   graphite carry every meaning; colour is punctuation with one job
   each — sage means act or affirm (one filled pill per view, the
   moderator mark), sky means new, here or yours (the original
   poster, arrivals, the live pill, "new since your last visit").
   No shadows. Inter for the interface, Source Serif 4 for titles.

   SCOPE
   Everything is prefixed `psan-`. The palette lives in the
   `--psan-*` block in section 1, the same way dashboard.css keeps
   its own in `--dash-*`; nothing here redefines a brand.css token.
   Overlays (dialogs, key help, toasts) are appended to <body>, so
   the token block is declared on `.psan-body` and on each overlay.

   TABLE OF CONTENTS
     1.  Scope, palette, component tokens, indent ramp
     2.  Primitives — focus, skip link, buttons, labels, fields
     3.  Page shell — canvas, header, breadcrumb, columns, rail
     4.  Feed — post list, post card
     5.  Post detail — header, title, byline, actions
     6.  Prose — bodyHtml rendering, mentions, tags, spoilers
     7.  Comment tree — node, rails, toggle, head, body, actions
     8.  Comment states — collapsed, deleted, distinguished, live
     9.  Vote controls — compact, inline and post variants
     10. Composer — textarea, toolbar, preview, mentions, gate
     11. Chrome — sort tabs, live status, more/continue, banners
     12. Submit form — kind choices
     13. Skeletons and empty states
     14. Toasts
     15. Dialogs — confirm and report, keyboard help
     16. Inbox — bell, panel, full page
     17. Moderation queue
     18. Hidden
     19. Responsive
     20. Reduced motion
     21. Forced colors / high contrast
     22. Print

   CONTRAST LEDGER  (WCAG 2.1 AA — 4.5:1 body text, 3:1 large text
   and UI glyphs.) Ratios computed from the resolved sRGB values of
   the section 1 palette, `color-mix()` results included; DARK
   first, LIGHT second.

     BODY AND METADATA
       ink        on canvas ...................... 15.51 / 14.99
       ink        on parchment ................... 14.14 / 13.27
       ink        on fog (selected tab) .......... 11.19 / 10.96
       graphite   on canvas (labels, meta) ....... 8.29 /  8.84
       graphite   on parchment .................... 7.56 /  7.83
       graphite   on fog .......................... 5.98 /  6.47

     ACCENTS AND STATUS INK
       on-accent  on sage (primary, MOD, vote) ... 10.42 / 10.42
       on-accent  on sage, hovered ................ 8.09 /  8.09
       on-accent  on sky (OP, live pill) .......... 8.69 /  8.69
       on-accent  on sky, hovered ................. 6.82 /  6.82
       canvas     on ink (toast, unread count) ... 15.51 / 14.99
       danger     on canvas ....................... 8.45 /  6.69
       danger     on parchment .................... 7.71 /  5.92
       danger     on danger tint .................. 6.48 /  5.72
       warning    on canvas ....................... 9.50 /  6.92
       warning    on parchment .................... 8.66 /  6.13
       ink        on sky tint (highlight, banner) . 10.40 / 13.24
       graphite   on sky tint ..................... 5.56 /  7.81
       ink        on sage tint (moderator) ....... 11.65 / 13.88
       graphite   on sage tint .................... 6.22 /  8.19

     NON-TEXT (3:1)
       focus ring (ink) vs canvas ................ 15.51 / 14.99
       focus ring (ink) vs parchment ............. 14.14 / 13.27
       field border vs canvas ..................... 5.49 /  3.53
       field border vs parchment .................. 5.00 /  3.12
       cast up-vote ring vs canvas ............... 13.94 /  4.56
       cast down-vote ring vs canvas ............. 12.89 /  5.12
       chosen kind ring (ink) vs canvas .......... 15.51 / 14.99
       unchosen kind ring (field line) ............ 5.49 /  3.53
       unread dot (ink) vs canvas ................ 15.51 / 14.99
       quote rule (graphite) vs canvas ............ 8.29 /  8.84

     DELIBERATE CHOICES
     * Hairlines and thread rails are fog (1.39 / 1.37 against
       canvas). They separate content that is already separated by
       spacing; none of them is the only boundary of a control. A
       rail's one action, collapse, is duplicated by
       `.psan-comment__toggle`, so 1.4.11 does not bind; on hover a
       rail turns ink.
     * Fog, parchment, sage and sky are all under 3:1 against white,
       so none of them is ever the only cue for a control or a state:
       a selected rail link or tab is also ink at weight 600 with
       aria-current / aria-selected, a cast vote also fills its glyph
       and sets aria-pressed, "New" is a word beside its sky dot, and
       the OP and MOD marks are words on their fills.
     * Form fields are the exception: a text input has no label
       inside it, so its edge is the control's boundary. Fields use
       `--psan-field-line` (ink mixed 55% into canvas) instead of fog,
       which is the only place this sheet departs from the dashboard.
     * The selected sort tab is a fog pill (1.39 / 1.37 against
       canvas), the one filled shape a tab strip keeps. Selection also
       moves the label from graphite to ink and from 500 to 600
       weight, and is exposed as aria-selected.
     * A cast vote fills its arrow with sage (up) or sky (down). Those
       fills are pale against white, so the cast state also draws a
       ring of the accent mixed 45% into ink, which clears 3:1.
     * The pending/optimistic dim is capped where graphite still
       clears 4.5:1 on canvas — 0.72 in dark, 0.8 in light (floors
       0.68 / 0.76). The pulsing bar, not the opacity, does the
       signalling.
     * Live and toast status dots are decorative: each sits beside
       text that says the same thing.
     ============================================================ */

/* ============ 1. SCOPE, PALETTE, COMPONENT TOKENS, INDENT RAMP ============ */

.psan-body,
.psan-thread,
[data-thread-root],
.psan-dialog,
.psan-keyhelp,
.psan-toasts,
[data-inbox-root] {
  /* ---- palette: dashboard.css's, under the same names ---- */
  --psan-canvas: #ffffff;
  --psan-parchment: #f3f1eb;
  --psan-ink: #28262a;
  --psan-graphite: #4a4a4c;
  --psan-fog: #dddcdd;
  --psan-silver: #cbcbcb;
  --psan-sky: #97cde5;
  --psan-sage: #c8dfaa;
  --psan-on-accent: #28262a; /* text on sage or sky, in both themes */
  --psan-danger: #a8321f;
  --psan-warning: #7a5200;
  /* status inks for the inverted (ink) toast surface */
  --psan-danger-on-ink: #f0a08f;
  --psan-warning-on-ink: #e3b964;

  /* ---- roles ---- */
  --psan-ink-meta: var(--psan-graphite);
  --psan-border: var(--psan-fog);
  --psan-card-border: transparent;
  --psan-field-line: color-mix(in srgb, var(--psan-ink) 55%, var(--psan-canvas));
  --psan-rail: var(--psan-fog);
  --psan-rail-hover: var(--psan-ink);
  --psan-selected: var(--psan-fog);
  --psan-hover: color-mix(in srgb, var(--psan-ink) 6%, transparent);
  --psan-primary-hover: color-mix(in srgb, var(--psan-sage) 86%, var(--psan-on-accent));
  --psan-live: color-mix(in srgb, var(--psan-sage) 45%, var(--psan-ink));

  /* ---- tints (opaque: mixed against canvas, never `transparent`,
     so a ratio in the ledger holds whatever sits behind) ---- */
  --psan-tint-danger: color-mix(in srgb, var(--psan-danger) 10%, var(--psan-canvas));
  --psan-tint-sky: color-mix(in srgb, var(--psan-sky) 24%, var(--psan-canvas));
  --psan-tint-sage: color-mix(in srgb, var(--psan-sage) 22%, var(--psan-canvas));
  --psan-subtree-tint: color-mix(in srgb, var(--psan-ink) 3%, transparent);
  --psan-scrim: color-mix(in srgb, var(--psan-ink) 40%, transparent);

  /* ---- type ---- */
  --psan-font-sans: "Inter", ui-sans-serif, system-ui, -apple-system,
    BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --psan-font-serif: "Source Serif 4", ui-serif, Georgia, Cambria,
    "Times New Roman", serif;
  --psan-font-mono: var(--font-mono);
  --psan-text-xs: 0.75rem;
  --psan-text-meta: 0.8125rem;
  --psan-text-sm: 0.875rem;
  --psan-text-prose: 0.9375rem;
  --psan-text-md: 1rem;
  --psan-text-lg: 1.125rem;
  --psan-text-xl: 1.25rem;
  --psan-text-title: clamp(1.875rem, 1.5rem + 1.6vw, 2.5rem);
  --psan-text-display: clamp(2.25rem, 1.9rem + 1.6vw, 3rem);
  --psan-track: -0.02em;
  --psan-track-caption: 0.02em;
  --psan-track-display: -0.03em;

  /* ---- geometry ---- */
  --psan-page-max: 75rem;
  --psan-rail-col: 18rem;
  --psan-prose-max: 42rem;
  /* the list pages' reading column: feed rows, inbox, queue, form */
  --psan-column-max: 44rem;
  --psan-gutter: clamp(var(--space-4), 4vw, var(--space-10));
  --psan-control: 2.5rem;
  --psan-tap: 2.75rem; /* 44px coarse-pointer target */
  --psan-hairline: 1px;
  --psan-bar: 2px;
  --psan-bar-strong: 3px;
  --psan-dot: 0.5rem;
  --psan-dot-sm: 0.375rem;
  --psan-radius-sm: 0.25rem;
  --psan-radius-control: 0.5rem;
  --psan-radius-field: 0.75rem;
  --psan-radius-card: 1rem;
  --psan-radius-stage: 1.5rem;
  --psan-radius-pill: var(--radius-full);
  /* The site navbar is sticky at a literal z-index of 9999
     (navbar.css), above every brand.css `--z-*` step, so anything
     that must cover it sits one layer higher. */
  --psan-z-overlay: 10000;

  /* ---- indent ramp ----
     The two anchors retune the whole tree; the easing itself lives
     on `.psan-comment` in the ramp block below, because a custom
     property substitutes its `var()`s at the element that declares
     it — computing `--psan-indent` up here would bake one depth in
     and inherit it to every node. Desktop: 20, 17, 14, 12, 10, then
     8px from depth 6 down. Twelve levels cost 129px, which leaves
     214px of text at a 375px viewport. */
  --psan-indent-max: var(--space-5);
  --psan-indent-min: var(--space-2);
  --psan-node-gap: var(--space-5);
  --psan-node-gap-tight: var(--space-4);
  --psan-rail-width: var(--psan-hairline);
  --psan-rail-hit: var(--space-3); /* invisible widening either side */

  /* ---- state ---- */
  --psan-pending-opacity: 0.8;

  /* ---- motion: the dashboard's 160ms on the shared ease-out ---- */
  --psan-motion: 160ms var(--ease-out);
  --psan-motion-slow: 240ms var(--ease-out);
}

/* Dark maps the same roles, as dashboard.css does. Without the
   white-on-cream step, cards need a fog edge to stay separate. */
[data-theme="dark"] .psan-body,
[data-theme="dark"] .psan-thread,
[data-theme="dark"] [data-thread-root],
[data-theme="dark"] .psan-dialog,
[data-theme="dark"] .psan-keyhelp,
[data-theme="dark"] .psan-toasts,
[data-theme="dark"] [data-inbox-root] {
  --psan-canvas: #1a191b;
  --psan-parchment: #232124;
  --psan-ink: #f3f1eb;
  --psan-graphite: #b5b2ad;
  --psan-fog: #353236;
  --psan-silver: #5a575c;
  --psan-danger: #f0a08f;
  --psan-warning: #e3b964;
  --psan-danger-on-ink: #a8321f;
  --psan-warning-on-ink: #7a5200;
  --psan-card-border: var(--psan-fog);
  --psan-tint-danger: color-mix(in srgb, var(--psan-danger) 14%, var(--psan-canvas));
  --psan-tint-sky: color-mix(in srgb, var(--psan-sky) 18%, var(--psan-canvas));
  --psan-tint-sage: color-mix(in srgb, var(--psan-sage) 12%, var(--psan-canvas));
  --psan-scrim: color-mix(in srgb, black 60%, transparent);
  --psan-pending-opacity: 0.72;
}

.psan-thread,
[data-thread-root],
.psan-dialog,
.psan-keyhelp,
.psan-toasts {
  color: var(--psan-ink);
  font-family: var(--psan-font-sans);
  font-size: var(--psan-text-md);
  line-height: 1.5;
  letter-spacing: var(--psan-track);
}

/* The depth ramp. `--psan-indent` is computed here, on every comment,
   so each node resolves the easing against its own `--psan-depth-t`;
   the base rule also restates the floor, because custom properties
   inherit and a node past the ramp would otherwise pick up a
   shallower ancestor's easing value. `--psan-indent` is the gutter a
   node opens for its children, so the value on a node at depth D is
   the indent seen at depth D+1. */
.psan-comment {
  --psan-depth-t: 1;
  --psan-indent: calc(
    var(--psan-indent-max) - (var(--psan-indent-max) - var(--psan-indent-min)) *
      var(--psan-depth-t)
  );
}

.psan-comment[data-depth="0"] {
  --psan-depth-t: 0;
}

.psan-comment[data-depth="1"] {
  --psan-depth-t: 0.25;
}

.psan-comment[data-depth="2"] {
  --psan-depth-t: 0.5;
}

.psan-comment[data-depth="3"] {
  --psan-depth-t: 0.67;
}

.psan-comment[data-depth="4"] {
  --psan-depth-t: 0.84;
}

.psan-comment[data-depth="5"] {
  --psan-depth-t: 1;
}

/* brand.css ships no global box-sizing reset, and a universal
   selector is out of bounds here, so the border-box is applied to
   exactly the elements that combine an explicit size with padding
   or a border. */
.psan-page,
.psan-btn,
.psan-action,
.psan-badge,
.psan-kind,
.psan-chip,
.psan-kbd,
.psan-field__input,
.psan-field__select,
.psan-kindcard,
.psan-post__link,
.psan-vote__up,
.psan-vote__down,
.psan-composer,
.psan-composer__textarea,
.psan-composer__preview,
.psan-composer__tool,
.psan-composer__mentions,
.psan-sort__tab,
.psan-sort__window,
.psan-inbox__tab,
.psan-inbox__bell,
.psan-inbox__badge,
.psan-inbox__panel,
.psan-dialog__panel,
.psan-keyhelp__panel,
.psan-toasts,
.psan-toast,
.psan-comment,
.psan-comment__children,
.psan-comment__toggle {
  box-sizing: border-box;
}

/* ============ 2. PRIMITIVES ============ */

.psan-sr-only,
.psan-sort__status,
.psan-toasts__announcer {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* One focus treatment for every surface, the dashboard's: a 2px
   ink outline. A real outline, not a box-shadow, because
   forced-colors mode repaints outlines and drops shadows. */
:where(.psan-thread, .psan-dialog, .psan-keyhelp, .psan-toasts) :focus-visible,
.psan-skip-link:focus-visible {
  outline: var(--psan-bar) solid var(--psan-ink);
  outline-offset: var(--psan-bar);
}

/* `:where()` keeps these resets at element weight, so any class
   rule below wins without a fight. */
:where(.psan-thread, .psan-dialog, .psan-keyhelp) a {
  color: inherit;
  text-decoration-color: var(--psan-silver);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

:where(.psan-thread, .psan-dialog, .psan-keyhelp) a:hover {
  text-decoration-color: currentColor;
}

:where(.psan-thread, .psan-dialog, .psan-keyhelp, .psan-toasts)
  :is(button, input, select, textarea) {
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
}

:where(.psan-thread, .psan-dialog, .psan-keyhelp, .psan-toasts) button {
  cursor: pointer;
}

:where(.psan-thread, .psan-dialog) :disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

/* --- skip links: off-screen until focused, then an ink pill. A
   skip link is only ever reached from the keyboard, so `:focus`
   rather than `:focus-visible` brings it in. --- */
.psan-skip-link {
  position: fixed;
  z-index: var(--psan-z-overlay);
  inset-block-start: var(--space-3);
  inset-inline-start: var(--space-3);
  display: inline-flex;
  align-items: center;
  min-block-size: var(--psan-tap);
  padding: 0 var(--space-5);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-ink);
  color: var(--psan-canvas);
  font-family: var(--psan-font-sans);
  font-size: var(--psan-text-sm);
  font-weight: 500;
  text-decoration: none;
  transform: translateY(calc(-100% - var(--space-6)));
  transition: transform var(--psan-motion);
}

.psan-skip-link:focus {
  transform: none;
}

/* The app shell injects its own "Skip to content" (app-shell.js). These
   pages are public, so signed out they keep the sticky site navbar, which
   sits at 9999 and would cover it; it is lifted to the overlay layer. */
.psan-body .app-skip {
  z-index: var(--psan-z-overlay);
}

/* --- buttons: the dashboard's pills --- */
.psan-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-block-size: var(--psan-control);
  padding: 0 var(--space-5);
  border: var(--psan-hairline) solid var(--psan-border);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-canvas);
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  font-weight: 500;
  line-height: 1.2;
  white-space: nowrap;
  text-decoration: none;
  transition:
    background-color var(--psan-motion),
    border-color var(--psan-motion),
    color var(--psan-motion);
}

.psan-btn:hover {
  border-color: var(--psan-ink);
}

.psan-btn--primary {
  border-color: transparent;
  background: var(--psan-sage);
  color: var(--psan-on-accent);
}

.psan-btn--primary:hover {
  border-color: transparent;
  background: var(--psan-primary-hover);
}

.psan-btn--ghost {
  border-color: transparent;
  background: transparent;
}

.psan-btn--ghost:hover {
  border-color: transparent;
  background: var(--psan-hover);
}

.psan-btn--danger {
  color: var(--psan-danger);
}

.psan-btn--danger:hover {
  border-color: var(--psan-danger);
  background: var(--psan-tint-danger);
}

/* --- quiet link: prose-weight navigation ("Open in context") --- */
.psan-quiet-link {
  color: var(--psan-ink-meta);
  font-weight: 500;
  text-decoration: none;
}

.psan-quiet-link:hover {
  color: var(--psan-ink);
  text-decoration: underline;
}

/* --- labels: words, not pills. Kind, state, pinned and locked are
   graphite at weight 600. Only the OP and MOD marks keep a fill,
   because they mark a person rather than a post: sky for the
   original poster (here, yours), sage for a moderator (affirm). --- */
.psan-badge,
.psan-kind {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1.4;
  white-space: nowrap;
}

.psan-badge--op,
.psan-badge--mod,
.psan-badge--distinguished,
.psan-badge--distinguished-author {
  padding: 0 0.3125rem;
  border-radius: var(--psan-radius-sm);
  color: var(--psan-on-accent);
  font-size: var(--psan-text-xs);
  letter-spacing: var(--psan-track-caption);
}

.psan-badge--op {
  background: var(--psan-sky);
}

.psan-badge--mod,
.psan-badge--distinguished,
.psan-badge--distinguished-author {
  background: var(--psan-sage);
}

.psan-badge--removed,
.psan-badge--deleted {
  color: var(--psan-danger);
}

/* "New" is a word in ink beside a sky dot: new, here or yours. The
   dot is decoration; the word carries the state. */
.psan-badge--new {
  color: var(--psan-ink);
}

.psan-badge--new::before {
  content: "";
  inline-size: var(--psan-dot-sm);
  block-size: var(--psan-dot-sm);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-sky);
}

/* --- chip: report reasons, as graphite text with counts --- */
.psan-chip {
  display: inline-flex;
  align-items: center;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  font-weight: 500;
  line-height: 1.4;
  white-space: nowrap;
}

.psan-chip + .psan-chip::before {
  content: "·";
  content: "·" / "";
  margin-inline: var(--space-2);
}

/* --- inline metadata row, dot-separated --- */
.psan-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  row-gap: var(--space-1);
  min-inline-size: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-meta__item + .psan-meta__item::before {
  content: "·";
  content: "·" / "";
  margin-inline: var(--space-2);
  color: var(--psan-ink-meta);
}

/* --- keys --- */
.psan-kbd,
:where(.psan-thread) kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 1.5rem;
  block-size: 1.5rem;
  padding: 0 0.375rem;
  border: var(--psan-hairline) solid var(--psan-border);
  border-radius: var(--psan-radius-control);
  background: var(--psan-parchment);
  color: var(--psan-ink);
  font-family: var(--psan-font-mono);
  font-size: var(--psan-text-xs);
  letter-spacing: 0;
  line-height: 1;
}

/* --- fields: the submit form and the report dialog --- */
.psan-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-inline-size: 0;
}

.psan-field--split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
}

.psan-field--split > * {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-inline-size: 0;
}

.psan-field__label {
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  font-weight: 500;
}

.psan-field__hint {
  margin: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-field__input,
.psan-field__select,
.psan-sort__window {
  inline-size: 100%;
  min-block-size: var(--psan-tap);
  padding: 0 var(--space-4);
  border: var(--psan-hairline) solid var(--psan-field-line);
  border-radius: var(--psan-radius-field);
  background-color: var(--psan-canvas);
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  transition: border-color var(--psan-motion);
}

textarea.psan-field__input {
  min-block-size: 6rem;
  padding-block: var(--space-3);
  line-height: 1.5;
  resize: vertical;
}

.psan-field__input::placeholder {
  color: var(--psan-ink-meta);
  opacity: 1;
}

/* A drawn chevron in `currentColor`, so it follows the theme. */
.psan-field__select,
.psan-sort__window {
  padding-inline-end: var(--space-10);
  appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    calc(100% - 1.25rem) 52%,
    calc(100% - 0.9375rem) 52%;
  background-repeat: no-repeat;
  background-size: 0.3125rem 0.3125rem;
}

.psan-field__input:hover,
.psan-field__select:hover,
.psan-sort__window:hover {
  border-color: var(--psan-ink);
}

/* The border turns ink and a 1px outline doubles it: one 2px ring,
   flush with the field rather than floating off it. */
.psan-field__input:focus-visible,
.psan-field__select:focus-visible,
.psan-sort__window:focus-visible,
.psan-composer__textarea:focus-visible {
  border-color: var(--psan-ink);
  outline: var(--psan-hairline) solid var(--psan-ink);
  outline-offset: 0;
}

.psan-field__error {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--psan-radius-field);
  background: var(--psan-tint-danger);
  color: var(--psan-danger);
  font-size: var(--psan-text-sm);
}

/* ============ 3. PAGE SHELL ============ */

/* The body is the canvas, as `.dashboard-page` is on the dashboard;
   without the reset the browser's 8px margin frames every page. */
.psan-body {
  margin: 0;
  background: var(--psan-canvas);
  color: var(--psan-ink);
  font-family: var(--psan-font-sans);
}

/* The page is a size container: whether the rail fits depends on the
   page's own width, not the viewport's, so the columns are right with
   or without the app shell's sidebar beside them (section 19). */
.psan-page {
  container: psan-page / inline-size;
  inline-size: 100%;
  max-inline-size: calc(var(--psan-page-max) + 2 * var(--psan-gutter));
  margin-inline: auto;
  padding: var(--space-12) var(--psan-gutter) var(--space-20);
}

/* Deep links land under the navbar without this. */
.psan-thread [id],
.psan-comment {
  scroll-margin-block-start: calc(var(--navbar-height) + var(--space-6));
}

.psan-page__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-6);
  margin-block-end: var(--space-10);
}

/* The headings grow from a modest basis, so the actions keep their
   place beside the title wherever there is room for both. */
.psan-page__headings {
  display: flex;
  flex: 1 1 24rem;
  flex-direction: column;
  min-inline-size: 0;
  max-inline-size: 46rem;
}

.psan-eyebrow {
  margin: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-xs);
  font-weight: 500;
  letter-spacing: var(--psan-track-caption);
}

.psan-page__title {
  margin: var(--space-2) 0 0;
  color: var(--psan-ink);
  font-family: var(--psan-font-serif);
  font-size: var(--psan-text-display);
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: var(--psan-track-display);
  text-wrap: balance;
}

.psan-page__lead {
  max-inline-size: 52ch;
  margin: var(--space-4) 0 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-lg);
  line-height: 1.45;
  text-wrap: pretty;
}

.psan-page__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* The thread page's header is a breadcrumb beside Follow, Mute and the bell. */
.psan-page--thread .psan-page__head {
  flex-wrap: nowrap;
  align-items: center;
  margin-block-end: var(--space-8);
}

/* On a phone the breadcrumb takes its own line and the controls sit under
   it at the end, instead of squeezing the trail into a sliver. */
@container psan-page (max-width: 40rem) {
  .psan-page--thread .psan-page__head {
    flex-wrap: wrap;
  }

  .psan-page--thread .psan-breadcrumb {
    flex-basis: 100%;
  }

  .psan-page--thread .psan-page__actions {
    margin-inline-start: auto;
  }
}

.psan-breadcrumb {
  display: flex;
  flex: 1 1 auto;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  min-inline-size: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
}

.psan-breadcrumb a {
  color: var(--psan-ink-meta);
  text-decoration: none;
}

.psan-breadcrumb a:hover {
  color: var(--psan-ink);
  text-decoration: underline;
}

.psan-breadcrumb [aria-hidden="true"] {
  color: var(--psan-silver);
}

.psan-breadcrumb [data-breadcrumb-current] {
  flex: 1 1 10rem;
  min-inline-size: 0;
  overflow: hidden;
  color: var(--psan-ink);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- columns: main, plus a rail once the page is wide enough
   (section 19) --- */
.psan-page__body {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
  gap: var(--space-10);
}

/* The inbox reads as one column; its header keeps to the same
   width so "Mark all read" and the bell sit over the list. */
.psan-page__body--narrow,
.psan-page--inbox .psan-page__head {
  max-inline-size: 48rem;
}

.psan-page__main {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-inline-size: 0;
}

/* Every page reads on the canvas, the way the thread page always
   did: no parchment stage, no white cards on it. The list pages keep
   one reading column so rows stay scannable beside the rail. */
.psan-page--feed .psan-page__main,
.psan-page--submit .psan-page__main,
.psan-page--inbox .psan-page__main,
.psan-page--mod .psan-page__main {
  gap: var(--space-4);
  max-inline-size: var(--psan-column-max);
}

/* Rail modules are headed lists with air between them, not tiles. */
.psan-page__rail {
  display: flex;
  flex-direction: column;
  gap: var(--space-10);
  min-inline-size: 0;
}

/* --- rail modules: a small tracked label over a list --- */
.psan-rail,
.psan-rail__module {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-inline-size: 0;
}

.psan-rail__heading {
  margin: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.psan-rail__list {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  margin: 0 calc(var(--space-3) * -1);
  padding: 0;
  list-style: none;
}

.psan-rail__link {
  display: flex;
  align-items: center;
  min-block-size: 2.25rem;
  padding: 0 var(--space-3);
  border-radius: var(--psan-radius-pill);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  font-weight: 500;
  text-decoration: none;
  transition:
    background-color var(--psan-motion),
    color var(--psan-motion);
}

.psan-rail__link:hover {
  background: var(--psan-hover);
  color: var(--psan-ink);
}

.psan-rail__link[aria-current="page"] {
  background: var(--psan-selected);
  color: var(--psan-ink);
  font-weight: 600;
}

/* A room is shaded in ink, not filled, while one of its spaces is the filter:
   the space keeps the one selection pill. */
.psan-rail__link--within {
  color: var(--psan-ink);
  font-weight: 600;
}

/* The spaces inside a room hang under it as quieter filters. */
.psan-rail__sublist {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  margin: 0.125rem 0 var(--space-1);
  padding: 0;
  list-style: none;
}

.psan-rail__sublink {
  display: flex;
  align-items: center;
  min-block-size: 2rem;
  padding: 0 var(--space-3) 0 calc(var(--space-3) + var(--space-4));
  border-radius: var(--psan-radius-pill);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  text-decoration: none;
  transition:
    background-color var(--psan-motion),
    color var(--psan-motion);
}

.psan-rail__sublink:hover {
  background: var(--psan-hover);
  color: var(--psan-ink);
}

.psan-rail__sublink[aria-current="page"] {
  background: var(--psan-selected);
  color: var(--psan-ink);
  font-weight: 600;
}

.psan-rail__note {
  margin: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  line-height: 1.5;
}

.psan-rail__facts {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  line-height: 1.5;
  list-style: none;
}

.psan-rail__facts strong {
  color: var(--psan-ink);
  font-weight: 600;
}

.psan-rail__dl {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-2) var(--space-4);
  margin: 0;
  font-size: var(--psan-text-sm);
}

.psan-rail__dl dt {
  color: var(--psan-ink-meta);
}

.psan-rail__dl dd {
  margin: 0;
  min-inline-size: 0;
  color: var(--psan-ink);
  text-align: end;
  overflow-wrap: anywhere;
}

/* ============ 4. FEED ============ */

/* A ranked list people compare reads best as a plain list: rows on
   the canvas, a fog hairline between them, nothing around them. */
.psan-feed {
  display: flex;
  flex-direction: column;
  min-inline-size: 0;
  border-block-start: var(--psan-hairline) solid var(--psan-border);
}

.psan-feed__loading {
  margin: 0;
  padding: var(--space-8) 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
}

.psan-feed__more {
  display: flex;
  justify-content: center;
}

.psan-post-card {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-inline-size: 0;
  padding-block: var(--space-5);
  border-block-end: var(--psan-hairline) solid var(--psan-border);
}

/* Hover tints a field that bleeds past the text, so the row reacts
   without its text moving or a box appearing at rest. */
.psan-post-card::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0 calc(var(--space-3) * -1);
  border-radius: var(--psan-radius-control);
  transition: background-color var(--psan-motion);
}

@media (hover: hover) {
  .psan-post-card:hover::before {
    background: var(--psan-hover);
  }
}

.psan-post-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-inline-size: 0;
}

/* "Question · Legislation · TX · 2h", then any state labels. */
.psan-post-card__top {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-3);
  min-inline-size: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-post-card__title {
  margin: 0;
  color: var(--psan-ink);
  font-family: var(--psan-font-serif);
  font-size: 1.1875rem;
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: -0.01em;
  text-wrap: pretty;
  /* titles carry raw bill numbers and URLs; never let one scroll */
  overflow-wrap: anywhere;
}

/* The signal line: what people did with it, never a score. */
.psan-post-card__signals {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  row-gap: var(--space-1);
  min-inline-size: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  font-variant-numeric: tabular-nums;
}

.psan-post-card__signals > * + *::before {
  content: "·";
  content: "·" / "";
  margin-inline: var(--space-2);
}

.psan-post-card__author {
  color: var(--psan-ink);
  font-weight: 500;
}

.psan-post-card__title a {
  color: inherit;
  text-decoration: none;
}

.psan-post-card__title a:hover {
  text-decoration: underline;
}

/* The whole row is clickable, but only the title is a real link:
   the overlay keeps the accessible name on the anchor. */
.psan-post-card__title a::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* Controls sit above the title overlay or they cannot be hit. */
.psan-post-card .psan-vote,
.psan-post-card__top a,
.psan-post-card__signals a {
  position: relative;
  z-index: var(--z-base);
}

.psan-post-card__excerpt {
  margin: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  line-height: 1.5;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
}

.psan-post-card[data-removed] .psan-post-card__title {
  color: var(--psan-ink-meta);
}

/* ============ 5. POST DETAIL ============ */

.psan-thread__post {
  min-inline-size: 0;
}

.psan-post__loading {
  margin: 0;
  color: var(--psan-ink-meta);
}

.psan-post {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-inline-size: 0;
  padding-block-end: var(--space-8);
  border-block-end: var(--psan-hairline) solid var(--psan-border);
}

.psan-post__flags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.psan-post__head {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-inline-size: 0;
}

.psan-post__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  min-inline-size: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-post__space {
  color: var(--psan-ink);
  font-weight: 600;
  text-decoration: none;
}

.psan-post__state {
  color: var(--psan-ink-meta);
  font-weight: 600;
  text-decoration: none;
}

.psan-post__space:hover,
.psan-post__state:hover {
  text-decoration: underline;
}

.psan-post__byline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
}

.psan-post__by,
.psan-post__time,
.psan-post__edited {
  color: var(--psan-ink-meta);
  white-space: nowrap;
}

.psan-post__author {
  color: var(--psan-ink);
  font-weight: 500;
  text-decoration: none;
}

.psan-post__author:hover {
  text-decoration: underline;
}

.psan-post__title {
  margin: 0;
  color: var(--psan-ink);
  font-family: var(--psan-font-serif);
  font-size: var(--psan-text-title);
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: -0.025em;
  text-wrap: balance;
  overflow-wrap: anywhere;
}

.psan-post__link {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-inline-size: 0;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--psan-radius-field);
  background: var(--psan-parchment);
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  font-weight: 500;
  text-decoration: none;
  overflow-wrap: anywhere;
}

.psan-post__link:hover {
  text-decoration: underline;
}

.psan-post__body {
  max-inline-size: var(--psan-prose-max);
  font-size: var(--psan-text-md);
}

/* An action post's hand-off to the Action Center: the view's one sage pill. */
.psan-post__act {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  margin: 0;
}

.psan-post__act-note {
  max-inline-size: 28rem;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-post__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* A bare `footer` picks up the site footer's band from brand.css
   (border, background, padding, margin), so every footer in the
   thread system restates all four. */
.psan-post__actions,
.psan-comment__actions,
.psan-inbox__panel-foot {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
}

.psan-post__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  min-inline-size: 0;
}

.psan-post__count {
  margin-inline-end: var(--space-2);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  white-space: nowrap;
}

/* ============ 6. PROSE ============ */

/* Everything `renderMarkdown` produces lands here: the post body,
   each comment body, previews and the moderation queue. The rules
   use only element selectors inside those hosts, so nothing leaks
   into the surrounding site. */
.psan-prose {
  max-inline-size: var(--psan-prose-max);
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) {
  min-inline-size: 0;
  color: var(--psan-ink);
  line-height: 1.6;
  overflow-wrap: break-word;
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) > :first-child {
  margin-block-start: 0;
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) > :last-child {
  margin-block-end: 0;
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) p {
  margin-block: 0 var(--space-3);
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) strong {
  font-weight: 600;
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) del {
  color: var(--psan-ink-meta);
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) a {
  color: var(--psan-ink);
  text-decoration: underline;
  text-decoration-color: var(--psan-silver);
  /* pasted URLs are the usual 320px culprit */
  overflow-wrap: anywhere;
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) a:hover {
  text-decoration-color: currentColor;
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) code {
  padding: 0.1em 0.35em;
  border-radius: var(--psan-radius-sm);
  background: var(--psan-parchment);
  font-family: var(--psan-font-mono);
  font-size: 0.875em;
  letter-spacing: 0;
  overflow-wrap: anywhere;
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) pre {
  max-inline-size: 100%;
  margin-block: 0 var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--psan-radius-field);
  background: var(--psan-parchment);
  /* a fenced block scrolls itself rather than the page */
  overflow-x: auto;
  tab-size: 2;
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) pre code {
  display: block;
  padding: 0;
  background: none;
  font-size: var(--psan-text-meta);
  line-height: 1.55;
  white-space: pre;
  overflow-wrap: normal;
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) blockquote {
  margin-block: 0 var(--space-4);
  margin-inline: 0;
  padding-inline-start: var(--space-4);
  border-inline-start: var(--psan-bar) solid var(--psan-silver);
  color: var(--psan-ink-meta);
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) :is(ul, ol) {
  margin-block: 0 var(--space-3);
  padding-inline-start: var(--space-5);
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) li {
  margin-block-end: var(--space-1);
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) li::marker {
  color: var(--psan-ink-meta);
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) hr {
  block-size: var(--psan-hairline);
  margin-block: var(--space-6);
  border: 0;
  background: var(--psan-border);
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) h3 {
  margin-block: var(--space-5) var(--space-2);
  font-size: var(--psan-text-lg);
  font-weight: 600;
  line-height: 1.35;
}

:is(.psan-prose, .psan-post__body, .psan-comment__body) img {
  max-inline-size: 100%;
  block-size: auto;
}

/* --- mention --- */
.psan-mention {
  padding: 0 0.2em;
  border-radius: var(--psan-radius-sm);
  background: var(--psan-tint-sky);
  color: var(--psan-ink);
  font-weight: 500;
  text-decoration: none;
  overflow-wrap: anywhere;
}

.psan-mention:hover {
  text-decoration: underline;
}

/* --- tag: inline in prose it is a link; in a post's tag list it is
   a parchment pill --- */
.psan-tag {
  color: var(--psan-ink-meta);
  font-weight: 500;
  text-decoration: none;
}

.psan-tag:hover {
  color: var(--psan-ink);
  text-decoration: underline;
}

.psan-post__tag .psan-tag {
  display: inline-flex;
  align-items: center;
  min-block-size: 1.75rem;
  padding: 0 var(--space-3);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-parchment);
  font-size: var(--psan-text-meta);
}

/* --- spoiler: blurred until clicked. Blur alone is not a security
   boundary, so the background is opaque too. --- */
.psan-spoiler {
  border-radius: var(--psan-radius-sm);
  background: var(--psan-ink);
  color: transparent;
  cursor: pointer;
  filter: blur(0.2em);
  transition:
    filter var(--psan-motion),
    color var(--psan-motion),
    background-color var(--psan-motion);
  user-select: none;
}

.psan-spoiler--revealed,
.psan-spoiler[aria-expanded="true"] {
  background: var(--psan-parchment);
  color: var(--psan-ink);
  cursor: auto;
  filter: none;
  user-select: auto;
}

/* ============ 7. COMMENT TREE ============ */

.psan-thread__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3) var(--space-4);
  padding-block-start: var(--space-4);
}

.psan-thread__heading {
  margin: 0;
  color: var(--psan-ink);
  font-size: var(--psan-text-xl);
  font-weight: 500;
  line-height: 1.3;
}

.psan-tree,
[data-comment-tree] {
  display: flex;
  flex-direction: column;
  gap: var(--psan-node-gap);
  min-inline-size: 0;
}

.psan-comment {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas:
    "toggle head"
    "toggle body"
    "toggle actions"
    "toggle reply"
    "children children";
  column-gap: var(--space-2);
  min-inline-size: 0;
  border-radius: var(--psan-radius-control);
  transition: background-color var(--psan-motion);
}

/* --- toggle: the canonical collapse control. Narrow strip, wide
   hit area, the glyph centred inside it. --- */
.psan-comment__toggle {
  grid-area: toggle;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: var(--space-2);
  inline-size: var(--space-5);
  padding: 0.125rem 0 0;
  border: 0;
  background: none;
  color: var(--psan-ink-meta);
  font-family: var(--psan-font-mono);
  font-size: var(--psan-text-xs);
  letter-spacing: 0;
  line-height: 1.6;
  white-space: nowrap;
  transition: color var(--psan-motion);
}

.psan-comment__toggle:hover {
  color: var(--psan-ink);
}

.psan-comment__toggle-count {
  font-family: var(--psan-font-sans);
}

.psan-comment__toggle-count:empty {
  display: none;
}

/* --- head --- */
.psan-comment__head {
  grid-area: head;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
  min-inline-size: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  line-height: 1.6;
}

.psan-comment__author {
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  font-weight: 600;
  text-decoration: none;
  overflow-wrap: anywhere;
}

.psan-comment__author:hover {
  text-decoration: underline;
}

.psan-comment__time,
.psan-comment__edited {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.psan-comment__threshold {
  font-style: italic;
}

/* --- body --- */
.psan-comment__body {
  grid-area: body;
  padding-block: var(--space-1) var(--space-2);
  font-size: var(--psan-text-prose);
}

/* --- actions: always visible. Touch has no hover to reveal them,
   and hiding them cost a contrast step for nothing. --- */
.psan-comment__actions {
  grid-area: actions;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  min-inline-size: 0;
  margin-inline-start: calc(var(--space-2) * -1);
}

.psan-action {
  display: inline-flex;
  align-items: center;
  min-block-size: 1.75rem;
  padding: 0 var(--space-2);
  border: 0;
  border-radius: var(--psan-radius-pill);
  background: transparent;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  font-weight: 500;
  white-space: nowrap;
  transition:
    background-color var(--psan-motion),
    color var(--psan-motion);
}

.psan-action:hover {
  background: var(--psan-hover);
  color: var(--psan-ink);
}

.psan-action--delete:hover,
.psan-action--report:hover {
  color: var(--psan-danger);
}

/* --- children + thread rail --- */
.psan-comment__children {
  grid-area: children;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--psan-node-gap);
  min-inline-size: 0;
  margin-block-start: var(--psan-node-gap);
  padding-inline-start: var(--psan-indent);
  transition: background-color var(--psan-motion);
}

/* An empty children host still holds the rail button, so `:empty`
   cannot hide it; a host with no comment inside it can be. */
.psan-comment__children:not(:has(> .psan-comment, > .psan-row)) {
  display: none;
}

/* The visible 1px rail, sitting on the start edge of the subtree,
   drawn by the renderer's rail button. Its hit area is widened
   either side so a pointer can find a hairline, leaning into the
   parent's gutter freely but reaching no further into the subtree
   than the indent itself — past depth 6 the indent is smaller than
   the widening, and an unclamped zone would sit over the next node's
   own toggle. */
.psan-comment__rail {
  position: absolute;
  inset-block: 0;
  inset-inline-start: calc(var(--psan-rail-hit) * -1);
  inline-size: calc(
    var(--psan-rail-hit) + min(var(--psan-rail-hit), var(--psan-indent))
  );
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.psan-comment__rail::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--psan-rail-hit);
  inline-size: var(--psan-rail-width);
  background: var(--psan-rail);
  transition: background-color var(--psan-motion);
}

/* Hovering a rail (render.js marks the owner `--subtree-hover`) or
   the owner's toggle darkens that level's rail and tints the
   subtree it would collapse — the "click to collapse" signal. */
.psan-comment__rail:hover::after,
.psan-comment--subtree-hover > .psan-comment__children > .psan-comment__rail::after,
.psan-comment__toggle:hover ~ .psan-comment__children > .psan-comment__rail::after,
.psan-comment__toggle:focus-visible ~ .psan-comment__children > .psan-comment__rail::after {
  background: var(--psan-rail-hover);
  inline-size: var(--psan-bar);
}

.psan-comment--subtree-hover > .psan-comment__children,
.psan-comment__toggle:hover ~ .psan-comment__children,
.psan-comment__toggle:focus-visible ~ .psan-comment__children {
  background: var(--psan-subtree-tint);
}

/* ============ 8. COMMENT STATES ============ */

/* --- collapsed: one graphite line, the reply count in the toggle
   beside the glyph --- */
.psan-comment[data-collapsed="true"] > .psan-comment__body,
.psan-comment[data-collapsed="true"] > .psan-comment__actions,
.psan-comment[data-collapsed="true"] > .psan-comment__children {
  display: none;
}

.psan-comment[data-collapsed="true"] > .psan-comment__head,
.psan-comment[data-collapsed="true"] > .psan-comment__head .psan-comment__author {
  color: var(--psan-ink-meta);
}

.psan-comment[data-collapsed="true"] > .psan-comment__toggle {
  inline-size: auto;
  padding-inline: var(--space-1);
}

/* --- deleted / removed: unmistakably not a real comment --- */
.psan-comment--gone > .psan-comment__head .psan-comment__author {
  color: var(--psan-ink-meta);
  font-style: italic;
  font-weight: 400;
  pointer-events: none;
  text-decoration: none;
}

.psan-comment__placeholder,
.psan-comment__placeholder-reason {
  margin: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  font-style: italic;
}

.psan-comment[data-removed] .psan-comment__placeholder {
  color: var(--psan-danger);
}

/* Moderator-only original content behind a removal. */
.psan-comment__mod-note {
  margin: var(--space-2) 0 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--psan-radius-control);
  background: var(--psan-tint-danger);
  color: var(--psan-danger);
  font-size: var(--psan-text-meta);
}

/* --- distinguished: a sage bar and tint. The MOD badge carries the
   same fact, so the state never rests on colour. --- */
.psan-comment[data-distinguished="moderator"] {
  padding: var(--space-2) var(--space-3) var(--space-2) var(--space-2);
  border-inline-start: var(--psan-bar-strong) solid var(--psan-sage);
  background: var(--psan-tint-sage);
}

/* --- pending / optimistic. The dim is capped where graphite still
   clears 4.5:1 (see the ledger), so the pulsing bar — not the
   opacity — does the signalling. --- */
.psan-comment[data-pending="true"] {
  opacity: var(--psan-pending-opacity);
  cursor: progress;
}

.psan-comment[data-pending="true"]::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: calc(var(--space-1) * -1);
  inline-size: var(--psan-bar);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-ink);
  animation: psan-pulse 1.4s var(--ease-in-out) infinite;
}

.psan-comment[data-pending="true"] .psan-vote,
.psan-comment[data-pending="true"] .psan-comment__actions {
  pointer-events: none;
}

@keyframes psan-pulse {
  50% {
    opacity: 0.45;
  }
}

/* --- newly arrived: a flash that fades, never a permanent tint --- */
.psan-comment--new {
  animation: psan-flash 2.4s var(--ease-out) 1 forwards;
}

@keyframes psan-flash {
  0%,
  70% {
    background-color: var(--psan-tint-sky);
  }
  100% {
    background-color: transparent;
  }
}

/* --- permalink target and keyboard cursor. The tint explains why
   the page scrolled here, so it persists for as long as sort.js keeps
   the thread focused (render.js's `--highlight` is the same tint as a
   brief flash); the cursor marks where j/k stand once focus has moved
   into a node's buttons. --- */
.psan-comment--focused,
.psan-comment--highlight,
.psan-comment:target {
  background: var(--psan-tint-sky);
}

.psan-comment--cursor {
  box-shadow: inset var(--psan-bar) 0 0 0 var(--psan-ink);
}

/* composer.js opens a reply under the node's actions, and an edit in
   place of the body it hides; an error row lands in the text column
   rather than under the toggle. */
.psan-comment > .psan-composer--reply {
  grid-area: reply;
}

.psan-comment > .psan-composer--edit {
  grid-area: body;
}

.psan-comment > .psan-thread__error {
  grid-column: 2;
}

/* ============ 9. VOTE CONTROLS ============ */

/* "Helpful" (a glyph, the word and how many found it helpful), then a
   quieter "Needs work" that only the author and moderators ever see the
   effect of. In Stories the one control is "Standing with you". There is
   no net score and nothing negative anywhere. A press changes the glyph's
   shape (outline to filled) and the weight, and sets aria-pressed; the
   sage tint is only a second cue. */
.psan-vote {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  flex-shrink: 0;
}

.psan-vote__up,
.psan-vote__down {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  min-block-size: 1.75rem;
  padding: 0 var(--space-2);
  border: 0;
  border-radius: var(--psan-radius-pill);
  background: transparent;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  transition:
    background-color var(--psan-motion),
    color var(--psan-motion);
}

.psan-vote__up:hover,
.psan-vote__down:hover {
  background: var(--psan-hover);
  color: var(--psan-ink);
}

.psan-vote__glyph {
  flex: none;
  inline-size: 1rem;
  block-size: 1rem;
  pointer-events: none;
}

.psan-vote__glyph path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linejoin: round;
  transition: fill var(--psan-motion);
}

.psan-vote__score {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

.psan-vote__score:empty {
  display: none;
}

/* Pressed: the glyph fills and the words go to ink at 600. */
.psan-vote__up[aria-pressed="true"],
.psan-vote[data-my-vote="1"] .psan-vote__up {
  background: var(--psan-tint-sage);
  color: var(--psan-ink);
  font-weight: 600;
}

.psan-vote__up[aria-pressed="true"] .psan-vote__glyph path,
.psan-vote[data-my-vote="1"] .psan-vote__up .psan-vote__glyph path {
  fill: currentColor;
}

.psan-vote__down[aria-pressed="true"],
.psan-vote[data-my-vote="-1"] .psan-vote__down {
  color: var(--psan-ink);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-thickness: var(--psan-hairline);
  text-underline-offset: 0.25em;
}

/* A cast vote confirms once: the changed digit ticks up and the glyph
   swells a little, with no overshoot. Section 20 turns both off. */
.psan-vote--bump[data-vote-bump="up"] .psan-vote__glyph {
  animation: psan-glyph-press 200ms var(--ease-out) 1;
}

.psan-vote--bump .psan-vote__score {
  animation: psan-tick 150ms var(--ease-out) 1;
}

@keyframes psan-glyph-press {
  50% {
    transform: scale(1.12);
  }
}

@keyframes psan-tick {
  from {
    opacity: 0;
    transform: translateY(0.25rem);
  }
}

/* --- the post's own control: the same, a size up --- */
.psan-vote--post {
  margin-inline-end: var(--space-2);
}

.psan-vote--post .psan-vote__up,
.psan-vote--post .psan-vote__down {
  min-block-size: 2.25rem;
  padding-inline: var(--space-3);
  font-size: var(--psan-text-sm);
}

/* --- signed-out / locked: visible but inert, with a real cursor
   hint rather than a silent no-op --- */
.psan-vote[data-disabled="true"] .psan-vote__up,
.psan-vote[data-disabled="true"] .psan-vote__down,
.psan-vote__up:disabled,
.psan-vote__down:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

/* --- thank, answer, source checked, and what only the author sees --- */
.psan-action--thank[aria-pressed="true"],
.psan-action--thank:disabled {
  color: var(--psan-ink);
  font-weight: 600;
  cursor: default;
  opacity: 1;
}

.psan-badge--answer {
  color: var(--psan-ink);
}

.psan-badge--answer::before {
  content: "✓";
  content: "✓" / "";
}

/* Sage marks an affirmation: the dot decorates, the words carry it. */
.psan-badge--checked {
  color: var(--psan-ink);
  font-weight: 500;
}

.psan-badge--checked::before {
  content: "";
  inline-size: var(--psan-dot-sm);
  block-size: var(--psan-dot-sm);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-sage);
}

.psan-private-note {
  flex-basis: 100%;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  font-style: italic;
}

/* A question's accepted answer, pinned under it on a sage rule. */
.psan-answered {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-inline-size: var(--psan-prose-max);
  padding-inline-start: var(--space-4);
  border-inline-start: var(--psan-bar-strong) solid var(--psan-sage);
}

.psan-answered__label,
.psan-answered__text {
  margin: 0;
  font-size: var(--psan-text-sm);
}

.psan-answered__label {
  color: var(--psan-ink-meta);
}

.psan-answered__label strong {
  color: var(--psan-ink);
  font-weight: 600;
}

.psan-answered__text {
  color: var(--psan-ink);
  line-height: 1.55;
}

.psan-answered .psan-quiet-link {
  align-self: flex-start;
  font-size: var(--psan-text-sm);
}

/* ============ 9b. THE CIVIC LOOP ============ */

/* One unboxed line of norms at the top of a thread, written for its space. */
.psan-norms {
  max-inline-size: var(--psan-prose-max);
  margin: 0 0 var(--space-4);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  line-height: 1.5;
}

.psan-norms a {
  color: var(--psan-ink);
  font-weight: 500;
}

/* Held for a privacy check: said to its author on a sky rule (sky is "yours"). */
.psan-held {
  max-inline-size: var(--psan-prose-max);
  margin: 0 0 var(--space-4);
  padding-inline-start: var(--space-4);
  border-inline-start: var(--psan-bar-strong) solid var(--psan-sky);
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  line-height: 1.5;
}

.psan-comment > .psan-held {
  margin-block: var(--space-2);
}

/* A story's consent, in its author's words. */
.psan-post__consent {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-3);
  max-inline-size: var(--psan-prose-max);
  margin: var(--space-3) 0 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  line-height: 1.5;
}

/* The action: on a sage rule like an accepted answer, never in a box. */
.psan-act {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-inline-size: var(--psan-prose-max);
  margin-block-start: var(--space-5);
  padding-inline-start: var(--space-4);
  border-inline-start: var(--psan-bar-strong) solid var(--psan-sage);
}

.psan-act > * {
  margin: 0;
}

.psan-act__eyebrow {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.psan-act__eyebrow .psan-act__undo {
  letter-spacing: normal;
  text-transform: none;
}

.psan-act__ask {
  color: var(--psan-ink);
  font-family: var(--psan-font-serif);
  font-size: var(--psan-text-xl);
  line-height: 1.3;
  letter-spacing: var(--psan-track);
}

.psan-act__facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: var(--space-1) var(--space-4);
  font-size: var(--psan-text-sm);
  line-height: 1.5;
}

.psan-act__fact {
  display: contents;
}

.psan-act__fact dt {
  color: var(--psan-ink-meta);
}

.psan-act__fact dd {
  margin: 0;
  color: var(--psan-ink);
  overflow-wrap: anywhere;
}

.psan-act__fact a,
.psan-billcard__source a,
.psan-billcard__eyebrow a {
  color: var(--psan-ink);
  font-weight: 500;
}

.psan-act__points summary {
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  font-weight: 500;
  cursor: pointer;
}

.psan-act__points ul {
  margin: var(--space-2) 0 0;
  padding-inline-start: var(--space-5);
  font-size: var(--psan-text-sm);
  line-height: 1.55;
}

.psan-act__points-note {
  margin: var(--space-2) 0 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-act__buttons {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

.psan-act__did[aria-pressed="true"] {
  background: var(--psan-tint-sage);
  font-weight: 600;
}

.psan-act__did .psan-vote__glyph path {
  stroke-width: 2.25;
}

/* A quiet text button: Undo, Edit the action, Update the outcome. */
.psan-act__undo {
  padding: 0;
  border: 0;
  background: none;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  font-weight: 500;
  text-decoration: underline;
  text-decoration-color: var(--psan-silver);
  text-underline-offset: 0.2em;
  cursor: pointer;
}

.psan-act__undo:hover {
  color: var(--psan-ink);
  text-decoration-color: currentColor;
}

.psan-act__count,
.psan-act__replies,
.psan-act__note {
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  line-height: 1.5;
}

.psan-act__count {
  color: var(--psan-ink);
  font-variant-numeric: tabular-nums;
}

.psan-act__followup {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.psan-act__followup-q {
  margin: 0;
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  font-weight: 500;
}

.psan-act__followup-choices {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.psan-act__passed {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
}

.psan-act__passed p {
  margin: 0;
}

.psan-pill {
  display: inline-flex;
  align-items: center;
  min-block-size: 2rem;
  padding: 0 var(--space-3);
  border: var(--psan-hairline) solid var(--psan-border);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-canvas);
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  font-weight: 500;
  cursor: pointer;
  transition: border-color var(--psan-motion), background-color var(--psan-motion);
}

.psan-pill:hover {
  border-color: var(--psan-ink);
}

/* The outcome: what happened, losses included, on an ink rule. */
.psan-outcome {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  max-inline-size: var(--psan-prose-max);
  margin-block-start: var(--space-4);
  padding-inline-start: var(--space-4);
  border-inline-start: var(--psan-bar-strong) solid var(--psan-ink);
  font-size: var(--psan-text-sm);
}

.psan-outcome > * {
  margin: 0;
}

.psan-outcome__label,
.psan-outcome__count {
  color: var(--psan-ink-meta);
}

.psan-outcome__label strong {
  color: var(--psan-ink);
  font-weight: 600;
}

.psan-outcome__note {
  color: var(--psan-ink);
  line-height: 1.55;
}

.psan-outcome .psan-act__undo {
  align-self: flex-start;
}

/* The bill: its timeline from the committee calendar and the bill record. */
.psan-billcard {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-inline-size: var(--psan-prose-max);
  margin-block-start: var(--space-6);
  padding-block-start: var(--space-5);
  border-block-start: var(--psan-hairline) solid var(--psan-border);
}

.psan-billcard > * {
  margin: 0;
}

.psan-billcard__eyebrow {
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-billcard__key {
  font-weight: 600;
}

.psan-billcard__title {
  color: var(--psan-ink);
  font-family: var(--psan-font-serif);
  font-size: var(--psan-text-lg);
  line-height: 1.35;
}

.psan-billcard__empty,
.psan-billcard__source {
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  line-height: 1.5;
}

.psan-timeline {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
}

.psan-timeline__item {
  position: relative;
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr);
  gap: var(--space-3);
  padding: var(--space-2) 0 var(--space-2) var(--space-5);
  font-size: var(--psan-text-sm);
}

/* A dot per event on a hairline rail; an upcoming one is filled sky and says so in words. */
.psan-timeline__item::before {
  position: absolute;
  inset-block-start: calc(var(--space-2) + 0.45em);
  inset-inline-start: 0;
  inline-size: var(--psan-dot);
  block-size: var(--psan-dot);
  border: var(--psan-hairline) solid var(--psan-graphite);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-canvas);
  content: "";
}

.psan-timeline__item::after {
  position: absolute;
  inset-block: calc(var(--space-2) + 0.45em + var(--psan-dot)) calc(-1 * (var(--space-2) + 0.45em));
  inset-inline-start: calc((var(--psan-dot) - var(--psan-hairline)) / 2);
  inline-size: var(--psan-hairline);
  background: var(--psan-border);
  content: "";
}

.psan-timeline__item:last-child::after {
  display: none;
}

.psan-timeline__item--ahead::before {
  border-color: var(--psan-ink);
  background: var(--psan-sky);
}

.psan-timeline__date {
  color: var(--psan-ink-meta);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.psan-timeline__text {
  color: var(--psan-ink);
  line-height: 1.45;
}

.psan-timeline__ahead {
  color: var(--psan-ink-meta);
}

/* Feed rows: an action's state in words, never a countdown. */
.psan-post-card__civic {
  color: var(--psan-ink);
  font-weight: 500;
}

/* Dialog bodies for the civic forms. */
.psan-actionform {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.psan-planform {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
}

.psan-editform textarea {
  min-block-size: 12rem;
}

.psan-idcheck {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  font-size: var(--psan-text-sm);
}

.psan-idcheck__list {
  margin: 0;
  padding-inline-start: var(--space-5);
  line-height: 1.6;
}

.psan-idcheck__note {
  margin: 0;
  color: var(--psan-ink-meta);
}

/* Submit: the action's and the story's fields, grouped by a rule, not a box. */
.psan-submit__group {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-inline-size: 0;
  margin: 0;
  padding: var(--space-5) 0 0;
  border: 0;
  border-block-start: var(--psan-hairline) solid var(--psan-border);
}

.psan-submit__group[hidden] {
  display: none;
}

.psan-field__input--short {
  max-inline-size: 16rem;
}

.psan-consent {
  display: flex;
  flex-direction: column;
}

.psan-consent__option {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-block-end: var(--psan-hairline) solid var(--psan-border);
  cursor: pointer;
}

.psan-consent__option:last-child {
  border-block-end: 0;
}

.psan-consent__option input {
  margin-block-start: 0.2em;
  accent-color: var(--psan-ink);
}

.psan-consent__text {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.psan-consent__label {
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  font-weight: 500;
}

.psan-consent__hint {
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

@media (pointer: coarse) {
  .psan-pill,
  .psan-act__undo {
    min-block-size: var(--psan-tap);
  }
}

@media (max-width: 40rem) {
  .psan-act__facts {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }

  .psan-act__fact dd {
    margin-block-end: var(--space-2);
  }

  .psan-timeline__item {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }

  .psan-planform {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ============ 10. COMPOSER ============ */

.psan-composer-shell {
  min-inline-size: 0;
}

.psan-composer {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-inline-size: 0;
}

/* The composer is the field itself. Its border is the one boundary a
   text input needs (field line, 3.5:1); no card sits around it. */
.psan-composer--reply,
.psan-composer--edit {
  margin-block: var(--space-2);
}

/* At rest the thread's composer is one line; it opens on focus, or
   whenever it holds text, and its tools and buttons come with it. */
.psan-composer--root .psan-composer__textarea {
  min-block-size: 3rem;
  transition:
    border-color var(--psan-motion),
    min-block-size var(--psan-motion);
}

.psan-composer--root:focus-within .psan-composer__textarea,
.psan-composer--root .psan-composer__textarea:not(:placeholder-shown) {
  min-block-size: 7.5rem;
}

.psan-composer--root:not(:focus-within):has(.psan-composer__textarea:placeholder-shown)
  :is(.psan-composer__toolbar, .psan-composer__actions, .psan-composer__hint) {
  display: none;
}

.psan-submit .psan-composer {
  padding: 0;
  background: none;
}

.psan-composer__label {
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  font-weight: 600;
}

.psan-composer__field {
  position: relative;
  min-inline-size: 0;
}

.psan-composer__textarea {
  display: block;
  inline-size: 100%;
  min-block-size: 6rem;
  /* the field grows with its content; `field-sizing` where it is
     supported, and composer.js sets the inline height elsewhere */
  field-sizing: content;
  max-block-size: 60vh;
  padding: var(--space-3) var(--space-4);
  border: var(--psan-hairline) solid var(--psan-field-line);
  border-radius: var(--psan-radius-field);
  background: var(--psan-canvas);
  color: var(--psan-ink);
  font-size: var(--psan-text-prose);
  line-height: 1.55;
  resize: vertical;
  transition: border-color var(--psan-motion);
}

.psan-composer__textarea:hover {
  border-color: var(--psan-ink);
}

.psan-composer__textarea::placeholder {
  color: var(--psan-ink-meta);
  opacity: 1;
}

.psan-composer__textarea:disabled {
  background: var(--psan-parchment);
  color: var(--psan-ink-meta);
  resize: none;
}

.psan-submit .psan-composer__textarea {
  min-block-size: 10rem;
}

/* --- toolbar --- */
.psan-composer__toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.125rem;
  min-inline-size: 0;
}

.psan-composer__toolbar-spacer {
  flex: 1 1 auto;
}

.psan-composer__tool {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 2rem;
  block-size: 2rem;
  padding: 0 var(--space-2);
  border: 0;
  border-radius: var(--psan-radius-control);
  background: transparent;
  color: var(--psan-ink-meta);
  transition:
    background-color var(--psan-motion),
    color var(--psan-motion);
}

.psan-composer__tool:hover {
  background: var(--psan-hover);
  color: var(--psan-ink);
}

.psan-composer__tool[aria-pressed="true"] {
  background: var(--psan-selected);
  color: var(--psan-ink);
}

.psan-composer__tool-glyph {
  font-size: var(--psan-text-meta);
  font-weight: 600;
  line-height: 1;
}

.psan-composer__tool[data-md="italic"] .psan-composer__tool-glyph {
  font-style: italic;
}

.psan-composer__tool[data-md="code"] .psan-composer__tool-glyph {
  font-family: var(--psan-font-mono);
}

.psan-composer__preview-toggle {
  min-block-size: 2rem;
  padding: 0 var(--space-4);
  border: var(--psan-hairline) solid var(--psan-border);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-canvas);
  color: var(--psan-ink);
  font-size: var(--psan-text-meta);
  font-weight: 500;
  transition: border-color var(--psan-motion);
}

.psan-composer__preview-toggle:hover {
  border-color: var(--psan-ink);
}

.psan-composer__preview-toggle[aria-pressed="true"] {
  border-color: var(--psan-selected);
  background: var(--psan-selected);
}

/* --- preview --- */
.psan-composer__preview {
  min-inline-size: 0;
  min-block-size: 6rem;
  padding: var(--space-3) var(--space-4);
  border: var(--psan-hairline) dashed var(--psan-field-line);
  border-radius: var(--psan-radius-field);
  background: var(--psan-canvas);
}

.psan-composer__preview-empty {
  margin: 0;
  color: var(--psan-ink-meta);
  font-style: italic;
}

/* --- mention autocomplete. composer.js positions it inline. --- */
.psan-composer__mentions {
  z-index: var(--z-dropdown);
  inline-size: max-content;
  min-inline-size: 14rem;
  max-inline-size: min(20rem, 100%);
  max-block-size: 14rem;
  margin: 0;
  padding: var(--space-1);
  border: var(--psan-hairline) solid var(--psan-border);
  border-radius: var(--psan-radius-field);
  background: var(--psan-canvas);
  list-style: none;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.psan-composer__mention {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-inline-size: 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--psan-radius-control);
  color: var(--psan-ink);
  cursor: pointer;
  font-size: var(--psan-text-sm);
}

.psan-composer__mention:hover,
.psan-composer__mention--active {
  background: var(--psan-selected);
}

.psan-composer__mention-handle {
  font-weight: 600;
  overflow-wrap: anywhere;
}

.psan-composer__mention-name {
  min-inline-size: 0;
  overflow: hidden;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- hint, counter, errors, draft notice --- */
.psan-composer__hint,
.psan-composer__counter,
.psan-composer__status {
  margin: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-composer__counter {
  font-variant-numeric: tabular-nums;
}

.psan-composer__counter[data-state="warn"] {
  color: var(--psan-warning);
  font-weight: 600;
}

.psan-composer__counter[data-state="error"] {
  color: var(--psan-danger);
  font-weight: 600;
}

.psan-composer__error {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--psan-radius-control);
  background: var(--psan-tint-danger);
  color: var(--psan-danger);
  font-size: var(--psan-text-sm);
}

.psan-composer__draft {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--psan-radius-control);
  background: var(--psan-canvas);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-composer__draft-discard {
  margin-inline-start: auto;
  padding: 0;
  border: 0;
  background: none;
  color: var(--psan-ink);
  font-weight: 500;
  text-decoration: underline;
  text-decoration-color: var(--psan-silver);
  text-underline-offset: 0.2em;
}

.psan-composer__draft-discard:hover {
  text-decoration-color: currentColor;
}

.psan-composer__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  min-inline-size: 0;
}

/* Submit and cancel are the dashboard's pills; composer.js gives
   them their own classes rather than `.psan-btn`. */
.psan-composer__submit,
.psan-composer__cancel,
.psan-composer__gate-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: var(--psan-control);
  padding: 0 var(--space-5);
  border: 0;
  border-radius: var(--psan-radius-pill);
  font-size: var(--psan-text-sm);
  font-weight: 500;
  transition: background-color var(--psan-motion);
}

.psan-composer__submit,
.psan-composer__gate-action {
  background: var(--psan-sage);
  color: var(--psan-on-accent);
}

.psan-composer__submit:hover,
.psan-composer__gate-action:hover {
  background: var(--psan-primary-hover);
}

.psan-composer__cancel {
  background: transparent;
  color: var(--psan-ink);
}

.psan-composer__cancel:hover {
  background: var(--psan-hover);
}

.psan-composer[data-composer-state="sending"] .psan-composer__submit {
  cursor: progress;
}

/* --- locked / signed-out / banned: the composer is replaced, not
   disabled, so the next step stays obvious --- */
.psan-composer__gate {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3) var(--space-4);
  padding-block: var(--space-3);
  border-block: var(--psan-hairline) solid var(--psan-border);
}

.psan-composer__gate-text {
  margin: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
}

/* ============ 11. CHROME ============ */

/* --- the feed's active filters, as a sentence with one way out --- */
.psan-filterline {
  margin: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  line-height: 1.5;
}

/* --- sort tabs: the dashboard's pill tabs. sort.js renders its own
   `.psan-sort` inside the `[data-sort-root]` host, so neither level
   carries a border or padding that could double up. --- */
.psan-sort {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  min-inline-size: 0;
}

.psan-sort__tabs,
.psan-inbox__tabs {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-inline-size: 0;
  max-inline-size: 100%;
  /* the tab strip scrolls itself at 320px rather than the page */
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}

.psan-sort__tabs::-webkit-scrollbar,
.psan-inbox__tabs::-webkit-scrollbar {
  display: none;
}

.psan-sort__tabs--pending {
  cursor: progress;
}

.psan-sort__tab,
.psan-inbox__tab {
  flex: none;
  min-block-size: 2.25rem;
  padding: 0 var(--space-4);
  border: 0;
  border-radius: var(--psan-radius-pill);
  background: transparent;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  font-weight: 500;
  white-space: nowrap;
  transition:
    background-color var(--psan-motion),
    color var(--psan-motion);
}

.psan-sort__tab:hover,
.psan-inbox__tab:hover {
  color: var(--psan-ink);
}

.psan-sort__tab[aria-selected="true"],
.psan-inbox__tab[aria-selected="true"] {
  background: var(--psan-selected);
  color: var(--psan-ink);
  font-weight: 600;
}

.psan-sort__window-label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-sort__window {
  inline-size: auto;
  min-block-size: 2.25rem;
  padding-inline-start: var(--space-3);
  border-radius: var(--psan-radius-pill);
  font-size: var(--psan-text-meta);
}

/* --- live: a status line, and a "N new replies" pill that merges on
   click. Neither floats over the tree, so neither can cover it. The
   pill is sky, not sage: it announces something new, it is not the
   view's primary action. --- */
.psan-live {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  min-inline-size: 0;
}

.psan-live__pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-block-size: 2.25rem;
  padding: 0 var(--space-4);
  border: 0;
  border-radius: var(--psan-radius-pill);
  background: var(--psan-sky);
  color: var(--psan-on-accent);
  font-size: var(--psan-text-sm);
  font-weight: 500;
  animation: psan-rise 180ms var(--ease-out) 1;
  transition: background-color var(--psan-motion);
}

.psan-live__pill:hover {
  background: color-mix(in srgb, var(--psan-sky) 86%, var(--psan-on-accent));
}

.psan-live__status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-inline-start: auto;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-xs);
  letter-spacing: var(--psan-track-caption);
}

.psan-live__dot {
  inline-size: var(--psan-dot-sm);
  block-size: var(--psan-dot-sm);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-live);
  animation: psan-pulse 2s var(--ease-in-out) infinite;
}

.psan-live__status[data-state="reconnecting"] .psan-live__dot {
  background: var(--psan-warning);
  animation: none;
}

.psan-live__status[data-state="offline"] .psan-live__dot {
  background: var(--psan-danger);
  animation: none;
}

@keyframes psan-rise {
  from {
    opacity: 0;
    transform: translateY(0.25rem);
  }
}

/* --- load more / continue thread / retry. These read as prose
   links: they are navigation inside a document, not commands. --- */
.psan-row {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  gap: var(--space-2);
  min-block-size: 2rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--psan-ink);
  font-size: var(--psan-text-meta);
  font-weight: 500;
  text-decoration: none;
}

.psan-row:hover {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.psan-row[data-loading="true"] {
  color: var(--psan-ink-meta);
  cursor: progress;
}

/* --- banners: "viewing a single thread", and the thread page's own
   context banner, which sort.js adopts for the same message. A line
   of text with its links, not a box. --- */
.psan-single-thread,
.psan-context-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
}

.psan-single-thread__text {
  margin: 0;
  font-weight: 500;
}

.psan-single-thread__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
}

.psan-single-thread__actions a {
  font-weight: 500;
  text-decoration: underline;
}

/* --- follow and mute, beside the bell. Text buttons: the pressed one
   reads "Following" / "Muted" in ink at 600 and sets aria-pressed. --- */
.psan-follow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.psan-follow__btn {
  min-block-size: var(--psan-control);
  padding: 0 var(--space-4);
  border: 0;
  border-radius: var(--psan-radius-pill);
  background: transparent;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  font-weight: 500;
  white-space: nowrap;
  transition:
    background-color var(--psan-motion),
    color var(--psan-motion);
}

.psan-follow__btn:hover {
  background: var(--psan-hover);
  color: var(--psan-ink);
}

.psan-follow__btn[aria-pressed="true"] {
  background: var(--psan-selected);
  color: var(--psan-ink);
  font-weight: 600;
}

/* --- "3 new since your last visit" beside the comments heading --- */
.psan-newsince {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-block-size: 2rem;
  margin-inline-end: auto;
  padding: 0;
  border: 0;
  background: none;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  text-decoration: underline;
  text-decoration-color: var(--psan-silver);
  text-underline-offset: 0.2em;
}

.psan-newsince:hover {
  color: var(--psan-ink);
  text-decoration-color: currentColor;
}

/* --- one calm line after posting: live, followed, a host will read it --- */
.psan-welcome {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-1);
  margin: 0 0 var(--space-6);
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  line-height: 1.5;
  animation: psan-rise 300ms var(--ease-out) 1;
}

.psan-welcome a {
  font-weight: 500;
}

.psan-welcome__quiet {
  padding: 0;
  border: 0;
  background: none;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  text-decoration: underline;
  text-decoration-color: var(--psan-silver);
  text-underline-offset: 0.2em;
}

.psan-welcome__quiet:hover {
  color: var(--psan-ink);
  text-decoration-color: currentColor;
}

.psan-welcome__close {
  inline-size: 1.75rem;
  block-size: 1.75rem;
  margin-inline-start: auto;
  padding: 0;
  border: 0;
  border-radius: var(--psan-radius-pill);
  background: none;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-lg);
  line-height: 1;
}

.psan-welcome__close:hover {
  background: var(--psan-hover);
  color: var(--psan-ink);
}

/* ============ 12. SUBMIT FORM ============ */

/* The form's fields sit on the canvas; their borders are the only
   boundaries it needs. */
.psan-submit {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

.psan-submit__kinds {
  min-inline-size: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.psan-submit__legend {
  margin-block-end: var(--space-3);
  padding: 0;
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  font-weight: 500;
}

/* Kinds are a radio list: a ring beside each choice, ink and filled
   on the chosen one, the label and blurb as plain text. */
.psan-kindgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 0 var(--space-6);
}

.psan-kindcard {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.125rem;
  min-inline-size: 0;
  padding: var(--space-3) var(--space-3) var(--space-3) calc(var(--space-3) + 1.75rem);
  border: 0;
  border-radius: var(--psan-radius-control);
  background: none;
  color: var(--psan-ink);
  text-align: start;
  transition: background-color var(--psan-motion);
}

.psan-kindcard:hover {
  background: var(--psan-hover);
}

.psan-kindcard::before {
  content: "";
  position: absolute;
  inset-block-start: calc(var(--space-3) + 0.125rem);
  inset-inline-start: var(--space-3);
  box-sizing: border-box;
  inline-size: 1.125rem;
  block-size: 1.125rem;
  border: var(--psan-bar) solid var(--psan-field-line);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-canvas);
}

/* Chosen: the ring turns ink and fills around a canvas centre, so the
   state is a shape, not a tint. */
.psan-kindcard--active::before,
.psan-kindcard[aria-checked="true"]::before {
  border: 0.3125rem solid var(--psan-ink);
}

.psan-kindcard__label {
  font-size: var(--psan-text-sm);
  font-weight: 600;
}

.psan-kindcard__blurb {
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  line-height: 1.4;
}

.psan-submit__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* ============ 13. SKELETONS AND EMPTY STATES ============ */

/* The dashboard's skeleton: fog bars that breathe. */
.psan-skeletons {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

.psan-skeleton {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-inline-size: 0;
}

.psan-skeleton-row {
  padding-block: var(--space-5);
}

.psan-skeleton-row + .psan-skeleton-row {
  border-block-start: var(--psan-hairline) solid var(--psan-border);
}

.psan-skeleton--comment[data-depth="1"] {
  padding-inline-start: var(--space-6);
}

.psan-skeleton--comment[data-depth="2"] {
  padding-inline-start: var(--space-12);
}

.psan-skeleton-line,
.psan-skeleton__bar,
.psan-inbox__skeleton-line,
.psan-inbox__skeleton-dot {
  display: block;
  block-size: 0.75rem;
  border-radius: var(--psan-radius-pill);
  background: var(--psan-fog);
  animation: psan-pulse 1.6s var(--ease-in-out) infinite;
}

.psan-skeleton-line {
  inline-size: 86%;
}

.psan-skeleton-line:first-child {
  block-size: 1rem;
  inline-size: 64%;
}

.psan-skeleton-line--short {
  inline-size: 40%;
}

.psan-skeleton--post .psan-skeleton__bar:nth-child(2) {
  block-size: 1.75rem;
}

/* --- empty and error states: a sentence and an action --- */
.psan-empty,
.psan-thread__empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-10) 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  text-align: center;
}

.psan-empty p {
  margin: 0;
}

.psan-empty--error p {
  color: var(--psan-danger);
}

.psan-empty__title {
  margin: 0;
  color: var(--psan-ink);
  font-size: var(--psan-text-lg);
  font-weight: 500;
}

.psan-empty__body {
  max-inline-size: 34rem;
  margin: 0;
}

/* The empty thread's "Start the discussion" is the page's primary
   action, so it takes the sage pill rather than the text action. */
.psan-thread__empty .psan-action {
  min-block-size: var(--psan-control);
  padding: 0 var(--space-5);
  background: var(--psan-sage);
  color: var(--psan-on-accent);
  font-size: var(--psan-text-sm);
}

.psan-thread__empty .psan-action:hover {
  background: var(--psan-primary-hover);
  color: var(--psan-on-accent);
}

.psan-thread__error {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--psan-radius-field);
  background: var(--psan-tint-danger);
  color: var(--psan-danger);
  font-size: var(--psan-text-sm);
}

.psan-thread__error-text {
  margin: 0;
}

.psan-thread__error .psan-row {
  color: inherit;
}

/* ============ 14. TOASTS ============ */

/* The dashboard's toast: an ink card at the bottom right. The kind
   shows as a dot beside the message, which already says it. */
.psan-toasts {
  position: fixed;
  z-index: calc(var(--psan-z-overlay) + 1);
  inset-block-end: var(--space-6);
  inset-inline-end: var(--space-6);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
  max-inline-size: min(24rem, calc(100vw - 2 * var(--space-6)));
  pointer-events: none;
}

.psan-toast {
  --psan-toast-dot: var(--psan-canvas);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  max-inline-size: 100%;
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-5);
  border-radius: var(--psan-radius-card);
  background: var(--psan-ink);
  color: var(--psan-canvas);
  font-size: var(--psan-text-sm);
  font-weight: 500;
  pointer-events: auto;
  overflow-wrap: anywhere;
  animation: psan-rise 180ms var(--ease-out) 1;
  transition:
    opacity 180ms var(--ease-out),
    transform 180ms var(--ease-out);
}

.psan-toast::before {
  content: "";
  flex: none;
  inline-size: var(--psan-dot);
  block-size: var(--psan-dot);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-toast-dot);
}

.psan-toast--success {
  --psan-toast-dot: var(--psan-sage);
}

.psan-toast--info {
  --psan-toast-dot: var(--psan-sky);
}

.psan-toast--warning {
  --psan-toast-dot: var(--psan-warning-on-ink);
}

.psan-toast--error {
  --psan-toast-dot: var(--psan-danger-on-ink);
}

.psan-toast[data-state="leaving"] {
  opacity: 0;
  transform: translateY(0.5rem);
}

.psan-toast__message {
  min-inline-size: 0;
}

.psan-toast__dismiss {
  display: inline-grid;
  flex: none;
  place-items: center;
  inline-size: 1.75rem;
  block-size: 1.75rem;
  padding: 0;
  border: 0;
  border-radius: var(--psan-radius-pill);
  background: transparent;
  color: inherit;
  font-size: var(--psan-text-lg);
  line-height: 1;
}

.psan-toast__dismiss:hover {
  background: color-mix(in srgb, var(--psan-canvas) 14%, transparent);
}

.psan-toasts :focus-visible {
  outline-color: var(--psan-canvas);
}

/* ============ 15. DIALOGS ============ */

/* index.js builds the confirm and report dialogs, keys.js the key
   help; both are a fixed overlay holding a scrim and a panel. keys.js
   also injects zero-specificity fallbacks for its own, which these
   rules replace. */
.psan-dialog,
.psan-keyhelp {
  position: fixed;
  z-index: var(--psan-z-overlay);
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
}

.psan-dialog__backdrop,
.psan-keyhelp__backdrop {
  position: absolute;
  inset: 0;
  background: var(--psan-scrim);
}

.psan-dialog__panel,
.psan-keyhelp__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  inline-size: min(30rem, 100%);
  max-block-size: calc(100vh - 2 * var(--space-4));
  padding: var(--space-6);
  border: var(--psan-hairline) solid var(--psan-card-border);
  border-radius: var(--psan-radius-stage);
  background: var(--psan-canvas);
  color: var(--psan-ink);
  overflow: auto;
}

.psan-keyhelp__panel {
  inline-size: min(40rem, 100%);
}

.psan-dialog__panel:focus-visible,
.psan-keyhelp__panel:focus-visible {
  outline-offset: calc(var(--psan-bar) * -1);
}

.psan-dialog__title,
.psan-keyhelp__title {
  margin: 0;
  color: var(--psan-ink);
  font-size: var(--psan-text-xl);
  font-weight: 500;
  line-height: 1.3;
}

.psan-dialog__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
}

.psan-dialog__body p {
  margin: 0;
}

.psan-dialog__description {
  margin: calc(var(--space-3) * -1) 0 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  line-height: 1.5;
}

/* --- moderator menu: a list of verbs, each a full-width row --- */
.psan-modmenu {
  display: flex;
  flex-direction: column;
  margin: 0 calc(var(--space-3) * -1);
  padding: 0;
  list-style: none;
}

.psan-modmenu__item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.125rem;
  inline-size: 100%;
  min-block-size: 2.75rem;
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--psan-radius-control);
  background: none;
  color: var(--psan-ink);
  font: inherit;
  text-align: start;
  cursor: pointer;
  transition: background-color var(--psan-motion);
}

.psan-modmenu__item:hover {
  background: var(--psan-hover);
}

.psan-modmenu__label {
  font-size: var(--psan-text-sm);
  font-weight: 600;
}

.psan-modmenu__item--danger .psan-modmenu__label {
  color: var(--psan-danger);
}

.psan-modmenu__hint {
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-dialog__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-2);
}

/* --- keyboard help: a reference, grouped, in columns --- */
.psan-keyhelp__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.psan-keyhelp__close {
  min-block-size: 2.25rem;
  min-inline-size: 0;
  padding: 0 var(--space-4);
  border: var(--psan-hairline) solid var(--psan-border);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-canvas);
  color: var(--psan-ink);
  font-size: var(--psan-text-meta);
  font-weight: 500;
}

.psan-keyhelp__close:hover {
  border-color: var(--psan-ink);
}

.psan-keyhelp__body {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-5) var(--space-8);
}

.psan-keyhelp__group {
  min-inline-size: 0;
  margin: 0;
}

.psan-keyhelp__group-title {
  margin: 0 0 var(--space-2);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-xs);
  font-weight: 500;
  letter-spacing: var(--psan-track-caption);
  text-transform: none;
}

.psan-keyhelp__table {
  inline-size: 100%;
  border-collapse: collapse;
  font-size: var(--psan-text-sm);
}

.psan-keyhelp__table :is(th, td) {
  padding: var(--space-2) 0;
  border-block-end: var(--psan-hairline) solid var(--psan-border);
  font-weight: 400;
  text-align: start;
  vertical-align: middle;
}

.psan-keyhelp__table th {
  inline-size: 7rem;
  padding-inline-end: var(--space-3);
  white-space: nowrap;
}

.psan-keyhelp__table td {
  color: var(--psan-ink-meta);
}

.psan-keyhelp__joiner {
  padding: 0 var(--space-1);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-xs);
}

.psan-keyhelp__note {
  margin: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

/* ============ 16. INBOX ============ */

/* --- bell: an icon button in the page header. It earns a tint on
   hover and while its popover is open, and no ring at rest. --- */
.psan-inbox--bell {
  position: relative;
  display: inline-flex;
}

.psan-inbox__bell {
  position: relative;
  display: inline-grid;
  place-items: center;
  inline-size: var(--psan-tap);
  block-size: var(--psan-tap);
  padding: 0;
  border: 0;
  border-radius: var(--psan-radius-pill);
  background: transparent;
  color: var(--psan-ink);
  font-size: var(--psan-text-lg);
  line-height: 1;
  transition: background-color var(--psan-motion);
}

.psan-inbox__bell:hover {
  background: var(--psan-hover);
}

.psan-inbox__bell[aria-expanded="true"] {
  background: var(--psan-selected);
}

.psan-inbox__badge {
  position: absolute;
  inset-block-start: -0.25rem;
  inset-inline-end: -0.25rem;
  min-inline-size: 1.25rem;
  block-size: 1.25rem;
  padding: 0 0.3125rem;
  border: var(--psan-bar) solid var(--psan-canvas);
  border-radius: var(--psan-radius-pill);
  background: var(--psan-ink);
  color: var(--psan-canvas);
  font-size: 0.6875rem;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1rem;
  text-align: center;
}

/* --- dropdown panel: the dashboard's filter popover --- */
.psan-inbox__panel {
  position: absolute;
  z-index: var(--z-dropdown);
  inset-block-start: calc(100% + var(--space-2));
  inset-inline-end: 0;
  display: flex;
  flex-direction: column;
  inline-size: min(25rem, calc(100vw - 2 * var(--psan-gutter)));
  max-block-size: min(36rem, 75vh);
  border: var(--psan-hairline) solid var(--psan-border);
  border-radius: var(--psan-radius-card);
  background: var(--psan-canvas);
  color: var(--psan-ink);
  font-size: var(--psan-text-md);
  overflow: hidden;
  text-align: start;
}

.psan-inbox__panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5) var(--space-3);
  border-block-end: var(--psan-hairline) solid var(--psan-border);
}

.psan-inbox__panel-title {
  margin: 0;
  font-size: var(--psan-text-md);
  font-weight: 600;
}

.psan-inbox__mark-all,
.psan-inbox__all-link,
.psan-inbox__refresh-hint {
  padding: 0;
  border: 0;
  background: none;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  font-weight: 500;
  text-decoration: none;
}

.psan-inbox__mark-all:hover,
.psan-inbox__all-link:hover,
.psan-inbox__refresh-hint:hover {
  color: var(--psan-ink);
  text-decoration: underline;
}

.psan-inbox__panel > .psan-inbox__list {
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.psan-inbox__panel-foot {
  padding: var(--space-3) var(--space-5);
  border-block-start: var(--psan-hairline) solid var(--psan-border);
  text-align: center;
}

/* --- rows, shared by the panel and the page --- */
.psan-inbox__list,
.psan-inbox__group-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.psan-inbox__row {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-6);
}

.psan-inbox__row + .psan-inbox__row {
  border-block-start: var(--psan-hairline) solid var(--psan-border);
}

.psan-inbox__dot {
  position: absolute;
  inset-block-start: 1.75rem;
  inset-inline-start: var(--space-2);
  inline-size: var(--psan-dot);
  block-size: var(--psan-dot);
  border-radius: var(--psan-radius-pill);
}

.psan-inbox__row[data-unread="true"] .psan-inbox__dot {
  background: var(--psan-ink);
}

.psan-inbox__link {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: var(--space-3);
  min-inline-size: 0;
  color: inherit;
  text-decoration: none;
}

.psan-inbox__link:hover .psan-inbox__subject {
  text-decoration: underline;
}

.psan-inbox__icon {
  display: inline-grid;
  place-items: center;
  inline-size: 2.25rem;
  block-size: 2.25rem;
  border-radius: var(--psan-radius-pill);
  background: var(--psan-parchment);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  line-height: 1;
}

.psan-inbox__content {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  min-inline-size: 0;
}

.psan-inbox__meta {
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-inbox__actor {
  color: var(--psan-ink);
  font-weight: 600;
}

.psan-inbox__subject {
  color: var(--psan-ink);
  font-size: var(--psan-text-sm);
  font-weight: 500;
  overflow-wrap: anywhere;
}

.psan-inbox__row[data-unread="true"] .psan-inbox__subject {
  font-weight: 600;
}

.psan-inbox__excerpt {
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
}

.psan-inbox__time {
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-xs);
}

.psan-inbox__read-toggle {
  display: inline-grid;
  place-items: center;
  inline-size: 2rem;
  block-size: 2rem;
  padding: 0;
  border: 0;
  border-radius: var(--psan-radius-pill);
  background: transparent;
  color: var(--psan-ink-meta);
  font-size: 0.5rem;
  line-height: 1;
  transition: background-color var(--psan-motion);
}

.psan-inbox__read-toggle:hover {
  background: var(--psan-hover);
  color: var(--psan-ink);
}

/* --- states --- */
.psan-inbox__empty,
.psan-inbox__end,
.psan-inbox__signin {
  margin: 0;
  padding: var(--space-6) var(--space-5);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  text-align: center;
}

.psan-inbox__signin {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
}

.psan-inbox__signin-text {
  margin: 0;
}

.psan-inbox__error {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
  margin: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--psan-radius-field);
  background: var(--psan-tint-danger);
  color: var(--psan-danger);
  font-size: var(--psan-text-sm);
}

.psan-inbox__skeleton {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: var(--space-2) 0;
  list-style: none;
}

.psan-inbox__skeleton-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
}

.psan-inbox__skeleton-dot {
  flex: none;
  inline-size: 2.25rem;
  block-size: 2.25rem;
}

.psan-inbox__skeleton-lines {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-2);
}

.psan-inbox__skeleton-line--short {
  inline-size: 60%;
}

.psan-inbox__sentinel {
  block-size: var(--psan-hairline);
}

/* --- the full page: tabs, then each day under a small caps header.
   Rows sit on the canvas; the day header is the only grouping. --- */
.psan-inbox--page {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-inline-size: 0;
}

.psan-inbox--page > .psan-inbox__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
}

.psan-inbox__group {
  min-inline-size: 0;
}

.psan-inbox__group-title {
  margin: 0;
  padding-block-end: var(--space-2);
  border-block-end: var(--psan-hairline) solid var(--psan-border);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.psan-inbox--page .psan-inbox__row {
  padding-inline-end: 0;
}

.psan-inbox--page .psan-inbox__row:last-child {
  border-block-end: var(--psan-hairline) solid var(--psan-border);
}

.psan-inbox__refresh-hint,
.psan-inbox__more {
  align-self: center;
}

/* ============ 17. MODERATION QUEUE ============ */

.psan-modqueue {
  display: flex;
  flex-direction: column;
  min-inline-size: 0;
  border-block-start: var(--psan-hairline) solid var(--psan-border);
}

.psan-modqueue__loading {
  margin: 0;
  padding: var(--space-6) 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
}

/* One hairline row per reported item, like the feed. */
.psan-modqueue__item {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-inline-size: 0;
  padding-block: var(--space-5);
  border-block-end: var(--psan-hairline) solid var(--psan-border);
}

.psan-modqueue__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-modqueue__head time {
  margin-inline-start: auto;
}

.psan-modqueue__reasons {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
}

/* The reported words, quoted: indented on a graphite rule so they read
   as someone else's text without a box around them. */
.psan-modqueue__content {
  max-inline-size: none;
  padding-inline-start: var(--space-4);
  border-inline-start: var(--psan-bar) solid var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
}

.psan-modqueue__notes {
  margin: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
  font-style: italic;
}

.psan-modqueue__content {
  margin: 0;
}

.psan-modqueue__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-4);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-modqueue__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.psan-modlog {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  list-style: none;
}

.psan-modlog__item {
  padding-block: var(--space-2);
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

.psan-modlog__item + .psan-modlog__item {
  border-block-start: var(--psan-hairline) solid var(--psan-border);
}

.psan-modlog__item strong {
  color: var(--psan-ink);
  font-weight: 600;
  text-transform: capitalize;
}

/* --- the queue's two tabs: reports, and the host desk --- */
.psan-modtabs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
}

.psan-modtabs__tab {
  min-block-size: 2.25rem;
  padding: 0 var(--space-4);
  border: 0;
  border-radius: var(--psan-radius-pill);
  background: transparent;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-sm);
  font-weight: 500;
  white-space: nowrap;
  transition:
    background-color var(--psan-motion),
    color var(--psan-motion);
}

.psan-modtabs__tab:hover {
  color: var(--psan-ink);
}

.psan-modtabs__tab[aria-selected="true"] {
  background: var(--psan-selected);
  color: var(--psan-ink);
  font-weight: 600;
}

.psan-modtabs__count {
  font-variant-numeric: tabular-nums;
}

/* --- host desk rows: what is waiting, for how long, and the reply --- */
.psan-desk__item {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-inline-size: 0;
  padding-block: var(--space-5);
  border-block-end: var(--psan-hairline) solid var(--psan-border);
}

.psan-desk__title {
  margin: 0;
  color: var(--psan-ink);
  font-family: var(--psan-font-serif);
  font-size: var(--psan-text-lg);
  font-weight: 500;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

.psan-desk__title a {
  color: inherit;
  text-decoration: none;
}

.psan-desk__title a:hover {
  text-decoration: underline;
}

.psan-desk__wait {
  color: var(--psan-ink);
  font-weight: 600;
}

.psan-desk__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
}

.psan-desk__hint {
  margin: 0;
  color: var(--psan-ink-meta);
  font-size: var(--psan-text-meta);
}

/* ============ 18. HIDDEN ============ */

/* The components show and hide almost everything with the `hidden`
   attribute, and a class that sets `display` outranks the UA rule
   for it. This answers every such class at once: it sits after them
   all, and at (0,2,0) it beats any one- or two-class rule above. */
.psan-body [hidden],
[data-thread-root] [hidden] {
  display: none;
}

/* ============ 19. RESPONSIVE ============ */

/* --- two columns: the rail beside the reading column. 58rem of
   content is what a 1024px viewport leaves without the app shell. --- */
@container psan-page (min-width: 58rem) {
  .psan-page__body {
    grid-template-columns: minmax(0, 1fr) var(--psan-rail-col);
  }

  .psan-page__body--narrow {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* WCAG 2.5.5: coarse pointers get the full target, and the visual
   size is held by the glyph so the layout does not inflate. */
@media (pointer: coarse) {
  .psan-vote__up,
  .psan-vote__down,
  .psan-vote--post .psan-vote__up,
  .psan-vote--post .psan-vote__down {
    min-block-size: var(--psan-tap);
  }

  .psan-comment__toggle {
    inline-size: var(--psan-tap);
    min-block-size: var(--psan-tap);
  }

  .psan-action,
  .psan-row,
  .psan-composer__tool,
  .psan-btn,
  .psan-sort__tab,
  .psan-inbox__tab,
  .psan-rail__link,
  .psan-inbox__read-toggle,
  .psan-toast__dismiss,
  .psan-keyhelp__close {
    min-block-size: var(--psan-tap);
  }

  .psan-comment {
    column-gap: var(--space-1);
  }
}

/* --- single column --- */
@media (max-width: 47.99rem) {
  .psan-thread {
    /* the ramp compresses from its two anchors, so every depth
       retunes at once: 12, 11, 10, 9, 9, then 8px */
    --psan-indent-max: var(--space-3);
    --psan-indent-min: var(--space-2);
    --psan-node-gap: var(--psan-node-gap-tight);
    --psan-rail-hit: var(--space-2);
  }

  .psan-page {
    padding-block: var(--space-8) var(--space-16);
  }

  .psan-page__head {
    gap: var(--space-4);
    margin-block-end: var(--space-6);
  }

  .psan-page__lead {
    font-size: var(--psan-text-md);
  }

  /* The actions take their own row, and the bell's panel opens across
     it rather than off the start edge of the screen. */
  .psan-page__actions {
    position: relative;
    flex: 1 1 100%;
  }

  .psan-page--thread .psan-page__actions {
    flex: none;
  }

  .psan-page__actions .psan-inbox--bell {
    position: static;
  }

  .psan-page__actions .psan-inbox__panel {
    inset-inline: 0;
    inline-size: auto;
  }

  .psan-page--thread .psan-page__actions .psan-inbox--bell {
    position: relative;
  }

  .psan-page--thread .psan-page__actions .psan-inbox__panel {
    inset-inline-start: auto;
    inline-size: calc(100vw - 2 * var(--psan-gutter));
  }

  .psan-post-card {
    padding-block: var(--space-4);
  }

  .psan-post-card__title {
    font-size: var(--psan-text-lg);
  }

  .psan-field--split {
    grid-template-columns: minmax(0, 1fr);
  }

  .psan-dialog__panel,
  .psan-keyhelp__panel {
    padding: var(--space-5);
    border-radius: var(--psan-radius-card);
  }

  .psan-toasts {
    inset-inline: var(--space-4);
    inset-block-end: var(--space-4);
    align-items: stretch;
    max-inline-size: none;
  }
}

/* --- 320px: nothing may scroll the page sideways. The usual
   offenders are long URLs, fenced code and the sort strip, each of
   which is handled at its own rule; these are the structural
   backstops. --- */
@media (max-width: 23.75rem) {
  .psan-comment__actions {
    gap: 0;
  }

  .psan-kindgrid {
    grid-template-columns: minmax(0, 1fr);
  }

  :is(.psan-prose, .psan-post__body, .psan-comment__body) {
    overflow-wrap: anywhere;
  }
}

/* ============ 20. REDUCED MOTION ============ */

/* Every transition and keyframe written above is answered here. The
   states all remain legible without their motion because each one
   also changes colour, weight or layout. */
@media (prefers-reduced-motion: reduce) {
  .psan-skip-link,
  .psan-btn,
  .psan-field__input,
  .psan-field__select,
  .psan-sort__window,
  .psan-rail__link,
  .psan-spoiler,
  .psan-comment,
  .psan-comment__toggle,
  .psan-comment__children,
  .psan-comment__rail::after,
  .psan-action,
  .psan-vote__up,
  .psan-vote__down,
  .psan-composer__textarea,
  .psan-composer__tool,
  .psan-composer__preview-toggle,
  .psan-composer__submit,
  .psan-composer__cancel,
  .psan-composer__gate-action,
  .psan-sort__tab,
  .psan-inbox__tab,
  .psan-live__pill,
  .psan-kindcard,
  .psan-toast,
  .psan-inbox__bell,
  .psan-inbox__read-toggle {
    transition-duration: 0.01ms;
    transition-delay: 0ms;
  }

  /* Bars, dots and the pending edge hold still at full strength
     rather than freezing mid-breath. */
  .psan-skeleton-line,
  .psan-skeleton__bar,
  .psan-inbox__skeleton-line,
  .psan-inbox__skeleton-dot,
  .psan-live__dot,
  .psan-comment[data-pending="true"]::before {
    animation: none;
    opacity: 1;
  }

  /* The flash would otherwise stop mid-keyframe and keep the tint. */
  .psan-comment--new {
    animation: none;
    background: var(--psan-tint-sky);
  }

  .psan-live__pill,
  .psan-toast,
  .psan-welcome,
  .psan-vote--bump .psan-vote__score,
  .psan-vote--bump .psan-vote__glyph {
    animation: none;
  }

  .psan-toast[data-state="leaving"] {
    transform: none;
  }

  .psan-thread {
    scroll-behavior: auto;
  }
}

/* ============ 21. FORCED COLORS / HIGH CONTRAST ============ */

@media (forced-colors: active) {
  /* Fills are dropped by the OS; borders have to carry every
     boundary and state that a fill was carrying. */
  /* Only surfaces that are still drawn as a fill: the de-boxed lists,
     rails, form and composer have no container to bring back. */
  .psan-comment[data-distinguished="moderator"],
  .psan-comment--focused,
  .psan-comment--highlight,
  .psan-thread__error,
  .psan-badge--op,
  .psan-badge--mod,
  .psan-badge--distinguished,
  .psan-badge--distinguished-author,
  .psan-post__link,
  .psan-post__tag .psan-tag,
  .psan-btn--primary,
  .psan-btn--ghost,
  .psan-composer__submit,
  .psan-composer__cancel,
  .psan-composer__gate-action,
  .psan-composer__error,
  .psan-composer__draft,
  .psan-field__error,
  .psan-live__pill,
  .psan-inbox__error,
  .psan-inbox__icon,
  .psan-toast,
  .psan-dialog__panel,
  .psan-keyhelp__panel,
  .psan-skip-link {
    border: 1px solid CanvasText;
  }

  .psan-comment--cursor {
    outline: var(--psan-bar) solid CanvasText;
    outline-offset: var(--psan-bar);
  }

  /* Selection and cast votes must not depend on a fill that is gone. */
  .psan-vote__up[aria-pressed="true"],
  .psan-vote__down[aria-pressed="true"],
  .psan-vote[data-my-vote="1"] .psan-vote__up,
  .psan-vote[data-my-vote="-1"] .psan-vote__down,
  .psan-composer__tool[aria-pressed="true"],
  .psan-composer__preview-toggle[aria-pressed="true"],
  .psan-sort__tab[aria-selected="true"],
  .psan-inbox__tab[aria-selected="true"],
  .psan-modtabs__tab[aria-selected="true"],
  .psan-follow__btn[aria-pressed="true"],
  .psan-rail__link[aria-current="page"],
  .psan-rail__sublink[aria-current="page"],
  .psan-composer__mention--active,
  .psan-inbox__bell[aria-expanded="true"] {
    outline: var(--psan-bar) solid Highlight;
    outline-offset: calc(var(--psan-bar) * -1);
  }

  /* The rail is a drawn line, and a background is exactly what the OS
     flattens; it is redrawn as a real border. */
  .psan-comment__rail::after {
    background: none;
    border-inline-start: 1px solid CanvasText;
  }

  /* Dots are pure fill, so they opt out of the override and paint
     themselves in the system text colour. */
  .psan-inbox__row[data-unread="true"] .psan-inbox__dot,
  .psan-badge--new::before,
  .psan-badge--checked::before,
  .psan-live__dot,
  .psan-toast::before,
  .psan-inbox__badge {
    forced-color-adjust: none;
    background: CanvasText;
    color: Canvas;
  }

  .psan-skeleton-line,
  .psan-skeleton__bar,
  .psan-inbox__skeleton-line,
  .psan-inbox__skeleton-dot {
    border: 1px solid GrayText;
  }

  /* The drawn chevron is a gradient, which the OS removes; the native
     control comes back instead. */
  .psan-field__select,
  .psan-sort__window {
    padding-inline-end: var(--space-4);
    appearance: auto;
    background-image: none;
  }

  .psan-spoiler {
    border: 1px solid CanvasText;
    color: CanvasText;
    filter: none;
  }

  .psan-kindcard::before {
    border-color: CanvasText;
  }

  .psan-kindcard[aria-checked="true"]::before {
    border-color: Highlight;
  }

  .psan-modqueue__content {
    border-inline-start-color: CanvasText;
  }
}

/* ============ 22. PRINT ============ */

/* The discussion prints as a document: every thread open, every
   control gone, the hierarchy carried by the rails alone. These
   rules sit last in the file so they win on source order rather
   than on `!important`. */
@media print {
  .psan-body,
  .psan-thread {
    --psan-indent-max: var(--space-4);
    --psan-indent-min: var(--space-2);
    --psan-node-gap: var(--space-3);
    --psan-subtree-tint: transparent;
    /* The one place this sheet steps outside the token layer: every
       ink token is theme-dependent, and a page printed from dark
       mode still has to be black on white. */
    background: white;
    color: black;
    font-size: 10.5pt;
  }

  .psan-page {
    max-inline-size: none;
    padding: 0;
  }

  .psan-page__body {
    display: block;
  }

  .psan-page__main {
    padding: 0;
    background: none;
  }

  .psan-skip-link,
  .psan-page__actions,
  .psan-page__rail,
  .psan-vote,
  .psan-comment__toggle,
  .psan-comment__actions,
  .psan-comment__rail,
  .psan-composer-shell,
  .psan-composer,
  .psan-sort,
  .psan-live,
  .psan-toasts,
  .psan-row,
  .psan-skeletons,
  .psan-skeleton,
  .psan-post__actions,
  .psan-modqueue__actions,
  .psan-feed__more,
  .psan-dialog,
  .psan-keyhelp {
    display: none;
  }

  /* Expand every collapsed thread. */
  .psan-comment[data-collapsed="true"] {
    padding: 0;
    background: none;
  }

  .psan-comment[data-collapsed="true"] > .psan-comment__body {
    display: block;
  }

  .psan-comment[data-collapsed="true"] > .psan-comment__children {
    display: flex;
  }

  .psan-comment {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "head"
      "body"
      "children";
    background: none;
    box-shadow: none;
    break-inside: avoid;
  }

  .psan-comment[data-distinguished="moderator"],
  .psan-comment--focused,
  .psan-comment--highlight,
  .psan-comment--new,
  .psan-comment[data-pending="true"] {
    animation: none;
    background: none;
    opacity: 1;
  }

  .psan-comment__children {
    border-inline-start: 1px solid color-mix(in srgb, black 40%, white);
  }

  .psan-comment__head,
  .psan-post__meta,
  .psan-meta {
    color: color-mix(in srgb, black 80%, white);
  }

  .psan-post__title,
  .psan-comment__author,
  :is(.psan-prose, .psan-post__body, .psan-comment__body) {
    color: black;
  }

  /* Spoilers are part of the record on paper. */
  .psan-spoiler {
    background: none;
    color: black;
    filter: none;
  }

  :is(.psan-prose, .psan-post__body, .psan-comment__body) a {
    color: black;
    text-decoration: underline;
  }

  /* A printed link is worthless without its target. */
  :is(.psan-prose, .psan-post__body, .psan-comment__body) a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 0.85em;
    word-break: break-all;
  }

  :is(.psan-prose, .psan-post__body, .psan-comment__body) pre {
    overflow: visible;
    white-space: pre-wrap;
    word-break: break-word;
  }

  .psan-post,
  .psan-post-card,
  .psan-inbox__row,
  .psan-modqueue__item {
    break-inside: avoid;
  }

  .psan-badge,
  .psan-kind,
  .psan-chip {
    background: none;
    color: black;
  }
}
