/*
 * Shared ViewComponent styles (Package 2b · FE-304…FE-317) — the class rules the
 * v3 mockup (documents/mockups/web-app-ui-v3.html) declares inline, ported into
 * the shipped pipeline so the ViewComponents under app/components/web/ resolve.
 * tokens.css owns the palette + shell; cover_art.css owns the .art-* / .mag-* plates
 * + .pc-thumb/.dropcap; THIS file owns the reusable chrome + card atoms.
 *
 * Rule: colors resolve against the v3 tokens (no hardcoded hex). Translucent
 * scrim/overlay tints on the dark editorial covers stay as rgba() (not hex),
 * exactly as the mockup layers them.
 */

/* ── Shared button atoms ─────────────────────────────────────────────────── */
.btn-primary {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-sm);
  background: var(--accent); color: var(--on-primary);
  font-family: var(--font-body); font-weight: 700; font-size: var(--fs-body-sm);
  border: none; border-radius: var(--r-action); padding: 10px 22px; cursor: pointer;
  text-decoration: none; line-height: 1; white-space: nowrap;
}
.btn-primary:hover { background: var(--primary-bright); }
.btn-primary.lg { padding: 13px 26px; font-size: 15.5px; }
.btn-primary.sm { padding: 8px 16px; font-size: var(--fs-caption); }
.btn-primary:disabled { opacity: .5; cursor: not-allowed; }
.btn-primary:disabled:hover { background: var(--accent); }
.btn-ghost {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-sm);
  background: transparent; color: var(--text);
  font-family: var(--font-body); font-weight: 700; font-size: var(--fs-body-sm);
  /* --outline, not --border: --border is aliased to --outline-variant, the token
     documented as a DECORATIVE rule. On a dark surface it measured 1.6:1 — an
     outlined button with no visible outline. --outline is the control-boundary
     token and is held to the 3:1 gate. */
  border: 1.5px solid var(--outline); border-radius: var(--r-action); padding: 10px 20px; cursor: pointer;
  text-decoration: none; line-height: 1; white-space: nowrap;
}
.btn-ghost.lg { padding: 13px 26px; font-size: 15.5px; }

/* The thread reply CTA (logged-in only) — a btn-primary link into the shared
   compose reply surface; this rule only spaces it below the focal post. */
.thread-reply { margin: 14px 0 4px; }

/* ── Form controls — global themed inputs / textareas / selects / checks ──────
   The layout links THIS stylesheet on EVERY web surface, so styling the bare
   element selectors gives every native control the Shiijak brand automatically —
   inputs, textareas, <select> (custom caret), checkboxes + radios — across every
   state: default / hover / focus-visible / placeholder / disabled / invalid. Links
   and buttons are deliberately left alone (they own their own atoms above /
   tokens.css). Every color resolves against the v3 tokens — no one-off hex (the
   light/dark <select> caret data-URIs are the one unavoidable exception: an inline
   SVG background can't read a CSS var, so its stroke mirrors --ink-2 light/dark).

   Specificity: the RESTING rules wrap their type list in :where() so they sit at
   bare-element weight (0,0,1). Every per-page class rule (.compose-input,
   .dm-*__input, .auth__form input, .onboard__chip-input, …) therefore still wins
   and layers its own shape/size/surface on top — no double borders, and the
   visually-hidden onboarding checkbox keeps its 1px geometry untouched. */
input:where([type="text"], [type="email"], [type="password"], [type="url"],
            [type="search"], [type="tel"], [type="number"], [type="date"],
            [type="datetime-local"], [type="month"], [type="week"], [type="time"]),
textarea,
select {
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: 1.4;
  color: var(--text);
  background: var(--surface-elevated);
  /* --outline (control boundary, 3:1 gate), NOT --border (a decorative-rule alias
     that fell to 1.2–1.6:1 on dark surfaces — a field you couldn't see the edge of,
     and press mode retires box-shadows, so nothing else was drawing the boundary). */
  border: 1px solid var(--outline);
  border-radius: var(--r-md);
  padding: 11px 14px;
  max-width: 100%;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
  -webkit-appearance: none;
     -moz-appearance: none;
          appearance: none;
}
textarea { line-height: 1.5; resize: vertical; }

/* Muted, engine-consistent placeholders (Firefox otherwise dims to ~0.54). */
:where(input, textarea)::placeholder { color: var(--text-muted); opacity: 1; }

/* Hover — a touch stronger hairline (skip while focused / disabled). */
input:where([type="text"], [type="email"], [type="password"], [type="url"],
            [type="search"], [type="tel"], [type="number"], [type="date"],
            [type="datetime-local"], [type="month"], [type="week"], [type="time"]):hover:not(:focus):not(:disabled),
textarea:hover:not(:focus):not(:disabled),
select:hover:not(:disabled) { border-color: var(--outline); }

/* Focus — the brand-violet ring (box-shadow, so it follows any border-radius,
   incl. the pill DM / search inputs). A visible indicator, never a bare
   outline:none-with-nothing. */
input:where([type="text"], [type="email"], [type="password"], [type="url"],
            [type="search"], [type="tel"], [type="number"], [type="date"],
            [type="datetime-local"], [type="month"], [type="week"], [type="time"]):focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--primary-container);
}

/* Invalid — only once the field has been touched (:user-invalid), plus an explicit
   aria-invalid hook: crimson hairline + a tint ring while focused. */
input:user-invalid,
textarea:user-invalid,
input[aria-invalid="true"],
textarea[aria-invalid="true"] { border-color: var(--tertiary); }
input:user-invalid:focus-visible,
textarea:user-invalid:focus-visible,
input[aria-invalid="true"]:focus-visible,
textarea[aria-invalid="true"]:focus-visible { box-shadow: 0 0 0 3px var(--tertiary-container); }

/* Disabled — dimmed + non-interactive. */
input:disabled,
textarea:disabled,
select:disabled {
  background: var(--surface-muted);
  color: var(--text-muted);
  cursor: not-allowed;
  opacity: 0.7;
}

/* <select> — themed like an input + a custom caret (native arrows can't be styled
   cross-browser, hence appearance:none above + this inline-SVG chevron). The option
   list stays native (unstyleable cross-browser) — that's fine. */
select {
  padding-right: 40px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='%23474556' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  background-size: 16px 16px;
  cursor: pointer;
}
/* Dark palette — swap the caret stroke to the dark --ink-2 (both no-JS default and
   the explicit toggle), mirroring how tokens.css applies the dark theme. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not(.theme-light) select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='%239AA4B6' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
  }
}
:root[data-theme="dark"] select,
.theme-dark select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='%239AA4B6' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
}
/* The Press — the caret stroke is a hex baked INTO the data-URI, so it cannot ride
   var(--ink-2): the light/dark twins above already special-case it, and the brand
   needs the same treatment. Without these, every <select> (settings language/
   timezone, admin filters, topic reassignment) wears the cool slate-violet #474556
   chevron on warm paper — a cool wash reads violet against the press ink scale.
   Press cuts of --ink-2: #6B6560 (paper) / #A79E90 (ink). */
[data-brand="press"] select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='%236B6560' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
}
@media (prefers-color-scheme: dark) {
  :root[data-brand="press"]:not([data-theme="light"]):not(.theme-light) select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='%23A79E90' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
  }
}
:root[data-brand="press"][data-theme="dark"] select,
[data-brand="press"] .theme-dark select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='%23A79E90' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
}

/* <input type="date"> — theme the field (above) + the calendar-picker button so it
   stays visible on the dark palette (the default glyph is near-black). */
input[type="date"]::-webkit-calendar-picker-indicator { cursor: pointer; opacity: 0.6; }
input[type="date"]:hover::-webkit-calendar-picker-indicator { opacity: 0.9; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not(.theme-light) input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1); opacity: 0.75; }
}
:root[data-theme="dark"] input[type="date"]::-webkit-calendar-picker-indicator,
.theme-dark input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1); opacity: 0.75; }

/* Checkboxes + radios — kept NATIVE (accessible, and Capybara check/choose still
   toggles them) and simply tinted to the brand via accent-color, with a slightly
   larger hit target + the same focus-visible ring. No hidden-input/label swap
   (that once broke a system spec). :where() keeps the size at bare-element weight
   so the visually-hidden .onboard__chip-input keeps its own 1px geometry. */
input:where([type="checkbox"], [type="radio"]) {
  accent-color: var(--accent);
  inline-size: 1.1rem;
  block-size: 1.1rem;
  cursor: pointer;
}
input[type="checkbox"]:focus-visible,
input[type="radio"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
input[type="checkbox"]:disabled,
input[type="radio"]:disabled { cursor: not-allowed; opacity: 0.6; }

/* ── Shared page chrome — masthead brand mark + footer ───────────────────────
   The layout's <header> (content_for :header) and the shared web/pages/_footer
   render on EVERY surface, so their styles live here (the globally-linked
   stylesheet) rather than web/pages.css (marketing-only). The masthead only
   appears on nav-less surfaces (see layout comment); on app surfaces the nav
   rail's .rail-logo is the single brand mark. */
/* The wordmark's line box must EXCEED its size (BRAND_FACE_LINE_HEIGHT = 1.3, the
   mobile lockup's constant): at / 1 the Newsreader ascenders and the j descender in
   "Shiijak" are clipped — and under press the Signature hairline below (0.22em gap)
   would land on the j's tail. Its siblings (.rail-logo, .mnav__logo) set no
   line-height and inherit the body's 1.6; only this one clamped it. */
.site-mark {
  max-width: var(--measure-wide); margin: 0 auto; padding: var(--space-lg);
  font: 600 var(--fs-title) / 1.3 var(--font-display); letter-spacing: -0.01em;
}
.site-mark a { color: var(--ink); text-decoration: none; }

/* C-S9 — the Press wordmark on the nav-less pages' .site-mark: the same
   construction as the rail logo (spec §0: Newsreader 600 — already this
   element's face — tightened to the fixed −1.5% wordmark tracking) with the
   1px Signature hairline beneath (§5: 1px below a 40px render; 0.22em gap).
   The markup already reads "Shiijak", so this is CSS-only under the flag. */
[data-brand="press"] .site-mark { letter-spacing: -0.015em; }
[data-brand="press"] .site-mark a {
  display: inline-block;
  padding-bottom: 0.22em;
  border-bottom: 1px solid var(--signature);
}

/* C-S9 — the Edition/home masthead plate (flag-gated shared chrome): dated
   eyebrow line with the folio right-aligned in tabular figures, the nameplate
   in the masthead Didone — --font-masthead at --fs-display-xl (min 40px, so
   the ≥28px Bodoni floor holds; the C-S14 seam repoints the face) — the brass
   Signature bar (2px at ≥40px, 1.4em long, 0.22em gap — spec §5, decorative
   brass at masthead scale per §1.1), and a thin hairline rule beneath. */
[data-brand="press"] .masthead-plate {
  max-width: var(--measure-wide);
  margin: 0 auto;
  padding: var(--space-lg) var(--space-lg) 0;
  text-align: center;
}
[data-brand="press"] .masthead-plate__date {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 10px; flex-wrap: wrap;
  font: 600 var(--fs-eyebrow) / var(--lh-eyebrow) var(--font-body);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ink-2);
  margin: 0;
  text-align: left;
}
[data-brand="press"] .masthead-plate__name {
  /* BRAND-FACE line box (1.3), NOT the text ramp's --lh-display-xl (1.01). This is
     the ONE consumer of --font-masthead (the Didone) and it renders the literal
     wordmark "Shiijak", which has BOTH tall ascenders (S/h/j/k) and a descender (j)
     — a 1.0-ish line box crops it top and bottom (the beheaded nameplate). Mobile
     guards exactly this with BRAND_FACE_LINE_HEIGHT = 1.3 (shared/ui/Masthead.tsx,
     shared/theme/fonts.ts) and .site-mark above already does it; the plate was the
     miss. --lh-display-xl stays 1.01 — that is correct for the display-xl TEXT step
     (spec §3), it just isn't a brand-face line box. It also lifts the brass
     Signature bar (0.22em below) off the j's tail. */
  font: 600 var(--fs-display-xl) / 1.3 var(--font-masthead);
  /* Re-stated after the font shorthand: the opsz axis sharpens the display cut. */
  font-optical-sizing: auto;
  letter-spacing: var(--ls-display-xl);
  color: var(--ink);
  margin: var(--space-md) 0 0;
}
[data-brand="press"] .masthead-plate__sig {
  display: block;
  width: 1.4em;
  height: 2px;
  background: var(--brass);
  margin: 0.22em auto 0;
}
[data-brand="press"] .masthead-plate__rule {
  border-bottom: 1px solid var(--outline-variant);
  margin: var(--space-lg) 0 0;
}

.site-foot {
  max-width: var(--measure-wide); margin: 0 auto; padding: var(--space-xl) var(--space-lg);
  border-top: 1px solid var(--border); display: flex; gap: var(--space-lg);
  justify-content: space-between; flex-wrap: wrap;
  font: 400 var(--fs-caption) / 1.4 var(--font-body); color: var(--text-muted);
}
.site-foot a { color: var(--text-muted); text-decoration: none; }
.site-foot a:hover { color: var(--accent); }
.site-foot nav { display: flex; gap: var(--space-lg); flex-wrap: wrap; }

/* ── Discovery shelves — "People to follow" rows (search + explore) ───────────
   Sibling of the .pick-row topic row: one clean horizontal row per person —
   avatar left, bold display-name in normal ink (NOT a blue underlined link) +
   muted @handle, a hairline divider, and any Follow pill pushed to the edge. */
