/*
 * Storefront stylesheet — Forum Appearance v2 (TODO.md Phase 16).
 *
 * Loaded ONLY for shops on `design == 'v2'`, whose markup is ours from
 * scratch. Nothing here is shared with app_proxy.css, and this file must never
 * grow a Bootstrap class name: classic and refresh keep that contract (D8),
 * v2 is not bound by it.
 *
 * ── Rules this file obeys ─────────────────────────────────────────────────
 *
 * 1. NO `!important`, anywhere. The merchant's `custom_css_v2` loads after us
 *    in `/styles.css` and must be able to win on an equal-specificity tie.
 * 2. NO utility classes, and no styling of one. Bootstrap's `p-3`/`mr-3`
 *    utilities baked into v1's markup are precisely what capped Phase 9 at
 *    "needs markup changes"; v2 styles semantic class names only.
 * 3. Colour comes from TOKENS, never literals. `colours_css_v2` compiles the
 *    merchant's nine colour settings into the custom properties below and
 *    serves them from `/styles.css`. The fallbacks here are what a shop sees
 *    before that file loads, and they must be legible on their own.
 * 4. Container queries over media queries wherever a component can appear in
 *    more than one column width.
 */

/* ── Cascade layers ────────────────────────────────────────────────────────
 *
 * Everything this file declares lives in a layer. The merchant's
 * `custom_css_v2` arrives later, from `/styles.css`, and is UNLAYERED — and
 * unlayered styles beat every layered style no matter how specific the layered
 * one is. So a merchant's `.champ-post { padding: 0 }` now wins against our
 * `.champ-design-v2 .champ-post`, without them having to out-specify us and
 * without a single `!important` on either side.
 *
 * That is the D8 contract expressed in the cascade rather than in discipline.
 * The old arrangement only worked while every rule in here stayed weak enough
 * to lose a tie, which is not a property anything could check, and which we
 * broke twice: `champ-button-quiet` lost to `.champ-design-v2 .champ-button`
 * (0,1,0 against 0,2,0), and `[hidden]` lost to any component rule that set
 * `display` — shipping a permanently-open share box and a 24.8px gap above
 * every header.
 *
 * `state` is last so `[hidden]` cannot lose again. It is the one thing that
 * must outrank component styling, and a layer says so structurally instead of
 * shouting it with `!important`. */
@layer champ.reset, champ.components, champ.state;

/* `reset` is FIRST, so it loses to every component rule whatever the
   specificity. It exists because of the chat launcher: its icon rendered in
   the body's grey instead of the contrasting white computed for it, because
   `.champ-design-v2 :is(button, input, select, textarea) { color: inherit }`
   is 0,1,1 and `.champ-chat-launcher` is 0,1,0. Thirteen component rules on
   form elements were losing the same way — the Send button's white text,
   `champ-button-small`'s font size (the `font` shorthand resets it), the
   icon buttons' subtle colour. A reset is supposed to be the floor that
   components build on, and a layer makes that structural. */
@layer champ.reset {
  .champ-design-v2 :is(button, input, select, textarea) {
    font: inherit;
    color: inherit;
  }

  /* Moved here for the preferences page's last Save button, which sat flush
     against the forum list: `.champ-subscription-list` asks for space below
     itself, and this reset (0,1,1) had been quietly zeroing it. Eight
     component rules were losing the same way, all small spacings the design
     asked for and never got. */
  .champ-design-v2 :is(p, ul, ol) {
    margin: 0;
  }
}

@layer champ.components {


/* ── Tokens ────────────────────────────────────────────────────────────── */

:root {
  /* Colour — every one of these is overridden per shop by colours_css_v2. */
  --champ-bg: #ffffff;
  --champ-canvas: #f5f6f8;
  --champ-surface: #ffffff;
  --champ-surface-sunken: #f6f7f8;
  --champ-text: #1f2328;
  --champ-text-subtle: #5c6570;
  --champ-link: #0071eb;
  --champ-border: rgb(0 0 0 / 0.12);
  --champ-primary: #156ef5;
  --champ-on-primary: #ffffff;
  --champ-header-bg: #156ef5;
  --champ-header-text: #ffffff;

  /* Type — family is the merchant's (set by colours_css_v2 or the font link);
     everything else is ours. */
  --champ-font: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --champ-text-sm: 0.875rem;
  --champ-text-base: 1rem;
  --champ-text-lg: 1.0625rem;
  --champ-text-xl: clamp(1.375rem, 1.2rem + 0.85vw, 1.875rem);
  --champ-leading: 1.55;
  --champ-leading-tight: 1.3;

  /* Weight does the hierarchy work, because the FAMILY is the merchant's and
     we cannot design around it. 450 reads as regular on most faces without
     looking thin next to 600. */
  --champ-weight-body: 450;
  --champ-weight-medium: 550;
  --champ-weight-strong: 650;

  /* Space — a 4px scale, used for every gap and pad in this file. */
  --champ-space-1: 0.25rem;
  --champ-space-2: 0.5rem;
  --champ-space-3: 0.75rem;
  --champ-space-4: 1rem;
  --champ-space-5: 1.5rem;
  --champ-space-6: 2rem;

  --champ-radius: 0.5rem;
  --champ-measure: 72ch;
  --champ-page-width: 72rem;
  --champ-gutter: var(--champ-space-4);
}

/* ── Reset ─────────────────────────────────────────────────────────────── */

.champ-design-v2 *,
.champ-design-v2 *::before,
.champ-design-v2 *::after {
  box-sizing: border-box;
}

.champ-design-v2 {
  margin: 0;
  background-color: var(--champ-canvas);
  color: var(--champ-text);
  font-family: var(--champ-font);
  font-size: var(--champ-text-base);
  line-height: var(--champ-leading);
  -webkit-text-size-adjust: 100%;
}

.champ-design-v2 :is(h1, h2, h3, h4, h5, h6) {
  margin: 0;
  line-height: 1.25;
  text-wrap: balance;
}

.champ-design-v2 a {
  color: var(--champ-link);
  overflow-wrap: break-word;
}

.champ-design-v2 img {
  max-width: 100%;
  height: auto;
}

.champ-design-v2 :focus-visible {
  outline: 2px solid var(--champ-link);
  outline-offset: 2px;
}

.champ-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── Icons ─────────────────────────────────────────────────────────────── */

/* The sprite must not occupy a line box. It is inline SVG at the top of
   <body>, and even at zero size an inline element still generates one — which
   put a ~25px gap above the header on every page. The `hidden` attribute does
   NOT solve this: it is an HTML attribute and Chrome's UA rule for it does not
   apply to elements in the SVG namespace.
   Absolute positioning rather than `display: none`, because that is the
   canonical sprite pattern — `display: none` on the root has broken `<use>`
   references in some browsers. */
.champ-icon-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.champ-icon {
  width: 1.25em;
  height: 1.25em;
  flex: none;
  vertical-align: -0.25em;
}

/* ── Shell ─────────────────────────────────────────────────────────────── */

.champ-main {
  max-width: var(--champ-page-width);
  margin-inline: auto;
  padding: var(--champ-space-5) var(--champ-gutter);
  /* The query container lives HERE, not on `.champ-layout`: an element cannot
     respond to a container query on itself, and the layout grid is the first
     thing that has to react to the available width. */
  container-type: inline-size;
  container-name: champ-layout;
}

/* The content panel. A tinted page with the reading surface lifted onto a
   lighter panel is the shape of every current forum, and it does real work:
   it bounds the measure without needing a rule, and it separates content from
   navigation without a border. */
.champ-layout-main {
  padding: var(--champ-space-5);
  border: 1px solid var(--champ-border);
  border-radius: calc(var(--champ-radius) * 1.5);
  background-color: var(--champ-bg);
}

.champ-notice {
  max-width: var(--champ-measure);
  margin-inline: auto;
  padding: var(--champ-space-5) var(--champ-gutter);
  color: var(--champ-text-subtle);
  text-align: center;
}

.champ-moderation-bar {
  padding: var(--champ-space-2) var(--champ-gutter);
  background-color: var(--champ-surface-sunken);
  text-align: center;
  font-size: var(--champ-text-sm);
}

/* ── Header ────────────────────────────────────────────────────────────── */

.champ-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--champ-space-4);
  padding: var(--champ-space-3) var(--champ-gutter);
  background-color: var(--champ-header-bg);
  color: var(--champ-header-text);
}

