/* Small project-specific styles layered over Bootstrap.
   Colour values NEVER appear here — everything consumes the --cx-* tokens from
   tokens.css, so one edit there re-themes both light and dark (spec §3.2). */

/* --------------------------------------------------------------------------
   BREAKPOINT MAP — what governs a given width, without grepping ten blocks:
     max-width: 991.98px      x6  mobile reflow (table->cards, Responders +
                                   Comments + Accounts + the FB review table,
                                   Task 10); bottom-nav body clearance + its
                                   :has() override; the "More filters"
                                   disclosure stacking full-width (spec §9.1);
                                   form-card padding + full-width buttons
                                   (Responders/Accounts forms, Task 9); the
                                   review-screen action row (Task 10).
     max-width: 359.98px      x1  nested inside the above: very narrow
                                   phones, comment-cards grid only.
     max-width: 575.98px      x1  independent: thread-reply indent, small
                                   phones (not part of the table<->cards
                                   system above).
     min-width: 992px         x3  desktop: Comments card look (Task 5),
                                   nav/user chrome, hides the mobile
                                   bottom-nav bar.
     prefers-reduced-motion   x2  badge pulse + reply-flash, switched off.
   1440px -> only min-width:992px blocks apply. 375px -> only the max-width
   blocks above that match, never a min-width:992px one. Three-layer
   convention (tokens.css -> components.css -> this file, pinned by
   tests/test_static_assets.py and base.html's link order) stays; this file
   is NOT split further to make the map shorter. */

/* --- Row emphasis (pre-existing; kept verbatim except comment-removed and
   comment-answered, both retuned for WCAG AA, and comment-message, whose bare
   rule was removed as dead CSS — see :426 below for where its cap now lives)
   ----- */
.comment-new      { font-weight: 600; }        /* unanswered stands out */
/* Composites --cx-ink onto --cx-bg (plain body text, no bg tint — unlike
   .comment-removed below, which sets an explicit color). At .55 this was
   3.33:1 in light — a real AA FAIL (see test_comment_answered_opacity_meets_
   wcag_aa, which proves it fails at .55). .7 clears AA with margin (5.12:1
   light / 9.63:1 dark) while still reading as dimmed. Do NOT lower this for
   aesthetics without re-checking the composited ratio — mirrors exactly what
   was done for .comment-removed. */
.comment-answered { opacity: .7; }             /* answered is dimmed */
.comment-own      { opacity: .7; font-style: italic; }
/* .comment-message never renders outside a `.table-cards`/`.comment-cards`
   table (it is always paired with `.c-comment` on the same <td> in
   dashboard/list.html), so a bare rule here would be dead: the desktop cap is
   `.comment-cards .c-comment { max-width: 75ch }` at :426 BELOW, which is
   (0,2,0) where this class alone is (0,1,0), so it always loses. Under 992px
   nothing caps the message at all (max-width's initial value is `none`). */

/* --- Status badges (spec §6.5) -------------------------------------------
   Tinted background + high-contrast ink, one rule per state. Dark needs NO
   separate rules: the variables re-resolve under [data-bs-theme="dark"]. */
.badge-status {
  display: inline-flex;
  align-items: center;
  gap: .4em;
  padding: .2em .6em;
  border-radius: 999px;
  font-size: .75rem;
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
}
.badge-status.is-answered   { background: var(--cx-answered-bg);   color: var(--cx-answered-ink); }
.badge-status.is-new,
.badge-status.is-retrying   { background: var(--cx-new-bg);        color: var(--cx-new-ink); }
.badge-status.is-processing { background: var(--cx-processing-bg); color: var(--cx-processing-ink); }
.badge-status.is-failed     { background: var(--cx-failed-bg);     color: var(--cx-failed-ink); }
.badge-status.is-own        { background: var(--cx-own-bg);        color: var(--cx-own-ink); }
.badge-status.is-ignored    { background: var(--cx-ignored-bg);    color: var(--cx-ignored-ink); }

/* `processing` is the ONE badge allowed to be louder than the calm tints, and
   `retrying` shares the motion cue: a pulsing dot, so "in flight" survives for a
   colour-blind user (spec §6.3, §6.4). A ::before is right here — it is purely
   decorative and assistive tech ignores pseudo-elements. (The pill's arrow is
   NOT a pseudo-element; that one must carry aria-hidden — spec §9.4.) */
.badge-status.is-processing::before,
.badge-status.is-retrying::before {
  content: "";
  width: .5em;
  height: .5em;
  border-radius: 50%;
  background: currentColor;
  animation: cx-pulse 1.4s ease-in-out infinite;
}
@keyframes cx-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: .35; transform: scale(.7); }
}
@media (prefers-reduced-motion: reduce) {
  .badge-status.is-processing::before,
  .badge-status.is-retrying::before { animation: none; }
}

/* --- Platform chips (spec §6.2) ------------------------------------------ */
.chip {
  display: inline-flex;
  align-items: center;
  gap: .35em;
  padding: .15em .55em;
  border-radius: var(--cx-radius);
  font-size: .75rem;
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
}
.chip-facebook  { background: var(--cx-fb-bg);      color: var(--cx-fb-ink); }
.chip-instagram { background: var(--cx-ig-bg);      color: var(--cx-ig-ink); }
.chip-neutral   { background: var(--cx-ignored-bg); color: var(--cx-ignored-ink); }

/* --- Character counter (spec §6.2) ---------------------------------------
   Replaces Bootstrap's text-warning (~1.5:1 on our cream — effectively
   invisible) and text-danger (~4.2:1). Both states now use validated pairs. */
.char-counter          { color: var(--cx-ink-muted); }
.char-counter.is-warn  { color: var(--cx-new-ink);    font-weight: 600; }
.char-counter.is-over  { color: var(--cx-failed-ink); font-weight: 600; }

/* Inline form/field error text (was `text-danger small`). */
.cx-error-text { color: var(--cx-failed-ink); font-size: .875rem; }

/* --- Retuned legacy colours (spec §6.2, last row) ------------------------ */
/* A removed comment: distinct and readable, never struck through. The opacity
   composites --cx-ignored-ink onto the page, and this class sits on the whole
   comment ROW (body text, not a decorative label), so it is WCAG-checked too
   (see test_status_classes.py). At .7 this was 3.90:1 in light — a real AA
   FAIL. .8 clears AA with margin (5.00:1 light / 6.34:1 dark) while still
   carrying the "no longer live on the platform" fade. Do NOT lower this for
   aesthetics without re-checking the composited ratio. */