.explore-shelf { margin: 4px 0 18px; }
.person-row {
  display: flex; align-items: center; gap: 12px; padding: 10px 0;
  border-bottom: 1px solid var(--border);
}
.person-row:last-child { border-bottom: none; }
.person-row__id {
  display: flex; align-items: center; gap: 12px; min-width: 0; flex: 1 1 auto;
  text-decoration: none; color: var(--text);
}
.person-row__name { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
.person-row__name b {
  font-family: var(--font-body); font-size: var(--fs-body-sm); font-weight: 700;
  color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.person-row__handle { font-size: 12.5px; color: var(--text-muted); }
.person-row__id:hover .person-row__name b { color: var(--accent); }
/* Inline follow (search rows) — the actionable profile-actions pill + ⋯ menu sits
   at the row's trailing edge, never squeezing the identity block. */
.person-row .profile-actions { flex: 0 0 auto; margin-left: auto; }

/* ── Avatar (FE-306) ─────────────────────────────────────────────────────── */
.av-pv {
  border-radius: 50%; overflow: hidden; flex: 0 0 auto;
  width: 44px; height: 44px; display: block;
  /* A calm, theme-aware neutral — NOT --cover (the near-black cover-art scrim) —
     so a still-loading photo avatar shows a soft stable disc instead of a stark
     black flash before it pops in (web image-flicker fix). Explicit width/height
     above (plus the <img>'s own width/height attrs) already keep this box from
     ever collapsing, so there's no layout shift either. */
  box-shadow: inset 0 0 0 1px rgba(20, 15, 10, .06); background: var(--surface-muted);
}
.av-pv svg, .av-pv .av-img { width: 100%; height: 100%; display: block; }
.av-pv .av-img {
  object-fit: cover;
  /* Crossfade the photo in over the placeholder disc once it's ready — see
     `av-img--pending` (avatar_controller.js) — rather than an instant, jarring
     pop. A cache hit skips `--pending` entirely (already `complete` by the time
     the controller connects), so a revisit still paints instantly. */
  opacity: 1; transition: opacity 200ms ease;
}
.av-pv .av-img--pending { opacity: 0; }
.theme-dark .av-pv { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .08); }
/* Default (no-photo) fallback: deterministic initials on a tinted disc (.av-1…7),
   not a bare dark circle. Fills + centers inside the .av-pv frame; .av-pv's
   overflow:hidden + radius clip it round, its inset ring stays on top. */
.av-pv__mono {
  width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
  /* NOT --on-primary. That token is the label for whatever sits on a PRIMARY
     BUTTON, and it flips to near-black ink in press+dark — which is correct for a
     paper-coloured CTA and catastrophic here, because the avatar plates below are
     dark in EVERY scheme. Steve saw "HS" and "DC" vanish at 1.0:1. The label belongs
     to the plate it sits on, so it takes the plate's own text token. */
  color: var(--cover-text); font-family: var(--font-display); font-weight: 600; line-height: 1;
  text-transform: uppercase; letter-spacing: .02em; user-select: none;
}
/* The uploaded-avatar image-error fallback: the <img> and the initials tile each
   set their own display, so the plain `hidden` attribute (which only sets
   display:none at low specificity) is overridden — force it here so the broken
   <img> truly hides and its pre-rendered initials tile stays hidden until the
   avatar controller reveals it (mirrors .lp-card__img[hidden]). */
.av-pv .av-img[hidden], .av-pv__mono[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .av-pv .av-img { transition: none; } }

/* ── Universal image crossfade (no-flicker) — generalized from the avatar fade ──
   Any content <img class="img-fade" data-controller="image"> (post photos, DM
   photos, link-preview thumbnails, collection covers, the post-edit photo, the
   settings current-photo) fades in once decoded instead of popping from its
   wrapper's solid placeholder. The <img> is fully opaque by default; image_
   controller.js adds `img-fade--pending` (opacity:0) ONLY while a not-yet-
   `complete` image is still loading, then removes it on `load` so CSS crossfades
   it in. A cache hit / Turbo revisit is already `complete` on connect → never
   pending → paints instantly. Because `--pending` is added only in JS (never in
   the server-rendered HTML), a no-JS client just sees the image render normally.
   Reduced-motion users get the swap with no animation. Pair with a calm neutral
   wrapper background (var(--surface-muted)) so the pre-paint frame is soft, not
   a stark --cover black flash. */
.img-fade { opacity: 1; transition: opacity 200ms ease; }
.img-fade--pending { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .img-fade { transition: none; } }

.av {
  border-radius: 50%; display: flex; align-items: center; justify-content: center;
  color: var(--on-primary); font-family: var(--font-display); font-weight: 600;
  flex: 0 0 auto; width: 44px; height: 44px; font-size: 16px;
}
/* Every plate is luminance-clamped so a light monogram clears WCAG AA (4.5:1) at
   BOTH gradient stops — worst case is now 5.0:1, vs 1.0:1 before. The hues are the
   originals; only lightness moved, and both stops moved together so the gradient
   keeps its depth. Adding a bucket? Keep its stops in this band or the initials
   disappear again — which is exactly how av-2 and av-6 got readable while the rest
   didn't: pure luck of the CRC32 bucket, not design. */
.av-1 { background: linear-gradient(135deg, #5838FF, #36287B); } /* 5.0:1 */
.av-2 { background: linear-gradient(135deg, #7C5C30, #3A2B16); } /* 5.0:1 */
.av-3 { background: linear-gradient(135deg, #5A35FF, #2D216A); } /* 5.1:1 */
.av-4 { background: linear-gradient(135deg, #4A5568, #2D3748); } /* 6.2:1 */
.av-5 { background: linear-gradient(135deg, #8C3B7A, #5C2560); } /* 5.7:1 */
.av-6 { background: linear-gradient(135deg, #6D6142, #3B3322); } /* 5.0:1 */
.av-7 { background: linear-gradient(135deg, #4E4A9E, #2A2860); } /* 6.2:1 */

/* The Press — the no-photo avatar's letter-fallback COLOUR ramp. This is not the
   avatar's shape (`.av` stays a 50% circle above — correct, and untouched): it is
   the 7-bucket duotone plate Web::AvatarComponent assigns deterministically
   (`av-#{crc % 7 + 1}`), so buckets 1 and 3 painted the RETIRED violet primitives
   (#6B4EFF, and its #7A5CFF family cut) on ~2 of every 7 avatarless accounts across
   the feed, post headers, DMs and profiles. Gradient stops are raw hex by
   necessity — a `linear-gradient()` in a base rule can't be re-pointed by a token
   flip, which is exactly why these two survived the press cascade — so they are
   re-inked here, flag-scoped, onto the press ink-family plate (spec §1.1: indigo
   #2B1E4A is the "ink-family plate/avatar field"; #3A3049 is its dark cut, the same
   pair the cover-art plates use). The other five buckets (brass, slate, plum, khaki,
   indigo) are already press-compatible and are deliberately left alone, so the ramp
   keeps its variety. With data-brand absent, the violet ramp ships byte-identically. */
[data-brand="press"] .av-1 { background: linear-gradient(135deg, #3A3049, #12100D); }
[data-brand="press"] .av-3 { background: linear-gradient(135deg, #2B1E4A, #12100D); }
/* The monogram sitting on those plates now takes --cover-text in its BASE rule
   (.av-pv__mono, above) — for every brand and both schemes, since the plates are dark
   everywhere. A press-scoped override used to live here targeting `.av`, a class this
   app never renders (the real one is .av-pv__mono): the reasoning was right, the
   selector was dead, and the bug shipped anyway. Nothing to override now. */

/* Verified badge — the ONE violet circular check beside a verified account's name
   (post cards + profile header, via web/shared/_verified_badge). fill=currentColor
   resolves --verified (no one-off hex); the single "Verified account" aria-label
   lives in the shared partial. .vchk stays for any legacy inline check. */
.vbadge { flex: 0 0 auto; color: var(--verified); vertical-align: middle; }
.vchk { flex: 0 0 auto; }

/* C-S10 — The Signature (verification device, press-token-spec §5): under
   press a verified member's display name carries the brass hairline rule —
   2px, the spec's member-name rule weight; --signature resolves the scheme's
   meaning-bearing brass cut (#96733F on paper / #B08D57 on ink, both ≥3:1).
   The violet check retires VISUALLY only: sr-only-style clipping keeps the
   SVG's role="img" + "Verified account" label in the accessibility tree, so
   the announcement survives the swap. With the flag off neither rule applies
   — the check renders exactly as before. */
[data-brand="press"] .sig-name {
  display: inline-block;
  border-bottom: 2px solid var(--signature);
  padding-bottom: 1px;
}
[data-brand="press"] .vbadge {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
}
.vlabel {
  display: inline-flex; align-items: center; gap: 4px;
  font-family: var(--font-body); font-size: 12px; font-weight: 600; color: var(--violet-600);
}

/* ── Mobile top bar (FE — the <768px nav; the rail is desktop-only) ───────────
   Below 768px the desktop rail is hidden and this compact bar (logo + hamburger)
   takes over, opening a right-edge drawer with the SAME destinations. The drawer
   is a native <details> so it toggles with JS OFF; the lazy `mobile-nav` Stimulus
   controller layers Esc / outside-click close + focus-trap + aria-expanded sync
   on top. Shown < 768px, hidden ≥ 768px (where .rail-full returns). */
.mnav {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-md);
  padding: 10px 16px; background: var(--surface); border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 50;
}
/* The Press wordmark (C-S9, base style since the C-S12 flip): "Shiijak"
   capitalized in the display serif (Newsreader 600 resolves first in
   --font-display) at the fixed −1.5% wordmark tracking, ink. */
.mnav__logo {
  display: inline-flex; align-items: center; text-decoration: none;
  font-family: var(--font-display); font-weight: 600; font-optical-sizing: auto;
  font-size: var(--fs-subtitle); color: var(--ink); letter-spacing: -0.015em;
}
.mnav__disc { position: static; margin: 0; }
/* An outlined, interactive control — the action corner, like the nav items it opens. */
.mnav__toggle {
  display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px;
  border-radius: var(--r-action); cursor: pointer; color: var(--text);
  background: transparent; border: 1px solid var(--border); list-style: none;
}
.mnav__toggle::-webkit-details-marker { display: none; }
.mnav__toggle::marker { content: ""; }
.mnav__toggle:hover { background: var(--accent-subtle); color: var(--accent); border-color: var(--accent-subtle); }
/* Scrim + drawer are shown ONLY while the <details> is [open] — author-controlled,
   so we never depend on the UA's (version-varying) hiding of closed <details>
   content. This is also what makes JS `details.open = false` (Esc / scrim click)
   reliably re-hide them. */
.mnav__scrim { display: none; position: fixed; inset: 0; z-index: 55; background: rgba(10, 10, 12, .5); }
.mnav__drawer {
  display: none; position: fixed; top: 0; right: 0; z-index: 56; height: 100%; width: min(84vw, 320px);
  overflow-y: auto; flex-direction: column; gap: 2px;
  padding: 18px 14px calc(18px + env(safe-area-inset-bottom, 0px));
  background: var(--surface); border-left: 1px solid var(--border);
  box-shadow: var(--shadow-drawer);
}
.mnav__disc[open] .mnav__scrim { display: block; }
.mnav__disc[open] .mnav__drawer { display: flex; }
/* The mobile-drawer twin of .rail-item — same control, same action corner. */
.mnav__item {
  display: flex; align-items: center; gap: 14px; padding: 12px; border-radius: var(--r-action);
  font-size: 16px; font-weight: 600; color: var(--text); text-decoration: none;
}
.mnav__item svg { flex: 0 0 auto; color: var(--text-muted); }
.mnav__item.on { background: var(--accent-subtle); color: var(--accent); }
.mnav__item.on svg { color: var(--accent); }
.mnav__item .rail-badge { margin-left: auto; }
.mnav__compose, .mnav__join {
  display: flex; align-items: center; justify-content: center; gap: var(--space-sm);
  background: var(--accent); color: var(--on-primary); font-weight: 700; font-size: var(--fs-body-sm);
  border: none; border-radius: var(--r-action); padding: 13px 0; margin-top: 12px; text-decoration: none;
}
.mnav__ghost {
  display: flex; align-items: center; justify-content: center; background: transparent; color: var(--text);
  font-weight: 700; font-size: var(--fs-body-sm); border: 1.5px solid var(--border);
  border-radius: var(--r-action); padding: 11px 0; margin-top: 12px; text-decoration: none;
}
/* Sign out in the authed mobile drawer — a red-inked ghost so logout is reachable
   on a phone without digging through Settings. */
.mnav__signout-form { margin: 12px 0 0; }
.mnav__signout {
  display: flex; align-items: center; justify-content: center; gap: var(--space-sm); width: 100%;
  background: transparent; color: var(--heart-red); font-weight: 700; font-size: var(--fs-body-sm);
  font-family: var(--font-body); border: 1.5px solid var(--border); border-radius: var(--r-action);
  padding: 11px 0; cursor: pointer;
}
.mnav__signout svg { flex: 0 0 auto; }
.mnav__signout:hover { border-color: var(--heart-red); background: rgba(255, 48, 64, .08); }
/* Slide/fade in only when the visitor hasn't asked for reduced motion (CSS
   animations replay when <details> reveals the drawer — display can't transition). */
@media (prefers-reduced-motion: no-preference) {
  .mnav__drawer { animation: mnav-slide-in .22s ease both; }
  .mnav__scrim  { animation: mnav-fade-in .22s ease both; }
}
@keyframes mnav-slide-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
@keyframes mnav-fade-in  { from { opacity: 0; } to { opacity: 1; } }

/* ── Nav rail (FE-304) ───────────────────────────────────────────────────── */
.rail-full {
  width: 224px; flex: 0 0 auto; background: var(--surface);
  border-right: 1px solid var(--border);
  display: none; flex-direction: column; padding: 22px 14px; gap: 2px; min-height: 100%;
}
/* The rail ↔ mobile-bar swap: the bar owns < 768px, the rail owns ≥ 768px. */
@media (min-width: 768px) {
  .mnav { display: none; }
  .rail-full { display: flex; }
}
/* C-S9 — the Press wordmark on the rail + mobile top-bar logos. Base style
   since the C-S12 default flip (the legacy SVG-mark lockup + its accent-em
   CSS retired with it): "Shiijak" capitalized in the display serif
   (Newsreader 600 resolves first in --font-display) at the fixed −1.5%
   wordmark tracking, ink, with the 1px Signature hairline beneath (§5: 1px
   below a 40px render; 0.22em gap). Base --signature/--ink keep the mark
   coherent on the violet palette when WEB_BRAND_PRESS is off (rollback). The
   wordmark is NEVER set in the masthead Didone (§0 — Bodoni breaks <28px). */
.rail-logo {
  display: inline-flex; align-items: center;
  font-family: var(--font-display); font-weight: 600; font-optical-sizing: auto;
  font-size: var(--fs-subtitle); color: var(--ink);
  padding: 6px 10px 24px; letter-spacing: -0.015em; text-decoration: none;
}
.rail-logo__word { display: inline-block; }
.rail-logo--signature {
  display: inline-block;
  padding-bottom: 0.22em;
  border-bottom: 1px solid var(--signature);
}

/* A nav link is an interactive control, so it takes the action corner — not the
   card/surface corner. On the press brand the 10px rounding + the lilac wash made
   the active item read as a violet PILL floating on a cream page: the one element
   still speaking the retired brand, right at the top of every screen. */
.rail-item {
  display: flex; align-items: center; gap: 14px; padding: 11px 12px; border-radius: var(--r-action);
  font-size: 15.5px; font-weight: 600; color: var(--text); text-decoration: none; cursor: pointer;
}
.rail-item svg { flex: 0 0 auto; color: var(--text-muted); }
.rail-item.on { background: var(--accent-subtle); color: var(--accent); }
.rail-item.on svg { color: var(--accent); }
.rail-badge {
  margin-left: auto; background: var(--badge-red); color: var(--on-primary);
  font-size: 11px; font-weight: 700; border-radius: var(--r-pill);
  min-width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; padding: 0 5px;
}
.rail-spacer { flex: 1; }
/* The account chip is a nav link into the same --accent-subtle wash as .rail-item —
   same control, same action corner (the doctrine note above .rail-item applies). */
.rail-user {
  display: flex; align-items: center; gap: 12px; padding: 12px;
  border-top: 1px solid var(--border); margin-top: 4px; font-size: 14.5px; font-weight: 600;
  color: var(--text); text-decoration: none; border-radius: var(--r-action);
}
a.rail-user:hover { background: var(--accent-subtle); color: var(--accent); }
/* Turbo Stream anchor around the rail avatar (settings avatar_saved re-renders it
   in place) — display:contents so the wrapper never affects the flex row. */
.rail-user__avatar-slot { display: contents; }

/* ── Nav-rail account menu (profile chip → View profile / Settings / Sign out) ──
   A native <details> popover: the summary IS the .rail-user chip; the menu opens
   downward, absolutely positioned so it never reflows the rail. Shown ONLY while
   [open] (author-controlled, so the user-menu controller's `open = false` reliably
   re-hides it). */
.rail-menu { position: relative; }
.rail-user__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-menu__toggle { list-style: none; cursor: pointer; }
.rail-menu__toggle::-webkit-details-marker { display: none; }
.rail-menu__toggle::marker { content: ""; }
.rail-menu__toggle:hover { background: var(--accent-subtle); color: var(--accent); }
.rail-menu__chev { flex: 0 0 auto; margin-left: auto; color: var(--text-muted); transition: transform 160ms ease; }
.rail-menu[open] .rail-menu__chev { transform: rotate(180deg); }
.rail-menu__pop {
  display: none; position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 60;
  flex-direction: column; gap: 2px; padding: 6px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
  box-shadow: var(--shadow-menu);
}
.rail-menu[open] .rail-menu__pop { display: flex; }
.rail-menu__item {
  display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--r-sm);
  font: 600 var(--fs-body-sm) / 1 var(--font-body); color: var(--text); text-decoration: none;
  background: transparent; border: none; cursor: pointer; text-align: left; width: 100%; box-sizing: border-box;
}
.rail-menu__item:hover, .rail-menu__item:focus-visible { background: var(--accent-subtle); color: var(--accent); }
.rail-menu__item--danger { color: var(--heart-red); }
.rail-menu__item--danger:hover, .rail-menu__item--danger:focus-visible { background: rgba(255, 48, 64, .1); color: var(--heart-red); }
.rail-menu__form { margin: 0; display: block; }
@media (prefers-reduced-motion: no-preference) {
  .rail-menu[open] .rail-menu__pop { animation: rail-menu-in .16s ease both; }
}
@keyframes rail-menu-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.rail-compose {
  display: flex; align-items: center; justify-content: center; gap: var(--space-sm);
  background: var(--accent); color: var(--on-primary); font-weight: 700; font-size: var(--fs-body-sm);
  border: none; border-radius: var(--r-action); padding: 13px 0; margin-top: 10px; cursor: pointer;
  font-family: var(--font-body); text-decoration: none;
}
.rail-ghost {
  display: flex; align-items: center; justify-content: center; background: transparent; color: var(--text);
  font-weight: 700; font-size: var(--fs-body-sm); border: 1.5px solid var(--border);
  border-radius: var(--r-action); padding: 11px 0; margin-top: 8px; cursor: pointer;
  font-family: var(--font-body); text-decoration: none;
}
.rail-join {
  display: flex; align-items: center; justify-content: center; background: var(--accent); color: var(--on-primary);
  font-weight: 700; font-size: var(--fs-body-sm); border: none; border-radius: var(--r-action);
  padding: 13px 0; margin-top: 8px; cursor: pointer; font-family: var(--font-body); text-decoration: none;
}
.rail-qual {
  font-family: var(--font-body); font-size: 11.5px; line-height: 1.4; color: var(--text-muted);
  text-align: center; margin-top: 9px; display: flex; align-items: center; justify-content: center; gap: 5px;
}
.rail-qual svg { color: var(--accent); flex: 0 0 auto; }

/* ── Newsstand rail (FE-305) ─────────────────────────────────────────────── */
.newsstand-rail {
  width: 296px; flex: 0 0 auto; border-left: 1px solid var(--border);
  padding: 28px 22px; min-height: 100%;
}
.rail-label {
  font-family: var(--font-body); font-size: 12.5px; font-weight: 700; letter-spacing: .14em;
  text-transform: uppercase; color: var(--text-muted); margin: 20px 0 10px;
}
.rail-label:first-child { margin-top: 0; }
.cover-mini {
  color: var(--cover-text); border-radius: var(--r-lg); padding: 22px 18px; margin-bottom: 4px;
  position: relative; overflow: hidden; display: block; text-decoration: none;
}
.cover-mini .ce { font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--primary-tint); font-weight: 700; }
.cover-mini .ct { font-family: var(--font-display); font-weight: 600; font-size: var(--fs-title); margin: 6px 0 4px; }
.cover-mini .cs { font-size: var(--fs-caption); color: rgba(245, 245, 245, .72); line-height: 1.35; }
.cover-mini .cc { margin-top: 10px; font-size: 12.5px; font-weight: 600; color: rgba(245, 245, 245, .62); }
.pick-row {
  display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 9px 0;
  border-bottom: 1px solid var(--border); font-size: var(--fs-body-sm); font-weight: 600;
  color: var(--text); text-decoration: none;
}
.pick-row .pr-l { display: flex; flex-direction: column; min-width: 0; }
.pick-row .pr-l span { font-size: 12.5px; font-weight: 500; color: var(--text-muted); }
/* When the topic name is promoted to an <h3> (Explore / Newsstand outline), keep
   it visually identical to the <span> row: inherit the .pick-row font, no UA
   heading size or margin. */
.pick-row h3.pr-l { font: inherit; margin: 0; }
.pick-row .pr-meta { font-size: 12.5px; color: var(--text-muted); }
.rail-seeall {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 12px; font-size: 13.5px;
  font-weight: 700; color: var(--accent); text-decoration: none;
}
.rail-seeall span { color: var(--text-muted); font-weight: 500; }
.syt-spot {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md);
  padding: 14px 16px; font-size: 13.5px; color: var(--text); line-height: 1.4;
}
.syt-spot b { display: block; font-family: var(--font-display); font-size: var(--fs-body-sm); margin-bottom: 1px; font-weight: 600; }
.syt-spot .cred { display: block; font-family: var(--font-body); font-size: 12px; color: var(--violet-600); font-weight: 600; margin-bottom: 7px; }
.syt-spot .desc { color: var(--text-muted); }
.syt-spot .syt-makeown { display: inline-flex; align-items: center; gap: 5px; margin-top: 10px; font-size: 12.5px; font-weight: 700; color: var(--accent); text-decoration: none; }
.rail-foot {
  margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--border);
  display: flex; flex-wrap: wrap; gap: 12px; font-size: 12.5px; color: var(--text-muted);
}
.rail-foot a { color: var(--text-muted); text-decoration: none; }
.rail-foot a:hover { color: var(--accent); }

/* ── Follow pill (FE-307/FE-317) ─────────────────────────────────────────── */
.follow-pill {
  display: inline-flex; align-items: center; gap: 5px; font-family: var(--font-body); font-size: var(--fs-caption);
  font-weight: 700; line-height: 1; color: var(--violet-600); background: var(--surface-elevated);
  border: 1.5px solid var(--violet-500); border-radius: var(--r-action); padding: 6px 13px 6px 10px;
  cursor: pointer; white-space: nowrap; text-decoration: none;
  transition: background .14s, color .14s, transform .1s;
}
.follow-pill svg { flex: 0 0 auto; }
.follow-pill:hover { background: var(--accent-subtle); }
.follow-pill:active { transform: scale(.96); }
.follow-pill.following { background: var(--violet-500); color: var(--on-primary); border-color: var(--violet-500); }
.follow-pill.staged { border-style: dashed; color: var(--violet-600); background: var(--surface-elevated); }
.follow-pill.sm { font-size: 12px; padding: 5px 11px 5px 9px; }
/* Explore discovery shelves are served from the SHARED anonymous page cache, so a
   viewer-personalized Follow control there is a dead control (it bounced logged-in
   members to /login and never followed). It renders as this neutral "View" link to
   the profile instead — muted, NOT the violet follow CTA, so it reads as a plain
   navigation that works for anon + authed alike. */
.follow-pill--view { color: var(--text-muted); border-color: var(--border); background: var(--surface); }
.follow-pill--view:hover { background: var(--accent-subtle); color: var(--text); }
.stage-cap { display: block; font-family: var(--font-body); font-size: 11.5px; line-height: 1.35; color: var(--text-muted); margin-top: 5px; }
.stage-cap b { color: var(--violet-600); font-weight: 600; }
.follow-pill-wrap { display: inline-block; }

/* ── Topic action row — a tidy SECONDARY Follow + Pin-to-Home group ───────────
   The topic Follow and "Pin to Home" pills are SECONDARY calls-to-action: full
   brand-violet is reserved for the PRIMARY CTAs (compose, save profile). So the
   two pills sit in one spaced group (never touching) and take the smaller, more
   restrained `.follow-pill--secondary` scale — a neutral outline when inactive
   and a soft violet TINT (not a full-saturation fill) when active. The profile
   Follow pill and the L1 staged pill keep the stronger base `.follow-pill`.

   Rules are scoped under `.topic-actions` so their specificity (0,2,0) reliably
   beats the `button.follow-pill { font: inherit }` reset below (0,1,1) — that
   reset otherwise pulls the button up to the 19px body font. */
.topic-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-md);
  margin: 14px 0 6px;
}
.topic-actions .topic-follow { margin: 0; }
.topic-actions .pf-action-form { display: inline; margin: 0; }

.topic-actions .follow-pill--secondary {
  font-size: 12.5px; font-weight: 600; min-height: 32px; padding: 6px 13px 6px 11px;
  color: var(--text-muted); background: var(--surface-elevated);
  border: 1px solid var(--border);
}
.topic-actions .follow-pill--secondary:hover { background: var(--accent-subtle); color: var(--accent); border-color: var(--accent); }
.topic-actions .follow-pill--secondary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Active ("Following" / "Pinned to Home") — a soft violet tint, not the primary
   fill: reads as ON without shouting for the primary-CTA color budget. */
.topic-actions .follow-pill--secondary.following {
  background: var(--primary-container); color: var(--on-primary-container);
  border-color: var(--primary-container);
}
.topic-actions .follow-pill--secondary.following:hover { background: var(--primary-container); border-color: var(--accent); }

/* ── L2 profile actions — interactive follow pill + block/mute overflow menu ── */
.profile-actions { display: flex; align-items: center; gap: 8px; }
.profile-actions .pf-action-form { display: inline; margin: 0; }
button.follow-pill { font: inherit; }
.pf-menu { position: relative; }
.pf-menu__trigger {
  display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px;
  border-radius: 50%; cursor: pointer; list-style: none; color: var(--text-muted);
  background: var(--surface-elevated); border: 1.5px solid var(--border);
  transition: background .14s, color .14s;
}
.pf-menu__trigger::-webkit-details-marker { display: none; }
.pf-menu__trigger:hover, .pf-menu[open] .pf-menu__trigger { color: var(--text); background: var(--accent-subtle); }
.pf-menu__panel {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 20; min-width: 152px;
  display: flex; flex-direction: column; padding: 6px; background: var(--surface-elevated);
  border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: var(--shadow-pop);
}
.pf-menu__form { display: block; margin: 0; }
.pf-menu__item {
  display: block; width: 100%; text-align: left; cursor: pointer; font-family: var(--font-body);
  font-size: var(--fs-body-sm); font-weight: 500; color: var(--text); background: transparent;
  border: 0; padding: 8px 10px; border-radius: var(--r-sm);
}
.pf-menu__item:hover { background: var(--accent-subtle); }

/* ── Post owner overflow menu (⋯ → Edit/Delete) — L2, author-only ─────────── */
.pc-menu { position: relative; margin-left: auto; }
.pc-menu__trigger {
  display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px;
  border-radius: 50%; cursor: pointer; list-style: none; color: var(--text-muted);
  background: transparent; border: 0; transition: background .14s, color .14s;
}
.pc-menu__trigger::-webkit-details-marker { display: none; }
.pc-menu__trigger:hover, .pc-menu[open] .pc-menu__trigger { color: var(--text); background: var(--accent-subtle); }
.pc-menu__panel {
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 20; min-width: 140px;
  display: flex; flex-direction: column; padding: 6px; background: var(--surface-elevated);
  border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: var(--shadow-pop);
}
.pc-menu__form { display: block; margin: 0; }
.pc-menu__item {
  display: block; width: 100%; text-align: left; text-decoration: none; cursor: pointer;
  font-family: var(--font-body); font-size: var(--fs-body-sm); font-weight: 500; color: var(--text);
  background: transparent; border: 0; padding: 8px 10px; border-radius: var(--r-sm);
}
.pc-menu__item:hover { background: var(--accent-subtle); }
.pc-menu__item--danger { color: var(--heart-red); }

/* ── The Press (C-S11): the ⋯ overflow / Copy-link menu as printed apparatus —
   the panel takes the soft end of the action-corner range and the items
   (Copy link et al.) the 3px action corner; the panel's hairline border
   already carries elevation (--shadow-pop retired to none under press). */
[data-brand="press"] .pc-menu__panel { border-radius: var(--r-sm); }
[data-brand="press"] .pc-menu__item { border-radius: var(--r-action); }
/* The two sibling popover menus are the SAME control (buttons/links in a panel):
   the nav-rail account menu (View profile / Settings / Sign out) and the profile
   overflow menu (Block / Mute). They were missed by the C-S11 sweep and still
   rendered at 6px while the post-card menu beside them rendered at 3px. */
[data-brand="press"] .rail-menu__item,
[data-brand="press"] .pf-menu__item { border-radius: var(--r-action); }
/* …and so are the two combobox option rows — the same "selectable row inside a
   panel" control, just driven by a Stimulus listbox instead of a <details> menu:
   the compose topic autocomplete (compose/topics.js) and the Settings timezone
   picker (tz_combobox_controller.js). Both are pointer/keyboard-actionable with an
   .is-active selection state, so they are ACTIONS and take the 3px corner; their
   containing panels (.compose-ac, .tz-combobox__list) stay soft surfaces. Left at
   6px they made one primitive wear two different corners on the same page. */
[data-brand="press"] .compose-ac__opt,
[data-brand="press"] .tz-combobox__option { border-radius: var(--r-action); }

/* ── Breadcrumb (FE-315) — no exact v3 markup; semantic, token-styled ─────── */
.breadcrumb { font-family: var(--font-body); font-size: var(--fs-caption); }
.breadcrumb__list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.breadcrumb__item { display: inline-flex; align-items: center; gap: 6px; color: var(--text-muted); }
.breadcrumb__item + .breadcrumb__item::before { content: "\203A"; color: var(--outline-variant); }
.breadcrumb__item a { color: var(--text-muted); text-decoration: none; font-weight: 600; }
.breadcrumb__item a:hover { color: var(--accent); }
.breadcrumb__current { color: var(--text); font-weight: 700; }

/* ── Pagination (FE-316) — no exact v3 markup; semantic, token-styled ─────── */
.pagination { margin: var(--space-xl) 0 var(--space-sm); }
.pagination__list { list-style: none; margin: 0; padding: 0; display: flex; align-items: center; gap: 6px; justify-content: center; flex-wrap: wrap; }
/* Outlined, interactive nav links — the action corner, not the card corner. */
.pagination__link {
  display: inline-flex; align-items: center; justify-content: center; min-width: 38px; height: 38px; padding: 0 12px;
  border-radius: var(--r-action); border: 1px solid var(--border); background: var(--surface-elevated);
  color: var(--text); font-family: var(--font-body); font-size: var(--fs-body-sm); font-weight: 600; text-decoration: none;
}
.pagination__link:hover { background: var(--accent-subtle); color: var(--accent); border-color: var(--accent-subtle); }
.pagination__current {
  display: inline-flex; align-items: center; justify-content: center; min-width: 38px; height: 38px; padding: 0 12px;
  border-radius: var(--r-action); background: var(--accent); color: var(--on-primary);
  font-family: var(--font-body); font-size: var(--fs-body-sm); font-weight: 700;
}
.pagination__gap { color: var(--text-muted); padding: 0 4px; }

/* ── Join bar (FE-308) ───────────────────────────────────────────────────── */
.join-bar {
  display: flex; align-items: center; gap: 16px; background: var(--surface-elevated);
  border: 1px solid var(--border); border-radius: var(--r-lg); padding: 15px 18px; margin-top: 16px;
  box-shadow: var(--shadow-pop); flex-wrap: wrap;
}
.join-proof { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 230px; }
.join-proof .avs { display: flex; flex: 0 0 auto; }
.join-proof .avs .av-pv { width: 30px; height: 30px; margin-left: -8px; box-shadow: 0 0 0 2px var(--surface-elevated); }
.join-proof .avs .av-pv:first-child { margin-left: 0; }
.join-proof .jtxt { font-family: var(--font-body); font-size: 13.5px; line-height: 1.4; color: var(--text-muted); }
.join-proof .jtxt b { color: var(--text); font-weight: 700; }
.join-proof .jtxt .vel { color: var(--violet-600); font-weight: 600; }
.join-bar .jact { display: flex; gap: 10px; flex: 0 0 auto; }
.join-bar .jact a, .join-bar .jact button { white-space: nowrap; }

/* ── Join nudge (FE-308) ─────────────────────────────────────────────────── */
.nudge {
  display: flex; align-items: center; gap: 16px; border: 1px solid var(--accent-subtle);
  background: var(--primary-container); border-radius: var(--r-lg); padding: 16px 18px; margin: 18px 0;
}
.nudge .nx {
  flex: 0 0 auto; width: 42px; height: 42px; border-radius: var(--r-md); background: var(--accent);
  color: var(--on-primary); display: flex; align-items: center; justify-content: center;
}
.nudge .nbody { flex: 1; min-width: 0; }
.nudge .nbody .nt { font-family: var(--font-body); font-size: var(--fs-body-sm); font-weight: 700; color: var(--text); line-height: 1.35; }
.nudge .nbody .ns { font-size: 13.5px; color: var(--text-muted); margin-top: 2px; }
.nudge .nbody .ns .vel { color: var(--violet-600); font-weight: 600; }

/* ── Post card (FE-311/FE-312) ───────────────────────────────────────────── */
.post-card { padding: 18px 0; border-bottom: 1px solid var(--border); display: flex; gap: 14px; }
.post-card:first-of-type { padding-top: 0; }
.post-card--flat { border-bottom: none; }
/* An interactive card opens its thread on click (post_card_controller) — signal it. */
.post-card--open { cursor: pointer; }
.post-card--open:hover { background: rgba(20, 15, 10, 0.02); }
.theme-dark .post-card--open:hover { background: rgba(255, 255, 255, 0.03); }
.pc-body { flex: 1; min-width: 0; }
/* "Reposted by @X" attribution (Following feed) — a muted, small caption above
   the author row, with the repeat glyph. Parity with the mobile card. */
.pc-repost-attr {
  display: flex; align-items: center; gap: 6px; margin: 0 0 4px;
  font-size: var(--fs-caption); font-weight: 600; color: var(--text-muted);
}
.pc-repost-attr svg { stroke: currentColor; fill: none; flex: none; }
.pc-meta { display: flex; align-items: center; gap: 6px; font-size: var(--fs-body-sm); flex-wrap: wrap; }
.pc-name { font-weight: 700; color: var(--text); text-decoration: none; }
a.pc-name:hover { color: var(--accent); }
.pc-hd { color: var(--text-muted); font-size: 14px; text-decoration: none; }
.pc-edited { color: var(--text-muted); font-size: 14px; }
.pc-role { color: var(--text-muted); font-size: 13.5px; margin-top: 1px; }
.pc-text { font-size: 18.5px; line-height: 1.58; color: var(--text); margin: 6px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }
/* The thread focal post's body is the page's single <h1> (SEO/a11y) but must READ
   as the editorial drop-cap body lead — reset the UA heading weight so it doesn't
   render as a big bold title. Font-size/line-height/margin already come from .pc-text
   (which beats the bare h1); .dropcap (cover_art.css) styles ::first-letter as before. */
h1.pc-text { font-weight: 400; }
/* Inline auto-linked URLs in a post body (mobile parity) */
.pc-link { color: var(--accent); text-decoration: none; overflow-wrap: anywhere; }
.pc-link:hover { text-decoration: underline; }
/* Long-post truncation (non-lead cards) — clamp the body to ~8 lines; the
   showmore controller un-hides the toggle only when the text actually overflows,
   and toggles .pc-clamp--open to reveal the full body. Full text stays in the DOM
   (SEO-safe); JS-off leaves it clamped (the whole card still opens the thread). */
.pc-clamp__text { display: -webkit-box; -webkit-line-clamp: 8; -webkit-box-orient: vertical; overflow: hidden; }
.pc-clamp--open .pc-clamp__text { display: block; -webkit-line-clamp: none; overflow: visible; }
.pc-clamp__toggle {
  margin-top: 3px; padding: 0; border: none; background: none; cursor: pointer;
  font-family: var(--font-body); font-size: var(--fs-caption); font-weight: 600; color: var(--accent);
}
.pc-clamp__toggle:hover { text-decoration: underline; }
.pc-topic-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
.pc-topic { display: inline-flex; font-size: var(--fs-caption); font-weight: 600; color: var(--topic-text); background: var(--topic-tint); border-radius: var(--r-action); padding: 4px 12px; text-decoration: none; }
/* Secondary ("also relevant") topic pill — the same pill, dimmed, + a muted note. */
.pc-topic--secondary { opacity: 0.72; }
.pc-topic-note { font-size: var(--fs-caption); font-weight: 600; color: var(--text-muted); margin-left: -4px; }
/* Calm neutral placeholder (not --cover's near-black) behind the photo while it
   loads/crossfades in — the .img-fade <img> covers it once decoded.
   Natural aspect ratio (no fixed height): the serialized width/height attributes
   let the browser reserve the right slot pre-paint (no layout shift), clamped to
   max-height 480px so very tall images don't dominate the feed. object-fit: cover
   only kicks in at the clamp. */
.pc-photo { margin-top: 12px; border-radius: var(--r-lg); overflow: hidden; position: relative; background: var(--surface-muted); }
.pc-photo img, .pc-photo svg { width: 100%; height: auto; max-height: 480px; display: block; object-fit: cover; }
/* Click-to-zoom trigger — a block anchor over the thumbnail. JS-off falls back to
   opening the full image in a new tab; JS opens the focus-trapped gallery lightbox. */
.pc-photo__zoom { display: block; width: 100%; cursor: zoom-in; }
/* ── Gallery lightbox — shared by post photos and DM images ──────────────────
   A native <dialog> (focus-trap + Esc for free). Backdrop at 0.94 opacity with a
   200ms fade-in on open; reduced-motion users get no animation. */
.pc-lightbox { border: none; padding: 0; max-width: 100vw; max-height: 100vh; width: 100%; height: 100%; background: transparent; overflow: hidden; }
.pc-lightbox::backdrop { background: rgba(10, 10, 10, 0.94); }
.pc-lightbox[open] { display: flex; align-items: center; justify-content: center; }
@media (prefers-reduced-motion: no-preference) {
  /* No fill mode: a retained fill keeps the dialog composited after the animation
     ends, which breaks ::backdrop painting in Chromium. The backdrop gets its own
     fade — animating only the dialog does not animate its backdrop. */
  .pc-lightbox[open] { animation: lx-open 200ms ease-out; }
  .pc-lightbox[open]::backdrop { animation: lx-backdrop-in 200ms ease-out; }
  @keyframes lx-open { from { opacity: 0; transform: scale(0.96); } }
  @keyframes lx-backdrop-in { from { opacity: 0; } }
}
/* The big image: preserve aspect ratio, no drop shadow. */
.pc-lightbox__img { max-width: min(90vw, 1200px); max-height: 85vh; width: auto; height: auto; object-fit: contain; display: block; }
/* Blur-up loading state: show thumbnail-resolution frame blurred, swap on full load. */
.pc-lightbox__img.lx-loading { filter: blur(12px); }
@media (prefers-reduced-motion: no-preference) { .pc-lightbox__img.lx-loading { transition: filter 240ms ease; } }
/* × close button — top-right, min 44×44 tap target. */
.pc-lightbox__close {
  position: fixed; top: 16px; right: 16px; z-index: 1; width: 44px; height: 44px; min-width: 44px; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
  border: none; border-radius: var(--r-action);
  background: rgba(255, 255, 255, 0.12); color: rgba(255, 255, 255, 0.8);
  font-size: 22px; line-height: 1; transition: background 120ms, color 120ms;
}
.pc-lightbox__close:hover { background: rgba(255, 255, 255, 0.24); color: #fff; }
/* Gallery nav: ‹ › chevron pills, 44px tap target, hidden for single-image. */
.pc-lightbox__nav {
  position: fixed; top: 50%; transform: translateY(-50%); z-index: 1;
  width: 44px; height: 44px; min-width: 44px; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
  border: none; border-radius: var(--r-action);
  background: rgba(255, 255, 255, 0.12); color: #fff;
  transition: background 120ms;
}
.pc-lightbox__nav:hover { background: rgba(255, 255, 255, 0.24); }
.pc-lightbox__nav[hidden] { display: none; }
.pc-lightbox__prev { left: 24px; }
.pc-lightbox__next { right: 24px; }
/* Image counter "2 / 5" — bottom-center, muted white, hidden for single-image. */
.pc-lightbox__counter {
  position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); z-index: 1;
  font-size: 12px; letter-spacing: 0.08em; color: rgba(255, 255, 255, 0.6); white-space: nowrap;
}
.pc-lightbox__counter[hidden] { display: none; }
/* Body scroll lock while the lightbox is open (added to <html> by the JS controller).
   --lx-sb-w compensates for the scrollbar disappearing so the layout doesn't shift. */
html.lx-lock { overflow: hidden; padding-right: var(--lx-sb-w, 0); }
.pc-photo .cap {
  position: absolute; left: 14px; bottom: 12px; font-family: var(--font-body); font-size: 12px; font-weight: 600;
  letter-spacing: .02em; color: var(--cover-text); text-shadow: 0 1px 6px rgba(0, 0, 0, .6);
  display: flex; align-items: center; gap: 6px;
}
.pc-unavailable { color: var(--text-muted); font-style: italic; font-size: 16.5px; margin: 6px 0 0; }

/* ── Quoted embed (a quote post's embedded original — feed / thread / profile) ── */
.pc-quote { margin-top: 12px; }
.pc-quote__card {
  display: block; border: 1px solid var(--border); border-radius: var(--r-lg);
  padding: 12px 14px; text-decoration: none; color: var(--text); background: var(--surface-elevated);
}
.pc-quote__card:hover { border-color: var(--accent); }
.pc-quote__author { display: block; font-weight: 700; font-size: var(--fs-body-sm); color: var(--text); }
.pc-quote__body {
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
  margin-top: 3px; font-size: 15.5px; line-height: 1.45; color: var(--text-muted); white-space: pre-wrap;
}
.pc-quote__unavailable { color: var(--text-muted); font-style: italic; font-size: 15.5px; margin-top: 12px; }

/* ── Reaction bar (FE-313, L1 display-only) ──────────────────────────────── */
.pc-actions { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; margin-left: -6px; }
.rx-btn {
  display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-body); font-size: 13.5px;
  font-weight: 600; color: var(--text-muted); background: transparent; border: none; border-radius: var(--r-action);
  padding: 6px 11px; cursor: default;
}
.rx-btn svg { stroke: currentColor; fill: none; }
.rx-btn.liked { color: var(--heart-red); }
.rx-btn.liked svg { fill: var(--heart-red); stroke: var(--heart-red); }
.rx-btn.saved { color: var(--violet-600); }
.rx-btn.saved svg { fill: var(--violet-600); stroke: var(--violet-600); }

/* ── React gate (FE-314, logged-out signup CTA) ──────────────────────────── */
.react-gate {
  border: 1px solid var(--border); background: var(--cover); color: var(--cover-text);
  border-radius: var(--r-lg); padding: 16px 18px; margin: 14px 0; max-width: 320px;
}
.react-gate .gp-t { font-family: var(--font-display); font-weight: 600; font-size: var(--fs-subtitle); margin-bottom: 3px; }
.react-gate .gp-s { font-size: var(--fs-body-sm); color: rgba(245, 245, 245, .78); line-height: 1.4; margin-bottom: 12px; }
.react-gate .btn-primary { width: 100%; }

/* ── Link preview card (FE-309) — renders click_url ONLY, never requested_url ─ */
.lp-card {
  display: flex; margin-top: 12px; border: 1px solid var(--border); border-radius: var(--r-lg);
  overflow: hidden; text-decoration: none; color: var(--text); background: var(--surface-elevated);
}
.lp-card__media { flex: 0 0 auto; width: 128px; min-height: 84px; position: relative; background: var(--surface-muted); }
.lp-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Branded fallback tile — a magazine-cover-style tinted panel carrying the
   domain monogram (or a neutral link glyph), shown when the publisher thumbnail
   is missing OR fails to load. Reuses the M3 container/on-container token pair so
   contrast is designed-in for both themes. Absolutely fills the media box so it
   overlays a broken <img> once the controller reveals it. No motion (reduced-
   motion safe by construction). */
.lp-card__fallback {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-subtle); color: var(--on-primary-container);
}
/* The `hidden` attribute must win over the author display rules above/on the img
   (a bare [hidden] would otherwise be overridden). Toggled by the controller. */
.lp-card__img[hidden], .lp-card__fallback[hidden] { display: none; }
.lp-card__monogram { font-family: var(--font-display); font-weight: 600; font-size: var(--fs-heading); line-height: 1; text-transform: uppercase; }
.lp-card__glyph { opacity: .75; }
.lp-card__body { padding: 12px 14px; min-width: 0; }
.lp-card__title { font-family: var(--font-body); font-weight: 700; font-size: var(--fs-body-sm); color: var(--text); line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.lp-card__desc { font-size: 13.5px; color: var(--text-muted); margin-top: 3px; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.lp-card__domain { font-size: 12px; color: var(--text-muted); margin-top: 6px; text-transform: lowercase; }
.lp-card:hover { border-color: var(--accent); }

/* ── Cover masthead (topic/section pages consume these later; used by rail) ── */
.cover-mast {
  color: var(--cover-text); border-radius: var(--r-xl); padding: 36px 32px; margin-bottom: 22px;
  position: relative; overflow: hidden;
}
.cover-mast .ce { font-family: var(--font-body); font-size: 12.5px; letter-spacing: .2em; text-transform: uppercase; color: var(--primary-tint); font-weight: 700; }
.cover-mast .ct { font-family: var(--font-display); font-weight: 600; font-size: 42px; line-height: 1.05; margin: 10px 0 8px; letter-spacing: -.01em; }
.cover-mast .cd { font-size: 15.5px; color: rgba(245, 245, 245, .78); max-width: 440px; line-height: 1.5; }
.cover-mast .cm { margin-top: 16px; font-size: 13.5px; color: rgba(245, 245, 245, .62); }

/* ── Index-page heading (Newsstand / Topics / Sections / Explore / Search hubs) ──
   A proper display H1 — the discovery hubs' page title. Sits ABOVE the smaller
   .rail-label section subheads so the hierarchy reads (an index H1 must not share
   the tiny eyebrow scale its own subheads use). */
.page-heading {
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-heading);
  line-height: 1.1; letter-spacing: -.01em; color: var(--text); margin: 4px 0 18px;
}

/* ── Sections index — a responsive cover-mini grid (BE-113) ───────────────────
   The masthead lists every section as an editorial cover-mini card. auto-fill /
   minmax gives 1 column on a phone and 2–3 across the boxed desktop column
   (never a single stack pinned far-left with an empty right half). */
.section-index {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
  gap: 14px; margin-top: 4px;
}

/* ── Sibling-topic chip row (BE-112 reflow) ───────────────────────────────────
   The "More in {Section}" siblings live in the desktop Newsstand aside (≥1100px).
   Below that the aside is hidden, so this inline horizontal chip row carries the
   same links — and is itself hidden exactly where the aside returns, so the two
   never render at once. */
.sibling-chips { margin: 26px 0 4px; }
.sibling-chips__label {
  display: block; font-family: var(--font-body); font-size: 12.5px; font-weight: 700;
  letter-spacing: .14em; text-transform: uppercase; color: var(--text-muted); margin-bottom: 10px;
}
.sibling-chips__row {
  display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.sibling-chips__row::-webkit-scrollbar { display: none; }
.sibling-chip {
  flex: 0 0 auto; font-family: var(--font-body); font-weight: 600; font-size: var(--fs-body-sm);
  color: var(--text); background: var(--surface-elevated); border: 1px solid var(--border);
  border-radius: var(--r-action); padding: 7px 14px; text-decoration: none; white-space: nowrap;
}
.sibling-chip:hover { border-color: var(--accent); color: var(--accent); }
@media (min-width: 1100px) { .sibling-chips { display: none; } }

/* ── Profile header (FE-320/FE-321) — /@username identity block ─────────────── */
.profile-header { padding: 4px 0 18px; border-bottom: 1px solid var(--border); margin-bottom: 4px; }
.profile-header__top { display: flex; align-items: center; gap: 16px; }
.profile-header__id { min-width: 0; flex: 1 1 auto; }
.profile-header__name {
  font-family: var(--font-display); font-weight: 600; font-size: 26px; line-height: 1.1;
  letter-spacing: -.01em; color: var(--text); display: flex; align-items: center; gap: 7px;
}
.profile-header__handle { font-size: var(--fs-body-sm); color: var(--text-muted); margin-top: 3px; }
.profile-header__action { flex: 0 0 auto; }
.profile-header__bio { margin-top: 14px; font-size: 15.5px; line-height: 1.5; color: var(--text); max-width: 48ch; }
.profile-header__counts { display: flex; gap: 26px; margin-top: 16px; }
.profile-header__counts .pf-count { display: flex; align-items: baseline; gap: 6px; }
.pf-count__value { font-family: var(--font-body); font-weight: 700; font-size: 16px; color: var(--text); }
.pf-count__label { font-size: var(--fs-body-sm); color: var(--text-muted); }

/* ── Profile tabs (posts / replies) ────────────────────────────────────────── */
.profile-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin: 6px 0 2px; }
.profile-tabs__tab {
  font-family: var(--font-body); font-weight: 600; font-size: var(--fs-body-sm); color: var(--text-muted);
  text-decoration: none; padding: 12px 16px; border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.profile-tabs__tab:hover { color: var(--text); }
.profile-tabs__tab.is-active { color: var(--violet-600); border-bottom-color: var(--violet-500); }

/* ── Share Your Ten (FE-322) — the owner's curated lists ───────────────────── */
.share-ten { margin: 22px 0; }
.share-ten__heading {
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-subtitle);
  color: var(--text); margin-bottom: 14px;
}
.share-ten__list {
  background: var(--surface-elevated); border: 1px solid var(--border); border-radius: var(--r-lg);
  padding: 16px 18px; margin-bottom: 12px;
}
.share-ten__category {
  font-family: var(--font-body); font-size: 12px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--violet-600); margin-bottom: 10px;
}
.share-ten__items { list-style: none; counter-reset: syt; margin: 0; padding: 0; }
.share-ten__item { counter-increment: syt; display: flex; gap: 10px; padding: 7px 0; }
.share-ten__item::before { content: counter(syt); flex: 0 0 auto; width: 20px; font-weight: 700; color: var(--text-muted); font-size: 13px; }
.share-ten__text { font-size: 15px; color: var(--text); font-weight: 600; }
.share-ten__note { display: block; font-size: 13.5px; color: var(--text-muted); margin-top: 2px; line-height: 1.4; }

/* ── Share Your Ten — owner entry + editor (reuses the .settings-* form theme) ─ */
.syt-owner-actions { margin: 6px 0 18px; }
.syt-owner-actions__edit {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--font-body); font-weight: 700; font-size: var(--fs-body-sm);
  color: var(--violet-600); text-decoration: none;
  border: 1px solid var(--border); border-radius: var(--r-action); padding: 8px 14px;   /* outlined control */
}
.syt-owner-actions__edit:hover { border-color: var(--violet-600); }
.syt-edit__rows { list-style: none; counter-reset: syt-edit; margin: 0 0 14px; padding: 0; }
.syt-edit__row {
  counter-increment: syt-edit; display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
  padding: 10px 0; border-bottom: 1px solid var(--border);
}
.syt-edit__row:last-child { border-bottom: none; }
.syt-edit__optional { color: var(--text-muted); font-weight: 400; }
@media (max-width: 560px) { .syt-edit__row { grid-template-columns: 1fr; gap: 7px; } }

/* ── Share-Your-Ten accordion (one category open at a time) ───────────────────
   Each category is a native <details>; the <summary> is the clickable heading.
   JS-off toggles natively; name="syt-cat" makes it exclusive in modern browsers. */
.syt-accordion { gap: 0; }
.syt-cat { padding: 0; }
.syt-cat > :not(summary) { padding-top: 14px; }
.syt-cat__summary {
  list-style: none; cursor: pointer; user-select: none;
  display: flex; align-items: center; gap: 10px;
  padding: 16px 4px; margin: 0;
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-subtitle); color: var(--text);
}
.syt-cat__summary::-webkit-details-marker { display: none; }
.syt-cat__summary::marker { content: ""; }
.syt-cat__summary::after {
  content: ""; flex: 0 0 auto; margin-left: auto;
  width: 9px; height: 9px; border-right: 2px solid var(--text-muted); border-bottom: 2px solid var(--text-muted);
  transform: rotate(45deg); transition: transform 160ms ease;
}
.syt-cat[open] > .syt-cat__summary::after { transform: rotate(-135deg); }
.syt-cat__summary:hover { color: var(--accent); }
.syt-cat__summary:hover::after { border-color: var(--accent); }
@media (prefers-reduced-motion: reduce) { .syt-cat__summary::after { transition: none; } }

/* ── Share-Your-Ten inline save confirmation + validation error (Turbo Stream) ──
   Both live regions sit empty on load next to a category's Save button; a save
   swaps their contents in place (no reload). The "Saved ✓" chip reuses the app's
   violet notice palette and auto-dismisses (syt-saved controller). */
.syt-saved { margin: 0; }
.syt-saved__msg {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px; border-radius: var(--r-action);
  background: var(--primary-container); color: var(--on-primary-container);
  font: 700 var(--fs-body-sm) / 1 var(--font-body);
}
.syt-saved__check { flex: 0 0 auto; }
.syt-error { margin: 8px 0 0; }
.syt-error__msg {
  margin: 0; padding: 10px 12px; border-radius: var(--r-md);
  background: rgba(255, 48, 64, .1); color: var(--heart-red);
  font-size: var(--fs-body-sm); line-height: 1.4;
}
@media (prefers-reduced-motion: no-preference) {
  .syt-saved__msg { animation: syt-saved-in .18s ease both; }
  .syt-saved__msg.is-dismissing { animation: syt-saved-out .28s ease both; }
}
@keyframes syt-saved-in { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } }
@keyframes syt-saved-out { from { opacity: 1; } to { opacity: 0; } }
/* ── Link tabs (BE-112, topic/section Top/New sort) — ported from the v3 mockup class name ── */
.link-tabs { display: flex; gap: 22px; border-bottom: 1px solid var(--border); margin: 22px 0 4px; }
.link-tabs a {
  font-family: var(--font-body); font-weight: 700; font-size: var(--fs-body-sm); color: var(--text-muted);
  text-decoration: none; padding: 0 2px 12px; border-bottom: 3px solid transparent;
}
/* Active tab: violet ink + a thicker violet underline (the 2px underline read as
   too subtle). The base reserves 3px transparent so toggling never shifts baseline. */
.link-tabs a.on { color: var(--accent); border-bottom-color: var(--accent); }
.link-tabs a:hover { color: var(--text); }
/* Per-type result count on a search tab (Posts 0 · People 1 · Topics 0) — subtle,
   so a glance shows which tab actually holds matches without switching. */
.link-tabs__count { font-weight: 700; font-size: 12px; color: var(--text-muted); margin-left: 1px; }
.link-tabs a.on .link-tabs__count { color: var(--accent); }

/* ── Blended "Top" search view — the non-empty People / Posts / Topics sections,
   each with a labelled header + a "See all N →" link to its full type tab. The
   rows reuse .explore-shelf / .person-row / .pick-row / .post-card above; only
   the section header is new here. ── */
.top-section { margin: 4px 0 22px; }
.top-section__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin: 18px 0 6px;
}
.top-section__label {
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-subtitle);
  color: var(--text); margin: 0; line-height: 1.2;
}
.top-section__seeall {
  flex: 0 0 auto; font: 700 var(--fs-body-sm) / 1 var(--font-body);
  color: var(--accent); text-decoration: none; white-space: nowrap;
}
.top-section__seeall:hover { text-decoration: underline; }

/* ── Search zero-state: recent searches (client-local, JS-rendered) + trending
   chips. Both live inside the #search_results frame's zero-state; JS-off the
   recents section stays empty+hidden and the chips + shelves carry the state. ── */
.search-recents { margin: 14px 0 4px; }
.search-recents[hidden] { display: none; }
.search-recents__head {
  display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 4px;
}
.search-recents__title {
  font-family: var(--font-body); font-weight: 700; font-size: var(--fs-eyebrow);
  letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); margin: 0;
}
.search-recents__clear {
  border: none; background: none; cursor: pointer; padding: 4px 2px;
  font: 700 var(--fs-caption) / 1 var(--font-body); color: var(--accent);
}
.search-recents__clear:hover { text-decoration: underline; }
.search-recents__list { list-style: none; margin: 0; padding: 0; }
.search-recents__row {
  display: flex; align-items: center; gap: 8px;
  border-bottom: 1px solid var(--border);
}
.search-recents__row:last-child { border-bottom: none; }
.search-recents__link {
  flex: 1 1 auto; min-width: 0; padding: 11px 2px; color: var(--text);
  text-decoration: none; font-size: var(--fs-body-sm); font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.search-recents__link:hover { color: var(--accent); }
.search-recents__x {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border: none; border-radius: 50%; cursor: pointer;
  background: none; color: var(--text-muted); font-size: 12px; line-height: 1;
}
.search-recents__x:hover { background: var(--surface-3); color: var(--text); }

.search-trending {
  display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 4px;
}
.topic-chip {
  display: inline-flex; align-items: center; text-decoration: none;
  padding: 7px 14px; border-radius: var(--r-action);
  border: 1.5px solid var(--border); background: var(--surface-1);
  color: var(--text); font: 700 var(--fs-caption) / 1 var(--font-body);
}
.topic-chip:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-subtle); }

/* ── L2 logged-in feed (BE-120) — the header + empty state; the tabs reuse
   .link-tabs and the rows reuse .post-card / .feed-list above. ── */
.feed-head { margin: 4px 0 2px; }
.feed-head__title {
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-title);
  color: var(--text); margin: 0; line-height: 1.15;
}
.feed-empty {
  color: var(--text-muted); font-size: var(--fs-body-lg); line-height: 1.5;
  padding: 40px 0; text-align: center;
}

/* Search zero-state dead-end (no curated picks) — a centred "Browse the Newsstand"
   CTA under calm copy, instead of two dead sentences + a gap. */
.search-zero-cta { text-align: center; padding: 8px 0 40px; }
.search-zero-cta .feed-empty { padding: 12px 0 18px; }

/* ── Saved (collections magazine-cover grid) — the tabs reuse .link-tabs, the
   header/empty state reuse .feed-head/.feed-empty above. ── */
.collection-back { margin: 0 0 6px; }
.collection-back a {
  color: var(--text-muted); text-decoration: none; font-weight: 700;
  font-size: var(--fs-body-sm);
}
.collection-back a:hover { color: var(--text); }
.collections-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--space-lg); margin: 22px 0 4px;
}
.collection-card { display: block; text-decoration: none; color: inherit; }
.collection-card__cover {
  position: relative; aspect-ratio: 3 / 4; border-radius: var(--r-md); overflow: hidden;
  display: flex; align-items: flex-end; padding: 12px;
  background-color: var(--cover);
  background-image:
    linear-gradient(160deg, rgba(10,10,12,.20) 0%, rgba(10,10,12,.62) 100%),
    var(--grain),
    radial-gradient(130% 120% at 78% 14%, #2a2260 0%, #14122e 46%, #0a0a0c 84%);
  background-size: cover, 150px 150px, cover;
  background-repeat: no-repeat, repeat, no-repeat;
  background-position: center;
}
.collection-card__photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.collection-card__cover-text {
  position: relative; z-index: 1; color: #F3F1FA; font-family: var(--font-body);
  font-size: 12.5px; line-height: 1.35;
}
/* The Press: the same ink-family plate the cover-art system wears (cover_art.css) —
   the violet gradient OVERPAINTS background-color: var(--cover), so the token flip
   alone never reached this card. Constant-dark plate, so the indigo/paper cuts are
   literal (a var would resolve the light-scheme cut onto a dark field). */
[data-brand="press"] .collection-card__cover {
  background-image:
    linear-gradient(160deg, rgba(18,16,13,.20) 0%, rgba(18,16,13,.62) 100%),
    var(--grain),
    radial-gradient(130% 120% at 78% 14%, #3A3049 0%, #2B1E4A 46%, #12100D 84%);
}
[data-brand="press"] .collection-card__cover-text { color: var(--cover-text); }
.collection-card__name {
  margin: 10px 0 1px; font-family: var(--font-body); font-weight: 700;
  font-size: var(--fs-body-sm); color: var(--text);
}
.collection-card__count { margin: 0; font-size: var(--fs-caption); color: var(--text-muted); }

/* ── Collections WRITE surface (create / add-to / rename / delete) — the write
   sibling of the read-only grid above. Reuses the .feed-head / .collection-back /
   .collections-grid / .btn-* atoms; these rules add only the create-cell, the
   forms, the collection-detail header row, and the add-to-collection sheet. ── */

/* The "New collection" grid cell — a dashed placeholder cover with a + glyph, the
   same 3:4 aspect + radius as a real cover so the grid stays even. */
.collection-card--new .collection-card__count { color: var(--text-muted); }
.collection-card__cover--new {
  background: none; border: 1.5px dashed var(--border);
  align-items: center; justify-content: center; color: var(--text-muted);
  transition: border-color .14s, color .14s;
}
.collection-card--new:hover .collection-card__cover--new { border-color: var(--accent); color: var(--accent); }

/* Empty-state CTA (no collections yet) — centres the create button under the copy. */
.collections-cta { display: flex; justify-content: center; margin: 4px 0 8px; }

/* Create / rename form — a single named field + the "always private" note, laid
   out inside the shared .compose-card. */
.collection-form__field { display: flex; flex-direction: column; gap: 8px; }
.collection-form__label {
  font-family: var(--font-body); font-weight: 700; font-size: var(--fs-body-sm); color: var(--text);
}
.collection-form__input { width: 100%; }
.collection-form__note {
  display: flex; align-items: center; gap: 7px; margin: 2px 0 0;
  font-size: var(--fs-caption); color: var(--text-muted); line-height: 1.4;
}

/* Report reason picker (report a post / a message) — a full-width, tappable
   vertical radio list inside the shared card treatment. Replaces the raw
   browser <fieldset> chrome + cramped inline radios. */
.report-reasons {
  margin: 16px 0 0; padding: 6px; border: 1px solid var(--outline-variant);
  border-radius: var(--r-lg); background: var(--surface-hi);
  display: flex; flex-direction: column; gap: 2px; min-inline-size: 0;
}
.report-reasons__legend {
  float: left; width: 100%; padding: 8px 10px 6px;
  font-family: var(--font-body); font-weight: 700; font-size: var(--fs-body-sm); color: var(--text);
}
.report-reason {
  display: flex; align-items: center; gap: 11px; width: 100%;
  padding: 13px 12px; border-radius: var(--r-md); cursor: pointer;
  transition: background-color .14s ease;
}
.report-reason:hover { background: var(--surface-muted); }
.report-reason:has(.report-reason__input:checked) { background: var(--surface-muted); }
.report-reason__input { flex: 0 0 auto; margin: 0; }
.report-reason__label {
  font-family: var(--font-body); font-size: var(--fs-body); color: var(--text); line-height: 1.35;
}
.report-reason:has(.report-reason__input:checked) .report-reason__label { font-weight: 650; }
.report-reason:has(.report-reason__input:focus-visible) {
  outline: 2px solid var(--accent); outline-offset: 1px;
}
.collection-form__note svg { flex: 0 0 auto; }
.collection-form__actions { display: flex; justify-content: flex-end; gap: var(--space-sm); }

/* Collection-detail header — the title block + the owner ⋯ menu (rename/delete)
   on a row. .collection-menu drops .pc-menu's margin-left:auto so the trigger
   sits at the header's right edge without stretching the title. */
.feed-head--collection {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-md); margin-bottom: 18px; /* breathing room before the first post's ⋯ */
}
.feed-head__lead { min-width: 0; }
/* Item-count subtitle under the title — grounds the header + separates the two ⋯. */
.feed-head__count { margin: 4px 0 0; font-size: var(--fs-body-sm); color: var(--text-muted); }
.collection-menu { margin-left: 0; flex: 0 0 auto; margin-top: 4px; }

/* Add-to-collection sheet — a list of toggle rows (one per collection) + the
   read-only "All saves" row + a "New collection" affordance. Each row is a
   full-width button_to form; the whole row is the tap target. */
.collect-sheet__excerpt {
  margin: 14px 0 6px; padding: 12px 14px; border-radius: var(--r-md);
  background: var(--surface-hi); border: 1px solid var(--outline-variant);
  font-family: var(--font-body); font-size: var(--fs-body-sm); font-style: italic;
  color: var(--text-muted); line-height: 1.45;
}
.collect-sheet { list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-direction: column; }
.collect-row { border-bottom: 1px solid var(--border); }
.collect-row:last-child { border-bottom: 0; }
.collect-row__form { margin: 0; }
.collect-row__toggle, .collect-row--static {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-md);
  width: 100%; padding: 14px 4px; text-align: left;
  background: transparent; border: 0; cursor: pointer;
}
.collect-row--static { cursor: default; }
.collect-row__toggle:hover { background: var(--accent-subtle); }
.collect-row__name {
  font-family: var(--font-body); font-weight: 600; font-size: var(--fs-body); color: var(--text);
}
.collect-row__state {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%; color: var(--text-muted);
}
.collect-row__state--on { color: var(--accent); }
.collect-sheet__new { margin: 18px 0 4px; display: flex; }