.champ-header a {
  color: inherit;
  text-decoration: none;
}

.champ-header a:hover {
  text-decoration: underline;
}

.champ-header-brand {
  font-size: var(--champ-text-lg);
  font-weight: var(--champ-weight-medium);
}

.champ-header-toggle {
  display: inline-flex;
  align-items: center;
  padding: var(--champ-space-2);
  border: 1px solid currentColor;
  border-radius: var(--champ-radius);
  background: none;
  cursor: pointer;
}

/* Closed by default on narrow viewports; the toggle flips `hidden`. Wider
   containers show it unconditionally, so the menu still works with no JS. */
.champ-header-menu[hidden] {
  display: none;
}

.champ-header-menu {
  display: flex;
  flex-wrap: wrap;
  gap: var(--champ-space-4);
  flex-basis: 100%;
}

.champ-header-links,
.champ-header-account {
  display: flex;
  flex-wrap: wrap;
  gap: var(--champ-space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.champ-header-account {
  margin-inline-start: auto;
}

.champ-header-toggle {
  margin-inline-start: auto;
}

.champ-alert-button {
  position: relative;
  display: inline-flex;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.champ-alert-dot {
  position: absolute;
  inset-block-start: -0.125rem;
  inset-inline-end: -0.125rem;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background-color: currentColor;
}

.champ-avatar {
  border-radius: 50%;
  object-fit: cover;
}

@media (min-width: 48rem) {
  .champ-header-toggle {
    display: none;
  }

  .champ-header-menu[hidden] {
    display: flex;
  }

  /* Brand left, search in the middle, account right — the arrangement every
     current forum uses, because search is the control people reach for from
     any page and the middle is where the eye already is. */
  .champ-header-menu {
    flex-basis: auto;
    flex: 0 0 auto;
    margin-inline-start: auto;
  }

  .champ-search {
    margin-inline: auto;
  }
}

/* On a phone the search field takes its own line under the brand: squeezing
   it between a logo and a menu button leaves room to type nothing. */
@media (max-width: 47.999rem) {
  .champ-search {
    order: 3;
    flex-basis: 100%;
    max-width: none;
  }
}

/* ── Layout ────────────────────────────────────────────────────────────────
 *
 * The four `layout` settings are four values of one grid. A CONTAINER query
 * does the column switch, not a media query: the forum is embedded in a
 * merchant's page and its available width is not the viewport's.
 */

.champ-layout {
  display: grid;
  gap: var(--champ-space-6);
}

.champ-layout-secondary,
.champ-layout-sidebar {
  display: flex;
  flex-direction: column;
  gap: var(--champ-space-5);
  min-width: 0;
}

.champ-layout-main {
  min-width: 0;
}

/* The rail sits on the canvas, not on the panel — it is navigation, not
   content, and the tint is what says so. */
.champ-layout-sidebar {
  font-size: var(--champ-text-sm);
}

@container champ-layout (inline-size > 52rem) {
  .champ-layout[data-champ-layout="left_60"] {
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  }

  .champ-layout[data-champ-layout="right_60"] {
    grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  }

  /* The sidebar is written after the main column in the markup, because that
     is the order it should be READ in. Grid moves it, the DOM keeps it. */
  .champ-layout[data-champ-layout="right_60"] .champ-layout-sidebar {
    grid-column: 1;
    grid-row: 1;
  }

  .champ-layout[data-champ-layout="top_sidebar"] .champ-layout-sidebar,
  .champ-layout[data-champ-layout="bottom_sidebar"] .champ-layout-sidebar {
    flex-direction: row;
    flex-wrap: wrap;
  }

  .champ-layout[data-champ-layout="top_sidebar"] .champ-layout-sidebar > *,
  .champ-layout[data-champ-layout="bottom_sidebar"] .champ-layout-sidebar > * {
    flex: 1 1 14rem;
  }

  .champ-layout[data-champ-layout="top_sidebar"] .champ-layout-sidebar {
    order: -1;
  }

  .champ-layout-split {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    gap: var(--champ-space-5);
  }
}

/* ── Conversation feed ─────────────────────────────────────────────────────
 *
 * The rhythm of this page comes from the avatar column, like a thread — not
 * from boxes. Rows are separated by hairlines and space; nothing is a card.
 * Cards-in-a-list is the default shape every generated interface reaches for,
 * and it makes 25 equal-weight rectangles out of content that is not equal.
 */

.champ-feed {
  margin: 0;
  padding: 0;
  list-style: none;
}

.champ-topic-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--champ-space-3);
  padding-block: var(--champ-space-4);
}

.champ-topic-row + .champ-topic-row {
  border-block-start: 1px solid var(--champ-border);
}

.champ-topic-avatar {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  object-fit: cover;
  background-color: var(--champ-surface-sunken);
}

.champ-topic-body {
  display: flex;
  flex-direction: column;
  gap: var(--champ-space-1);
  min-width: 0;
}

.champ-design-v2 .champ-topic-title {
  color: var(--champ-heading);
  font-size: var(--champ-text-lg);
  font-weight: var(--champ-weight-medium);
  line-height: var(--champ-leading-tight);
  text-decoration: none;
  text-wrap: pretty;
}

.champ-design-v2 .champ-topic-title:hover {
  color: var(--champ-link);
}

/* Meta reads as a row of distinct facts, spaced — not a string joined by
   middle dots, which turns four separate things into one grey sentence. */
.champ-topic-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--champ-space-1) var(--champ-space-3);
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
}

/* A quiet chip, not an underlined word: underlining grey text next to an
   ink-coloured title reads as a broken link rather than a category. */
.champ-design-v2 .champ-topic-forum {
  padding: 0.0625rem var(--champ-space-2);
  border-radius: 999px;
  background-color: var(--champ-surface-sunken);
  color: inherit;
  text-decoration: none;
  font-size: 0.8125rem;
}

.champ-design-v2 .champ-topic-forum:hover {
  color: var(--champ-on-primary);
  background-color: var(--champ-primary);
}

.champ-topic-replies {
  font-variant-numeric: tabular-nums;
}

.champ-topic-flag {
  display: inline-flex;
  align-items: center;
  gap: var(--champ-space-1);
  color: var(--champ-text);
  font-weight: var(--champ-weight-medium);
}

.champ-topic-flag .champ-icon {
  width: 1em;
  height: 1em;
}

/* A question nobody has answered is the most useful thing on a shop's forum,
   so it is marked — but quietly, with a rule and a change of weight rather
   than a colour that competes with the merchant's brand. */
/* A marker, not an indent. The padding shifted the whole row right, so a feed
   with a few unanswered topics had a ragged left edge and the rule read as
   nesting. The bar sits outside the row's box instead. */
.champ-topic-unanswered {
  position: relative;
}

.champ-topic-unanswered::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: calc(var(--champ-space-3) * -1);
  width: 2px;
  border-radius: 1px;
  background-color: var(--champ-primary);
}

.champ-topic-unanswered .champ-topic-replies {
  color: var(--champ-text);
  font-weight: var(--champ-weight-medium);
}

.champ-feed-empty {
  padding-block: var(--champ-space-6);
  color: var(--champ-text-subtle);
  text-align: center;
}

.champ-feed-actions {
  padding-block-start: var(--champ-space-5);
}

@container champ-layout (inline-size > 34rem) {
  .champ-topic-row {
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--champ-space-4);
    align-items: start;
  }

  .champ-topic-avatar {
    width: 2.75rem;
    height: 2.75rem;
  }
}