.comment-removed { color: var(--cx-ignored-ink); opacity: .8; }

/* DM-sent "M" badge. The old dark variant was white on #08e217 — 1.76:1, one of
   the two real contrast bugs this PBI fixes. Now the validated answered pair. */
.badge-dm-sent {
  background-color: var(--cx-answered-bg);
  color: var(--cx-answered-ink);
  width: 1.5rem;
  height: 1.5rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  font-weight: 700;
  font-size: .75rem;
  line-height: 1;
}

/* Reopen: "make this need attention again" -> the `new` amber, not lime. */
.btn-reopen {
  --bs-btn-color: var(--cx-new-ink);
  --bs-btn-bg: var(--cx-new-bg);
  --bs-btn-border-color: var(--cx-new-bg);
  --bs-btn-hover-color: var(--cx-new-ink);
  --bs-btn-hover-bg: var(--cx-new-bg);
  --bs-btn-hover-border-color: var(--cx-new-ink);
  --bs-btn-active-color: var(--cx-new-ink);
  --bs-btn-active-bg: var(--cx-new-bg);
  --bs-btn-active-border-color: var(--cx-new-ink);
  --bs-btn-focus-shadow-rgb: var(--cx-brand-rgb);
}

/* --- The "new threads" pill (spec §9.4) -----------------------------------
   AMBER, not coral: it means "new content arrived for you", which is the `new`
   semantic, and it rhymes with the amber `new` badges on the very comments it
   loads. Coral stays reserved for primary actions; clickability comes from
   shape + arrow + hover lift.
   It must NOT reuse .badge-status/is-new: that class also renders on an actual
   comment row, and this pill's markup is always present in the DOM (just
   hidden via the parent's d-none), so a shared class would make "is this
   comment's badge is-new" checks true on every page load regardless of any
   comment's real status. Was `badge text-bg-primary` pre-PBI-17. */
/* `display: flex` here has to lose to Bootstrap's `.d-none` when the pill is hidden,
   and it does: `.d-none` is itself `display: none !important`, which outranks any
   ordinary declaration whatever its selector. An explicit `#newCommentsPill.d-none`
   override used to sit here restating that; it changed nothing in any state and cost a
   reader a specificity analysis to confirm as much. */
#newCommentsPill { display: flex; justify-content: center; }
.pill-new {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  min-height: 2.5rem;                       /* a real ~40px tap target */
  padding: .5rem .9rem;
  border-radius: 999px;
  font-weight: 600;
  text-align: left;
  line-height: 1.25;
  overflow-wrap: anywhere;
  text-decoration: none;
  cursor: pointer;
  background: var(--cx-new-bg);
  color: var(--cx-new-ink);
  box-shadow: 0 2px 8px rgba(var(--cx-ink-rgb), .18);
  transition: transform .1s ease, box-shadow .1s ease;
}
.pill-new:hover  { transform: translateY(-1px); color: var(--cx-new-ink); }
.pill-new:active { transform: translateY(0); }
.pill-new:focus-visible { outline: 2px solid var(--cx-new-ink); outline-offset: 2px; }
[data-bs-theme="dark"] .pill-new {
  /* The second layer is byte-identical to --cx-shadow in dark; naming the
     token (not the literal) means retuning it there reaches the pill too —
     this file's own header says colour values never appear here. */
  box-shadow: 0 0 0 1px var(--cx-outline), var(--cx-shadow);
}

/* --------------------------------------------------------------------------
   PBI 11 - Mobile view. EVERYTHING below applies ONLY below the `lg` breakpoint
   (< 992px). At desktop widths these rules do not apply, so desktop rendering is
   unchanged. Spec: docs/specs/2026-07-11-pbi-11-mobile-view-design.md
   -------------------------------------------------------------------------- */