/* ── Cookie-consent banner (LEG-903) — fixed to the viewport bottom, on every
   Web:: page (this file loads unconditionally from the layout, unlike pages.css
   which only some views link). Dismisses itself: once a choice is recorded the
   component just doesn't render on the next page. ── */
.consent-banner {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-lg);
  justify-content: space-between;
  padding: var(--space-lg) var(--space-xl);
  background: var(--surface-elevated); border-top: 1px solid var(--outline-variant);
  box-shadow: var(--shadow-banner);
}
.consent-banner__copy {
  margin: 0; flex: 1 1 320px; max-width: 60ch;
  font: 400 var(--fs-body-sm) / 1.5 var(--font-body); color: var(--ink-2);
}
.consent-banner__copy a { color: var(--primary); }
.consent-banner__actions { display: flex; gap: var(--space-sm); flex: 0 0 auto; margin: 0; }

/* Reserve room at the page bottom so a viewport-fixed banner (verify-email /
   consent) never overlaps the last content row or the footer. The layout adds
   .has-fixed-banner to <body> only when such a banner will actually render, so
   pages without one keep their normal spacing. Mobile reserves more (the banner
   wraps its copy + actions onto multiple rows). */
body.has-fixed-banner { padding-bottom: 152px; }
@media (min-width: 560px) { body.has-fixed-banner { padding-bottom: 92px; } }