/* ── Forum page ──────────────────────────────────────────────────────────── */

.champ-breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--champ-space-2);
  margin-block-end: var(--champ-space-3);
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
}

.champ-design-v2 .champ-breadcrumb a {
  color: inherit;
}

.champ-forum-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--champ-space-3);
}

.champ-page-title {
  font-size: var(--champ-text-xl);
  font-weight: var(--champ-weight-strong);
  letter-spacing: -0.015em;
  color: var(--champ-heading);
}

.champ-forum-summary {
  max-width: 66ch;
  margin-block-start: var(--champ-space-2);
  color: var(--champ-text-subtle);
}

.champ-sort {
  display: flex;
  align-items: center;
  gap: var(--champ-space-2);
  margin-block: var(--champ-space-5) var(--champ-space-2);
  font-size: var(--champ-text-sm);
}

.champ-sort-label {
  color: var(--champ-text-subtle);
}

.champ-select {
  padding: var(--champ-space-1) var(--champ-space-2);
  border: 1px solid var(--champ-border);
  border-radius: calc(var(--champ-radius) - 2px);
  background-color: var(--champ-surface);
  color: var(--champ-text);
  font-size: var(--champ-text-sm);
}

/* ── Sidebar navigation ──────────────────────────────────────────────────── */

.champ-forum-nav-list {
  display: flex;
  flex-direction: column;
  margin: 0 0 var(--champ-space-4);
  padding: 0;
  list-style: none;
}

.champ-design-v2 .champ-forum-nav-item a {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--champ-space-3);
  padding: var(--champ-space-2) var(--champ-space-2);
  border-radius: calc(var(--champ-radius) - 2px);
  color: inherit;
  text-decoration: none;
}

.champ-forum-nav-item a:hover {
  background-color: var(--champ-surface-sunken);
}

.champ-forum-nav-count {
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
  font-variant-numeric: tabular-nums;
}

/* Sentence case, at reading size. An ALL-CAPS tracked-out label above every
   block is the commonest tell of a templated interface, and it shouts at
   content that is only navigation. */
.champ-sidebar-heading {
  margin-block: var(--champ-space-2) var(--champ-space-1);
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
  font-weight: var(--champ-weight-medium);
  letter-spacing: 0;
  text-transform: none;
}

/* ── Pagination ──────────────────────────────────────────────────────────── */

.champ-pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--champ-space-1);
  padding-block-start: var(--champ-space-5);
  font-size: var(--champ-text-sm);
}

.champ-design-v2 .champ-page {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2rem;
  padding: var(--champ-space-1) var(--champ-space-2);
  border-radius: calc(var(--champ-radius) - 2px);
  color: var(--champ-text-subtle);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}

.champ-page:hover {
  background-color: var(--champ-surface-sunken);
  color: var(--champ-text);
}

.champ-page-current {
  background-color: var(--champ-primary);
  color: var(--champ-on-primary);
  font-weight: var(--champ-weight-medium);
}

.champ-page-gap {
  min-width: auto;
}

/* ── Buttons ─────────────────────────────────────────────────────────────── */

.champ-design-v2 .champ-button {
  display: inline-flex;
  align-items: center;
  gap: var(--champ-space-2);
  padding: var(--champ-space-2) var(--champ-space-4);
  border: 1px solid transparent;
  border-radius: var(--champ-radius);
  background-color: var(--champ-primary);
  color: var(--champ-on-primary);
  font-weight: var(--champ-weight-medium);
  text-decoration: none;
  cursor: pointer;
}

.champ-button:hover {
  filter: brightness(0.94);
}

/* Scoped to match `.champ-design-v2 .champ-button`, which otherwise wins on
   specificity and paints every "quiet" button solid primary. */
.champ-design-v2 .champ-button-quiet {
  background: none;
  border-color: var(--champ-border);
  color: var(--champ-text);
}

/* ── Search ──────────────────────────────────────────────────────────────── */

.champ-search-form {
  display: flex;
  gap: var(--champ-space-2);
}

.champ-input {
  flex: 1;
  min-width: 0;
  padding: var(--champ-space-2) var(--champ-space-3);
  border: 1px solid var(--champ-border);
  border-radius: var(--champ-radius);
  background-color: var(--champ-surface);
  color: var(--champ-text);
}

/* ── Unread ──────────────────────────────────────────────────────────────── */

.champ-design-v2 .champ-unread-link {
  display: flex;
  align-items: baseline;
  gap: var(--champ-space-2);
  padding: var(--champ-space-3);
  border: 1px solid var(--champ-border);
  border-radius: var(--champ-radius);
  color: inherit;
  text-decoration: none;
}

.champ-unread-count {
  color: var(--champ-heading);
  font-size: var(--champ-text-lg);
  font-weight: var(--champ-weight-strong);
  font-variant-numeric: tabular-nums;
}

/* ── Sidebar sections ────────────────────────────────────────────────────── */

.champ-sidebar-section {
  min-width: 0;
}

.champ-sidebar-list {
  display: flex;
  flex-direction: column;
  gap: var(--champ-space-2);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--champ-text-sm);
}

.champ-sidebar-empty {
  color: var(--champ-text-subtle);
}

.champ-users-online-list {
  flex-direction: row;
  flex-wrap: wrap;
  gap: var(--champ-space-2);
}

/* ── Footer ──────────────────────────────────────────────────────────────── */

.champ-footer {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--champ-space-3);
  max-width: var(--champ-page-width);
  margin-inline: auto;
  padding: var(--champ-space-5) var(--champ-gutter);
  border-block-start: 1px solid var(--champ-border);
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
}

.champ-footer p {
  margin: 0;
}

/* The merchant sets the height (inline, from `logo_height`); width follows. */
.champ-header-logo {
  width: auto;
  max-height: 200px;
}

@media (prefers-reduced-motion: reduce) {
  .champ-design-v2 *,
  .champ-design-v2 *::before,
  .champ-design-v2 *::after {
    animation-duration: 0.01ms;
    transition-duration: 0.01ms;
  }
}

/* ── Header search ─────────────────────────────────────────────────────────
 *
 * Centred in the top bar. It is the one control a reader reaches for from any
 * page, so it gets the middle of the header and a fixed maximum measure — a
 * search field the width of a desktop window looks like a mistake.
 */

.champ-search {
  position: relative;
  display: flex;
  align-items: center;
  flex: 1 1 18rem;
  max-width: 34rem;
}

.champ-search-icon {
  position: absolute;
  inset-inline-start: var(--champ-space-3);
  width: 1rem;
  height: 1rem;
  opacity: 0.6;
  pointer-events: none;
}

.champ-search-input {
  width: 100%;
  padding: var(--champ-space-2) var(--champ-space-3);
  padding-inline-start: calc(var(--champ-space-3) * 2 + 1rem);
  border: 1px solid rgb(255 255 255 / 0.35);
  border-radius: 999px;
  background-color: rgb(255 255 255 / 0.15);
  color: inherit;
}

.champ-search-input::placeholder {
  color: inherit;
  opacity: 0.7;
}

.champ-search-input:focus {
  background-color: var(--champ-surface);
  color: var(--champ-text);
  outline: none;
  border-color: var(--champ-surface);
}

/* ── Forum colour dots ─────────────────────────────────────────────────────
 *
 * Every forum gets a stable colour derived from its title. It is what makes a
 * long rail scannable, and it needs no merchant setting.
 */

.champ-forum-dot {
  display: inline-block;
  width: 0.5rem;
  height: 0.5rem;
  flex: none;
  border-radius: 2px;
  background-color: var(--champ-forum-color, var(--champ-primary));
}

.champ-design-v2 .champ-topic-forum {
  display: inline-flex;
  align-items: center;
  gap: var(--champ-space-1);
}

.champ-forum-nav-item a {
  gap: var(--champ-space-2);
}

