/*
 * 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-pill); padding: 10px 22px; cursor: pointer;
  text-decoration: none; line-height: 1;
}
.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);
  border: 1.5px solid var(--border); border-radius: var(--r-pill); padding: 10px 20px; cursor: pointer;
  text-decoration: none; line-height: 1;
}
.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);
  border: 1px solid var(--border);
  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");
}

/* <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. */
.site-mark {
  max-width: var(--measure-wide); margin: 0 auto; padding: var(--space-lg);
  font: 600 var(--fs-title) / 1 var(--font-display); letter-spacing: -0.01em;
}
.site-mark a { color: var(--ink); text-decoration: none; }
.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;
  color: var(--on-primary); 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;
}
.av-1 { background: linear-gradient(135deg, #6B4EFF, #3D2E8C); }
.av-2 { background: linear-gradient(135deg, #B98A4B, #7A5A2E); }
.av-3 { background: linear-gradient(135deg, #7A5CFF, #3A2A88); }
.av-4 { background: linear-gradient(135deg, #4A5568, #2D3748); }
.av-5 { background: linear-gradient(135deg, #8C3B7A, #5C2560); }
.av-6 { background: linear-gradient(135deg, #9C8B5E, #6B5D3D); }
.av-7 { background: linear-gradient(135deg, #4E4A9E, #2A2860); }

/* 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; }
.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;
}
.mnav__logo {
  display: inline-flex; align-items: center; gap: 9px; text-decoration: none;
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-subtitle);
  color: var(--text); letter-spacing: -.01em;
}
.mnav__logo em { color: var(--accent); font-style: normal; }
.mnav__disc { position: static; margin: 0; }
.mnav__toggle {
  display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px;
  border-radius: var(--r-md); 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: -18px 0 40px -24px rgba(10, 10, 12, .5);
}
.mnav__disc[open] .mnav__scrim { display: block; }
.mnav__disc[open] .mnav__drawer { display: flex; }
.mnav__item {
  display: flex; align-items: center; gap: 14px; padding: 12px; border-radius: var(--r-md);
  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-pill); 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-pill); 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-pill);
  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; }
}
.rail-logo {
  display: inline-flex; align-items: center; gap: 9px;
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-subtitle);
  color: var(--text); padding: 6px 10px 24px; letter-spacing: -.01em; text-decoration: none;
}
.rail-logo__mark { flex: none; display: block; }
.rail-logo__mark-bg { fill: var(--accent); }
.rail-logo__word { display: inline-block; }
.rail-logo em { color: var(--accent); font-style: normal; }
.rail-item {
  display: flex; align-items: center; gap: 14px; padding: 11px 12px; border-radius: var(--r-md);
  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; }
.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-md);
}
a.rail-user:hover { background: var(--accent-subtle); color: var(--accent); }

/* ── 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: 0 12px 32px -12px rgba(10, 10, 12, .45);
}
.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-pill); 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-pill); 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-pill);
  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-pill); 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: 0 6px 20px -8px rgba(20, 15, 10, .12);
}
.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: 0 6px 20px -8px rgba(20, 15, 10, .12);
}
.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); }

/* ── 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; }
.pagination__link {
  display: inline-flex; align-items: center; justify-content: center; min-width: 38px; height: 38px; padding: 0 12px;
  border-radius: var(--r-md); 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-md); 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: 0 6px 20px -8px rgba(20, 15, 10, .12); 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-pill); 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. */
.pc-photo { margin-top: 12px; border-radius: var(--r-lg); height: 184px; overflow: hidden; position: relative; background: var(--surface-muted); }
.pc-photo img, .pc-photo svg { width: 100%; height: 100%; 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 lightbox. */
.pc-photo__zoom { display: block; width: 100%; height: 100%; cursor: zoom-in; }
/* Full-screen photo lightbox — a native <dialog> (focus-trap + Esc for free). */
.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(8, 8, 12, 0.92); }
.pc-lightbox[open] { display: flex; align-items: center; justify-content: center; }
.pc-lightbox__img { max-width: 94vw; max-height: 90vh; width: auto; height: auto; object-fit: contain; border-radius: var(--r-md); }
.pc-lightbox__close {
  position: fixed; top: 18px; right: 18px; z-index: 1; width: 44px; height: 44px;
  display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
  border: 1px solid rgba(255, 255, 255, 0.28); border-radius: var(--r-pill);
  background: rgba(255, 255, 255, 0.14); color: #fff;
}
.pc-lightbox__close:hover { background: rgba(255, 255, 255, 0.24); }
.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-pill);
  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-pill); 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-md); padding: 8px 14px;
}
.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-pill);
  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-pill);
  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;
}
.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: 0 -12px 30px -20px rgba(10, 10, 12, .35);
}
.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: 0 -12px 30px -20px rgba(10, 10, 12, .35);
}
.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; }
/* ── 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-pill); box-shadow: 0 6px 20px -8px rgba(20, 15, 10, .12);
}
.rx-reactions__form { display: contents; }
.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-sm); 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: 0 1px 2px rgba(10, 10, 12, .04), 0 12px 30px -18px rgba(10, 10, 12, .16);
  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(--accent) 20%, transparent),
              0 14px 34px -16px rgba(82, 45, 230, .22);
}
.theme-dark .compose-card {
  box-shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 14px 34px -18px rgba(0, 0, 0, .55);
}

/* 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-pill);
}
.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-pill); 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: 0 1px 2px rgba(10, 10, 12, .12);
}
.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-pill);
  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-pill); 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-pill);
}
.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: 0 14px 34px -16px rgba(10, 10, 12, .28);
  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-pill);
  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); }

/* ── 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: 0 2px 6px rgba(10, 10, 12, .14);
}
.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-pill);
  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-pill);
}
.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: 0 6px 20px -8px rgba(20, 15, 10, .12);
}
.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 ─── */
.admin-nav {
  display: flex; gap: 6px; flex-wrap: wrap;
  margin: 0 0 20px; padding-bottom: 12px; border-bottom: 1px solid var(--border);
}
.admin-nav__link {
  font-family: var(--font-body); font-weight: 700; font-size: var(--fs-body-sm);
  color: var(--text-muted); text-decoration: none;
  padding: 8px 14px; border-radius: var(--r-pill);
}
.admin-nav__link:hover { color: var(--text); background: var(--surface-muted); }
.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; gap: 8px; 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-pill);
}
.admin-chip.is-active { color: var(--on-primary); background: var(--accent); border-color: var(--accent); }

/* 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: 10px 8px; border-bottom: 1px solid var(--border); vertical-align: top;
  overflow-wrap: anywhere; /* long unbreakable emails wrap instead of blowing out the layout */
}
.admin-table th { color: var(--text-muted); font-weight: 700; }

.admin-pill {
  display: inline-block; font-size: var(--fs-caption); font-weight: 700;
  padding: 2px 10px; border-radius: var(--r-pill); 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; font-size: var(--fs-caption); font-weight: 700; text-transform: capitalize;
  padding: 2px 10px; border-radius: var(--r-pill); background: var(--surface-muted); color: var(--text);
}
.admin-sev--low    { background: var(--surface-muted); color: var(--text-muted); }
.admin-sev--medium { background: #C77700; color: #fff; }
.admin-sev--high   { background: var(--badge-red); color: #fff; }

/* Colour swatch input (curation accent). Trim the native chrome to a tidy square. */
.admin-swatch {
  width: 44px; height: 32px; padding: 2px; 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; }

.admin-edit-row { padding: 14px 0; border-bottom: 1px solid var(--border); }
.admin-edit-row:last-child { border-bottom: none; }
.admin-edit-row__title { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
.admin-edit-row__inline { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }

/* ── 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-pill); 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-bubble__photo { border-radius: var(--r-md); overflow: hidden; max-width: 280px; background: var(--surface-muted); }
.dm-bubble__photo:not(:last-child) { margin-bottom: 6px; }
.dm-bubble__photo img { display: block; width: 100%; height: auto; }
/* 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: 0 1px 3px rgba(0, 0, 0, .16);
}
.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; }

/* 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-pill);
}
.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-pill);
  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-pill);
  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-pill);
  background: var(--surface-elevated); color: var(--text);
  border: 1px solid var(--border);
  box-shadow: 0 8px 28px rgba(20, 16, 40, 0.18);
  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. Scoped to --compact, so the
   settings avatar + DM uploaders are visually untouched. 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-pill);
  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; }

/* ── 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-pill); 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; }