/* ── "Verify your email" soft-gate banner — same viewport-bottom treatment as the
   consent banner, shown only to a logged-in visitor whose email is unverified.
   Dismissible for the session; NEVER blocks browsing. ── */
.verify-banner {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-lg);
  justify-content: space-between;
  padding: var(--space-lg) var(--space-xl);
  background: var(--surface-elevated); border-top: 1px solid var(--outline-variant);
  box-shadow: var(--shadow-banner);
}
.verify-banner__copy {
  margin: 0; flex: 1 1 320px; max-width: 60ch;
  font: 400 var(--fs-body-sm) / 1.5 var(--font-body); color: var(--ink-2);
}
.verify-banner__actions { display: flex; align-items: center; gap: var(--space-sm); flex: 0 0 auto; margin: 0; }
.verify-banner__resend { display: contents; }

/* ── "Add your name" nudge — INLINE (normal block flow), top of the feed, for a
   logged-in viewer whose profile has no display_name (legacy accounts predating
   the required-name signup rule). Deliberately NOT position:fixed like
   .verify-banner, so the two callouts never collide if both would apply. ── */
.name-nudge {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-md);
  justify-content: space-between;
  margin-bottom: var(--space-lg);
  padding: var(--space-md) var(--space-lg);
  background: var(--surface-elevated); border: 1px solid var(--outline-variant);
  border-radius: var(--r-md);
}
.name-nudge__copy {
  margin: 0; flex: 1 1 240px; max-width: 60ch;
  font: 400 var(--fs-body-sm) / 1.5 var(--font-body); color: var(--ink-2);
}
.name-nudge__cta { flex: 0 0 auto; }

/* ── Global flash toast (layouts/web.html.erb → web/shared/_flash_toast) ───────
   PRG confirmations, pinned top-center. JS ON: the flash-toast controller auto-
   dismisses it and wires the ✕ (revealed only once [data-flash-toast-ready] is
   stamped on connect, so a dead control never shows JS-off). JS OFF: it stays put
   as a static banner — a soft confirmation, never a blocker. Colors reuse the same
   notice/alert tokens as .auth__flash / .settings-flash for one visual language. */
.flash-toasts {
  position: fixed; top: var(--space-lg); left: 50%; transform: translateX(-50%);
  z-index: 70; display: flex; flex-direction: column; gap: var(--space-sm);
  width: min(92vw, 460px); pointer-events: none;
}
.flash-toast {
  pointer-events: auto;
  display: flex; align-items: flex-start; gap: var(--space-md);
  /* Toasts are an ACTION-corner surface (spec §2: "toasts may run 3–6px"). */
  padding: 12px 14px; border-radius: var(--r-action); border: 1px solid var(--outline-variant);
  font: 500 var(--fs-body-sm) / 1.4 var(--font-body);
  box-shadow: var(--shadow-toast);
  transition: opacity .3s ease, transform .3s ease;
}
.flash-toast--leaving { opacity: 0; transform: translateY(-8px); }
.flash-toast--notice { background: var(--primary-container); color: var(--on-primary-container); }
.flash-toast--alert  { background: var(--tertiary-container); color: var(--tertiary); }
.flash-toast__msg { flex: 1 1 auto; min-width: 0; }
.flash-toast__close {
  flex: 0 0 auto; display: none; /* enhancement-only — shown once the controller connects */
  background: transparent; border: 0; cursor: pointer;
  font-size: 20px; line-height: 1; color: inherit; opacity: .7; padding: 0 2px; margin: -1px -2px 0 0;
}
.flash-toast__close:hover { opacity: 1; }
.flash-toast[data-flash-toast-ready] .flash-toast__close { display: block; }
@media (prefers-reduced-motion: reduce) { .flash-toast { transition: none; } }
/* ── L2 (logged-in) actionable reaction bar (BE-121) ─────────────────────────
   button_to wraps each control in a <form>; display:contents lets the button be
   the direct flex child so the row lays out exactly like the display-only L1 bar
   (whose inert spans above are unchanged). */
.pc-actions form.rx-form { display: contents; }
button.rx-btn { cursor: pointer; }
a.rx-btn { cursor: pointer; text-decoration: none; }
button.rx-btn:hover, a.rx-btn:hover { color: var(--text); }
.rx-btn.reposted { color: var(--violet-600); }
.rx-btn.reposted svg { fill: none; stroke: var(--violet-600); }

/* ── Reaction picker (endorsements, L2 interactive) ──────────────────────────
   A native <details> popover so the picker works with JS OFF (the summary toggles
   it; each kind is its own button_to form). The trigger sits inline as an rx-btn;
   the panel floats above the row. The viewer's current reaction is highlighted and
   its button submits DELETE (tap-to-remove); every other kind submits a PUT swap. */
.rx-reactions { position: relative; display: inline-flex; }
.rx-reactions__trigger { cursor: pointer; list-style: none; }
.rx-reactions__trigger::-webkit-details-marker { display: none; }
.rx-reactions__trigger:hover, .rx-reactions[open] .rx-reactions__trigger { color: var(--text); background: var(--accent-subtle); }
.rx-reactions__trigger.reacted { color: var(--violet-600); }
.rx-reactions__trigger.reacted svg { stroke: var(--violet-600); }
.rx-reactions__panel {
  position: absolute; left: 0; bottom: calc(100% + 6px); z-index: 20; display: flex; gap: 2px;
  padding: 6px; background: var(--surface-elevated); border: 1px solid var(--border);
  border-radius: var(--r-action); box-shadow: var(--shadow-pop);
}
.rx-reactions__form { display: contents; }
/* Each emoji in the reaction picker is a real <button> (button_to, role=menuitem —
   see web/reaction_bar_component.html.erb), so it takes the 3px action corner like
   every other button here: its trigger's sibling `.rx-btn` and its own containing
   `.rx-reactions__panel` are already var(--r-action). It was a 6px button sitting
   inside a 3px panel. */
.rx-reaction {
  display: inline-flex; flex-direction: column; align-items: center; gap: 3px; cursor: pointer;
  font-family: var(--font-body); font-size: 11.5px; font-weight: 600; color: var(--text-muted);
  background: transparent; border: 0; border-radius: var(--r-action); padding: 6px 9px;
}
.rx-reaction svg { stroke: currentColor; fill: none; }
.rx-reaction:hover { color: var(--text); background: var(--accent-subtle); }
.rx-reaction--active { color: var(--violet-600); background: var(--accent-subtle); }
.rx-reaction--active svg { stroke: var(--violet-600); }

/* ── Compose surface (BE-121, redesigned) ─────────────────────────────────────
   The composer lives in ONE cohesive, elevated card (not bare fields on the
   canvas): a threaded surface (own avatar beside the input; a subtle connector
   from a reply's parent), an auto-growing borderless textarea, progressive media
   disclosure, and a live character-counter ring. Colors resolve against the v3
   tokens — no one-off hex. The `compose` Stimulus controller adds the shine on
   top of a form that works fully JS-off. */
.compose-error {
  padding: 12px 14px; margin: 16px 0; border-radius: var(--r-md);
  background: rgba(255, 48, 64, .1); color: var(--heart-red);
  font-size: var(--fs-body-sm); line-height: 1.4;
}

/* The elevated composer card. focus-within lifts it with a soft brand glow —
   the "refined glow, not a hard box" that replaces per-field rings inside. */
.compose-card {
  margin-top: 16px;
  background: var(--surface-hi);
  border: 1px solid var(--outline-variant);
  border-radius: var(--r-lg);
  padding: var(--space-lg);
  box-shadow: var(--shadow-card);
  transition: border-color .18s ease, box-shadow .18s ease;
}
.compose-card:focus-within {
  border-color: var(--primary-tint);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus-ring) 20%, transparent),
              var(--shadow-card-focus);
}
.theme-dark .compose-card {
  box-shadow: var(--shadow-card-dark);
}

/* Give the composer a touch more room than the standard reading column so a
   500-char post has a comfortable, full-width writing space. Scoped to the
   compose surface via :has() — every other surface keeps the --measure-wide
   column, and a browser without :has() gracefully falls back to it too. */
.web-shell__main:has(.compose-card) { max-width: 688px; }

.compose-form { display: flex; flex-direction: column; gap: 16px; }

/* Threaded composer — parent context + connector + own avatar + field. */
.compose-thread { position: relative; }
.compose-thread__row { display: flex; gap: 14px; }
.compose-thread__gutter { flex: 0 0 auto; }
.compose-thread__field { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 10px; }

/* Parent/quoted context — compact + muted, reusing the shared post card. The
   muting is applied to the card itself so the connector rail stays crisp. */
.compose-thread__context { position: relative; margin-bottom: 2px; }
.compose-thread__context .post-card { opacity: .68; padding-bottom: 12px; }
.compose-thread__context .pc-text { font-size: var(--fs-body-sm); }

/* Reply connector — a subtle rail down the parent's empty left gutter (below its
   44px avatar) into the composer's own avatar. Anchored to the avatars + block
   edges, so it holds at any parent-body length (Threads-style). */