.champ-forum-nav-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Topic page ────────────────────────────────────────────────────────────
 *
 * A thread, read top to bottom. The avatar rail sets the left edge; posts are
 * separated by hairlines and space rather than boxed, so a long conversation
 * reads as one thing instead of forty cards.
 */

.champ-topic-header {
  display: flex;
  gap: var(--champ-space-4);
  padding-block-end: var(--champ-space-4);
  border-block-end: 1px solid var(--champ-border);
}

.champ-topic-heading {
  display: flex;
  flex-direction: column;
  gap: var(--champ-space-3);
  min-width: 0;
}

.champ-topic-header .champ-page-title {
  text-wrap: balance;
}

.champ-topic-vote {
  flex: none;
}

.champ-topic-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--champ-space-2);
}

.champ-topic-share {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--champ-space-3);
  padding: var(--champ-space-3);
  border: 1px solid var(--champ-border);
  border-radius: var(--champ-radius);
  background-color: var(--champ-surface-sunken);
  font-size: var(--champ-text-sm);
}

.champ-topic-share code {
  overflow-wrap: anywhere;
}

/* "Add tag" is a <details> now (Phase 20, #9): the pill is its <summary>,
   and the form drops beneath it when open. */
.champ-tag-add {
  position: relative;
  display: inline-block;
}

.champ-tag-add > summary {
  list-style: none;
  padding: 0.0625rem var(--champ-space-2);
  border: 1px dashed var(--champ-border);
  border-radius: 999px;
  color: var(--champ-text-subtle);
  font-size: 0.8125rem;
  cursor: pointer;
}

.champ-tag-add > summary::-webkit-details-marker {
  display: none;
}

.champ-tag-add[open] > summary {
  border-style: solid;
  color: var(--champ-text);
}

.champ-tag-form {
  position: absolute;
  z-index: 10;
  inset-block-start: calc(100% + var(--champ-space-1));
  inset-inline-start: 0;
  display: flex;
  gap: var(--champ-space-2);
  align-items: center;
  min-width: 16rem;
  padding: var(--champ-space-2);
  border: 1px solid var(--champ-border);
  border-radius: var(--champ-radius);
  background: var(--champ-surface);
  box-shadow: 0 6px 18px rgba(15, 25, 40, 0.12);
}

.champ-topic-tag-list {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--champ-space-1);
}

/* ── Thread ──────────────────────────────────────────────────────────────── */

.champ-thread {
  margin: 0;
  padding: 0;
  list-style: none;
}

.champ-post {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--champ-space-3);
  padding-block: var(--champ-space-5);
}

.champ-post + .champ-post,
.champ-thread-notice + .champ-post {
  border-block-start: 1px solid var(--champ-border);
}

.champ-post-hidden {
  opacity: 0.6;
}

.champ-post-avatar {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  object-fit: cover;
  background-color: var(--champ-surface-sunken);
}

.champ-post-main {
  display: flex;
  flex-direction: column;
  gap: var(--champ-space-2);
  min-width: 0;
}

.champ-post-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--champ-space-2);
  font-size: var(--champ-text-sm);
}

.champ-post-author {
  color: var(--champ-heading);
  font-weight: var(--champ-weight-medium);
}

.champ-post-time {
  margin-inline-start: auto;
  color: var(--champ-text-subtle);
}

/* Prose. The measure is capped here and nowhere else — this is the only place
   on the forum where someone reads more than two lines at a stretch. */
.champ-post-content {
  max-width: 68ch;
  overflow-wrap: break-word;
}

.champ-post-content > * + * {
  margin-block-start: var(--champ-space-3);
}

.champ-post-content :is(ul, ol) {
  padding-inline-start: var(--champ-space-5);
}

.champ-post-content img {
  border-radius: var(--champ-radius);
}

.champ-post-content blockquote {
  margin: 0;
  padding-inline-start: var(--champ-space-4);
  border-inline-start: 2px solid var(--champ-border);
  color: var(--champ-text-subtle);
}

.champ-post-content pre {
  padding: var(--champ-space-3);
  border-radius: var(--champ-radius);
  background-color: var(--champ-surface-sunken);
  overflow-x: auto;
}

.champ-post-edited,
.champ-post-signature {
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
}

.champ-post-signature {
  padding-block-start: var(--champ-space-2);
  border-block-start: 1px solid var(--champ-border);
}

.champ-post-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--champ-space-3);
  font-size: var(--champ-text-sm);
}

.champ-post-upvotes {
  color: var(--champ-text-subtle);
  font-variant-numeric: tabular-nums;
}

.champ-thread-notice {
  padding-block: var(--champ-space-4);
  color: var(--champ-text-subtle);
}

/* A quiet action. Discourse's post controls are almost invisible until you
   look for them, which is right: they belong to one reader at a time. */
.champ-icon-button {
  display: inline-flex;
  align-items: center;
  gap: var(--champ-space-1);
  padding: var(--champ-space-1) var(--champ-space-2);
  border: 0;
  border-radius: calc(var(--champ-radius) - 2px);
  background: none;
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
  text-decoration: none;
  cursor: pointer;
}

.champ-design-v2 .champ-icon-button:hover {
  background-color: var(--champ-surface-sunken);
  color: var(--champ-text);
}

.champ-icon-button .champ-icon {
  width: 1rem;
  height: 1rem;
}

/* ── Moderator controls ──────────────────────────────────────────────────── */

.champ-moderator-controls {
  display: flex;
  flex-direction: column;
  gap: var(--champ-space-2);
  padding: var(--champ-space-3);
  border: 1px solid var(--champ-border);
  border-radius: var(--champ-radius);
  background-color: var(--champ-surface-sunken);
  font-size: var(--champ-text-sm);
}

.champ-moderator-label {
  color: var(--champ-text-subtle);
  font-weight: var(--champ-weight-medium);
}

.champ-moderator-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--champ-space-2);
}

.champ-moderator-ip {
  margin: 0;
  color: var(--champ-text-subtle);
}

.champ-button-small {
  padding: var(--champ-space-1) var(--champ-space-3);
  font-size: var(--champ-text-sm);
}

.champ-design-v2 .champ-button-danger {
  background: none;
  border-color: var(--champ-border);
  color: #b4232c;
}

.champ-badge-staff {
  border-color: var(--champ-primary);
  color: var(--champ-primary);
}

.champ-badge-hidden {
  color: #b4232c;
  border-color: currentColor;
}

/* ── Composer ────────────────────────────────────────────────────────────── */

/* The composer. A reply is written in the same shape it will be read in —
   avatar on the left, field where the post body will sit — so the form looks
   like the thing it produces rather than a settings panel bolted to the
   bottom of the page. */
.champ-composer {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--champ-space-3);
  margin-block-start: var(--champ-space-5);
  padding-block-start: var(--champ-space-5);
  border-block-start: 1px solid var(--champ-border);
}

/* `.champ-design-v2` is needed here: the reset zeroes every heading margin at
   a higher specificity, which is why this heading used to sit on top of the
   textarea's focus ring. */
.champ-design-v2 .champ-composer-heading {
  margin-block-end: var(--champ-space-2);
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
  font-weight: var(--champ-weight-medium);
}

.champ-composer-avatar {
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  object-fit: cover;
  background-color: var(--champ-surface-sunken);
}

.champ-composer-body {
  min-width: 0;
}

.champ-composer-footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--champ-space-3);
  margin-block-start: var(--champ-space-3);
}

.champ-composer-status {
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
}

.champ-composer-status[hidden] {
  display: none;
}

.champ-composer-error {
  color: #b4232c;
}

.champ-composer .champ-textarea {
  min-height: 6.5rem;
}

.champ-composer .champ-form-group {
  margin-block-end: 0;
}

/* A post being submitted stays on screen, dimmed, so nothing appears to
   vanish while the request is in flight. */