@media (max-width: 991.98px) {

  /* Table -> cards: full block-context reflow of any `.table-cards` table. */
  .table-cards        { display: block; width: 100%; }
  .table-cards thead  { display: none; }              /* header redundant per card */
  .table-cards tbody  { display: block; }

  /* Only DATA rows (`.card-row`) become cards; empty-state / future group rows
     stay as plain blocks so their colspan message still reads normally. */
  .table-cards tbody tr.card-row {
    display: flex;
    flex-direction: column;
    width: 100%;
    border: 1px solid var(--cx-card-border);
    border-radius: var(--cx-radius-lg);
    padding: .5rem .75rem;
    margin-bottom: .75rem;
    background: var(--cx-surface-lowest);
    box-shadow: var(--cx-shadow);
  }
  .table-cards tbody td                   { display: block; width: 100%; border: 0; padding: .15rem 0; }
  .table-cards tbody tr:not(.card-row)    { display: block; }
  .table-cards tbody tr:not(.card-row) td { display: block; }

  /* Long URLs / unbroken tokens wrap instead of overflowing the card. */
  .table-cards td, .table-cards a { overflow-wrap: anywhere; }
  /* Field label for compact metadata cells only (data-label="..."). */
  .table-cards td[data-label]::before {
    content: attr(data-label) ": ";
    font-weight: 600;
    color: var(--bs-secondary-color);
  }

  /* Comfortable tap-target height inside cards. min-height wins even against
     Bootstrap's `.py-0` !important padding, so no padding fight is needed. */
  .table-cards .btn-sm { min-height: 2.25rem; }
  /* .cx-toggle carries no .btn class at all, so it misses the bump above —
     it is the only control in the Responders card that would otherwise stay
     at its bare 1.75rem. The `.table-cards` prefix is load-bearing beyond
     scoping: this block sits EARLIER in the file than the bare `.cx-toggle
     { min-height: 1.75rem }` rule below, so an unprefixed `.cx-toggle` here
     would tie on specificity (0,1,0) and LOSE on source order. `.table-cards
     .cx-toggle` is (0,2,0) and wins regardless of where either rule sits —
     do not "simplify" this prefix away. */
  .table-cards .cx-toggle { min-height: 2.25rem; }

  /* Comments card cell order. DOM order is Status, Platform, Account, Commenter,
     Comment, Post, Received, Actions; these reorder it visually on mobile. */
  .table-cards .c-received  { order: 1; color: var(--bs-secondary-color); font-size: .85rem; }
  .table-cards .c-status    { order: 2; }
  .table-cards .c-commenter { order: 3; font-weight: 600; }
  .table-cards .c-comment   { order: 4; }
  .table-cards .c-platform  { order: 5; }
  .table-cards .c-account   { order: 6; }
  .table-cards .c-post      { order: 7; }
  .table-cards .c-actions   { order: 8; }

  /* Comments actions: beat Bootstrap `d-inline` (!important) so the Answered /
     Ignore / Reopen buttons stack full-width for thumb use. */
  .table-cards .c-actions form { display: block !important; }
  .table-cards .c-actions .btn { width: 100%; min-height: 2.5rem; margin-top: .35rem; }

  /* Responders card cell order. DOM order is Order-arrows, Platform, Keyword,
     DM, Replies, Used, Active, Actions. The three control cells (arrows / on-off
     toggle / edit-delete) each become their own line; buttons stay INLINE within
     a cell (so the up/down arrows sit side by side), tall via `.btn-sm` above. */
  .table-cards .r-keyword  { order: 1; font-weight: 600; }
  .table-cards .r-platform { order: 2; }
  .table-cards .r-dm       { order: 3; }
  .table-cards .r-replies  { order: 4; }
  .table-cards .r-used     { order: 5; }
  .table-cards .r-order    { order: 6; }
  .table-cards .r-active   { order: 7; }
  .table-cards .r-controls { order: 8; }

  /* Responders: relax desktop truncation / nowrap so the full DM text and all
     controls show and wrap. (Bootstrap `text-truncate`/`text-nowrap` are
     !important, hence the !important overrides here.) */
  .table-cards .r-dm {
    max-width: none !important; white-space: normal !important;
    overflow: visible !important; text-overflow: clip !important;
  }
  .table-cards .r-order, .table-cards .r-controls { white-space: normal !important; }

  /* Accounts card cell order (spec §8.2 — this supersedes PBI 11's decision to
     leave Accounts as a scrolling table). */
  .table-cards .a-name     { order: 1; font-weight: 600; }
  .table-cards .a-platform { order: 2; }
  .table-cards .a-health   { order: 3; }
  .table-cards .a-event    { order: 4; }
  .table-cards .a-expiry   { order: 5; }
  .table-cards .a-test     { order: 6; }
  .table-cards .a-controls { order: 7; white-space: normal !important; }
  .table-cards .a-controls .btn,
  .table-cards .a-controls form { display: block !important; width: 100%; }
  .table-cards .a-controls .btn { width: 100%; margin-top: .35rem; }

  /* Facebook review card: the two checkboxes read as choices, the name leads. */
  .table-cards .p-name    { order: 1; font-weight: 600; }
  .table-cards .p-status  { order: 2; }
  .table-cards .p-monitor { order: 3; margin-top: .35rem; }
  .table-cards .p-test    { order: 4; }
  .table-cards.review-cards .form-check-input { width: 1.35rem; height: 1.35rem; }

  /* Comments filter panel (Task 5): full-width controls when open on mobile.
     No .btn left in .comment-filters since Task 7 removed the Filter button
     (spec §9.1) — the selector list below is exhaustive again. */
  .comment-filters .form-select,
  .comment-filters .form-control { width: 100%; }

  /* Pagination wraps instead of overflowing on narrow screens. */
  .pagination { flex-wrap: wrap; gap: .25rem; }

  /* Comments card: a 3-column grid (scoped to `.comment-cards`, so the
     Responders card keeps its plain vertical stack) that puts three items on
     the top line - time (left), platform badge (centre), account (right) - and
     From/status on the next line. Higher specificity than the base flex rule so
     it wins; named grid-areas make the placement explicit and readable. */
  .table-cards.comment-cards tbody tr.card-row {
    display: grid;
    grid-template-columns: auto 1fr auto;
    grid-template-areas:
      "time    platform account"
      "from    from     status"
      "comment comment  comment"
      "post    post     post"
      "actions actions  actions";
    column-gap: .5rem;
    align-items: baseline;
  }
  /* Grid tracks control width now; drop the base full-width block behaviour and
     let cells shrink (min-width:0) instead of overflowing on long content. */
  .table-cards.comment-cards tbody td { width: auto; min-width: 0; }

  .comment-cards .c-received  { grid-area: time; }
  .comment-cards .c-platform  { grid-area: platform; justify-self: center; }
  .comment-cards .c-account   { grid-area: account; justify-self: end; text-align: right; }
  .comment-cards .c-commenter { grid-area: from; }
  .comment-cards .c-status    { grid-area: status; justify-self: end; text-align: right; }
  /* Below 992px the badge needs NO rule of its own — the absence is what places
     it, not an oversight (same reasoning as the desktop `.c-comment` cap comment
     further down in this file, which deletes a rule "as dead CSS precisely
     because it could never win"). `.cx-dm-line` is a flex item of `.c-status`
     at EVERY width (unconditional, further down), but the full-row `flex: 0 0
     100%` that gives it a line of its own is scoped to the >=992px block.
     Without that basis here the badge simply shares the wrapped mobile line
     with the `details` link, landing AFTER it because `order: 1` — also
     unconditional — puts it last (PBI 20 §2.5/§2.7). Mobile has no free space
     under the status area for a dedicated line anyway: `comment` sits directly
     below it, spanning all three columns. This also covers the <360px variant
     further down, where `status` keeps its own row next to `from`.
     A real behavioural consequence, invisible to any height check: this used to
     be `.cx-dm-line { display: inline-block; margin: 0 0 0 .35rem; vertical-
     align: middle; }`, deleted here because the flex conversion made it
     entirely inert — `display: inline-block` blockifies to `block` on a flex
     item (no effect), `vertical-align` does nothing on a flex item and does
     not inherit (no effect anywhere), and `margin: 0` overrode nothing. The
     badge's vertical alignment on mobile is now the flex container's
     `align-items: baseline` instead of that dead `vertical-align: middle` — a
     POSITION change, not a height change, so no card-height measurement would
     ever have caught it. */
  .comment-cards .c-comment   { grid-area: comment; padding: .45rem 0; }  /* subtle space above/below the comment text */
  .comment-cards .c-post      { grid-area: post; }
  .comment-cards .c-actions   { grid-area: actions; }

  /* The zone abbreviation (e.g. "UTC"/"CEST") is desktop-only noise on the
     compact mobile time line; hide it here (still shown on the desktop table). */
  .c-received .c-tz { display: none; }
}