.compose-thread--reply .compose-thread__row { position: relative; padding-top: 18px; }
.compose-thread--reply .compose-thread__context::after {
  content: ""; position: absolute; left: 21px; top: 50px; bottom: -2px; width: 2px;
  background: var(--outline-variant); border-radius: var(--r-pill);
}
.compose-thread--reply .compose-thread__gutter::before {
  content: ""; position: absolute; left: 21px; top: 0; height: 18px; width: 2px;
  background: var(--outline-variant); border-radius: var(--r-pill);
}

/* Auto-growing, borderless textarea — blends into the card; the card owns the
   focus glow, so no hard per-field ring. Native field-sizing where supported,
   with the JS reset-first scrollHeight fallback (.compose-input--auto) otherwise.
   A generous ~6-line default height (176px) so a 500-char post opens onto a real
   writing space, not a cramped one-line slot — still auto-growing to the cap. */
.compose-input {
  width: 100%; box-sizing: border-box; resize: vertical; min-height: 176px;
  border: none; background: transparent; padding: 8px 2px; margin: 0;
  font: 400 var(--fs-body-lg) / 1.55 var(--font-body); color: var(--text);
  overflow-wrap: anywhere;
}
.compose-input:focus-visible { outline: none; box-shadow: none; }
.compose-input--auto { resize: none; overflow: hidden; }
@supports (field-sizing: content) {
  .compose-input { field-sizing: content; min-height: 176px; max-height: 60vh; resize: none; overflow-y: auto; }
}

/* Media block — the compact uploader + the alt field (revealed on attach). */
.compose-media { display: flex; flex-direction: column; gap: 10px; }

/* The "Describe the photo" alt field is shown by default (JS-off fallback); the
   controller hides it (.is-composing) until a photo is attached (.is-shown). */
.compose-alt { display: block; }
.compose-form.is-composing .compose-alt { display: none; }
.compose-form.is-composing .compose-alt.is-shown { display: block; }
.compose-alt__input { width: 100%; }

/* who-can-quote — a labelled inline SEGMENTED control (radio pills) that shows
   all three options at a glance: no dropdown, one tap to choose. Native radios
   keep it fully accessible — arrow-key nav + labels for free — while submitting
   the SAME `quotes_setting` param the old <select> did. */
.compose-quotes { display: flex; flex-direction: column; gap: 8px; }
.compose-quotes__label {
  display: inline-flex; align-items: center; gap: 6px;
  font: 600 var(--fs-body-sm) / 1.3 var(--font-body); color: var(--text-muted);
}
.compose-quotes__icon { flex: 0 0 auto; color: var(--text-muted); }

/* The pill group — one rounded track hugging its three segments (not stretched
   to the column width). */
.compose-seg {
  display: inline-flex; align-self: flex-start; align-items: stretch;
  flex-wrap: wrap; gap: 3px; padding: 3px; max-width: 100%;
  background: var(--surface-2); border: 1px solid var(--outline-variant);
  border-radius: var(--r-action);
}
.compose-seg__option { display: inline-flex; margin: 0; cursor: pointer; }

/* The real radio is visually hidden (sr-only clip) yet keeps native semantics,
   focus + arrow-key group nav; the pill is the visible, clickable control. */
.compose-seg__input {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
.compose-seg__pill {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 8px 15px; border-radius: var(--r-action); white-space: nowrap;
  font: 600 var(--fs-body-sm) / 1 var(--font-body); color: var(--text-muted);
  transition: background .16s ease, color .16s ease;
}
.compose-seg__pill svg { flex: 0 0 auto; display: block; }
.compose-seg__option:hover .compose-seg__pill { color: var(--text); background: var(--surface-3); }
.compose-seg__input:checked + .compose-seg__pill {
  background: var(--accent); color: var(--on-primary);
  box-shadow: var(--shadow-chip);
}
.compose-seg__input:checked + .compose-seg__pill svg { color: var(--on-primary); }
.compose-seg__input:focus-visible + .compose-seg__pill {
  outline: 2px solid var(--primary); outline-offset: 2px;
}
@media (max-width: 420px) {
  .compose-seg { display: flex; align-self: stretch; }
  .compose-seg__option { flex: 1 1 auto; }
  .compose-seg__pill { width: 100%; padding-left: 10px; padding-right: 10px; }
}

/* A "Replying to @handle" chip — the threadline's context header. */
.compose-replying {
  display: inline-flex; align-items: center; gap: 6px; margin-bottom: 10px;
  padding: 5px 12px; border-radius: var(--r-action);
  background: var(--primary-container); color: var(--on-primary-container);
  font: 600 var(--fs-caption) / 1 var(--font-body);
}
.compose-replying__handle { color: var(--on-primary-container); text-decoration: none; font-weight: 700; }
.compose-replying__handle:hover { text-decoration: underline; }

/* Restored-draft chip — offered, never forced. */
.compose-draft {
  display: inline-flex; align-items: center; gap: 8px; margin-bottom: 2px;
  padding: 6px 8px 6px 12px; border: 1px solid var(--outline-variant);
  border-radius: var(--r-action); background: var(--surface-1); color: var(--text-muted);
  font: 600 var(--fs-caption) / 1 var(--font-body);
}
.compose-draft[hidden] { display: none; }
.compose-draft__discard {
  border: none; background: var(--surface-3); color: var(--accent);
  font: 700 var(--fs-caption) / 1 var(--font-body); cursor: pointer;
  padding: 5px 11px; border-radius: var(--r-action);
}
.compose-draft__discard:hover { background: var(--primary-container); }

/* Action bar — the char-counter ring + a quiet ⌘↵ hint + Cancel + primary. */
.compose-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding-top: 14px; border-top: 1px solid var(--outline-variant);
}
.compose-bar__actions { display: flex; align-items: center; gap: 10px; }
.compose-bar .btn-primary:disabled { opacity: .45; cursor: not-allowed; }
.compose-bar .btn-primary:disabled:hover { background: var(--accent); }
.compose-bar .btn-primary:not(:disabled):active { transform: scale(.97); }

.compose-hint { display: inline-flex; align-items: center; gap: 4px; color: var(--text-muted); font: 500 var(--fs-caption) / 1 var(--font-body); }
.compose-hint kbd {
  font-family: var(--font-body); font-size: 11px; line-height: 1; padding: 3px 5px;
  border-radius: var(--r-sm); background: var(--surface-3); color: var(--text-muted);
  border: 1px solid var(--outline-variant);
}
@media (max-width: 560px) { .compose-hint { display: none; } }

/* The circular character counter — a conic-gradient donut (no SVG math). JS-on
   only; the remaining NUMBER surfaces just for the last stretch (anti-anxiety). */
.compose-counter { display: none; position: relative; align-items: center; justify-content: center; width: 24px; height: 24px; flex: 0 0 auto; }
.compose-form.is-composing .compose-counter { display: inline-flex; }
.compose-counter__ring {
  --pct: 0; --ring: var(--accent);
  width: 22px; height: 22px; border-radius: 50%;
  background: conic-gradient(var(--ring) calc(var(--pct) * 1%), var(--outline-variant) 0);
  -webkit-mask: radial-gradient(circle 6.5px at center, transparent 98%, rgba(0, 0, 0, 1) 100%);
          mask: radial-gradient(circle 6.5px at center, transparent 98%, rgba(0, 0, 0, 1) 100%);
}
.compose-counter__num { position: absolute; font: 600 10.5px / 1 var(--font-body); color: var(--text-muted); }
.compose-counter.is-warn .compose-counter__ring { --ring: var(--tertiary); }
.compose-counter.is-warn .compose-counter__num { color: var(--tertiary); }
.compose-counter.is-over .compose-counter__ring { --ring: var(--like); }
.compose-counter.is-over .compose-counter__num { color: var(--like); }

/* ── Topic #autocomplete (JS-on) — a combobox listbox anchored under the writing
   area. Fed by Web::ComposeTopics (the SAME Topic.search scope the mobile topics
   search uses); selecting inserts a removable chip whose slug is submitted as
   topic_slug / secondary_topic_slug. Disabled in reply mode (no markup rendered). */
.compose-editor { position: relative; }
.compose-ac {
  position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 30;
  margin: 0; padding: 6px; list-style: none;
  background: var(--surface-hi); border: 1px solid var(--outline-variant);
  border-radius: var(--r-md); box-shadow: var(--shadow-float);
  max-height: 280px; overflow-y: auto;
}
.compose-ac[hidden] { display: none; }
.compose-ac__opt {
  display: flex; align-items: baseline; gap: 9px; padding: 9px 11px;
  border-radius: var(--r-sm); cursor: pointer; color: var(--text);
}
.compose-ac__opt.is-active { background: var(--primary-container); color: var(--on-primary-container); }
.compose-ac__slug { flex: 0 0 auto; font: 700 var(--fs-body-sm) / 1.2 var(--font-body); }
.compose-ac__name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-caption); color: var(--text-muted);
}
.compose-ac__opt.is-active .compose-ac__name { color: inherit; opacity: .8; }

/* Selected-topic chips — brand pills with a ✕ remove; the second reads
   "also relevant" (the secondary topic). */
.compose-topics-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.compose-topics-chips[hidden] { display: none; }
.compose-topic-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 6px 5px 12px; border-radius: var(--r-action);
  background: var(--primary-container); color: var(--on-primary-container);
  font: 700 var(--fs-caption) / 1 var(--font-body);
}
.compose-topic-chip__also {
  font-weight: 600; opacity: .78; padding-left: 6px;
  border-left: 1px solid color-mix(in srgb, var(--on-primary-container) 30%, transparent);
}
.compose-topic-chip__remove {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border: none; border-radius: 50%; cursor: pointer;
  background: color-mix(in srgb, var(--on-primary-container) 14%, transparent);
  color: var(--on-primary-container); font-size: 10px; line-height: 1;
}
.compose-topic-chip__remove:hover { background: color-mix(in srgb, var(--on-primary-container) 26%, transparent); }

/* ── Assist (workstream D) — "fix the grammar, keep the person". The PROPOSAL
   panel: quiet, secondary, and visibly NOT the draft — it reads as a suggestion
   sitting beside the member's own words, which stay in the textarea untouched
   until they press "Use this". Error state reuses the same panel (muted, no
   accept button) so a failed assist costs them nothing but a glance. */
.compose-assist {
  margin-top: 10px; padding: 10px 12px;
  border: 1px dashed var(--border); border-radius: 12px;
  background: var(--surface-2, var(--surface)); display: grid; gap: 8px;
}
.compose-assist[hidden] { display: none; }
.compose-assist__label { margin: 0; color: var(--text-muted); font: 500 var(--fs-caption) / 1.3 var(--font-body); }
.compose-assist__text { margin: 0; color: var(--text); font: 400 var(--fs-body) / 1.5 var(--font-body); white-space: pre-wrap; }
.compose-assist__actions { display: flex; align-items: center; gap: 8px; }
.compose-assist__accept[hidden] { display: none; }
.compose-assist--error .compose-assist__text { color: var(--text-muted); font-size: var(--fs-caption); }
.compose-assist--error .compose-assist__label { display: none; }
.compose-assist__button { display: inline-flex; align-items: center; gap: 5px; }
.compose-assist__button[aria-busy="true"] { opacity: .55; cursor: progress; }
.compose-assist__button:disabled { opacity: .55; cursor: progress; }

/* ── Live link-unfurl preview (JS-on) — the resolved card (reusing .lp-card) the
   post will carry, shown BEFORE posting, with a floating dismiss ✕ that suppresses
   it. A shimmer skeleton fills the gap while the crawl resolves. */
.compose-preview { position: relative; }
.compose-preview[hidden] { display: none; }
.compose-preview .lp-card { margin-top: 0; }
.compose-preview__dismiss {
  position: absolute; top: 8px; right: 8px;
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%; cursor: pointer;
  border: 1px solid var(--outline-variant); background: var(--surface-hi); color: var(--text-muted);
  font-size: 12px; line-height: 1; box-shadow: var(--shadow-dismiss);
}
.compose-preview__dismiss:hover { color: var(--text); border-color: var(--accent); }

.lp-card--skeleton { pointer-events: none; }
.lp-card--skeleton .lp-card__media { min-height: 84px; }
.lp-skel {
  display: block; height: 12px; border-radius: var(--r-sm);
  background: linear-gradient(90deg, var(--surface-3) 0, var(--surface-2) 40%, var(--surface-3) 80%);
  background-size: 200% 100%; animation: compose-shimmer 1.3s ease-in-out infinite;
}
.lp-skel--title { width: 62%; height: 14px; margin: 2px 0 8px; }
.lp-skel--line { width: 88%; }
@keyframes compose-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

@media (prefers-reduced-motion: reduce) {
  .compose-card { transition: none; }
  .compose-bar .btn-primary:active { transform: none; }
  .compose-seg__pill { transition: none; }
  .lp-skel { animation: none; }
}

/* ── Activity/notifications page — sticky Today / This week / Earlier date
   sections, one row per notification (avatar + reason + snippet), a subtle
   per-row "Why?" disclosure, and per-filter empty states. The filter chips
   reuse .link-tabs above. ── */
.notif-group { margin: 0; }
.notif-group__head {
  position: sticky; top: 0; z-index: 1;
  margin: 0; padding: 14px 4px 6px;
  background: var(--surface);
  font-family: var(--font-display); font-weight: 600;
  font-size: var(--fs-caption); letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--text-muted);
}
.notif-list { display: flex; flex-direction: column; }
.notif-item { border-bottom: 1px solid var(--border); }
.notif-row {
  display: flex; align-items: flex-start; gap: 14px; padding: 14px 4px;
  text-decoration: none; color: inherit; position: relative;
}
.notif-row--unread { background: var(--accent-subtle); border-radius: var(--r-md); }
.nr-body { flex: 1 1 auto; min-width: 0; }
.nr-text { margin: 0; font-size: var(--fs-body-sm); color: var(--text); line-height: 1.4; }
.nr-snippet {
  margin: 4px 0 0; font-size: var(--fs-body-sm); color: var(--text-muted);
  font-style: italic; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nr-time { display: block; margin-top: 4px; font-size: var(--fs-caption); color: var(--text-muted); }
.nr-dot {
  flex: 0 0 auto; width: 9px; height: 9px; border-radius: 50%;
  background: var(--accent); margin-top: 6px;
}

/* "Why am I seeing this?" — a native <details> disclosure (no JS, keyboard-
   accessible). Sits below the row, OUTSIDE the anchor, so the toggle never
   navigates. Aligned under the row text (avatar 44px + 14px gap). */
.notif-why { margin: -4px 0 8px; padding-left: 58px; }
.notif-why__summary {
  display: inline-block; list-style: none; cursor: pointer;
  padding: 2px 0; font-size: var(--fs-caption); font-weight: 700; color: var(--text-muted);
}
.notif-why__summary::-webkit-details-marker { display: none; }
.notif-why__summary::after { content: " \203A"; opacity: 0.7; }
.notif-why[open] .notif-why__summary::after { content: " \02C5"; }
.notif-why__summary:hover { color: var(--accent); }
.notif-why[open] .notif-why__summary { color: var(--text); }
.notif-why__reason {
  margin: 4px 0 2px; font-size: var(--fs-caption); color: var(--text-muted); line-height: 1.5;
}

/* Per-filter empty state — a calm glyph (a caught-up ring for All, a first-run
   bell for the per-type lanes) + warm copy. */
.notif-empty {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  padding: 56px 0 40px; gap: 4px;
}
.notif-empty__glyph {
  display: inline-flex; align-items: center; justify-content: center; margin-bottom: 12px;
}
.notif-empty__glyph--caught { color: var(--accent); }
.notif-empty__glyph--bell {
  width: 56px; height: 56px; border-radius: var(--r-lg);
  background: var(--surface-muted); color: var(--text-muted);
}
.notif-empty__title {
  margin: 0; font-family: var(--font-display); font-weight: 600;
  font-size: var(--fs-body-lg); color: var(--text);
}
.notif-empty__body {
  margin: 0; font-size: var(--fs-body-sm); color: var(--text-muted);
  line-height: 1.5; max-width: 34ch;
}

/* ── Follow-requests inbox (/requests) — one row per incoming pending request:
   requester (avatar + name) on the left, Accept / Reject on the right. The
   header + empty state reuse .feed-head / .feed-empty above. ── */
.req-list { display: flex; flex-direction: column; }
.req-row {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 14px 4px; border-bottom: 1px solid var(--border);
}
.req-who { display: flex; align-items: center; gap: 12px; min-width: 0; text-decoration: none; color: inherit; }
.req-id { display: flex; flex-direction: column; min-width: 0; }
.req-name { font-size: var(--fs-body-sm); font-weight: 700; color: var(--text); }
.req-handle { font-size: var(--fs-caption); color: var(--text-muted); }
.req-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.req-actions .pf-action-form { display: inline; margin: 0; }
.follow-pill.req-reject { color: var(--text-muted); border-color: var(--border); }
.follow-pill.req-reject:hover { background: var(--surface); color: var(--text); }

/* A message-request row (Messages "Requests" lane) reuses .req-* above; the
   last-message preview sits under the sender name (truncated, muted). */
.req-preview {
  font-size: var(--fs-caption); color: var(--text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 340px;
}

/* Count pill on the "Requests" tab (the viewer's pending message requests). */
.tab-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 5px; border-radius: var(--r-pill);
  background: var(--accent); color: var(--on-primary);
  font-size: var(--fs-caption); font-weight: 700; vertical-align: middle;
}

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ── L2 Settings hub — the header/empty-state reuse .feed-head/.feed-empty
   above; the sign-in flash tokens (.auth__flash) are mirrored here under a
   .settings- prefix rather than shared, since the two pages never render
   together (see the settings build report for the dedup note). ── */
.settings-page { display: flex; flex-direction: column; gap: 34px; margin-top: 8px; }
.settings-section {
  padding-bottom: 28px; border-bottom: 1px solid var(--border);
}
.settings-section:last-child { border-bottom: none; padding-bottom: 0; }
.settings-section h2 {
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-subtitle);
  color: var(--text); margin: 0 0 6px;
}
.settings-section h3 {
  font-family: var(--font-body); font-weight: 700; font-size: var(--fs-body-sm);
  color: var(--text); margin: 0 0 8px;
}
.settings-subsection { margin-top: 18px; }
.settings-subsection:first-of-type { margin-top: 12px; }
.settings-hint { font-size: var(--fs-body-sm); color: var(--text-muted); line-height: 1.5; margin: 0 0 14px; }
.settings-empty { color: var(--text-muted); font-size: var(--fs-body-sm); margin: 0 0 14px; }
.settings-flash { padding: 12px 14px; border-radius: var(--r-md); font-size: var(--fs-body-sm); line-height: 1.4; margin: 16px 0 0; }
.settings-flash--notice { background: var(--primary-container); color: var(--on-primary-container); }
.settings-flash--alert { background: var(--tertiary-container); color: var(--tertiary); }
.settings-error { padding: 10px 12px; margin: 0 0 12px; border-radius: var(--r-md); background: rgba(255, 48, 64, .1); color: var(--heart-red); font-size: var(--fs-body-sm); line-height: 1.4; }

.settings-form .field { margin-bottom: 16px; display: flex; flex-direction: column; gap: 7px; max-width: 420px; }
.settings-form label { font: 600 var(--fs-body-sm) / 1 var(--font-body); color: var(--text); }
.settings-form legend { font: 600 var(--fs-body-sm) / 1 var(--font-body); color: var(--text); padding: 0; margin: 0 0 8px; }
.settings-form fieldset { border: none; padding: 0; margin: 0 0 16px; }
/* The border/background/radius/focus all come from the global Form-controls base
   above; the settings column just wants its text fields, textareas + <select>
   full-width (the radios / checks / file input stay at their natural size). */
.settings-form input[type="text"],
.settings-form input[type="password"],
.settings-form textarea,
.settings-form select { width: 100%; box-sizing: border-box; }
.settings-radio, .settings-check {
  display: flex; align-items: center; gap: 8px; font: 400 var(--fs-body-sm) / 1 var(--font-body);
  color: var(--text); margin-bottom: 8px; cursor: pointer;
}
.settings-actions { display: flex; gap: 10px; margin-top: 4px; }
.settings-danger { color: var(--heart-red); border-color: var(--heart-red); }

.settings-list { list-style: none; margin: 0 0 16px; padding: 0; max-width: 420px; }
.settings-list__item {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 0; border-bottom: 1px solid var(--border); font-size: var(--fs-body-sm); color: var(--text);
}
.settings-list__item:last-child { border-bottom: none; }
.settings-inline-form { display: contents; }
.settings-add-form { display: flex; gap: 10px; max-width: 420px; }
.settings-add-form input { flex: 1; min-width: 0; }

/* One mental model: the avatar is the subject; Change + Remove are grouped beside
   it (a stacked control column), not floating loose across the row. */
.settings-avatar { display: flex; align-items: flex-start; gap: var(--space-lg); flex-wrap: wrap; }
.settings-avatar__figure { position: relative; flex: 0 0 auto; width: 74px; height: 74px; }
/* The just-picked photo, mirrored over the circle until saved/discarded (JS-on). */
.settings-avatar__pending {
  position: absolute; inset: 0; width: 100%; height: 100%;
  border-radius: 50%; object-fit: cover; box-shadow: 0 0 0 2px var(--accent);
}
.settings-avatar__pending-note {
  margin: var(--space-xs) 0 0; max-width: 22ch;
  color: var(--accent); font: 600 var(--fs-caption) / 1.3 var(--font-body);
}
.settings-avatar__controls { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; }
.settings-avatar__controls input[type="file"] { font-size: var(--fs-body-sm); color: var(--text); max-width: 260px; }

/* ── Dynamic settings (Turbo Streams) ───────────────────────────────────────
   Muted words / accounts render as removable chips (replacing the old stacked
   rows + big "Remove" buttons), and every save flips an inline "Saved ✓" beside
   its button instead of a top-of-page flash that scroll-jumps the whole page. */
.settings-chip-list { max-width: 460px; margin: 0 0 14px; }
.settings-chips { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; }
.settings-chip {
  display: inline-flex; align-items: center; gap: 4px; max-width: 100%;
  padding: 5px 5px 5px 12px; border: 1px solid var(--border); border-radius: var(--r-action);
  background: var(--surface); font-size: var(--fs-body-sm); color: var(--text);
}
.settings-chip__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.settings-chip__remove {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; padding: 0; border: none; border-radius: 50%;
  background: transparent; color: var(--text-muted); cursor: pointer;
  font-size: 16px; line-height: 1;
}
.settings-chip__remove:hover { background: var(--surface-elevated); color: var(--text); }
.settings-chip__remove:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The inline confirmation region sits beside the Save button (a persistent
   aria-live target the save stream fills). Reserve height so the row never jumps
   as it appears/clears. */