.champ-composer[data-champ-busy] .champ-textarea {
  opacity: 0.6;
}

.champ-form-group {
  margin-block-end: var(--champ-space-3);
}

.champ-label {
  display: block;
  margin-block-end: var(--champ-space-1);
  font-size: var(--champ-text-sm);
  font-weight: var(--champ-weight-medium);
}

.champ-textarea,
.champ-input {
  width: 100%;
  padding: var(--champ-space-3);
  border: 1px solid var(--champ-border);
  border-radius: var(--champ-radius);
  background-color: var(--champ-bg);
  color: var(--champ-text);
  resize: vertical;
}

/* The honeypot (Phase 14). Taken out of flow rather than `display: none`, so a
   bot that skips hidden fields still sees it. Never remove this rule without
   reading the spam notes in TODO.md. */
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.champ-post-editor[hidden] {
  display: none;
}

.champ-post-editor {
  padding-block-start: var(--champ-space-3);
}

/* ── Search and tag results ──────────────────────────────────────────────── */

.champ-results {
  display: flex;
  flex-direction: column;
}

.champ-design-v2 .champ-result {
  display: block;
  padding-block: var(--champ-space-4);
  color: inherit;
  text-decoration: none;
}

.champ-result + .champ-result {
  border-block-start: 1px solid var(--champ-border);
}

.champ-design-v2 .champ-result:hover .champ-result-title {
  color: var(--champ-link);
}

.champ-result-title {
  font-size: var(--champ-text-lg);
  font-weight: var(--champ-weight-medium);
  color: var(--champ-heading);
}

.champ-result-time,
.champ-result-count {
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
}

.champ-result-body mark {
  padding: 0 0.125rem;
  border-radius: 2px;
  background-color: var(--champ-surface-sunken);
  color: inherit;
  font-weight: var(--champ-weight-medium);
}

.champ-post-upvotes {
  display: inline-flex;
  align-items: center;
  gap: var(--champ-space-1);
}

.champ-post-upvotes[hidden] {
  display: none;
}

.champ-post-upvotes .champ-icon {
  width: 0.875rem;
  height: 0.875rem;
}

/* ── Forms ─────────────────────────────────────────────────────────────────
 *
 * Composing pages: one column, fields at a comfortable measure, one primary
 * action. Nothing is boxed — the page is already a panel.
 */

.champ-form {
  max-width: 44rem;
  margin-block-start: var(--champ-space-4);
}

.champ-form .champ-form-group {
  margin-block-end: var(--champ-space-4);
}

.champ-form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--champ-space-3);
  margin-block-start: var(--champ-space-4);
}

.champ-poll-maker {
  margin-block-end: var(--champ-space-4);
  padding: var(--champ-space-3) var(--champ-space-4);
  border: 1px solid var(--champ-border);
  border-radius: var(--champ-radius);
}

.champ-poll-maker summary {
  cursor: pointer;
  font-weight: var(--champ-weight-medium);
}

.champ-poll-maker-body {
  padding-block-start: var(--champ-space-4);
}

.champ-poll-choice {
  display: flex;
  align-items: center;
  gap: var(--champ-space-2);
  margin-block-end: var(--champ-space-2);
}

.champ-poll-choice .champ-input {
  flex: 1;
}

/* The wrapper the remove-association link hides. `.nested-fields` is cocoon's
   name and is read by dashboard.js; the storefront never reads it, so v2 uses
   its own — ⚠️ a future v2 port of the remove behaviour must target
   `.champ-nested-fields`, not the cocoon name. */
.champ-nested-fields[style*="display: none"] {
  display: none;
}

/* The add-association link is rendered by a shim that takes no options, so it
   is styled off its data hook. */
[data-champ-add-association] {
  display: inline-flex;
  align-items: center;
  padding: var(--champ-space-1) var(--champ-space-3);
  border: 1px solid var(--champ-border);
  border-radius: var(--champ-radius);
  color: var(--champ-text);
  font-size: var(--champ-text-sm);
  text-decoration: none;
}

.champ-upload-thumb {
  display: block;
  max-width: 100%;
  border-radius: calc(var(--champ-radius) - 2px);
}

.champ-upload-file {
  width: 1.75rem;
  height: 1.75rem;
  color: var(--champ-text-subtle);
}

/* ══════════════════════════════════════════════════════════════════════════
 * Pages converted in the second sweep.
 *
 * These templates were written in one batch and shipped without their styles,
 * which is why the profile page looked unstyled. Everything below is the
 * missing half.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ── Shared page furniture ─────────────────────────────────────────────── */

.champ-section-heading {
  margin-block-end: var(--champ-space-3);
  font-size: var(--champ-text-lg);
  font-weight: var(--champ-weight-medium);
  color: var(--champ-heading);
}

.champ-breadcrumb-current {
  color: var(--champ-text);
}

.champ-muted {
  color: var(--champ-text-subtle);
}

.champ-strong {
  font-weight: var(--champ-weight-medium);
}

.champ-center {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--champ-space-2);
  padding: var(--champ-space-6) var(--champ-space-4);
  color: var(--champ-text-subtle);
  text-align: center;
}

.champ-design-v2 .champ-plain-link {
  color: inherit;
  text-decoration: none;
}

.champ-notice-success {
  padding: var(--champ-space-3);
  border: 1px solid var(--champ-border);
  border-radius: var(--champ-radius);
  background-color: var(--champ-surface-sunken);
}

.champ-panel {
  border: 1px solid var(--champ-border);
  border-radius: var(--champ-radius);
  background-color: var(--champ-surface);
}

.champ-panel-body {
  padding: var(--champ-space-4);
}

.champ-table {
  width: 100%;
  border-collapse: collapse;
}

.champ-table th,
.champ-table td {
  padding: var(--champ-space-2) 0;
  border-block-end: 1px solid var(--champ-border);
  text-align: start;
}

.champ-table th {
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
  font-weight: var(--champ-weight-medium);
}

.champ-table-end {
  text-align: end;
}

/* ── Profile ───────────────────────────────────────────────────────────── */

.champ-profile {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--champ-space-4);
}

.champ-profile-avatar {
  width: 4.5rem;
  height: 4.5rem;
  border-radius: 50%;
  object-fit: cover;
  background-color: var(--champ-surface-sunken);
}

.champ-profile-identity {
  display: flex;
  flex-direction: column;
  gap: var(--champ-space-2);
  min-width: 0;
}

.champ-profile-handles {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--champ-space-2);
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
}

/* A definition list, because that is what these are: four labelled facts.
   Laid out as columns so they read across rather than as a stack of pairs. */
.champ-profile-stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--champ-space-5);
  margin: 0;
}

.champ-profile-stats dt {
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
}

.champ-profile-stats dd {
  margin: 0;
  color: var(--champ-heading);
  font-weight: var(--champ-weight-medium);
  font-variant-numeric: tabular-nums;
}

.champ-profile-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--champ-space-2);
}

.champ-profile-description {
  max-width: 68ch;
  margin-block-start: var(--champ-space-5);
  padding-block-start: var(--champ-space-4);
  border-block-start: 1px solid var(--champ-border);
}

.champ-profile-posts {
  margin-block-start: var(--champ-space-6);
  padding-block-start: var(--champ-space-4);
  border-block-start: 1px solid var(--champ-border);
}

/* ── Leaderboard ───────────────────────────────────────────────────────── */

.champ-leaderboard {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: champ-rank;
}

.champ-leaderboard-row {
  display: flex;
  align-items: center;
  gap: var(--champ-space-3);
  padding-block: var(--champ-space-3);
  counter-increment: champ-rank;
}

.champ-leaderboard-row + .champ-leaderboard-row {
  border-block-start: 1px solid var(--champ-border);
}

/* The position IS the content on a leaderboard, so it is rendered. */
.champ-leaderboard-row::before {
  content: counter(champ-rank);
  min-width: 1.5rem;
  color: var(--champ-text-subtle);
  font-variant-numeric: tabular-nums;
  text-align: end;
}