/* Desktop counterpart to the Facebook review card rules above. The per-row Monitor /
   Test labels are visual noise at >=992px — there the column headers carry the meaning
   — but each is also its checkbox's ONLY accessible name, so they must be hidden from
   the EYE and not from the accessibility tree. `d-lg-none` (display: none) removed them
   from both: on the one screen that decides which pages get ingested, a screen reader
   heard "checkbox, unchecked" twice per row with nothing to tell Monitor from Test.
   Bootstrap ships `.visually-hidden` but no responsive variant of it, hence a class
   here rather than a utility. No !important: app.css already loads last, and
   `.form-check-label` sets none of these properties. */
@media (min-width: 992px) {
  .cx-lg-visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
}

/* Very narrow screens (small phones / long account names): the top line can't
   hold time + badge + account comfortably, so drop the platform badge to its
   own line between the top line and the From/status line. Separate media query
   because plain CSS can't nest one @media inside another. */
@media (max-width: 359.98px) {
  .table-cards.comment-cards tbody tr.card-row {
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "time     account"
      "platform platform"
      "from     status"
      "comment  comment"
      "post     post"
      "actions  actions";
  }
}

/* --- Comments: the card look at DESKTOP widths too (spec §8.1) ------------
   PBI 17 keeps the <table> and pushes the card look through CSS; the real
   DOM->cards conversion is a follow-up PBI (Asana 1216853205381012). Scoped to
   `.comment-cards` on purpose: `.table-cards` is shared with Responders, whose
   accepted desktop mockup keeps a real table.
   Documented compromise: the <thead> is hidden here, so desktop loses its column
   headers — that is inherent to making rows read as cards. */
@media (min-width: 992px) {
  .comment-cards        { display: block; width: 100%; }
  .comment-cards thead  { display: none; }
  .comment-cards tbody  { display: block; }

  .comment-cards tbody tr.card-row {
    display: grid;
    /* 11rem is the actions column: wide enough for the widest stacked action
       button label (Answered/Ignore/Reopen/Use responder) without wrapping;
       narrower and the column-fixed `actions` grid-area starts clipping or
       wrapping the longest label — widen this first if a future label is
       longer. */
    grid-template-columns: auto auto 1fr auto 11rem;
    grid-template-areas:
      "platform commenter received status  actions"
      "comment  comment   comment  status  actions"
      "account  post      .        status  actions";
    column-gap: .75rem;
    row-gap: .25rem;
    align-items: start;
    padding: 1rem 1.25rem;
    margin-bottom: .75rem;
    border: 1px solid var(--cx-card-border);
    border-radius: var(--cx-radius-lg);
    background: var(--cx-surface-lowest);
    box-shadow: var(--cx-shadow);
  }
  .comment-cards tbody td { display: block; width: auto; min-width: 0; border: 0; padding: 0; }
  .comment-cards tbody tr:not(.card-row)    { display: block; }
  /* The line above zeroes padding for EVERY td, including the empty-state row
     (`<tr><td colspan="8">No comments match.</td></tr>`, which is not a
     `.card-row`) — that row has no `c-*` cell to restore its own spacing, so
     without this it renders flush against the container edge at >=992px,
     invisible to any browser check on a populated list. This selector is
     more specific (0,2,3 vs 0,1,2) and wins back some of it. */
  .comment-cards tbody tr:not(.card-row) td { display: block; padding: .5rem 0; }

  .comment-cards .c-platform  { grid-area: platform; }
  .comment-cards .c-commenter { grid-area: commenter; font-weight: 600; }
  .comment-cards .c-received  { grid-area: received; color: var(--cx-ink-muted); font-size: .875rem; }
  .comment-cards .c-status    { grid-area: status; justify-self: end; }
  /* The DM "M" badge on its own line UNDER the pill (PBI 20 §2.4). The three-row
     `status` span above is what makes that possible and is not optional: a <td> is
     the grid ITEM, so its children cannot be placed into the row's grid — appending
     a second line to a one-row-tall cell grows row 1 and the whole card with it.
     Measured, the span version leaves every card height unchanged and REMOVES an
     existing 30px unevenness (a DM badge used to make a three-button card taller
     than the same card without one). The cost is that `comment` no longer spans
     column 4: 69px of text width at a 1065px viewport, 0px above ~1200px where the
     75ch cap binds first. `.c-status` needs no align-self — the row's
     `align-items: start` already keeps the pill at the top of the taller cell.
     `flex: 0 0 100%` is what forces the badge onto a line of its own inside the
     flex row declared further down; `order: 1` there keeps it below the
     <details> link rather than above it. `display: block` here is redundant
     WHILE the patch span's `display: contents` is in force — but keep it, do
     not prune it as dead code: it is the correct fallback layout if that rule
     is ever removed, and `flex: 0 0 100%` (not this) is what actually does
     the work of putting the badge on its own line today. */
  .comment-cards .cx-dm-line { flex: 0 0 100%; display: block; text-align: right; margin-top: .35rem; }
  /* `details` takes a row of its own directly under the status badges, and the
     DM badge (order: 1) drops to the row below it. Andy's call on reviewing the
     rendered result: the link explains the auto-reply badge above it, so it
     reads as belonging to that badge rather than to the M circle.
     Measured at 1050px and 1450px: the card stays 132px — the three-row `status`
     span from §2.4 already has the room, so this costs no height. Deliberately
     NOT applied below 992px: there the status area already stacks, `details` is
     already on the row under the badges, and forcing a fourth row costs ~27px on
     every card carrying an auto-reply error, DM badge or not. */
  .comment-cards .c-status details { flex: 0 0 100%; text-align: right; margin-top: .35rem; }
  /* The desktop cap lives HERE, not on the bare `.comment-message` class near the
     top of this file: that rule was deleted as dead CSS precisely because it could
     never win — it is (0,1,0) where this selector is (0,2,0), so this one always
     applies on the real page (measured 978px / ~120 characters when this said
     `none`). 75ch is "readable measure, not a table column" (spec §8.1 intent);
     do not move it back to a bare class rule, it cannot win from there. */
  .comment-cards .c-comment   { grid-area: comment; padding-block: .5rem; max-width: 75ch; }
  .comment-cards .c-account   { grid-area: account; color: var(--cx-ink-muted); font-size: .875rem; }
  .comment-cards .c-post      { grid-area: post; font-size: .875rem; }
  /* The gap has to be on the SPAN as well as the cell. The cell's only child is
     `<span data-testid="actions-N">` — B2's and the poll's patch target — and the
     Answered/Ignore form and the "Use responder" link are siblings INSIDE it. So a
     gap on the cell alone spaces nothing (one child), the form's own gap spaces
     only Answered-from-Ignore, and the form sits flush against the link: measured
     5px then 0px. Andy caught it in Polish, where "Użyj automatycznej odpowiedzi"
     wraps to two lines and the touch is obvious; English had the same 0px, just
     harder to see. Do not remove the span to "simplify" — it is the patch target. */
  .comment-cards .c-actions,
  .comment-cards .c-actions > span { display: flex; flex-direction: column; gap: .35rem; align-items: stretch; }
  .comment-cards .c-actions   { grid-area: actions; }
  .comment-cards .c-actions form { display: flex !important; flex-direction: column; gap: .35rem; }

  /* Pull the OPEN replies panel up to meet its card, on `.collapse.show` +
     `.collapsing` — never the always-present <tr> itself (that reintroduces
     the -8px live-app overlap bug; see
     test_thread_replies_row_pull_up_lives_on_the_collapse_descendant).
     `.collapsing` is Bootstrap's transient mid-transition class (~0.35s, not
     display:none) — `.collapse.show` alone leaves the panel detached for it. */
  .comment-cards tbody tr.thread-replies-row .collapse.show,
  .comment-cards tbody tr.thread-replies-row .collapsing {
    display: block;
    margin: -.75rem 0 .75rem;
    background: var(--cx-surface-low);
    border-radius: 0 0 var(--cx-radius-lg) var(--cx-radius-lg);
  }
}