.settings-status { min-height: 1.5em; display: inline-flex; align-items: center; }
.settings-saved {
  display: inline-flex; align-items: center; gap: 5px;
  color: var(--primary); font: 600 var(--fs-body-sm) / 1.4 var(--font-body);
  animation: settings-saved-fade 3.2s ease forwards;
}
.settings-saved::after { content: "\2713"; font-weight: 700; } /* decorative check */
@keyframes settings-saved-fade { 0%, 70% { opacity: 1; } 100% { opacity: 0; } }
/* Reduced-motion: no fade — hold, then disappear in a single step (no animation). */
@media (prefers-reduced-motion: reduce) {
  .settings-saved { animation: settings-saved-hold 3.2s steps(1, end) forwards; }
  @keyframes settings-saved-hold { 0%, 99% { opacity: 1; } 100% { opacity: 0; } }
}

/* ── Settings in-page index — a WRAPPED strip of anchor-link chips so a member can
   jump to any section instead of scrolling the whole ~3,700px page. Pure anchors →
   JS-off. It wraps to 2–3 rows (flex-wrap, like .admin-nav) so EVERY chip stays
   on-screen + clickable at any width — incl. the last "Account" one that the old
   single nowrap-scroll row pushed off the hidden-scrollbar edge (the FE-audit bug).
   Deliberately NOT sticky: a wrapped multi-row bar trailing the scroll would eat
   too much of the viewport (worst on phones), so it sits once at the top. ── */
.settings-index {
  display: flex; gap: 6px; flex-wrap: wrap;
  margin: 8px -4px 0; padding: 10px 4px;
  background: var(--surface); border-bottom: 1px solid var(--border);
}
.settings-index a {
  flex: 0 0 auto; white-space: nowrap; text-decoration: none;
  font: 700 var(--fs-caption) / 1 var(--font-body); color: var(--text-muted);
  padding: 7px 13px; border: 1px solid var(--border); border-radius: var(--r-action);
}
.settings-index a:hover { color: var(--text); border-color: var(--accent); }
/* The index no longer sticks, so an anchor jump only has to clear the sticky mobile
   top bar (.mnav, ~46px). Keeps the target heading out from under it on phones. */
.settings-page .settings-section h2[id] { scroll-margin-top: 66px; }

/* ── Settings timezone combobox (tz-combobox) — the searchable, keyboard-driven
   upgrade over the native <select>. JS-off the <select> shows and this is unused.
   The wrapper anchors the absolutely-positioned listbox; the input inherits the
   global form-control base (border/radius/focus-ring). ── */
.tz-combobox { position: relative; max-width: 420px; }
.tz-combobox__input { width: 100%; box-sizing: border-box; }
.tz-combobox__list {
  position: absolute; z-index: 20; left: 0; right: 0; top: calc(100% + 4px);
  max-height: 320px; overflow-y: auto; margin: 0; padding: 4px; list-style: none;
  background: var(--surface-elevated); border: 1px solid var(--border);
  border-radius: var(--r-md); box-shadow: var(--shadow-pop);
}
.tz-combobox__option {
  padding: 9px 12px; border-radius: var(--r-sm); cursor: pointer;
  font: 400 var(--fs-body-sm) / 1.3 var(--font-body); color: var(--text);
}
.tz-combobox__option:hover { background: var(--surface-muted); }
.tz-combobox__option.is-active { background: var(--accent-subtle); color: var(--text); }
.tz-combobox__option[aria-selected="true"] { font-weight: 700; }
/* The "Detected from your browser — change anytime" note under the picker. */
.tz-detected { margin: 8px 0 0; color: var(--accent); font-weight: 600; }

/* ── Admin console (BE-ADMIN) — internal, admin-only surface, never indexed ─── */

/* Back-office identity — applied to every admin screen via web/admin/_head +
   web/admin/_nav. Two restrained, coordinated signals: an "Admin console" eyebrow
   over the page title, and a violet-tinted control bar for the sub-nav. Reads as
   unmistakably admin while staying Shiijak (warm paper, serif title, violet
   accent) — no redesign, all tokens. */
.admin-head { margin: 4px 0 6px; }
.admin-eyebrow {
  display: inline-flex; align-items: center; gap: 8px; margin: 0 0 5px;
  font: 700 var(--fs-eyebrow) / 1 var(--font-body);
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent);
}
.admin-eyebrow::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
}

.admin-nav {
  display: flex; gap: 4px; flex-wrap: wrap; align-items: center;
  margin: 0 0 22px; padding: 5px;
  background: var(--accent-subtle);
  border: 1px solid var(--border); border-radius: var(--r-lg);
}
.admin-nav__link {
  font-family: var(--font-body); font-weight: 700; font-size: var(--fs-body-sm);
  color: var(--text); text-decoration: none;
  padding: 7px 15px; border-radius: var(--r-action); white-space: nowrap;
}
.admin-nav__link:hover { color: var(--text); background: var(--surface-elevated); }
.admin-nav__link.is-active { color: var(--on-primary); background: var(--accent); }

.admin-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.admin-stat {
  display: flex; flex-direction: column; gap: 4px;
  padding: 18px 20px; border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--surface-elevated); text-decoration: none; color: var(--text);
}
.admin-stat:hover { border-color: var(--accent); }
.admin-stat__value { font-family: var(--font-display); font-weight: 600; font-size: var(--fs-title); }
.admin-stat__label { font-size: var(--fs-body-sm); color: var(--text-muted); }

.admin-filters { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 16px; margin: 0 0 16px; }
.admin-chip {
  font-size: var(--fs-caption); font-weight: 700; text-decoration: none;
  color: var(--text-muted); padding: 6px 14px; border: 1px solid var(--border); border-radius: var(--r-action);
}
.admin-chip.is-active { color: var(--on-primary); background: var(--accent); border-color: var(--accent); }

/* The Users admin filter bar (search box + 4 selects + actions), sharing the
   .admin-filters flex row above with the moderation status chips. Each control
   gets its own [label above, control below] column so the bar reads as one
   aligned row instead of loose floating fields; .admin-filters__actions pins
   Search/Clear to the row's trailing edge on wide screens and folds back
   inline once the bar wraps at narrower widths. */
.admin-filters__search { display: flex; flex-direction: column; gap: 6px; flex: 2 1 240px; min-width: 200px; }
.admin-filters__search input { width: 100%; }
.admin-filters__field { display: flex; flex-direction: column; gap: 6px; flex: 1 1 150px; min-width: 130px; }
.admin-filters__field select { width: 100%; }
.admin-filters__label { font: 600 var(--fs-caption) / 1 var(--font-body); color: var(--text-muted); }
.admin-filters__actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }

/* The "N users" / "Showing the first 50 of N" line above the table. */
.admin-filters__count { margin: 0 0 12px; font-size: var(--fs-body-sm); color: var(--text-muted); }

/* ≤560px: the wrapping row gets cramped once selects shrink below their
   option-text width — stack everything full-width instead. */
@media (max-width: 560px) {
  .admin-filters { flex-direction: column; align-items: stretch; }
  .admin-filters__search, .admin-filters__field { flex: 1 1 auto; min-width: 0; }
  .admin-filters__actions { margin-left: 0; }
}

/* ONE consistent admin width across every console page (no per-page flip-flop
   between boxed forms and edge-to-edge tables). Wider than the member reading
   measure so data tables breathe, but capped with real side padding so it never
   hits the viewport edge — and fluid below the cap (fills the grid's 1fr track on
   smaller screens, since admin has no Newsstand rail). Set via body.admin-page
   (web/admin/_head), so it applies uniformly to tables, forms, and the agent. */
body.admin-page .web-shell__main { max-width: var(--admin-measure, 1040px); }

/* Responsive-table wrapper: on narrow screens the wide admin tables scroll INSIDE
   their own box instead of forcing the whole page to scroll sideways. */
.admin-table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.admin-table { width: 100%; border-collapse: collapse; font-size: var(--fs-body-sm); }
.admin-table th, .admin-table td {
  text-align: left; padding: 11px 20px 11px 0; border-bottom: 1px solid var(--border);
  vertical-align: middle;
  white-space: nowrap; /* short columns (user / role / status / action) never wrap mid-word */
}
.admin-table th { color: var(--text-muted); font-weight: 700; }
/* Only long free-text columns opt back into wrapping (email) — and only by
   breaking, capped so ONE long value wraps instead of stretching every column. */
.admin-table td.admin-table__wrap { white-space: normal; overflow-wrap: anywhere; max-width: 360px; }

.admin-pill {
  display: inline-block; white-space: nowrap; font-size: var(--fs-caption); font-weight: 700;
  padding: 3px 11px; border-radius: var(--r-action); background: var(--surface-muted); color: var(--text);
}
.admin-pill--open, .admin-pill--suspended { background: var(--badge-red); color: #fff; }
.admin-pill--resolved, .admin-pill--active { background: var(--verified); color: #fff; }

/* Moderation severity chip — colour-coded low → high (a case's at-a-glance urgency).
   Shares the pill shape; an unknown/blank level falls back to the neutral base. */
.admin-sev {
  display: inline-block; white-space: nowrap; font-size: var(--fs-caption); font-weight: 700; text-transform: capitalize;
  padding: 3px 11px; border-radius: var(--r-action); background: var(--surface-muted); color: var(--text);
}
.admin-sev--low    { background: var(--surface-muted); color: var(--text-muted); }
.admin-sev--medium { background: var(--badge-amber); color: #fff; }
.admin-sev--high   { background: var(--badge-red); color: #fff; }

/* Colour swatch input (curation accent). Trim the native chrome to a tidy plate,
   sized to sit at the same height as the text inputs it shares a row with. */
.admin-swatch {
  width: 56px; height: 44px; padding: 3px; vertical-align: middle;
  border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface-elevated); cursor: pointer;
}

.admin-dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; font-size: var(--fs-body-sm); }
.admin-dl dt { color: var(--text-muted); font-weight: 700; }
.admin-dl dd { margin: 0; }

.admin-muted { color: var(--text-muted); font-size: var(--fs-caption); }
.admin-post-body { white-space: pre-wrap; }

/* Editor cards + an aligned two-column form grid — the fix for the "ragged form
   rows" complaint. Each section/topic editor (and the create <details>) is an
   .admin-card carrying the row rhythm; inside, every .admin-field is a
   [label 140px | control 1fr] grid, so every label shares one left edge and every
   control shares one edge + width. Reads like a calm settings form, not scattered
   boxes. Controls inherit the global input theme (border/font/focus) — the grid
   only owns layout. */
.admin-card { padding: 18px 0; border-bottom: 1px solid var(--border); }
.admin-card:last-child { border-bottom: none; }
.admin-card__title { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; margin: 0 0 14px; }
/* The editorial serif name — styled via the bare <strong> the request specs
   assert on (`<strong>Business Weekly</strong>`), so no class rides on it. */
.admin-card__title strong,
.admin-topic__title strong {
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-body-lg);
  color: var(--text); line-height: 1.2;
}
/* Slugs are permanent code-like identifiers — a muted mono treatment sets them
   apart from the editorial serif name without competing with it. */
.admin-card__slug {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--fs-caption); color: var(--text-muted);
}
/* The Desk's "Publish failed: …" line. It used to pin its colour in an inline
   style="" attribute — which outranks every selector, so no brand could re-colour
   it — against a var(--color-danger) that is defined NOWHERE, meaning it always
   fell through to a literal brick red belonging to no palette. --tertiary is the
   editorial crimson the press palette deliberately KEEPS (spec §1.1). */
.admin-card__slug--error { color: var(--tertiary); }

.admin-form { display: flex; flex-direction: column; gap: 12px; }
.admin-field {
  display: grid; grid-template-columns: 140px minmax(0, 1fr);
  align-items: center; gap: 4px 16px;
}
.admin-field__label { font: 600 var(--fs-body-sm) / 1.3 var(--font-body); color: var(--text); }
/* Controls fill the column but cap at a readable field width, so the wider admin
   container doesn't stretch a "Display name" box across the whole page. */
.admin-field > input,
.admin-field > select,
.admin-field > textarea { width: 100%; max-width: 560px; box-sizing: border-box; }

/* Compact meta row (Position · Accent · Active): small, individually-labelled
   controls sharing one baseline, aligned to the same control edge as the fields
   above. Bottom-aligned so the number field, colour plate + checkbox line up. */
.admin-meta { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 14px 22px; }
.admin-meta__cell { display: flex; flex-direction: column; gap: 6px; }
.admin-meta__label { font: 600 var(--fs-caption) / 1 var(--font-body); color: var(--text-muted); }
.admin-meta__cell input[type="number"] { width: 96px; }
.admin-meta__check { flex-direction: row; align-items: center; gap: 8px; }
.admin-meta__check input { margin: 0; }
.admin-meta__check .admin-meta__label { color: var(--text); }

.admin-actions { display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap; }

/* Topic reassignment rows — a tidy [name · section-select · Save] row. The name
   grows so the selects + buttons align into one right-hand column across every
   row; wraps gracefully on phones. Reuses the .admin-card name/slug ramp. */