.champ-leaderboard-avatar {
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  object-fit: cover;
}

.champ-leaderboard-name {
  flex: 1;
  min-width: 0;
}

.champ-leaderboard-points {
  color: var(--champ-heading);
  font-weight: var(--champ-weight-medium);
  font-variant-numeric: tabular-nums;
}

/* ── Forms: preferences, editing ───────────────────────────────────────── */

.champ-prefs-section {
  max-width: 44rem;
  margin-block-end: var(--champ-space-6);
  padding-block-start: var(--champ-space-5);
  border-block-start: 1px solid var(--champ-border);
}

.champ-prefs-section:first-child {
  padding-block-start: 0;
  border-block-start: 0;
}

.champ-prefs-avatar {
  width: 4rem;
  height: 4rem;
  border-radius: 50%;
  margin-block-end: var(--champ-space-3);
}

.champ-field-hint {
  margin-block-start: var(--champ-space-1);
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
}

.champ-field-error,
.champ-no-notifications {
  margin-block-start: var(--champ-space-1);
  color: #b4232c;
  font-size: var(--champ-text-sm);
}

.champ-field-error[hidden] {
  display: none;
}

.champ-checkbox {
  display: flex;
  align-items: center;
  gap: var(--champ-space-2);
  margin-block-end: var(--champ-space-2);
}

.champ-fieldset {
  border: 0;
  padding: 0;
  margin-inline: 0;
}

.champ-subscription-list {
  margin: 0 0 var(--champ-space-4);
  padding: 0;
  list-style: none;
}

.champ-subscription-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--champ-space-3);
  padding-block: var(--champ-space-2);
  border-block-end: 1px solid var(--champ-border);
}

/* ── Polls ─────────────────────────────────────────────────────────────── */

.champ-poll {
  padding-block: var(--champ-space-4);
  border-block-end: 1px solid var(--champ-border);
}

.champ-poll-question {
  margin-block-end: var(--champ-space-3);
  font-size: var(--champ-text-lg);
  font-weight: var(--champ-weight-medium);
}

.champ-poll-already-voted {
  margin-block-end: var(--champ-space-3);
  color: var(--champ-text-subtle);
  font-size: var(--champ-text-sm);
}

.champ-poll-results {
  display: flex;
  flex-direction: column;
  gap: var(--champ-space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.champ-poll-result {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--champ-space-1) var(--champ-space-3);
  font-size: var(--champ-text-sm);
}

/* The bar is decoration; the percentage beside it is the actual answer, so it
   is always written out. A bar alone tells a screen reader nothing. */
.champ-poll-bar {
  grid-column: 1 / -1;
  height: 0.5rem;
  border-radius: 999px;
  background-color: var(--champ-surface-sunken);
  background-image: linear-gradient(var(--champ-primary), var(--champ-primary));
  background-size: var(--champ-poll-fill, 0%) 100%;
  background-repeat: no-repeat;
}

.champ-poll-result-count {
  color: var(--champ-text-subtle);
  font-variant-numeric: tabular-nums;
  text-align: end;
}

.champ-poll-edit {
  display: inline-block;
  margin-block-start: var(--champ-space-2);
  font-size: var(--champ-text-sm);
}

.champ-poll-choices {
  display: flex;
  flex-direction: column;
  gap: var(--champ-space-2);
}

.champ-poll-choice-actions {
  margin-block-start: var(--champ-space-2);
}

/* ── Topic voting ──────────────────────────────────────────────────────── */

.champ-topic-upvote {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--champ-space-1);
  min-width: 2.5rem;
}

.champ-design-v2 .champ-upvote-button {
  display: inline-flex;
  padding: var(--champ-space-1);
  border-radius: calc(var(--champ-radius) - 2px);
  color: var(--champ-text-subtle);
  text-decoration: none;
}

.champ-design-v2 .champ-upvote-button:hover,
.champ-design-v2 .champ-upvote-button-on {
  color: var(--champ-primary);
  background-color: var(--champ-surface-sunken);
}

.champ-upvote-count {
  color: var(--champ-heading);
  font-weight: var(--champ-weight-medium);
  font-variant-numeric: tabular-nums;
}

/* ── Tags ──────────────────────────────────────────────────────────────── */

.champ-tag {
  display: inline-flex;
  align-items: center;
  gap: var(--champ-space-1);
}

.champ-tag-name,
.champ-badge-tag {
  padding: 0.0625rem var(--champ-space-2);
  border: 1px solid var(--champ-border);
  border-radius: 999px;
  background: none;
  color: var(--champ-text-subtle);
  font-size: 0.8125rem;
  cursor: pointer;
}

.champ-tag-remove {
  border: 0;
  background: none;
  color: var(--champ-text-subtle);
  cursor: pointer;
  line-height: 1;
}

.champ-badge-locked {
  color: var(--champ-text-subtle);
}

/* ── Password gate ─────────────────────────────────────────────────────── */

.champ-gate {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--champ-space-5);
  max-width: 24rem;
  margin-inline: auto;
  padding: var(--champ-space-6) var(--champ-gutter);
}

.champ-gate-form {
  display: flex;
  flex-direction: column;
  gap: var(--champ-space-3);
  width: 100%;
}

/* ── Odds and ends ─────────────────────────────────────────────────────── */

.champ-avatar-small {
  width: 1.25rem;
  height: 1.25rem;
}

.champ-footer-text,
.champ-footer-preference {
  margin: 0;
}

.champ-topic-time,
.champ-topic-views,
.champ-unread-label {
  color: var(--champ-text-subtle);
}

.champ-page-step {
  min-width: auto;
  padding-inline: var(--champ-space-3);
}

.champ-composer-prompt,
.champ-login-prompt-link {
  display: inline-block;
  margin-block-start: var(--champ-space-4);
}

.champ-upload-files:not(:empty) {
  display: flex;
  flex-wrap: wrap;
  gap: var(--champ-space-2);
  margin-block-end: var(--champ-space-2);
}

.champ-tagged-topics-empty {
  padding-block: var(--champ-space-6);
}

.champ-design-v2 .champ-author-link {
  color: var(--champ-heading);
  text-decoration: none;
}

.champ-design-v2 .champ-author-link:hover {
  text-decoration: underline;
}

/* ══════════════════════════════════════════════════════════════════════════
 * Chat widget (Phase 13), copied from app_proxy.css.
 *
 * v2 does not load the classic sheet, so the docked widget rendered
 * completely unstyled. The block is already fully `champ-chat-*` namespaced
 * with no Bootstrap and no utilities — it was written that way deliberately —
 * so it transfers verbatim. Keep the two copies in step until the classic
 * path is retired.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ------------------------------------------- docked chat widget (v2 only)

   CHAT-TODO §5. Not from v1: v1's chat was a page, and every rule in its
   component block above is chained off `.champ-message`, the page container.

   ⚠️ The panel is NAMESPACED on purpose. It first reused v1's `champ-message*`
   class names so the three production shops that style them would keep
   matching — and measured against their real CSS on staging, that is exactly
   what broke it: rules written for the 800px page squeezed the composer to
   36px. Everything the panel renders now uses `champ-chat-*` and carries NO
   Bootstrap utilities, so no merchant rule aimed at the old page reaches it
   and every shop gets the same widget. `chat_widget_test.rb` guards it.

   Dropping the utilities also removed every `!important` this block used to
   need: there is nothing left to out-shout.

   The launcher's colour comes from the shop's own palette through the two
   custom properties set on `.champ-chat` (see `chat_launcher_style`), because
   a hardcoded near-black disappears on a dark storefront — measured on shop
   167, whose page background is rgb(36,36,36) against the launcher's
   rgb(22,32,44). */

.champ-chat {
  position: fixed;
  right: 20px;
  bottom: 20px;
  /* Below Bootstrap's modal backdrop (1040) so the notifications modal still
     covers the widget rather than the launcher punching through it. */
  z-index: 1030;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 12px;
}