/* --- Comments (spec §8.1), continued — two rules that outlive the single
   `@media` block above but still belong to this section, not a new one. -----
   `.comment-ignored`'s dimming is a genuine BOTH-WIDTHS rule: neither the
   mobile nor the desktop block sets an ignored-row opacity anywhere else, so
   this is the single source of truth at every width, always.
   `.card-row`'s background line right above it is NOT load-bearing the same
   way — both media blocks already set this identical value — it is belt-and-
   braces for the ~0.02px sliver between `max-width: 991.98px` and `min-width:
   992px` that neither query covers. Real viewports never land there; this
   just costs nothing to close.
   `.comment-removed` is deliberately NOT paired into the opacity rule below
   (a DEVIATION from the plan's literal Step 5 text, which paired it in at
   .6): `.comment-cards tbody tr.comment-removed` is MORE SPECIFIC than the
   plain, WCAG-vetted `.comment-removed { opacity: .8; }` above (5.00:1 light
   / 6.34:1 dark), so pairing it in here would silently WIN the cascade on the
   real table and drop removed rows to opacity .6 — 3.06:1 light / 4.04:1
   dark, a real AA regression the old per-class opacity tests could not have
   caught (they matched only the exact `.comment-removed {` selector, not
   this scoped one). `.comment-ignored` had no opacity rule before Task 5, so
   giving it one here is a genuine addition, not an override; .7 (not the
   plan's .6) because ignored rows are plain --cx-ink body text with no bg
   tint — .6 composites to an AA-failing 3.84:1 light, .7 clears 4.5:1 in
   both themes (see test_dimmed_row_opacity_meets_wcag_aa). */
.comment-cards tbody tr.card-row { background: var(--cx-surface-lowest); }
.comment-cards tbody tr.comment-ignored { opacity: .7; }

/* The status cell lays out its own contents (PBI 20 §2.4/§2.5, revised after
   Andy reviewed the rendered result). A flex row, not plain inline flow,
   because of TWO things that are not obvious from reading the markup:

   1. ORDER. The error <details> link explains the auto-reply badge rendered
      just before it, so it must sit immediately after that badge — and it
      CANNOT be moved next to it in the DOM. <details> is deliberately OUTSIDE
      <span data-testid="status-N"> (list.html), because that span is the
      live-poll patch target and re-rendering it would collapse an error the
      user had expanded. The DM badge is deliberately INSIDE it, so a responder
      completion gains its badge live. That fixes DOM order as
      [pill … M][details], with the badge wedged between the two things that
      belong together. `order: 1` on .cx-dm-line moves the badge last VISUALLY
      without moving either element in the DOM.
   2. `display: contents` on the patch span is what makes that possible at all.
      `order` applies only to flex ITEMS — direct children — and the pill and
      the badge sit one level deeper, inside that span. Dropping the span's own
      box promotes its children into this flex row. Layout-only: the span keeps
      its identity in the DOM, so the live patch still finds and replaces it.

   `gap` replaces the literal spaces between the badges: a flex container drops
   whitespace-only text nodes, so without it the pill and the badges would
   collide. */
.comment-cards .c-status {
  display: flex; flex-wrap: wrap; justify-content: flex-end;
  align-items: baseline; gap: .25rem;
}
.comment-cards .c-status > [data-testid^="status-"] { display: contents; }
.comment-cards .cx-dm-line { order: 1; }

/* PBI 15: wildcard help "ⓘ" trigger — a subtle inline info affordance next to
   the wildcard checkbox label; link-coloured, no underline, sized to the text. */
.wildcard-info { font-size: 1rem; line-height: 1; text-decoration: none; }
.wildcard-info:hover,
.wildcard-info:focus { text-decoration: none; }

/* --- PBI 12.1: threaded comments (spec §6.1) ---------------------------------
   Indentation + connector lines go down to level 3 and stop.

   Bootstrap utility classes carry !important and silently beat plain custom CSS —
   this bit PBI 11 three separate times (CLAUDE_NOTES.md). These rules therefore
   own properties no utility on these elements controls (border-left, margin-left
   via a custom class), rather than fighting a spacing utility. */
.thread-replies {
  padding: 0.5rem 0.75rem;
}
.thread-reply {
  position: relative;
  padding: 0.4rem 0 0.4rem 0.75rem;
  border-left: 2px solid var(--bs-border-color);
}
.thread-level-2 { margin-left: 1rem; }
.thread-level-3 { margin-left: 2.5rem; }
/* The connector elbow: a short horizontal stub off the vertical line. */
.thread-reply::before {
  content: "";
  position: absolute;
  left: 0;
  top: 1rem;
  width: 0.6rem;
  border-top: 2px solid var(--bs-border-color);
}
.thread-reply-body { white-space: pre-wrap; }
.thread-reply-actions { margin-top: 0.25rem; }