.admin-topic {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
  padding: 14px 0; border-bottom: 1px solid var(--border);
}
.admin-topic:last-child { border-bottom: none; }
.admin-topic__title { flex: 1 1 200px; min-width: 0; display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.admin-topic select { flex: 0 1 240px; min-width: 150px; }

.admin-create { margin-top: 18px; }
.admin-create > summary { cursor: pointer; list-style: none; display: inline-flex; }
.admin-create > summary::-webkit-details-marker { display: none; }
.admin-create[open] > summary { margin-bottom: 14px; }

/* Phones: the label column would crowd the control, so stack label-over-control
   and let the topic row's select + button take the full width. */
@media (max-width: 560px) {
  .admin-field { grid-template-columns: 1fr; align-items: start; gap: 6px; }
  .admin-topic select { flex: 1 1 100%; }
}

/* ── Admin agent (ADMIN-AGENT POC) ──────────────────────────────────────────
   Chat thread + the two-phase proposed-action cards — built from the same card,
   pill + control atoms as the rest of the console so it reads as one surface. */
.agent-thread { display: flex; flex-direction: column; gap: 10px; margin: 14px 0; }
/* Chat bubbles never span the full (now wider) admin column — a message bubble
   reads best at a bounded width. */
.agent-msg { max-width: min(88%, 620px); padding: 10px 14px; border-radius: var(--r-lg); }
.agent-msg--user { align-self: flex-end; background: var(--accent); color: var(--on-primary); border-bottom-right-radius: var(--r-sm); }
.agent-msg--assistant { align-self: flex-start; background: var(--surface-muted); color: var(--text); border-bottom-left-radius: var(--r-sm); }
.agent-msg__who {
  display: block; margin-bottom: 3px;
  font: 700 var(--fs-eyebrow) / 1 var(--font-body);
  letter-spacing: 0.08em; text-transform: uppercase; opacity: .7;
}
.agent-msg__body { font-size: var(--fs-body-sm); line-height: 1.45; }
.agent-msg__body p { margin: 0 0 6px; } .agent-msg__body p:last-child { margin-bottom: 0; }

/* Composer — a stacked column; the textarea keeps the global input theme
   (font/border/focus) so its type matches every other control (no double border). */
.agent-composer { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.agent-composer textarea { width: 100%; box-sizing: border-box; }
.agent-composer__actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.agent-inline-form { display: inline-flex; }

/* Proposal cards — a light surface card with a status-coloured left rail; the
   header keeps the tool name left + the status pill right. */
.agent-action {
  padding: 14px 16px; margin: 0 0 12px;
  background: var(--surface-elevated); border: 1px solid var(--border);
  border-left: 3px solid var(--border); border-radius: var(--r-md);
}
.agent-action:last-child { margin-bottom: 0; }
.agent-action--proposed { border-left-color: var(--accent); }
.agent-action--executed { border-left-color: var(--verified); }
.agent-action--rejected, .agent-action--failed { border-left-color: var(--badge-red); }
.agent-action__head { display: flex; gap: 8px; align-items: center; justify-content: space-between; flex-wrap: wrap; }
.agent-action__head > strong { font-size: var(--fs-body-sm); }
.agent-action__preview { margin: 8px 0 0; font-size: var(--fs-body-sm); line-height: 1.45; }
.agent-action__params { margin-top: 8px; }
.agent-action__params code { font-size: var(--fs-caption); word-break: break-all; color: var(--text-muted); }
.agent-action__error { color: var(--badge-red); font-size: var(--fs-caption); margin: 8px 0 0; }
.agent-action__buttons { display: flex; gap: 8px; margin-top: 12px; }
.agent-pill--proposed { background: var(--accent); color: var(--on-primary); }
.agent-pill--executed { background: var(--verified); color: #fff; }
.agent-pill--rejected, .agent-pill--failed { background: var(--badge-red); color: #fff; }
.agent-pill--approved { background: var(--surface-muted); color: var(--text); }

/* ── Messages / DM (direct-messaging CORE) ─────────────────────────────────
   The inbox rows + thread bubbles + composer reuse .feed-head / .feed-empty
   for the header + empty state; only the DM-specific pieces are tokenized here. */
.dm-new { display: flex; gap: 8px; margin: 10px 0 18px; }
.dm-new__input {
  flex: 1 1 auto; padding: 10px 14px; border: 1px solid var(--border);
  border-radius: var(--r-action); background: var(--surface-elevated); color: var(--text);
  font: 400 var(--fs-body-sm) / 1.4 var(--font-body);
}
/* focus ring inherited from the global Form-controls base */
.dm-new__btn { flex: 0 0 auto; }

.dm-list { display: flex; flex-direction: column; }
.dm-row {
  display: flex; align-items: center; gap: 12px; padding: 12px 6px;
  border-bottom: 1px solid var(--border); text-decoration: none; color: var(--text);
}
.dm-row:hover { background: var(--accent-subtle); }
.dm-row__body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.dm-row__name { font-weight: 700; font-size: var(--fs-body-sm); }
.dm-row__snippet {
  color: var(--text-muted); font-size: var(--fs-body-sm);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dm-row--unread .dm-row__name, .dm-row--unread .dm-row__snippet { color: var(--text); font-weight: 700; }
.dm-row__badge {
  flex: 0 0 auto; min-width: 20px; height: 20px; padding: 0 6px; border-radius: var(--r-pill);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--on-primary); font-weight: 700; font-size: var(--fs-caption);
}

/* ── Full-height chat layout (the DM thread) ────────────────────────────────
   The familiar chat pattern (iMessage / WhatsApp Web / Slack): a fixed-height
   column that fills the shell's main area — header pinned top, the message list
   filling the space and scrolling INTERNALLY (newest at the bottom), the composer
   pinned to the bottom, always reachable. Scoped to the DM thread via the
   `dm-chat-page` body class so no other authed surface is touched.

   The shell's main normally is a short, padded ~640px reading column. On the chat
   page we neutralize that: make it a full-viewport-height flex host (no measure
   cap, no vertical padding) so the .dm-chat column can own the height. 100dvh
   (dynamic viewport height) keeps the composer above the mobile browser's
   URL/toolbar chrome. On phones the sticky top bar (.mnav, ~62px) eats into that;
   the shell grid places the main in its own track, so subtracting the bar height
   keeps the composer on-screen without overlapping it. */
body.dm-chat-page .web-shell__main {
  /* A chat is not an editorial reading column: widen it well past the 640px
     --measure-wide the shell gives every other authed page, so a DM fills the
     screen instead of a narrow centered strip (still centered + fluid below the
     cap). Drop the vertical padding and make it a full-viewport-height flex host
     so the .dm-chat column (which fills 100% width/height of this) owns the chat
     geometry. */
  max-width: var(--measure-chat);
  padding: 0;
  height: 100dvh;
  align-self: stretch;
  min-height: 0;
}
@media (min-width: 768px) {
  /* Desktop: no mobile top bar to subtract, the nav rail is a side column. */
  body.dm-chat-page .web-shell__main { height: 100dvh; }
}
/* When the email-verify banner is fixed at the bottom, shrink the chat column so
   the composer sits ABOVE it (not under it). Mirrors the body padding-bottom the
   banner reserves elsewhere (152px phones / 92px ≥560px). */
body.dm-chat-page.has-fixed-banner .web-shell__main { height: calc(100dvh - 152px); }
@media (min-width: 560px) {
  body.dm-chat-page.has-fixed-banner .web-shell__main { height: calc(100dvh - 92px); }
}

.dm-chat {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0; /* let the scroll child shrink below content height (flex quirk) */
  /* Fills the (already measure-capped + centered) shell main. Restores the
     reading-column horizontal rhythm the shell padding used to give, so
     header/scroll/composer share one gutter. */
  padding: 0 var(--space-lg);
  width: 100%;
  box-sizing: border-box;
}
/* Header pinned at the top of the chat column, above the scrolling list. */
.dm-chat__head {
  flex: 0 0 auto;
  padding: var(--space-md) 0;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}
/* The scroll region: fills the space between header and composer and scrolls
   internally. min-height:0 is load-bearing — without it a flex item won't shrink
   below its content, so the list would push the composer off-screen instead of
   scrolling. */
.dm-chat__scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain; /* a chat scroll shouldn't chain to the page */
}

.dm-head { display: flex; align-items: center; gap: 10px; }
.dm-head__back { text-decoration: none; color: var(--text-muted); font-size: 20px; line-height: 1; }
.dm-head__back:hover { color: var(--accent); }
.dm-head__title { margin: 0; }
.dm-head__title a { color: inherit; text-decoration: none; }
.dm-head__title a:hover { color: var(--accent); }

/* The message list. min-height:100% + margin-top:auto push a SHORT thread to the
   BOTTOM of the scroll region (chat convention: the newest message sits just above
   the composer even when there are only a few), while a long thread scrolls
   naturally. Comfortable vertical breathing room via padding. */
.dm-thread {
  display: flex; flex-direction: column; gap: 10px;
  padding: var(--space-lg) 0;
  min-height: 100%; box-sizing: border-box;
  justify-content: flex-end;
}
.dm-thread__empty { padding: 24px 0; margin: auto 0; }
/* Bubble line-length is capped independently of the (now wider) chat column: on a
   900px thread an 82% bubble would run uncomfortably long, so cap at the smaller of
   80% (phones) and a readable 600px measure (desktop). */
.dm-bubble { display: flex; align-items: flex-end; gap: 8px; max-width: min(80%, 600px); align-self: flex-start; }
.dm-bubble--mine { align-self: flex-end; flex-direction: row-reverse; }
.dm-bubble__inner {
  padding: 9px 13px; border-radius: var(--r-lg); background: var(--surface);
  color: var(--text);
  /* A quiet hairline gives the counterpart's bubble edge definition on the warm
     canvas without a heavy fill (the sent bubble carries its own ink fill). */
  border: 1px solid var(--outline-variant);
}
/* Sent ("mine") bubble: an ink/monochrome fill, NOT the loud violet accent — softer
   in the eye, AA-legible in both themes, and matching the mobile chat treatment.
   Violet is reserved for actions (Send) + links, where it earns attention. */
.dm-bubble--mine .dm-bubble__inner { background: var(--dm-mine-bg); color: var(--dm-mine-text); border-color: transparent; }
.dm-bubble__text { margin: 0; font-size: var(--fs-body-sm); line-height: 1.4; white-space: pre-wrap; word-break: break-word; }
/* DM inline photo: wider max (360px) so a single image reads clearly. Height is
   natural (aspect ratio from width/height attrs on the <img>), clamped to 400px. */
.dm-bubble__photo { border-radius: var(--r-md); overflow: hidden; max-width: 360px; background: var(--surface-muted); }
.dm-bubble__photo:not(:last-child) { margin-bottom: 6px; }
/* The click-to-expand anchor wraps the thumbnail; JS-off → opens full image in new tab. */
.dm-bubble__photo-link { display: block; cursor: zoom-in; }
.dm-bubble__photo img { display: block; width: 100%; height: auto; max-height: 400px; object-fit: cover; }
/* Frameless photo: when a bubble is photo-only (no text/reply/link), the image IS
   the bubble — drop the fill/border/padding so there's no heavy frame around it,
   just a soft radius + shadow. The timestamp then sits quietly below on the canvas. */
.dm-bubble__inner:has(.dm-bubble__photo):not(:has(.dm-bubble__text)):not(:has(.dm-bubble__reply)):not(:has(.dm-bubble__link)) {
  background: transparent; border-color: transparent; padding: 0;
}
.dm-bubble__inner:has(.dm-bubble__photo):not(:has(.dm-bubble__text)):not(:has(.dm-bubble__reply)):not(:has(.dm-bubble__link)) .dm-bubble__photo {
  box-shadow: var(--shadow-photo);
}
.dm-bubble__inner:has(.dm-bubble__photo):not(:has(.dm-bubble__text)):not(:has(.dm-bubble__reply)):not(:has(.dm-bubble__link)) .dm-bubble__time,
.dm-bubble__inner:has(.dm-bubble__photo):not(:has(.dm-bubble__text)):not(:has(.dm-bubble__reply)):not(:has(.dm-bubble__link)) .dm-bubble__receipt {
  color: var(--text-muted); /* on the canvas now, not on the ink fill */
}
.dm-bubble__deleted { font-size: var(--fs-body-sm); color: var(--text-muted); }
.dm-bubble__time { display: block; margin-top: 3px; font-size: var(--fs-caption); color: var(--text-muted); }
/* Metadata on the ink fill: a translucent tint of the bubble's own text so it reads
   as quiet-but-legible in both themes (fixes the old violet-on-violet AA failure). */
.dm-bubble--mine .dm-bubble__time { color: color-mix(in srgb, var(--dm-mine-text) 58%, transparent); }

/* Quoted reply strip — a single-line preview of the replied-to message, above the
   body (mobile MessageBubble reply_preview parity). */
.dm-bubble__reply {
  margin-bottom: 4px; padding: 3px 8px; border-left: 2px solid var(--border);
  border-radius: 4px; background: var(--surface-muted);
}
.dm-bubble--mine .dm-bubble__reply {
  border-left-color: color-mix(in srgb, var(--dm-mine-text) 42%, transparent);
  background: color-mix(in srgb, var(--dm-mine-text) 12%, transparent);
}
.dm-bubble__reply-text {
  display: block; font-size: var(--fs-caption); color: var(--text-muted); line-height: 1.35;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.dm-bubble--mine .dm-bubble__reply-text { color: color-mix(in srgb, var(--dm-mine-text) 78%, transparent); }

/* A URL unfurl card inside a text DM — reuses .lp-card; margin lifts it off the body. */
.dm-bubble__link { margin-top: 6px; }
.dm-bubble__link .lp-card { max-width: 280px; }

/* The single "Read" receipt under the viewer's own latest read message. */
.dm-bubble__receipt {
  display: block; margin-top: 2px; text-align: right;
  font-size: var(--fs-caption); color: var(--text-muted);
}
.dm-bubble--mine .dm-bubble__receipt { color: color-mix(in srgb, var(--dm-mine-text) 58%, transparent); }

/* Counterpart-bubble ⋯ overflow menu (→ Report). Reuses the shared pc-menu trigger/
   panel/item styles; only the container placement is bubble-specific (a small,
   quiet control beside the counterpart's bubble — no post-header margin-left:auto). */
.dm-bubble__menu { position: relative; align-self: center; flex: 0 0 auto; list-style: none; }
.dm-bubble__menu .pc-menu__trigger { width: 24px; height: 24px; opacity: .5; }
.dm-bubble__menu[open] .pc-menu__trigger, .dm-bubble__menu .pc-menu__trigger:hover { opacity: 1; }

/* ── DM reaction chips (DME-106) ──────────────────────────────────────────────
   A curated-5 row that floats below the bubble content. Each chip is either a
   button_to (flag ON, write-path interactive) or a <span> (flag OFF but reactions
   exist — read-only; counts are data even when the write path is dark). The chip
   is a badge/surface → soft rounded corner (--r-sm), not action-square (--r-action).
   button_to wraps its <button> in a form.button_to; display:contents makes the
   button the direct flex child so the row lays out like plain spans (same pattern
   as .rx-reactions__form in the reaction-bar).

   NO BORDER. A hairline box around a two-glyph badge reads as a control (a tiny
   button) when it is really just a mark someone left; five of them across a thread
   turn a quiet conversation into a form. The chip is shaped by a FILL instead — and
   the fill is mixed from `currentColor`, not a fixed token, because the chip sits
   inside .dm-bubble__inner and the two bubbles have opposite inks: the received
   bubble is paper (dark text) and the sent one is an ink fill (light text). A fixed
   --surface-elevated fill was invisible on the received bubble (same color as the
   bubble — the border WAS the chip) and a bright white slab on the sent one. A
   currentColor mix lands on the right side of whichever bubble it inherits. */
.dm-reaction-chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
.dm-reaction-chips form.button_to { display: contents; }
.dm-chip {
  display: inline-flex; align-items: center; gap: 3px;
  /* +1px over the old padding on each side: it absorbs the removed 1px border, so
     the chip keeps its footprint and the row doesn't reflow. */
  padding: 4px 9px; border: 0; border-radius: var(--r-sm);
  background: color-mix(in srgb, currentColor 8%, transparent);
  color: inherit;
  font: 400 var(--fs-body-sm) / 1 var(--font-body); cursor: pointer;
  /* Reset <button> defaults */
  appearance: none; font-family: inherit;
}
.dm-chip:hover { background: color-mix(in srgb, currentColor 14%, transparent); }
/* "You reacted" was carried by an accent border. With the border gone it is carried
   by weight of fill + weight of type — the same signal, one fewer line. */
.dm-chip--mine { background: color-mix(in srgb, currentColor 20%, transparent); font-weight: 600; }
.dm-chip--mine:hover { background: color-mix(in srgb, currentColor 26%, transparent); }
/* Read-only chip (flag OFF, reactions from a prior flip-on→flip-off): no pointer,
   no hover affordance — it is a badge, not a control. */
.dm-chip--readonly { cursor: default; pointer-events: none; }
.dm-chip__emoji { font-size: 1em; line-height: 1; }
/* currentColor + opacity, not --text-muted: --text-muted is a light-theme ink and was
   sitting on the DARK sent bubble at ~1.6:1. Inheriting the bubble's own ink and
   dimming it keeps the count legible on both. */
.dm-chip__count { font-size: var(--fs-caption); color: inherit; opacity: .7; }
.dm-chip--mine .dm-chip__count { opacity: 1; }

/* NOTE: the react-bar's styles live in ONE place, further down — see "DM quick-react
   bar (Slack's model)". A duplicate block used to sit here (two sessions fixed the
   same bug at once); the later block won by source order, so edits here would have
   silently done nothing. Don't reintroduce it. */

/* ── DME-FIX-B: inline sticker (mini-emoticon riding with the text body) ─────
   A text message can carry a sticker alongside its body. Mobile renders it as a
   small inline glyph (~text-height) after the body. Web was silently dropping it
   before this fix. Size to ~28px (roughly one line of body text) with lazy loading.
   .dm-sticker (freestanding sticker-kind, 150×150) is unchanged — it has no CSS of
   its own and leans on width/height attributes, which we intentionally do not touch. */
.dm-bubble__inline-sticker {
  display: block; margin-top: 4px;
  width: 28px; height: 28px; object-fit: contain;
}

/* The composer, pinned to the bottom of the chat column (flex-end via the .dm-chat
   stack). Compact by design on a text-first surface: the message row (textarea +
   Send) sits on top; the photo affordance (a slim compact uploader + alt field)
   sits UNDER it and stays collapsed until the member picks a photo, so it never
   eats vertical space. */
.dm-compose {
  flex: 0 0 auto;
  display: flex; flex-direction: column; gap: 8px;
  padding: var(--space-md) 0 calc(var(--space-md) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--border);
  background: var(--bg);
}
/* The primary send row: the growable input + the Send button, always on one line. */
.dm-compose__row { display: flex; gap: 8px; align-items: flex-end; }
.dm-compose__input {
  flex: 1 1 auto; resize: none; padding: 10px 14px; border: 1px solid var(--border);
  border-radius: var(--r-md); background: var(--surface-elevated); color: var(--text);
  font: 400 var(--fs-body-sm) / 1.4 var(--font-body);
  /* Auto-grow with content up to a cap so a long draft expands the input, not the
     page (JS-off it stays a scrollable single line; field-sizing grows it JS-on). */
  min-height: 42px; max-height: 40vh; field-sizing: content; overflow-y: auto;
}
/* focus ring inherited from the global Form-controls base */
.dm-compose__photo { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; }
/* The uploader + the alt-text input sit side by side on wide screens but WRAP to
   stacked full-width rows on phones (basis 240px), so the row never overflows. */
.dm-compose__photo > * { flex: 1 1 240px; min-width: 0; }
.dm-compose__photo-label { flex: 0 0 auto; cursor: pointer; font-size: var(--fs-body); }
.dm-compose__file { flex: 0 0 auto; font-size: var(--fs-caption); color: var(--text-muted); }
.dm-compose__alt {
  flex: 1 1 auto; min-width: 0; padding: 8px 12px; border: 1px solid var(--border);
  border-radius: var(--r-md); background: var(--surface-elevated); color: var(--text);
  font: 400 var(--fs-body-sm) / 1.4 var(--font-body);
}
/* The "Describe the photo (optional)" alt field is only meaningful once a photo is
   actually attached — so keep it out of the composer until then. JS-on (the uploader
   root carries .is-enhanced), hide it (and its sr-only label) until the uploader has
   a preview item; the :has() reacts live as file-upload appends/removes items, and it
   re-hides itself after a send (dm-compose#clearUploaderState empties the previews).
   JS-off there is no live preview to key off, so the selector never matches and the
   field stays available, so a native photo post can still carry alt text. */
.dm-compose__photo:has(.file-upload.is-enhanced):not(:has(.file-upload__item)) .dm-compose__alt,
.dm-compose__photo:has(.file-upload.is-enhanced):not(:has(.file-upload__item)) label[for="dm_alt"] {
  display: none;
}
/* focus ring inherited from the global Form-controls base */
.dm-compose__btn { flex: 0 0 auto; }

/* The composer's blocked state — a pending recipient must Accept the request first. */
.dm-compose-blocked {
  margin: 8px 0 4px; padding: 12px 0 4px; border-top: 1px solid var(--border);
  color: var(--text-muted); font-size: var(--fs-body-sm); text-align: center;
}

/* ── DM message-request band (inline, in an opened pending thread) ──────────
   The Accept/Decline band at the top of a thread the viewer is the pending
   recipient of. Reuses .req-actions + .follow-pill from the Requests lane. */
.dm-request-band {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px;
  margin: 12px 0; padding: 14px 16px;
  border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface-muted);
}
.dm-request-band__note { flex: 1 1 220px; margin: 0; font-size: var(--fs-body-sm); color: var(--text-muted); line-height: 1.4; }
.dm-request-band__note strong { color: var(--text); }

/* ── DM recipient picker (GET /messages/new) ───────────────────────────────
   Recent + Following + debounced search rows; each row is the whole POST /messages
   open-or-reuse door with a Message/Request hint. */
.dm-new-link { margin: 6px 0 12px; }
.dm-new-link__btn { display: inline-block; text-decoration: none; }
/* The single "start a conversation" block: search input, then the JS-off exact-username
   fallback (hidden once the search is enhanced) — one clear path, no redundant fields. */
.dm-picker-start { margin: 10px 0 8px; }
.dm-picker-start .dm-new { margin: 8px 0 0; }
/* The controller sets [hidden] on the fallback JS-on; win over .dm-new's display:flex. */
.dm-picker-start [hidden] { display: none; }
.dm-picker-search { margin: 2px 0 6px; }
.dm-picker-search__input { width: 100%; }
.dm-picker { display: block; }
.dm-picker-section {
  margin: 16px 0 4px; font: 700 var(--fs-caption) / 1 var(--font-body);
  text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted);
}
.dm-picker-list { display: flex; flex-direction: column; }
.dm-picker-row-form { margin: 0; display: block; }
.dm-picker-row {
  display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 6px;
  border: 0; border-bottom: 1px solid var(--border); background: none; text-align: left;
  color: var(--text); cursor: pointer; font: inherit;
}
.dm-picker-row:hover { background: var(--accent-subtle); }
.dm-picker-row__avatar { flex: 0 0 auto; display: inline-flex; }
.dm-picker-row__id { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.dm-picker-row__name { font-weight: 700; font-size: var(--fs-body-sm); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dm-picker-row__handle { font-size: var(--fs-caption); color: var(--text-muted); }
.dm-picker-row__note { font-size: var(--fs-caption); color: var(--text-muted); }
.dm-picker-row__hint {
  flex: 0 0 auto; font-size: var(--fs-caption); font-weight: 700; color: var(--accent);
  padding: 4px 10px; border: 1px solid var(--border); border-radius: var(--r-action);
}
.dm-picker-row__hint--request { color: var(--text-muted); }

/* ─────────────────────────────────────────────────────────────────────────────
   Web video call + screen share (feature-flagged; DM thread header + overlays).
   ───────────────────────────────────────────────────────────────────────────── */
.dm-call { margin-left: auto; }

.dm-call__start {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 14px; border: 1px solid var(--primary); border-radius: var(--r-action);
  background: var(--primary); color: #fff; cursor: pointer;
  font: 600 var(--fs-body-sm) / 1 var(--font-body);
}
.dm-call__start:hover { background: var(--primary-bright); border-color: var(--primary-bright); }
.dm-call__start-label { line-height: 1; }

/* The in-call stage — a fixed, full-viewport dark theatre over everything. */
.call-stage {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 18px; padding: 24px; background: rgba(8, 8, 12, 0.94);
}
.call-stage[hidden] { display: none; }

.call-stage__frame {
  position: relative; width: min(92vw, 720px); aspect-ratio: 16 / 10;
  border-radius: var(--r-lg); overflow: hidden;
  background: #0b0b10; box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}
.call-stage__remote { width: 100%; height: 100%; object-fit: cover; background: #0b0b10; }
.call-stage__local {
  position: absolute; right: 14px; bottom: 14px; width: 28%; max-width: 190px;
  aspect-ratio: 16 / 10; object-fit: cover; border-radius: var(--r-md);
  border: 2px solid rgba(255, 255, 255, 0.85); background: #16181c;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45); transform: scaleX(-1); /* mirror self-view */
}
.call-stage__status {
  position: absolute; left: 0; right: 0; top: 14px; margin: 0; text-align: center;
  color: rgba(255, 255, 255, 0.92); font: 600 var(--fs-body) / 1.3 var(--font-body);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); pointer-events: none;
}

.call-stage__controls, .call-ring__actions {
  display: flex; flex-wrap: wrap; gap: 10px; justify-content: center;
}

.call-ctl {
  min-width: 96px; padding: 11px 18px; cursor: pointer;
  border: 1px solid rgba(255, 255, 255, 0.28); border-radius: var(--r-action);
  background: rgba(255, 255, 255, 0.12); color: #fff;
  font: 600 var(--fs-body-sm) / 1 var(--font-body);
}
.call-ctl:hover { background: rgba(255, 255, 255, 0.2); }
.call-ctl--active { background: var(--primary); border-color: var(--primary); }
.call-ctl--accept { background: var(--primary); border-color: var(--primary); }
.call-ctl--accept:hover { background: var(--primary-bright); }
.call-ctl--end { background: var(--heart-red); border-color: var(--heart-red); }
.call-ctl--end:hover { filter: brightness(1.08); }

/* Incoming ring — a compact centred card over the theatre backdrop. */
.call-ring {
  position: fixed; inset: 0; z-index: 1001;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 18px; padding: 24px; background: rgba(8, 8, 12, 0.9);
}
.call-ring[hidden] { display: none; }
.call-ring__who {
  margin: 0; color: #fff; text-align: center;
  font: 500 var(--fs-body-lg) / 1.4 var(--font-body);
}
.call-ring__who strong { font-weight: 700; }
/* ── In-app realtime toast (nav_live_controller) ──────────────────────────
   A brief, calm notification when a new DM / activity / follow request lands
   while a tab is open. Fixed bottom-centre pill, quiet elevation, slides up +
   fades in, auto-dismisses. Premium/calm brand: one short toast, never a nag. */
.nav-toast {
  position: fixed; left: 50%; bottom: 28px; transform: translate(-50%, 12px);
  z-index: 60; max-width: min(90vw, 420px);
  display: flex; align-items: center; gap: var(--space-sm);
  padding: 12px 18px; border-radius: var(--r-action);
  background: var(--surface-elevated); color: var(--text);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-copy-toast);
  font: 600 var(--fs-body-sm) / 1.3 var(--font-body);
  opacity: 0; pointer-events: none;
  transition: opacity 220ms ease, transform 220ms ease;
}
.nav-toast::before {
  content: ""; flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent);
}
.nav-toast--in { opacity: 1; transform: translate(-50%, 0); }
@media (prefers-reduced-motion: reduce) {
  .nav-toast { transition: opacity 220ms ease; }
  .nav-toast, .nav-toast--in { transform: translate(-50%, 0); }
}

/* ── File uploader (Web::FileUploadComponent / file_upload_controller) ──────
   ONE reusable uploader across settings avatar, compose photo and DM photo.
   Progressive enhancement: JS-OFF shows a plain native <input type=file> (the
   decorative dropzone chrome stays hidden — it would be a lie without JS);
   JS-ON, .is-enhanced reveals the drag-drop zone and the native input becomes a
   transparent full-cover click/keyboard/attach_file target. */
.file-upload { display: block; }
.file-upload__label {
  display: block; margin-bottom: var(--space-xs);
  font: 600 var(--fs-body-sm) / 1.3 var(--font-body); color: var(--text);
}
.file-upload__hint {
  margin: 0 0 var(--space-sm); color: var(--text-muted);
  font: 400 var(--fs-caption) / 1.4 var(--font-body);
}
.file-upload__zone { position: relative; }

/* Decorative zone chrome — enhancement only. */
.file-upload__icon,
.file-upload__prompt,
.file-upload__meta { display: none; }
.file-upload.is-enhanced .file-upload__zone {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; min-height: 132px; padding: var(--space-md); cursor: pointer;
  border: 1.5px dashed var(--border); border-radius: var(--r-lg);
  background: var(--surface-1); text-align: center;
  transition: border-color 160ms ease, background 160ms ease;
}
.file-upload.is-enhanced .file-upload__zone:hover,
.file-upload.is-enhanced .file-upload__zone:focus-within {
  border-color: var(--accent); background: var(--surface-2);
}
.file-upload.is-enhanced .file-upload__zone.is-dragover {
  border-color: var(--accent); border-style: solid;
  background: var(--primary-tint, var(--surface-2));
}
.file-upload.is-enhanced .file-upload__icon { display: block; width: 30px; height: 30px; color: var(--accent); }
.file-upload.is-enhanced .file-upload__prompt {
  display: block; margin: 0; color: var(--text);
  font: 500 var(--fs-body-sm) / 1.4 var(--font-body);
}
.file-upload__prompt-strong { font-weight: 700; }
.file-upload__browse-text { color: var(--accent); font-weight: 600; text-decoration: underline; }
.file-upload.is-enhanced .file-upload__meta {
  display: block; margin: 0; color: var(--text-muted);
  font: 400 var(--fs-caption) / 1.3 var(--font-body);
}

/* Native input: visible + normal JS-off (the fallback control). JS-on it is
   visually hidden with the accessible sr-only clip pattern — NOT display:none /
   visibility:hidden / opacity:0, so it stays keyboard-focusable AND
   Capybara/Cuprite-visible (attach_file drives it). The zone is the visual
   control; a zone click delegates to input.click(). */
.file-upload__input { display: block; max-width: 320px; font-size: var(--fs-body-sm); color: var(--text); }
.file-upload.is-enhanced .file-upload__input {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Preview cards. */
.file-upload__previews { list-style: none; margin: var(--space-sm) 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.file-upload__item {
  display: flex; align-items: center; gap: var(--space-sm);
  padding: 8px; border: 1px solid var(--border); border-radius: var(--r-md);
  background: var(--surface-1);
}
.file-upload__item.is-done { border-color: var(--accent); }
.file-upload__thumb { flex: 0 0 auto; width: 48px; height: 48px; border-radius: var(--r-sm); overflow: hidden; background: var(--surface-2); }
.file-upload__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.file-upload__item-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.file-upload__item-name { font: 500 var(--fs-caption) / 1.3 var(--font-body); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-upload__progress { display: block; height: 5px; border-radius: var(--r-pill); background: var(--surface-3, var(--border)); overflow: hidden; }
.file-upload__progress-bar { display: block; height: 100%; width: 0; background: var(--accent); border-radius: var(--r-pill); transition: width 160ms ease; }
.file-upload__item-status { font: 400 var(--fs-caption) / 1.2 var(--font-body); color: var(--text-muted); }
.file-upload__item-status.is-error { color: var(--badge-red, var(--heart-red)); }

/* Transient progress: the filling bar shows ONLY during the upload. On completion
   (.is-done) it collapses — a full-width bar reading "ready" is noise — leaving a
   compact "✓ ready" confirmation beside the thumbnail + discard ×. Errors never get
   .is-done, so they keep their inline red status (no ✓). */
.file-upload__item.is-done .file-upload__progress { display: none; }
.file-upload__item.is-done .file-upload__item-status { color: var(--accent); font-weight: 600; }
.file-upload__item.is-done .file-upload__item-status::before { content: "✓"; margin-right: 5px; font-weight: 700; }

.file-upload__remove {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border: none; border-radius: 50%;
  background: var(--surface-2); color: var(--text-muted); cursor: pointer;
}
.file-upload__remove:hover { background: var(--surface-3, var(--border)); color: var(--text); }

.file-upload__current { display: flex; align-items: center; gap: var(--space-sm); margin-top: var(--space-sm); }
.file-upload__current-img { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; }
.file-upload__current-note { font: 400 var(--fs-caption) / 1.2 var(--font-body); color: var(--text-muted); }

.file-upload__notice { margin: var(--space-xs) 0 0; color: var(--badge-red, var(--heart-red)); font: 500 var(--fs-caption) / 1.3 var(--font-body); }

/* Settings avatar: the form is a 420px flex row (input + submit); let the
   uploader own a full row so the "Upload photo" button wraps below it instead of
   being squeezed. Scoped to the avatar form — the muted-words/accounts forms that
   share .settings-add-form still fit their input + button on one line. */
.settings-add-form { flex-wrap: wrap; }
.settings-add-form .file-upload { flex: 1 1 100%; }

@media (prefers-reduced-motion: reduce) {
  .file-upload.is-enhanced .file-upload__zone,
  .file-upload__progress-bar { transition: none; }
}

/* ── Compact uploader variant (compose only) ──────────────────────────────────
   FileUploadComponent(compact: true) adds .file-upload--compact. JS-on, it
   collapses the tall dropzone into a slim inline "add photo" pill (progressive
   disclosure of media) while keeping the SAME input / drag-drop / preview /
   Direct-Upload behaviour. Placed AFTER the base .file-upload rules so these
   equal-specificity overrides win by source order. Compose + DM opt in; the
   settings avatar renders the full-size zone (drag-a-headshot is the natural
   gesture there — 2026-07-06 UX review). JS-off, .is-enhanced is absent, so the
   plain native input shows exactly as before (the fallback). */
.file-upload--compact .file-upload__label,
.file-upload--compact .file-upload__hint {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
.file-upload--compact.is-enhanced .file-upload__zone {
  display: inline-flex; flex-direction: row; align-items: center; justify-content: flex-start;
  width: auto; min-height: 0; gap: 8px; padding: 9px 16px 9px 13px;
  border: 1.5px solid var(--border); border-radius: var(--r-action);
  background: var(--surface-1); text-align: left;
}
.file-upload--compact.is-enhanced .file-upload__icon { width: 18px; height: 18px; }
.file-upload--compact.is-enhanced .file-upload__prompt { white-space: nowrap; font-size: var(--fs-body-sm); }
.file-upload--compact.is-enhanced .file-upload__meta { display: none; }

/* Window-level drag rescue: the compact pill is a small drop target, so while a
   FILE drag is live anywhere over the page (file_upload_controller adds
   .is-expanded) the pill grows back into the full dashed dropzone — a real
   target, not just cosmetics. Collapses on drop / dragend / leaving the window.
   Touch and JS-off never see this (drag events don't fire there). AFTER the
   compact block so equal-specificity overrides win by source order. */
.file-upload--compact.is-enhanced .file-upload__zone.is-expanded {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  width: 100%; min-height: 112px; gap: 6px; padding: var(--space-md);
  border-style: dashed; border-radius: var(--r-lg);
  text-align: center;
}
.file-upload--compact.is-enhanced .file-upload__zone.is-expanded .file-upload__icon { width: 24px; height: 24px; }
.file-upload--compact.is-enhanced .file-upload__zone.is-expanded .file-upload__prompt { white-space: normal; }
.file-upload--compact.is-enhanced .file-upload__zone.is-expanded .file-upload__meta { display: block; }

/* ── Large gallery preview variant (compose only) ─────────────────────────────
   FileUploadComponent(preview: :gallery) adds .file-upload--gallery. It re-lays
   the SAME preview markup (no template change → the settings avatar + DM
   uploaders are byte-identical) into a big Threads/X/Instagram-style inline image
   preview: a full-width thumbnail, a floating ✕ remove overlay, and the filename +
   upload status/progress as a quiet caption over a bottom scrim. Placed AFTER the
   base .file-upload rules so equal-specificity overrides win by source order. */
.file-upload--gallery .file-upload__previews { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; margin-top: 12px; }
.file-upload--gallery .file-upload__item {
  position: relative; display: block; padding: 0; overflow: hidden;
  aspect-ratio: 3 / 2; border-radius: var(--r-lg); border: 1px solid var(--outline-variant);
  background: var(--surface-3);
}
.file-upload--gallery .file-upload__item.is-done { border-color: var(--outline-variant); }
.file-upload--gallery .file-upload__thumb {
  position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 0;
}
.file-upload--gallery .file-upload__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Floating remove — an ✕ on a dark scrim disc at the image's top-right corner. */
.file-upload--gallery .file-upload__remove {
  position: absolute; top: 10px; right: 10px; z-index: 3;
  width: 32px; height: 32px; color: #fff;
  background: rgba(10, 10, 12, .6); backdrop-filter: blur(3px);
}
.file-upload--gallery .file-upload__remove:hover { background: rgba(10, 10, 12, .82); color: #fff; }

/* Caption strip — filename + status + progress over a bottom gradient scrim. */
.file-upload--gallery .file-upload__item-body {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  padding: 26px 14px 11px; gap: 6px;
  background: linear-gradient(to top, rgba(10, 10, 12, .8), rgba(10, 10, 12, .28) 62%, transparent);
}
.file-upload--gallery .file-upload__item-name { color: #fff; font-weight: 600; }
.file-upload--gallery .file-upload__item-status { color: rgba(255, 255, 255, .82); }
.file-upload--gallery .file-upload__progress { background: rgba(255, 255, 255, .28); }
.file-upload--gallery .file-upload__item.is-done .file-upload__progress { display: none; }
/* The confirmed "✓ ready" caption sits over the image's dark scrim here — keep it
   white for contrast (the base rule tints it accent for the light row preview). */
.file-upload--gallery .file-upload__item.is-done .file-upload__item-status { color: #fff; }

/* ── Password-strength meter (auth signup / reset / set-password) ─────────────
   A decorative 4-segment bar + a plain caption, filled live by the
   `password-strength` Stimulus controller. The container is aria-hidden (the
   caption text carries the meaning) and stays hidden until the field is
   non-empty (the controller toggles [hidden] — never pre-validates). The
   segment colors mirror the mobile app's danger → warning → success ramp as
   locally-scoped vars, so dark mode swaps them without touching the tokens. */
.pw-meter {
  --pw-weak:   #DC2626;
  --pw-fair:   #D97706;
  --pw-strong: #16A34A;
  display: flex; flex-direction: column; gap: 6px; margin: 2px 0;
}
.pw-meter[hidden] { display: none; }
.pw-meter__bar { display: flex; gap: 6px; }
.pw-meter__seg {
  flex: 1; height: 4px; border-radius: var(--r-sm);
  background: var(--surface-muted); transition: background .15s ease;
}
.pw-meter[data-score="1"] .pw-meter__seg--on { background: var(--pw-weak); }
.pw-meter[data-score="2"] .pw-meter__seg--on { background: var(--pw-fair); }
.pw-meter[data-score="3"] .pw-meter__seg--on,
.pw-meter[data-score="4"] .pw-meter__seg--on { background: var(--pw-strong); }
.pw-meter__label { margin: 0; font: 500 var(--fs-caption) / 1.4 var(--font-body); color: var(--text-muted); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not(.theme-light) .pw-meter {
    --pw-weak: #F87171; --pw-fair: #FBBF24; --pw-strong: #4ADE80;
  }
}
:root[data-theme="dark"] .pw-meter,
.theme-dark .pw-meter {
  --pw-weak: #F87171; --pw-fair: #FBBF24; --pw-strong: #4ADE80;
}

/* ── Deletion review page (GET /settings/deletion) — consequences + reauth +
   type-@username confirm gate before the destructive request. Reuses the
   .settings-* card chrome; adds the honest what-happens / what-stays lists, the
   danger callout, and the armed-when-typed destructive button. ── */
.delete-review { max-width: 560px; }
.delete-review__list { list-style: none; margin: 0 0 18px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.delete-review__list li {
  position: relative; padding-left: 18px;
  font: 400 var(--fs-body-sm) / 1.5 var(--font-body); color: var(--text-muted);
}
.delete-review__list li::before {
  content: ""; position: absolute; left: 2px; top: 8px;
  width: 5px; height: 5px; border-radius: 50%; background: var(--outline);
}
.delete-review__note {
  padding: 12px 14px; border-radius: var(--r-md); margin: 0 0 18px;
  font: 500 var(--fs-body-sm) / 1.5 var(--font-body);
  background: var(--tertiary-container); color: var(--tertiary);
}
.delete-review__actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 6px; }
.btn-danger {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 11px 20px; border-radius: var(--r-action); border: 1px solid transparent;
  font: 600 var(--fs-body-sm) / 1 var(--font-body); cursor: pointer;
  background: var(--heart-red); color: #fff; transition: opacity .12s ease, transform .12s ease;
}
.btn-danger:hover { transform: translateY(-1px); }
.btn-danger:disabled { opacity: .5; cursor: not-allowed; transform: none; }

/* ── Private-profile follow-wall (LOW-UX) — the calm explanatory block that fills
   the void the withheld feed leaves below a private account's identity header.
   Reuses tokens only; never hints at what's inside (privacy fails CLOSED). ── */
.private-wall {
  margin: 22px auto 8px; max-width: 460px; text-align: center;
  display: flex; flex-direction: column; align-items: center;
  padding: 30px 24px; border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--surface);
}
.private-wall__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 52px; height: 52px; border-radius: 50%; margin-bottom: 14px;
  background: var(--primary-container); color: var(--accent);
}
.private-wall__title {
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-title);
  color: var(--text); margin: 0 0 8px; line-height: 1.2;
}
.private-wall__lead {
  color: var(--text-muted); font-size: var(--fs-body-lg); line-height: 1.55; margin: 0;
}
.private-wall__lead b { color: var(--text); font-weight: 700; }
.private-wall__points {
  list-style: none; margin: 18px 0 0; padding: 16px 0 0; text-align: left;
  border-top: 1px solid var(--border); width: 100%;
  display: flex; flex-direction: column; gap: 10px;
}
.private-wall__points li {
  position: relative; padding-left: 22px;
  color: var(--text-muted); font-size: var(--fs-body-sm); line-height: 1.5;
}
.private-wall__points li::before {
  content: ""; position: absolute; left: 4px; top: 8px;
  width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
}

/* ── Post-edit form (LOW-UX) — the live char counter + the read-only current
   photo, so a photo post's image is visibly safe while editing text. ── */
.edit-field { position: relative; }
.edit-counter {
  margin: 6px 2px 0; text-align: right;
  font: 600 var(--fs-caption) / 1 var(--font-body); color: var(--text-muted);
}
.edit-counter.is-warn { color: var(--violet-600); }
.edit-counter.is-over { color: var(--heart-red); }
.edit-photo {
  margin: 0; border: 1px solid var(--border); border-radius: var(--r-lg);
  overflow: hidden; background: var(--surface);
}
.edit-photo__img { display: block; width: 100%; max-height: 320px; object-fit: cover; }
.edit-photo__note {
  padding: 10px 14px; color: var(--text-muted);
  font: 500 var(--fs-caption) / 1.45 var(--font-body);
  border-top: 1px solid var(--border);
}

/* ── Generic "not available" state page (MED-UX) — the self-contained, byte-stable
   fallback (web/public/unavailable.html.erb, rendered layout: false). Brand-dressed
   with tokens + the shared button atoms; every rule here is constant (no per-request
   anything) so the non-enumeration byte-identical invariant holds. ── */
.unavailable-page { background: var(--bg); color: var(--text); min-height: 100vh; margin: 0; font-family: var(--font-body); }
.unavailable {
  min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 30px; padding: 40px 20px; text-align: center;
}
.unavailable__mark {
  font-family: var(--font-display); font-weight: 600; font-size: 24px; letter-spacing: -0.01em;
  color: var(--text); text-decoration: none;
}
.unavailable__card {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  max-width: 420px; padding: 36px 28px;
  border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--surface);
}
.unavailable__glyph {
  display: inline-flex; align-items: center; justify-content: center;
  width: 58px; height: 58px; border-radius: 50%; margin-bottom: 4px;
  background: var(--primary-container); color: var(--accent);
}
.unavailable__title {
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-title);
  color: var(--text); margin: 0; line-height: 1.2;
}
.unavailable__lead {
  color: var(--text-muted); font-size: var(--fs-body-lg); line-height: 1.55; margin: 0; max-width: 340px;
}
.unavailable__actions { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; margin-top: 8px; }

/* Admin 2FA (feature/admin-2fa-totp) — enroll QR + backup-code reveal. Minimal,
   reuses the settings-section chrome; a later pass handles visual refinement. */
.otp-setup__lead, .otp-setup__manual, .otp-status { color: var(--text-muted); line-height: 1.5; }
/* The QR must stay square, UNCLIPPED, and on a white field in both themes — a
   scanner needs the contrast the code was drawn with, and the quiet zone (baked
   into the SVG via Otp::QR_QUIET_ZONE) must not be cropped. Padding + background
   live on the WRAPPER, not the <svg> (padding on an SVG root isn't reliably
   honoured); the svg fills the box and now scales correctly because it carries a
   viewBox — without one it rendered at its intrinsic 318px and was cut off. */
.otp-qr {
  width: 220px; max-width: 100%; margin: 16px 0; padding: 10px;
  background: #fff; border-radius: var(--r-md); border: 1px solid var(--border);
}
.otp-qr svg { width: 100%; height: auto; display: block; }
.otp-secret { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.12em; font-size: var(--fs-body-lg); word-break: break-all; }
.otp-form { margin-top: 16px; max-width: 360px; }
.otp-codes { list-style: none; margin: 12px 0 20px; padding: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; max-width: 360px; }
.otp-codes code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: var(--fs-body-lg); letter-spacing: 0.08em; }

/* ── Desk · the "Draft with AI" modal (SEEDING_DRAFTER_ENABLED) ──────────────
   The redraft dialog from documents/mockups/ai-desk-screens.html §1b, rebuilt on
   the console's OWN tokens rather than the mockup's hard-coded hexes — so it
   themes light/dark with the rest of the admin surface instead of being a white
   rectangle punched through a dark page.

   A native <dialog>: showModal() gives the focus trap, the inert page behind and
   Esc-to-close for free — no JS library, nothing to keep in sync. */
.desk-modal {
  width: min(620px, calc(100vw - 32px));
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface-elevated);
  color: var(--text);
  box-shadow: var(--shadow-dialog);
  overflow: hidden;
}
.desk-modal::backdrop { background: rgba(10, 10, 12, .55); }

.desk-modal__head { padding: 16px 20px; border-bottom: 1px solid var(--border); }
.desk-modal__title {
  margin: 0;
  font-family: var(--font-display); font-weight: 600;
  font-size: var(--fs-subtitle); line-height: 1.2; color: var(--text);
}
/* The source context ("headline" · host · your clip) — muted, one line, the thing
   the editor checks before they ask the machine for anything. */
.desk-modal__sub { margin: 4px 0 0; font-size: var(--fs-body-sm); color: var(--text-muted); }

.desk-modal__body { padding: 18px 20px; }
.desk-modal__label {
  display: block; margin: 0 0 8px;
  font: 700 var(--fs-eyebrow) / 1 var(--font-body);
  letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted);
}
/* Inherits the global input theme (border/font/focus ring) — this rule only owns
   the box, so the prompt matches every other control in the console. */
.desk-modal__prompt { width: 100%; box-sizing: border-box; min-height: 74px; resize: vertical; }

/* The preset chips. Toggles, not links: .on (+ aria-pressed) is the selected
   state, and each selected chip's fixed phrase is folded into the one instruction
   string the drafter endpoint receives. */
.desk-chips { display: flex; gap: 8px; flex-wrap: wrap; margin: 0 0 18px; }
.desk-chip {
  font: 600 var(--fs-body-sm) / 1 var(--font-body);
  padding: 9px 13px; border-radius: var(--r-action);
  border: 1px solid var(--border); background: var(--surface-elevated);
  color: var(--text); cursor: pointer; white-space: nowrap;
}
.desk-chip:hover { border-color: var(--accent); }
.desk-chip.on {
  background: var(--primary-container);
  border-color: var(--accent);
  color: var(--on-primary-container);
}

.desk-modal__foot {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 14px 20px; border-top: 1px solid var(--border); background: var(--surface);
}
/* The honest expectation, set before they press: this takes a beat, and it does
   not hold the queue hostage while it runs. */
.desk-modal__caption { margin-left: auto; font-size: var(--fs-caption); color: var(--text-muted); }
.desk-modal__busy {
  display: inline-flex; align-items: center; gap: 8px;
  font: 600 var(--fs-body-sm) / 1 var(--font-body); color: var(--accent);
}
.desk-modal__busy[hidden] { display: none; }
.desk-spin {
  width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid var(--accent-subtle); border-top-color: var(--accent);
  animation: desk-spin .8s linear infinite;
}
@keyframes desk-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .desk-spin { animation: none; } }

/* Phones: the caption drops to its own line rather than crushing the buttons. */
@media (max-width: 560px) {
  .desk-modal__caption { margin-left: 0; flex-basis: 100%; }
}

/* ── Emoji picker (post composer + DM composer) ──────────────────────────────
   A desktop browser has no emoji key — this is the surface where a picker earns
   its keep. Press shape rules: the trigger and every cell are ACTIONS, so they
   take --r-action (3px), never a pill. */
.emoji-picker {
  position: relative;
  display: inline-flex;
}

.emoji-picker__button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 6px;
  border-radius: var(--r-action);
  color: var(--ink-2);
  background: transparent;
  border: 1px solid transparent;
  cursor: pointer;
}
.emoji-picker__button:hover,
.emoji-picker__button[aria-expanded="true"] {
  color: var(--ink);
  background: var(--surface-3);
  border-color: var(--line, var(--surface-4));
}