/* The other corner. The panel follows the launcher, so both sides only need
   the one rule; `align-items` is what keeps the panel's edge flush with it. */
.champ-chat[data-champ-chat-position="left"] {
  right: auto;
  left: 20px;
  align-items: flex-start;
}

/* ---------------------------------------------------------- the launcher */

.champ-chat-launcher {
  position: relative;
  width: 56px;
  height: 56px;
  flex: none;
  /* The ring is what separates the launcher from a background of the SAME
     colour — the accent alone cannot, whatever we set it to. */
  border: 2px solid rgba(255, 255, 255, 0.85);
  border-radius: 50%;
  background-color: var(--champ-chat-accent, #16202c);
  color: var(--champ-chat-on-accent, #ffffff);
  box-shadow: 0 4px 14px rgba(15, 25, 40, 0.28);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.champ-chat-launcher:hover {
  transform: translateY(-1px);
  box-shadow: 0 6px 18px rgba(15, 25, 40, 0.34);
}

.champ-chat-launcher:active {
  transform: translateY(0);
}

/* The launcher swaps its glyph rather than its position, so the control stays
   in one place whether the panel is open or shut. */
.champ-chat-launcher .champ-chat-launcher-close {
  display: none;
}

.champ-chat[data-champ-chat-open] .champ-chat-launcher .champ-chat-launcher-open {
  display: none;
}

.champ-chat[data-champ-chat-open] .champ-chat-launcher .champ-chat-launcher-close {
  display: block;
}

.champ-chat-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 20px;
  height: 20px;
  padding: 0 5px;
  border-radius: 10px;
  border: 2px solid #fff;
  background-color: #e5484d;
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
}

.champ-chat-badge[hidden] {
  display: none;
}

/* ------------------------------------------------------------- the panel */

.champ-chat-panel {
  width: 380px;
  height: 540px;
  max-height: calc(100vh - 120px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: 14px;
  background-color: #fff;
  box-shadow: 0 12px 38px rgba(15, 25, 40, 0.26);
}

.champ-chat-panel[hidden] {
  display: none;
}

.champ-chat-header {
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 52px;
  padding: 0 8px 0 16px;
  background-color: var(--champ-chat-accent, #16202c);
  color: var(--champ-chat-on-accent, #ffffff);
}

.champ-chat-title {
  flex: 1;
  min-width: 0;
  font-size: 15px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.champ-chat-header-button {
  flex: none;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: inherit;
  opacity: 0.75;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.champ-chat-header-button:hover {
  opacity: 1;
  background-color: rgba(255, 255, 255, 0.12);
}

.champ-chat-header-button[hidden] {
  display: none;
}

.champ-chat-back {
  margin-left: -8px;
}

.champ-chat-body {
  flex: 1;
  min-height: 0;
  position: relative;
}

.champ-chat-view {
  height: 100%;
}

.champ-chat-view[hidden] {
  display: none;
}

/* ------------------------------------------ list view (message_groups#index) */

.champ-chat-panel .champ-chat-list {
  height: 100%;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.champ-chat-row {
  border-bottom: 1px solid #edf0f4;
}

/* The rows are <button> now: they act rather than navigate. This is the same
   reset the `a` rule used to be, plus the UA button styling to undo. */
.champ-chat-row > button,
.champ-chat-archive-row > button {
  appearance: none;
  background: none;
  border: 0;
  margin: 0;
  font: inherit;
  text-align: left;
  text-decoration: none;
  color: inherit;
  display: block;
  width: 100%;
  cursor: pointer;
}

.champ-chat-conversation {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 60px;
  padding: 8px 14px;
  color: #1c2430;
}

.champ-chat-conversation:hover {
  background-color: #f4f6f9;
}

.champ-chat-conversation.active {
  background-color: #eef1f6;
}

.champ-chat-media {
  display: flex;
  align-items: center;
  width: 100%;
  min-width: 0;
}

/* v1 hid the name and shrank the avatar to fit a 40px page rail, and hid the
   labels below `lg`. The panel is ~366px wide even on a phone, so the words
   fit — and a bare icon is not a control anyone can read. */
.champ-chat-name {
  flex: 1;
  min-width: 0;
  font-size: 14px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.champ-chat-conversation .champ-chat-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  margin-right: 10px;
  flex: none;
}

.champ-chat-new {
  justify-content: center;
  min-height: 48px;
  background-color: #f7f8fa;
  font-weight: 600;
}

.champ-chat-new-icon {
  margin-right: 8px;
}

.champ-chat-archive-row {
  text-align: center;
  padding: 10px 0;
}

.champ-chat-archive-row > button {
  color: #6b7480;
  text-decoration: none;
  font-size: 13px;
  text-align: center;
}

.champ-chat-clock {
  margin-right: 6px;
}

/* --------------------------------------------- thread view (messages#index) */

.champ-chat-panel .champ-chat-thread,
.champ-chat-panel .champ-chat-wrapper {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.champ-chat-history {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 14px 14px 4px;
  background-color: #f7f8fa;
}

.champ-chat-next-page {
  text-align: center;
  font-size: 13px;
  margin: 0 0 10px;
}

.champ-chat-composer {
  flex: none;
  display: flex;
  align-items: flex-end;
  gap: 8px;
  padding: 10px 12px;
  border-top: 1px solid #e6e9ef;
  background-color: #fff;
}

.champ-chat-textarea {
  flex: 1;
  min-width: 0;
  height: 38px;
  max-height: 96px;
  padding: 9px 12px;
  border: 1px solid #d9dee7;
  border-radius: 19px;
  font-size: 14px;
  line-height: 18px;
  resize: none;
}

.champ-chat-textarea:focus {
  outline: 0;
  border-color: var(--champ-chat-accent, #16202c);
}

.champ-chat-send {
  flex: none;
}

.champ-chat-send-btn {
  border: 0;
  border-radius: 19px;
  padding: 9px 16px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  background-color: var(--champ-chat-accent, #16202c);
  color: var(--champ-chat-on-accent, #ffffff);
}

.champ-chat-send-btn:disabled {
  opacity: 0.55;
  cursor: default;
}

/* The composer's failure message (CHAT-TODO §3b). */
.champ-chat-error {
  flex: none;
  padding: 8px 14px;
  background-color: #fdecee;
  color: #a4262c;
  font-size: 13px;
  line-height: 17px;
}

.champ-chat-error[hidden] {
  display: none;
}

/* v1 floats this over the history, top-right, where the panel header now is. */
.champ-chat-archive {
  display: none;
}

/* ------------------------------------------------------------- the bubbles */

.champ-chat-message {
  display: flex;
  align-items: flex-start;
  margin-bottom: 12px;
}

.champ-chat-message-own {
  justify-content: flex-end;
}

.champ-chat-bubble {
  max-width: 78%;
  padding: 8px 12px;
  border-radius: 16px;
  font-size: 14px;
  line-height: 19px;
  word-break: break-word;
  background-color: #eef1f6;
  color: #1c2430;
}

.champ-chat-message-own .champ-chat-bubble {
  background-color: #2f6fed;
  color: #fff;
  border-bottom-right-radius: 5px;
}

.champ-chat-message-other .champ-chat-bubble {
  border-bottom-left-radius: 5px;
}

/* Each row carries both avatars; which one shows is a matter of side. */
.champ-chat-message-own .champ-chat-avatar-before,
.champ-chat-message-other .champ-chat-avatar-after {
  display: none;
}

.champ-chat-message .champ-chat-avatar {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  flex: none;
}

.champ-chat-avatar-before .champ-chat-avatar {
  margin-right: 8px;
}

.champ-chat-avatar-after .champ-chat-avatar {
  margin-left: 8px;
}

/* --------------------------------------------- user search (message_users) */

.champ-chat-panel #message-users-search-results {
  height: auto !important;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.champ-chat-panel .champ-chat-thread > .p-3 {
  flex: none;
}

/* ---------------------------------------------------------------- phones */

@media (max-width: 575.98px) {
  /* Both corners collapse to the same full-width sheet on a phone. */
  .champ-chat,
  .champ-chat[data-champ-chat-position="left"] {
    right: 12px;
    bottom: 12px;
    left: 12px;
  }

  .champ-chat[data-champ-chat-position="left"] {
    align-items: flex-start;
  }

  .champ-chat-panel {
    width: 100%;
    /* `vh` first as the fallback, then `dvh`: on mobile Safari `100vh` is the
       LARGE viewport (URL bar retracted), and since the widget is anchored to
       the bottom, an over-tall panel pushes its own header — the back and
       close buttons — off the top of the screen. */
    height: calc(100vh - 100px);
    height: calc(100dvh - 100px);
    max-height: none;
  }
}

/* Names invented while converting the chat fragments: each replaced a
   Bootstrap utility that WAS doing layout work, so each needs a rule.
   (The other bare `champ-*` names in those templates are hooks that carry no
   styling in the classic sheet either — they exist for merchant CSS.) */

.champ-chat-search {
  padding: var(--champ-space-3);
  border-block-end: 1px solid var(--champ-border);
  background-color: var(--champ-surface);
}

.champ-chat-empty {
  padding: var(--champ-space-3);
  color: var(--champ-text-subtle);
}

.champ-design-v2 .champ-chat-user {
  display: block;
  padding: var(--champ-space-3);
  color: inherit;
  text-decoration: none;
}

.champ-design-v2 .champ-chat-user:hover {
  background-color: var(--champ-surface-sunken);
}

.champ-gate-brand {
  text-align: center;
}

.champ-poll-result-label {
  min-width: 0;
}

/* A topic row with no avatar — the profile's post list and the unread page
   both render rows that lead with the title. */
.champ-topic-row:not(:has(.champ-topic-avatar)) {
  grid-template-columns: minmax(0, 1fr);
}

.champ-topic-excerpt {
  color: var(--champ-text-subtle);
}

/* A navigation in flight. Deliberately restrained — a spinner on a fetch that
   usually resolves in a few hundred ms is more distracting than no spinner. */
.champ-navigating .champ-main {
  opacity: 0.65;
  transition: opacity 120ms ease-in;
}

@media (prefers-reduced-motion: reduce) {
  .champ-navigating .champ-main {
    transition: none;
  }
}

/* ------------------------------------- chat: the pieces Phase 18 introduced */

/* `champ-chat-close` and the two view modifiers reach the stylesheet for the
   first time here. They existed in the shared v1 partial, which the style
   guard never scanned because it only reads `+v2` templates — so they went
   unstyled and unnoticed until the widget got a v2 variant of its own. */
.champ-chat-close {
  margin-inline-start: auto;
}

.champ-chat-view-list,
.champ-chat-view-thread {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* Sits between the loaded messages and the ones above them. */
.champ-chat-older {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: 13px;
  color: var(--champ-link, #2f5bea);
  cursor: pointer;
  text-decoration: underline;
}

/* v1 sized this `height: calc(100% - 71px)` — a magic number tied to the old
   header's height. The panel is a flex column, so it can just take the rest. */
.champ-chat-results {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.champ-chat-results-hint {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 100%;
  margin: 0;
  padding: 24px;
  text-align: center;
  color: #6b7480;
}

.champ-chat-search-input {
  border-radius: 20px;
}
  /* One attached file under a composer (`upload_controller.js`). These rows
     used to be Bootstrap grid markup (`row`, `col-2`) built in JavaScript,
     which v2 does not style. */
  .champ-upload {
    display: grid;
    gap: var(--champ-space-2);
  }

  .champ-upload > [data-upload-widget] {
    justify-self: start;
  }

  .champ-upload-item {
    flex: 1 1 100%;
    display: flex;
    align-items: center;
    gap: var(--champ-space-3);
    padding-block: var(--champ-space-2);
    border-block-end: 1px solid var(--champ-border);
  }

  .champ-upload-name {
    flex: 1;
    min-width: 0;
    overflow-wrap: anywhere;
    font-size: var(--champ-text-sm);
  }

  .champ-upload-remove {
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
    font-size: var(--champ-text-sm);
    color: var(--champ-link);
    text-decoration: underline;
  }

  /* ── Post reactions (Phase 20) ─────────────────────────────────────────
     The heart fills when the reader has upvoted. `aria-pressed` is the
     state, so assistive tech hears it as a toggle, and the CSS keys off the
     same attribute rather than a parallel class. */
  .champ-reaction[aria-pressed="true"] {
    color: var(--champ-danger, #c0392b);
  }

  .champ-reaction[aria-pressed="true"] .champ-icon {
    fill: currentColor;
  }

  .champ-reaction:disabled {
    opacity: 0.6;
  }

  /* ── Poll slot (Phase 20) ──────────────────────────────────────────────
     The <li> in the thread that holds the poll's Turbo Frame. When the frame
     has loaded but the poll is hidden (v1 hides it while a topic's only post
     awaits approval), the slot collapses instead of leaving an empty row. */
  .champ-poll-slot {
    list-style: none;
  }

  .champ-poll-slot:has(turbo-frame[complete]):not(:has(.champ-poll)) {
    display: none;
  }

  /* ── Image lightbox (Phase 20) ───────────────────────────────────────── */
  .champ-post-content img:not(a img) {
    cursor: zoom-in;
  }

  .champ-lightbox {
    max-width: min(92vw, 1200px);
    max-height: 92vh;
    padding: 0;
    border: 0;
    border-radius: var(--champ-radius);
    background: transparent;
    overflow: visible;
  }

  .champ-lightbox::backdrop {
    background: rgba(10, 14, 20, 0.82);
  }

  .champ-lightbox-figure {
    margin: 0;
    display: grid;
    gap: var(--champ-space-2);
    justify-items: center;
  }

  .champ-lightbox-image {
    display: block;
    max-width: min(92vw, 1200px);
    max-height: 85vh;
    width: auto;
    height: auto;
    border-radius: var(--champ-radius);
    background: var(--champ-surface);
  }

  .champ-lightbox-caption {
    color: #fff;
    font-size: var(--champ-text-sm);
    text-align: center;
  }

  .champ-lightbox-close {
    position: absolute;
    inset-block-start: -44px;
    inset-inline-end: 0;
    width: 36px;
    height: 36px;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.15);
    color: #fff;
    cursor: pointer;
  }

  .champ-lightbox-close:hover {
    background: rgba(255, 255, 255, 0.28);
  }

  /* ── Merchant snippets written for Bootstrap ───────────────────────────
     A merchant's own Liquid snippets are injected into our pages, and many
     were written against the classic design, which loads Bootstrap. v2 does
     not, so Bootstrap stops hiding things it used to hide — the demo store's
     image lightbox rendered its "View Image" heading and Close buttons under
     the footer on every page.

     A closed Bootstrap modal is `display: none` until Bootstrap adds `.show`,
     so that invariant is restored here and nothing more. v2 never emits these
     classes itself (the markup test forbids Bootstrap names), so this can
     only reach merchant markup — and it sits in the components layer, where a
     merchant's own CSS still wins. */
  .champ-design-v2 .modal:not(.show),
  .champ-design-v2 .modal-backdrop:not(.show) {
    display: none;
  }

  /* The access bounce on `moderate#index`. Centred and quiet: it is on screen
     for a moment with JS on, and is the whole page without it. */
  .champ-leaving {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    padding: 48px 16px;
    text-align: center;
  }
}

@layer champ.state {
  /* The UA `[hidden]` invariant, restored. Any component rule that sets
     `display` used to beat it on specificity; a later layer cannot be beaten
     by an earlier one at all. */
  .champ-design-v2 [hidden] {
    display: none;
  }
}