/* A reply the text search matched: highlighted, never merely revealed by the open
   fold (spec §6.3). border-left is already owned by .thread-reply above, so this
   overrides a plain custom rule rather than a Bootstrap utility — no !important
   fight here. A fully opaque --cx-new-bg fill (was an 8%-alpha warning wash
   pre-PBI-17) — plan-specified and contrast-safe. */
.thread-reply-match {
  border-left-color: var(--cx-new-ink);
  background-color: var(--cx-new-bg);
}

/* Mobile: the card reflow already stacks rows, so deep indentation would eat the
   viewport. Keep the tiers visually distinct but cheap. */
@media (max-width: 575.98px) {
  .thread-level-2 { margin-left: 0.5rem; }
  .thread-level-3 { margin-left: 1.25rem; }
}

/* PBI 12.2: one-time flash on a live-injected reply (spec §9.1). Animates
   background-color, which no Bootstrap utility sets on .thread-reply, so it wins;
   verify in the browser. The class is removed on animationend (JS). */
@keyframes reply-flash {
  from { background-color: var(--cx-new-bg); }
  to   { background-color: transparent; }
}
.thread-reply-flash { animation: reply-flash 2s ease-out 1; }
@media (prefers-reduced-motion: reduce) {
  .thread-reply-flash { animation: none; }
}

/* --- Shell & chrome (spec §7) --------------------------------------------- */
/* Real sticky-footer layout, not the `.cx-main { min-height: 60vh }` magic
   number it replaces: that guessed a viewport fraction tall enough to push the
   footer down on most screens, but on the shortest page in the app — login,
   one card and nothing else — it still fell short, leaving bare --cx-bg
   between the footer and the viewport edge (Task 11 review). `body` as the
   flex column and `.cx-main { flex: 1 }` below push the footer to the true
   bottom of the viewport (or lower, if content overflows it) on every page,
   with no guess involved.
   Compatible with the fixed `.cx-bottomnav` (Task 11 review): position:fixed
   takes it out of the document flow entirely, so it is not a flex item and
   does not affect body's column sizing — it keeps painting pinned to the
   viewport, unaffected by body becoming a flex container. The bottom-nav
   clearance below (`body { padding-bottom: ... }`) still applies: a flex
   container's padding still reserves space after its last item exactly as it
   would for a block container.
   The two `min-height` declarations are a progressive pair, not a duplicate:
   `vh` resolves against the LARGE viewport (browser chrome collapsed), so on
   a mobile browser with a dynamic toolbar `100vh` overshoots the visible
   area — with the URL bar showing, a one-card page like login would gain
   ~60-90px of otherwise-empty scroll it never had before this fix. `svh` (the
   SMALL viewport, chrome always shown) is the correct target and wins in any
   engine that understands it; older engines that don't just keep the `vh`
   line above it. */
body {
  background: var(--cx-bg);
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}
html { scroll-padding-top: 4.5rem; }   /* the sticky header is 4rem + breathing room */

.cx-shell {
  width: 100%;
  max-width: var(--cx-shell-max);
  margin-inline: auto;
  padding-inline: 1rem;
}
.cx-main { padding-block: 1.5rem; flex: 1; }

.cx-header {
  background: var(--cx-surface-low);
  box-shadow: var(--cx-shadow);
  /* tokens.css's convention: light gets the soft shadow above (visible on
     cream) and NO structural border; dark gets a 1px hairline instead, because
     that same shadow is only rgba(0,0,0,.5) on a near-black page — measured
     1.039:1 surface-vs-page, i.e. no visible edge at all without this.
     --cx-card-border is transparent in light, so light is untouched. */
  border-bottom: 1px solid var(--cx-card-border);
  position: sticky;
  top: 0;
  z-index: 1030;
}
.cx-header-inner {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  min-height: 4rem;
}
.cx-brand {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
  font-size: 1.5rem;
  font-weight: 700;
  /* 2.40:1 in light — below AA's 4.5:1 for text, deliberately unfixed. WCAG
     1.4.3 exempts "text that is part of a logo or brand name", and this is the
     approved brand wordmark colour, always paired with the `<img>` mark beside
     it. Measured (headless Chromium, composited): 2.40:1 light / high in dark. */
  color: var(--cx-brand);
  text-decoration: none;
  letter-spacing: -.01em;
}
.cx-brand-mark { border-radius: .35rem; }
.cx-nav { display: none; gap: 1.5rem; align-self: stretch; align-items: center; }
.cx-nav-link {
  color: var(--cx-ink-muted);
  font-weight: 500;
  text-decoration: none;
  padding-block: .25rem;
  border-bottom: 2px solid transparent;
}
.cx-nav-link:hover { color: var(--cx-ink); }
.cx-nav-link.is-active {
  /* The coral is the INDICATOR (underline), not the label. Brand-on-surface-low
     measures 2.40:1 in light — below AA, and it made the active item LESS legible
     than the inactive ones, inverting the hierarchy. Ink + weight + a coral
     underline is the same pattern .badge-status uses: tinted surface, ink text.
     Measured after this change: 11.8:1 light, high in dark. */
  color: var(--cx-ink);
  font-weight: 700;
  border-bottom-color: var(--cx-brand);
}
.cx-header-actions {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin-left: auto;
}
.cx-user { display: none; color: var(--cx-ink-muted); font-size: .875rem; }
/* Every inline SVG emitted by _icons.html carries .cx-icon. Without a base rule they
   inherit `vertical-align: baseline`, which only shows up where the icon is NOT inside
   a flex container: the order arrows in responders/list.html and the back link in
   use_responder.html then sit on the text baseline, riding high with the font's
   descender space left below them. Nearly every other icon site happens to be flex
   (.cx-icon-btn, .cx-tab, .cx-empty, .cx-search, summary), which is exactly why the
   gap was easy to miss. `flex: none` stops a long neighbouring label squashing the
   icon in those flex containers. */
.cx-icon { vertical-align: middle; flex: none; }
.cx-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;                 /* ~44px tap target (design-tokens.md) */
  height: 2.5rem;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--cx-ink-muted);
  text-decoration: none;
}
/* Scoped to the real interactive elements only: the onboarding header's
   centring spacer reuses .cx-icon-btn's box for layout (`<span class="cx-icon-
   btn" aria-hidden="true">`) but is not clickable, and an unscoped :hover would
   still light up a 40px circle over it on mouseover. */