/* The panel hangs off the trigger. In the DM composer it opens UPWARD (the
   composer is docked to the bottom of the thread). */
.emoji-picker__panel {
  position: absolute;
  z-index: 40;
  top: calc(100% + 6px);
  inset-inline-end: 0;
  width: min(320px, 90vw);
  max-height: 260px;
  overflow-y: auto;
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 2px;
  padding: 8px;
  background: var(--surface-1);
  border: 1px solid var(--surface-4);
  border-radius: var(--r-action);
  box-shadow: 0 10px 30px rgb(30 27 24 / 18%);
}
.emoji-picker__panel--up {
  top: auto;
  bottom: calc(100% + 6px);
}
/* `display: grid` above beats the UA's `[hidden] { display: none }`, so the panel
   would stay on screen after Stimulus hid it. Say it explicitly. */
.emoji-picker__panel[hidden] {
  display: none;
}

.emoji-picker__cell {
  font-size: 20px;
  line-height: 1.4;          /* a glyph is taller than its font size — give it room */
  padding: 2px 0;
  background: transparent;
  border: 0;
  border-radius: var(--r-action);
  cursor: pointer;
}
.emoji-picker__cell:hover,
.emoji-picker__cell:focus-visible {
  background: var(--surface-3);
}


/* ── DM quick-react bar (Slack's model) ──────────────────────────────────────
   Five buttons stamped into every bubble, always on, was visual noise: it made a
   quiet thread look like a control panel. The reactions people LEFT are content
   and stay put; the ones you MIGHT leave are a control, so they float in on hover
   like Slack's. Pure CSS — no JS, and :focus-within keeps it keyboard-reachable. */
.dm-bubble { position: relative; }
/* The bar's containing block is the MESSAGE BOX, not the whole row — the row also
   holds the avatar, and a bar anchored to the row's edge would ride over it. */
.dm-bubble__inner { position: relative; }

.dm-react-bar {
  position: absolute;
  /* Clear of the words: the bar rides ABOVE the bubble's top edge, like Slack's,
     rather than sitting on top of the first line. */
  top: -20px;
  /* Anchored to the bubble's OUTER edge, so the overhang runs INWARD — into the
     thread, which always has room. The bar is a FIXED width and a bubble can be a
     single word: anchored the other way, an "ok" bubble (82px) wearing a 154px bar
     pushed it 61px PAST the left edge of a narrow window — two of the five buttons
     unreachable, no scrollbar to reach them — and the mirrored case gave the whole
     page a horizontal scrollbar. Same rule as the mobile overlay: a control may
     overhang its anchor, never its window. */
  inset-inline-start: 6px;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  background: var(--surface-1);
  /* No outline round the emoji: a boxed glyph reads as a form control. The shadow
     alone lifts the bar off the thread — that's all the separation it needs. */
  border: 0;
  border-radius: var(--r-action);
  box-shadow: 0 4px 16px rgb(30 27 24 / 16%);
  opacity: 0;
  pointer-events: none;
  transition: opacity .12s ease;
}
/* Own bubbles sit on the other side of the thread, so the bar mirrors — its outer
   edge is the RIGHT one, and its overhang runs left, back into the thread. */
.dm-bubble--mine .dm-react-bar { inset-inline-start: auto; inset-inline-end: 8px; }

.dm-bubble:hover .dm-react-bar,
.dm-bubble:focus-within .dm-react-bar {
  opacity: 1;
  pointer-events: auto;
}

.dm-react-bar form.button_to { display: contents; }

.dm-react-bar__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  font-size: 16px;
  line-height: 1;
  background: transparent;
  border: 0;
  border-radius: var(--r-action);
  cursor: pointer;
}
.dm-react-bar__btn:hover,
.dm-react-bar__btn:focus-visible { background: var(--surface-3); }
/* Belt and braces: no borders on the glyphs themselves, ever. */
.dm-react-bar__btn,
.dm-react-bar__btn:hover,
.dm-react-bar__btn:focus-visible { border: 0; }

/* Coarse pointers (touch) have no hover — show the bar, quietly, rather than
   hiding a feature behind a gesture that device can't make. */
@media (hover: none) {
  .dm-react-bar { opacity: 1; pointer-events: auto; position: static; box-shadow: none; border: 0; background: transparent; padding: 0; margin-top: 4px; }
}