a.cx-icon-btn:hover, button.cx-icon-btn:hover {
  background: var(--cx-surface-hover);
  color: var(--cx-ink);
}
.cx-icon-btn:focus-visible {
  /* --cx-brand is 2.40:1 on the header surface — under WCAG 1.4.11's 3:1 floor
     for a non-text focus indicator. --cx-ink clears it in both themes. */
  outline: 2px solid var(--cx-ink);
  outline-offset: 2px;
}

/* The onboarding variant: brand + back affordance, no tabs (spec §7.3). */
.cx-header-onboarding .cx-header-inner { justify-content: space-between; }
.cx-header-onboarding .cx-brand { margin-inline: auto; }

.cx-footer {
  border-top: 1px solid var(--cx-outline);
  padding-block: 1rem;
  margin-top: 2rem;
  color: var(--cx-ink-muted);
}

@media (min-width: 992px) {
  .cx-nav { display: flex; }
  .cx-user { display: inline; }
}

/* --- Mobile bottom tab bar (spec §7.2) ------------------------------------
   Replaces the hamburger navbar below `lg`. Fixed, safe-area aware, and the
   body gets matching bottom padding so the last list item is never trapped
   behind it. */
.cx-bottomnav {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1040;
  display: flex;
  justify-content: space-around;
  align-items: stretch;
  gap: .25rem;
  padding: .5rem .5rem calc(.5rem + env(safe-area-inset-bottom));
  background: var(--cx-surface-low);
  border-top: 1px solid var(--cx-card-border);
  border-radius: var(--cx-radius-lg) var(--cx-radius-lg) 0 0;
  box-shadow: 0 -2px 12px rgba(var(--cx-ink-rgb), .10);
}
.cx-tab {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .15rem;
  min-height: 3rem;
  padding: .25rem;
  border-radius: 999px;
  color: var(--cx-ink-muted);
  font-size: .75rem;
  font-weight: 500;
  text-decoration: none;
}
.cx-tab.is-active {
  /* Same reasoning as .cx-nav-link.is-active: the coral tint is the indicator,
     the label takes the ink. Was 2.16:1 in light on the composited tint. */
  color: var(--cx-ink);
  font-weight: 700;
  background: rgba(var(--cx-brand-rgb), .12);
}
/* --cx-brand is 2.40:1 on this surface — under WCAG 1.4.11's 3:1 floor for a
   non-text focus indicator. --cx-ink clears it in both themes. */
.cx-tab:focus-visible { outline: 2px solid var(--cx-ink); outline-offset: -2px; }

@media (min-width: 992px) {
  .cx-bottomnav { display: none; }
}
@media (max-width: 991.98px) {
  /* Room for the bar itself + the iPhone home indicator. */
  body { padding-bottom: calc(5rem + env(safe-area-inset-bottom)); }
}
/* Screens that render no bottom bar (login, the OAuth review screens) do not need
   the clearance above and would otherwise show ~80px of dead space under the
   footer on mobile. Guarded by @supports on purpose: if :has() is unavailable
   the unconditional rule above still stands, so the failure mode is a little
   dead space, never content trapped behind a bar that IS there. */
@supports selector(:has(*)) {
  @media (max-width: 991.98px) {
    body:not(:has(.cx-bottomnav)) { padding-bottom: 0; }
  }
}

/* --- Comments filter bar (spec §9.1) --------------------------------------
   Radius: standardised on --cx-radius (Bootstrap's own --bs-border-radius
   default) everywhere in this bar, not a one-off 999px pill. The prefs
   "Newest thread" select right below and the two date inputs inside the
   disclosure never had an override and already render at --cx-radius; a pill
   only on platform/account/status/search would be a THIRD radius convention
   on one screen for no reason (quality review). */
.cx-search { position: relative; display: flex; align-items: center; }
.cx-search .cx-icon {
  position: absolute;
  left: .6rem;
  color: var(--cx-ink-muted);
  pointer-events: none;
}
.cx-search .form-control { padding-left: 2rem; }

.cx-more-filters > summary {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .25rem .75rem;
  min-height: 2rem;
  /* --cx-brand is 2.54:1 on the page in light — under WCAG 1.4.11's 3:1 floor
     for a non-text UI component boundary, and visibly fainter than the
     --cx-outline-strong (4.17:1) border every .form-select next to it uses
     (--bs-border-color, tokens.css). Match it instead of introducing a
     second, weaker border colour on the same row. */
  border: 1px solid var(--cx-outline-strong);
  border-radius: var(--cx-radius);
  color: var(--cx-ink);
  font-size: .875rem;
  cursor: pointer;
  list-style: none;                 /* hide the default disclosure triangle */
}
.cx-more-filters > summary::-webkit-details-marker { display: none; }
.cx-more-filters > summary:hover { background: var(--cx-surface-hover); }
.cx-more-filters[open] > summary { background: rgba(var(--cx-brand-rgb), .12); }
.cx-more-filters > summary:focus-visible {
  /* Same reasoning as .cx-icon-btn:focus-visible / .cx-tab:focus-visible:
     --cx-brand fails WCAG 1.4.11 as a non-text indicator; --cx-ink clears it
     in both themes. This is a native <summary>, so the UA default ring would
     otherwise be whatever the browser ships. */
  outline: 2px solid var(--cx-ink);
  outline-offset: 2px;
}
.cx-more-filters-body {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin-top: .5rem;
  padding: .75rem;
  background: var(--cx-surface-low);
  border-radius: var(--cx-radius);
}
@media (max-width: 991.98px) {
  /* .form-control at 100% width is already covered by the identical rule in
     the "Comments filter panel (Task 5)" media block above at the same
     specificity — nothing extra needed for the date inputs in here. */
  .cx-more-filters, .cx-more-filters > summary { width: 100%; justify-content: center; }
}

/* --- Responders ------------------------------------------------------------
   A state switch, not an action button: pill-shaped, two states, same footprint
   in both so the row does not jump when it flips (spec §6.2). */
.cx-toggle {
  min-width: 3rem;
  min-height: 1.75rem;
  padding: .15rem .7rem;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: .8125rem;
  font-weight: 700;
  line-height: 1.4;
  cursor: pointer;
}
.cx-toggle.is-on  { background: var(--cx-answered-bg); color: var(--cx-answered-ink); }
.cx-toggle.is-off { background: var(--cx-ignored-bg);  color: var(--cx-ignored-ink); }
/* The pill's own border is already reserved (transparent, above) — revealing
   it in the pill's own ink is a hover cue that works in both themes without a
   new token and does not change the footprint. A filter: brightness() cue
   (the earlier draft) darkens an already-dark pill in dark theme — backwards. */
.cx-toggle:hover  { border-color: currentColor; }
/* Same reasoning as .cx-icon-btn:focus-visible / .cx-tab:focus-visible /
   .cx-more-filters > summary:focus-visible: --cx-brand is under WCAG 1.4.11's
   3:1 floor for a non-text indicator (2.73:1 on the card face in light);
   --cx-ink clears it in both themes. Pinned by
   test_chrome.py::test_chrome_focus_outlines_meet_wcag_aa_in_both_themes. */
.cx-toggle:focus-visible { outline: 2px solid var(--cx-ink); outline-offset: 2px; }

.cx-flip .cx-icon { transform: rotate(180deg); }

/* --- Form sheets ------------------------------------------------------------
   Shared by responders/form.html and accounts/form.html so both read as one
   sheet on a surface, not a bare set of inputs (Task 9). */
.cx-form-card {
  max-width: 44rem;
  padding: 1.5rem;
  background: var(--cx-surface-lowest);
  border: 1px solid var(--cx-card-border);
  border-radius: var(--cx-radius-lg);
  box-shadow: var(--cx-shadow);
}
.cx-form-card .form-label { font-weight: 600; }

/* Exists so a page built from a `.cx-form-card` plus OTHER siblings (Task 11's
   use_responder.html: the summary card, then the choices form and its action
   row below it) reads as one column instead of a 704px card sitting on top of
   a 1208px-wide rest-of-page (measured at 1440 — the plan called for
   `.cx-form-card` here but said nothing about its siblings, and 44rem only
   constrains its own element). `.cx-choice`/`.cx-form-actions` are
   deliberately NOT capped directly: `.cx-form-actions` is also used on
   review_facebook.html below a full-width table, and a 44rem cap there would
   narrow a screen this fix has no business touching. */
.cx-narrow { max-width: 44rem; }

@media (max-width: 991.98px) {
  .cx-form-card { padding: 1rem; }
  /* Direct-child combinator, deliberately: Save/Cancel are the form's only
     DIRECT-child .btn (child combinator to match). The wildcard-info ⓘ trigger
     lives inside a .form-check, and the reply +/- buttons inside a
     .comment-reply-field — a bare descendant selector here forced all three
     onto their own full-width lines (the ⓘ to ~410px, breaking its popover
     anchor point; the reply buttons to ~300px, collapsing the textarea
     column). */
  .cx-form-card > .btn { width: 100%; margin-bottom: .5rem; }
}

/* Explicit submit/cancel action row (Task 10 — the two OAuth review screens).
   Scoped to `.cx-form-actions .btn`, not a bare `.btn`: this wrapper only ever
   holds the confirm/cancel pair (verified against every current usage), so
   there is no repeat of the Task 9 unscoped-`.btn` lesson (CLAUDE_NOTES.md). */
.cx-form-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; }
/* Compound selector, deliberately (same fix pattern as `.table-cards .cx-toggle`
   above): both this and `.cx-form-card` are (0,1,0), so a bare `.cx-confirm-card
   { max-width: 32rem }` would only beat `.cx-form-card`'s 44rem because it happens
   to sit later in the file — reordering these sections would silently widen the
   IG confirmation card. `.cx-form-card.cx-confirm-card` is (0,2,0) and wins
   regardless of source order. */
.cx-form-card.cx-confirm-card { max-width: 32rem; }
@media (max-width: 991.98px) {
  .cx-form-actions .btn { width: 100%; }
}

/* --- Empty states (spec §8.3) --------------------------------------------- */
/* Every list's `{% else %}` leg — first-run beta testers see these more than
   anyone, so they get the same warmth as a populated screen: an icon, the
   sentence, and (where there is one) the action that fixes it lives right
   above in the same template. */
.cx-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .5rem;
  padding: 2.5rem 1rem;
  color: var(--cx-ink-muted);
  text-align: center;
}
.cx-empty p { margin: 0; }

/* --- Login (spec §8.2) ------------------------------------------------------
   A single centred card — no nav, nothing to explore, just the way in. */
.cx-auth { display: flex; justify-content: center; padding-block: 2rem; }
.cx-auth-card {
  width: 100%;
  max-width: 24rem;
  padding: 2rem;
  background: var(--cx-surface-lowest);
  border: 1px solid var(--cx-card-border);
  border-radius: var(--cx-radius-lg);
  box-shadow: var(--cx-shadow-lift);
}

/* --- Selectable option card (Use responder, spec §8.2) ---------------------
   `--cx-outline` here is deliberate, not a slip: this border is a DECORATIVE
   card edge (the same role .cx-form-card's border plays), not an interactive
   control boundary, so it does not need `--cx-outline-strong`'s 3:1 floor.
   The `:has()` highlight below is a SEPARATE case — see its own comment. */
.cx-choice {
  border: 1px solid var(--cx-outline);
  border-radius: var(--cx-radius);
  margin-bottom: .5rem;
  background: var(--cx-surface-lowest);
}
/* Purely decorative, unlike the border above: this is the selected-state
   cue, and WCAG 1.4.11 would require 3:1 for it IF it were the only way to
   tell which option is picked. It is not — the radio input inside
   `.cx-choice-label` is a real `<input type="radio">`, and its own native
   checked indicator (a filled dot, themed by Bootstrap/components.css, plus
   the underlying `checked` state for assistive tech) is what actually carries
   the selection. This coral border is a bonus emphasis on top of that, not a
   replacement for it. --cx-brand on --cx-surface-lowest (the card face this
   sits on) is 2.73:1 in light — the same pair already pinned at that exact
   ratio by .cx-toggle:focus-visible's comment above and by
   test_chrome_focus_outlines_meet_wcag_aa_in_both_themes — under 3:1, and
   deliberately unfixed for the reason above. If `:has()` does not resolve in
   the target Safari, the radio itself still shows the selection, so nothing
   is lost. */
.cx-choice:has(input:checked) {
  border-color: var(--cx-brand);
  box-shadow: 0 0 0 2px rgba(var(--cx-brand-rgb), .25);
}
.cx-choice-label { display: block; padding: .875rem 1rem; cursor: pointer; margin: 0; }
