/* ============================================================================
   BitcoinPuzzles — challenges
   Index grid, challenge card, detail page and tabs.
   Depends on tokens.css and base.css.
   ========================================================================= */

/* ---------------------------------------------------------------------------
   Page head — a compact title band, not a full-viewport hero. The visitor
   came to look at challenges; the challenges start above the fold.
   ------------------------------------------------------------------------ */

.puzzles-page {
  padding-block: var(--sp-10) var(--sp-16);
}

.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-6);
  flex-wrap: wrap;
  margin-bottom: var(--sp-6);
}

.page-head__title {
  font-size: var(--fs-3xl);
  letter-spacing: var(--tr-display);
  margin-bottom: var(--sp-2);
}

.page-head__subtitle {
  color: var(--ink-400);
  font-size: var(--fs-md);
  max-width: 56ch;
}

/* ---------------------------------------------------------------------------
   Ledger strip — platform totals at data scale, the way an explorer prints
   its status bar. Deliberately not a hero-metric block: these numbers are
   context for the grid below, not the headline.
   ------------------------------------------------------------------------ */

/* A 1px gap over a rule-coloured ground: separators resolve on both axes and
   in both directions, without per-cell borders (which would drop a dangling
   rule beside a wrapped cell and leave it with no rule above).

   Flex, not grid, because the strip is used with two cells (index) and three
   (home), and only flex sizes the track count to the cells that actually
   exist. `repeat(auto-fit, minmax(150px, 1fr))` cannot: auto-fit collapses
   *empty* tracks, so pairing it with a full-row span on the odd last cell was
   self-defeating — the spanning cell occupies every track, nothing collapses,
   and the strip resolved to eight tracks with six of them showing bare ground
   next to a stranded pair. `flex: 1 1 150px` grows the cells to close the row
   exactly, at any count, and a wrapped row stretches to close itself too. */
.ledger-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 1px;
  margin: 0 0 var(--sp-8);
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--line);
  overflow: hidden;
}

.ledger-strip__cell {
  flex: 1 1 150px;
  min-width: 0;
  padding: var(--sp-3) var(--sp-5);
  background: var(--ink-900);
  display: flex;
  flex-direction: column;
}

.ledger-strip__value {
  margin: 0;
  /* Seat every figure on the cell floor. Cells stretch to the tallest sibling,
     so a label that wraps to two lines at narrow widths would otherwise push
     its own figure down and leave the row's totals on three baselines. */
  margin-block-start: auto;
  padding-block-start: 2px;
  font-size: var(--fs-xl);
  font-weight: 600;
  color: var(--ink-050);
  line-height: 1.2;
  /* A machine value is one atom. ru delimits thousands with U+0020, so without
     this a prize breaks mid-figure across two lines. */
  white-space: nowrap;
}

.ledger-strip__value--accent {
  color: var(--accent);
}

.ledger-strip__unit {
  font-size: var(--fs-xs);
  font-weight: 600;
  margin-inline-start: 0.35em;
  opacity: 0.7;
}

/* USD conversion hung under a prize figure. It lives inside the value's <dd>
   so figure and conversion travel as one cell when the strip wraps, and it
   stays block-level so the conversion never competes with the figure for the
   same line. */
.ledger-strip__sub {
  display: block;
  padding-block-start: 2px;
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--ink-300);
}

/* Values are seated on the cell floor, so a hung conversion lifts its own
   figure. When any cell in the strip carries one, cells without it reserve
   the same slot — every main figure stays on one shared baseline. Browsers
   without :has() fall back to the lifted figures, which is only a nuance. */
.ledger-strip:has(.ledger-strip__sub) .ledger-strip__value:not(:has(.ledger-strip__sub)) {
  padding-block-end: calc(var(--fs-xs) * 1.2 + 2px);
}

/* ---------------------------------------------------------------------------
   Challenge grid
   ------------------------------------------------------------------------ */

.puzzles-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: var(--sp-5);
}

/* ---------------------------------------------------------------------------
   Challenge card
   The whole card is one target: the title carries a stretched link so the
   grid stops shouting seven identical orange buttons at the visitor.
   ------------------------------------------------------------------------ */

.pz-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--ink-850);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  transition: border-color var(--dur-2) var(--ease),
    transform var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease);
}

.pz-card:hover {
  border-color: var(--line-hi);
  transform: translateY(-2px);
  box-shadow: var(--sh-3);
}

.pz-card:focus-within {
  border-color: var(--accent);
}

/* --- Media -------------------------------------------------------------- */

/* The band is two layers: the address-derived mark on a plain sunken ground,
   and the artwork over it. The mark is not the imageless case's consolation —
   it is the band's floor in every case, so a request that is slow, blocked or
   404 leaves the card reading as a record rather than as a hole in the grid.
   See the `media` controller for which layer ends up standing. */
.pz-card__media {
  --identicon-size: 68px;
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--ink-900);
  border-bottom: 1px solid var(--line);
}

.pz-card__media .media-fill {
  transition: transform var(--dur-3) var(--ease-out);
}

.pz-card:hover .pz-card__media .media-fill {
  transform: scale(1.03);
}

/* The chip sits over artwork of unknown brightness, so it carries its own
   opaque plate rather than relying on the image behind it. */
.pz-card__state {
  position: absolute;
  top: var(--sp-3);
  inset-inline-start: var(--sp-3);
  background: rgba(11, 12, 14, 0.82);
  backdrop-filter: blur(8px);
  box-shadow: var(--sh-1);
}

.pz-card__state.chip--open {
  border-color: rgba(62, 201, 192, 0.5);
}

.pz-card__state.chip--solved {
  border-color: rgba(91, 196, 122, 0.5);
}

/* --- Body --------------------------------------------------------------- */

.pz-card__body {
  padding: var(--sp-4) var(--sp-5) var(--sp-3);
  flex: 1;
}

.pz-card__title {
  font-size: var(--fs-md);
  line-height: var(--lh-snug);
  margin-bottom: var(--sp-2);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: calc(2 * var(--lh-snug) * var(--fs-md));
}

.pz-card__title a {
  color: var(--ink-050);
}

/* Stretched link: one primary target covering the card. */
.pz-card__title a::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

.pz-card:hover .pz-card__title a {
  color: var(--accent);
}

.pz-card__desc {
  font-size: var(--fs-sm);
  color: var(--ink-400);
  line-height: var(--lh-body);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: calc(2 * var(--lh-body) * var(--fs-sm));
}

/* --- Facts -------------------------------------------------------------- */

.pz-card__facts {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-4);
  margin: 0;
  padding: var(--sp-3) var(--sp-5);
  border-top: 1px solid var(--line);
}

.pz-card__fact {
  min-width: 0;
}

.pz-card__fact dd {
  margin: 2px 0 0;
}

.pz-card__fact:last-child {
  text-align: end;
  flex: 1;
  min-width: 0;
}

.pz-card__prize {
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--accent);
  line-height: 1.2;
  white-space: nowrap;
}

.pz-card__unit {
  font-size: var(--fs-2xs);
  font-weight: 600;
  margin-inline-start: 0.3em;
  opacity: 0.7;
}

.pz-card__usd {
  display: block;
  padding-block-start: 1px;
  font-size: var(--fs-2xs);
  font-weight: 500;
  color: var(--ink-300);
}

/* --- Foot --------------------------------------------------------------- */

.pz-card__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-5);
  border-top: 1px solid var(--line);
  background: var(--ink-900);
  min-height: 48px;
}

/* No `min-width: 0` here. The footer's other child cannot shrink, so zeroing
   this block's automatic min-content floor let it collapse to 0px whenever the
   two didn't fit — avatar, name and badge then overflowed and painted on top of
   each other. The floor stays; the name below is the one part that gives. */
.pz-card__author {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex: 1 1 auto;
}

/* Raised above the stretched link so the author stays independently clickable. */
.pz-card__author-name {
  position: relative;
  z-index: 2;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--ink-300);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* The avatar and the level badge are the fixed anchors of the byline: both
   carry meaning that truncation would destroy, so neither shrinks. */
.pz-card__author > .avatar,
.pz-card__author > .user-level-badge {
  flex: none;
}

.pz-card__author-name:hover {
  color: var(--accent);
}

.pz-card__solver {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
  color: var(--solved);
  position: relative;
  z-index: 2;
}

.pz-card__solver-link {
  display: flex;
  border-radius: var(--r-full);
}

.pz-card__solver-link .avatar {
  transition: border-color var(--dur-1) var(--ease);
}

.pz-card__solver-link:hover .avatar {
  border-color: var(--solved);
}

.pz-card--solved .pz-card__media {
  opacity: 0.75;
}

.pz-card--solved:hover .pz-card__media {
  opacity: 1;
}

/* ---------------------------------------------------------------------------
   Index footer + pagination
   ------------------------------------------------------------------------ */

.puzzles-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-4);
  margin-top: var(--sp-16);
  padding-top: var(--sp-10);
  border-top: 1px solid var(--line);
  text-align: center;
}

.puzzles-footer p {
  color: var(--ink-400);
  font-size: var(--fs-base);
}

.puzzles-pagination {
  margin-top: var(--sp-10);
  display: flex;
  justify-content: center;
}

.pagy.nav {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  flex-wrap: wrap;
  justify-content: center;
}

.pagy.nav a,
.pagy.nav a.gap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  height: 34px;
  padding: 0 var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink-300);
  background: var(--ink-850);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  transition: background-color var(--dur-1) var(--ease),
    border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}

.pagy.nav a:hover {
  background: var(--ink-750);
  border-color: var(--line-hi);
  color: var(--ink-050);
}

.pagy.nav a.current {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

/* Pagy marks the current page as `a.current[aria-disabled="true"]`, which ties
   this rule's specificity with the one above and — being later — used to erase
   the current page marker on every paginated surface. */
.pagy.nav a[aria-disabled="true"]:not(.current),
.pagy.nav a.gap {
  opacity: 0.4;
  pointer-events: none;
  background: transparent;
  border-color: transparent;
}

/* ---------------------------------------------------------------------------
   Legacy button aliases — older partials still ask for these names.
   ------------------------------------------------------------------------ */

.btn--outline {
  background: var(--ink-800);
  border-color: var(--line-hi);
  color: var(--ink-050);
}

.btn--outline:hover {
  background: var(--ink-750);
  border-color: var(--line-hard);
  color: var(--ink-050);
}

.btn--small {
  --btn-py: 0.3125rem;
  --btn-px: 0.625rem;
  font-size: var(--fs-sm);
}

/* ============================================================================
   Challenge detail
   ========================================================================= */

.puzzle-detail {
  padding-block: var(--sp-6) var(--sp-16);
}

.puzzle-detail__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  margin-bottom: var(--sp-6);
}

.puzzle-detail__back {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--ink-400);
}

.puzzle-detail__back:hover {
  color: var(--ink-050);
}

[dir="rtl"] .puzzle-detail__back .icon {
  transform: scaleX(-1);
}

.puzzle-detail__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-6);
  align-items: start;
}

/* Flex and grid items default to min-width:auto, so the horizontally
   scrolling tab bar would otherwise force the column wider than the page. */
.puzzle-detail__head,
.puzzle-detail__main,
.puzzle-detail__side {
  min-width: 0;
}

.puzzle-detail__head {
  margin-bottom: var(--sp-6);
}

.puzzle-detail__title {
  font-size: var(--fs-3xl);
  letter-spacing: var(--tr-display);
  margin: var(--sp-3) 0 var(--sp-4);
}

.puzzle-detail__byline {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  font-size: var(--fs-sm);
  color: var(--ink-400);
}

.byline__who {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}

.byline__name {
  font-weight: 600;
  color: var(--ink-100);
}

.byline__who:hover .byline__name {
  color: var(--accent);
}

.byline__sep {
  color: var(--ink-600);
}

.byline__date {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
}

/* The challenge artwork, which is also the button that opens the lightbox.
   Frame and trigger are one element rather than a framed wrapper around a
   `div[role=button]`: the div took a click handler and no key handler, so the
   image could not be opened from a keyboard at all, and the wrapper's
   `overflow: hidden` clipped the focus ring off whatever was inside it. */
.puzzle-detail__zoom {
  position: relative;
  display: block;
  inline-size: 100%;
  margin-block-end: var(--sp-6);
  padding: 0;
  overflow: hidden;
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  cursor: zoom-in;
}

.puzzle-detail__zoom img {
  inline-size: 100%;
}

/* The hint is the affordance, so it is absent until the pointer or the keyboard
   arrives — at rest the cell is the artwork and nothing else. A scrim rather
   than the card chip's frosted plate: this covers the whole image, and
   `backdrop-filter` over a full-size photo is the one place it costs a frame. */
.puzzle-detail__zoom-hint {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-050);
  background: rgba(0, 0, 0, 0.55);
  opacity: 0;
  transition: opacity var(--dur-1) var(--ease);
}

.puzzle-detail__zoom:hover .puzzle-detail__zoom-hint,
.puzzle-detail__zoom:focus-visible .puzzle-detail__zoom-hint {
  opacity: 1;
}

/* ---------------------------------------------------------------------------
   The stake — prize, the wallet it is staked on, and that wallet's live
   balance, read as one record because they are one record.
   ------------------------------------------------------------------------ */

.stake {
  overflow: hidden;
}

.stake__prize {
  padding: var(--sp-5);
  background: var(--accent-wash);
  border-bottom: 1px solid var(--accent-dim);
}

.stake__amount {
  margin-top: var(--sp-1);
  font-size: var(--fs-3xl);
  font-weight: 600;
  line-height: 1.1;
  color: var(--accent);
  letter-spacing: var(--tr-tight);
}

.stake__unit {
  font-size: var(--fs-md);
  font-weight: 600;
  margin-inline-start: 0.3em;
  opacity: 0.7;
}

.stake__usd {
  display: block;
  padding-block-start: 2px;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--ink-300);
}

.stake__address,
.stake__balance {
  padding: var(--sp-4) var(--sp-5);
}

.stake__code {
  display: block;
  margin: var(--sp-2) 0 var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--ink-100);
  background: var(--ink-900);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  word-break: break-all;
  line-height: 1.5;
}

.stake__actions {
  display: flex;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

.copy-text-copied {
  display: none;
}

.copied .copy-text {
  display: none;
}

.copied .copy-text-copied {
  display: inline;
}

.stake__balance-amount {
  margin-top: var(--sp-2);
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--ink-200);
}

.stake__balance-amount.has-balance {
  color: var(--solved);
}

.balance-loading {
  color: var(--ink-400);
  font-size: var(--fs-sm);
  font-family: var(--font-ui);
}

.stake__balance-tx {
  margin-top: var(--sp-2);
}

.balance-info {
  color: var(--ink-400);
  font-size: var(--fs-xs);
  font-family: var(--font-mono);
}

/* --- Call to action ----------------------------------------------------- */

.puzzle-detail__cta {
  margin-top: var(--sp-4);
}

.solved-banner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-5);
  text-align: center;
  background: var(--solved-wash);
  border: 1px solid rgba(91, 196, 122, 0.3);
  border-radius: var(--r-lg);
  color: var(--solved);
}

.solved-banner > span {
  font-weight: 700;
  font-size: var(--fs-md);
}

.solved-banner small {
  color: var(--ink-300);
  font-size: var(--fs-sm);
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  justify-content: center;
}

.solved-by-link {
  color: var(--ink-050);
  font-weight: 600;
}

.solved-by-link:hover {
  color: var(--accent);
}

/* ---------------------------------------------------------------------------
   Tabs
   ------------------------------------------------------------------------ */

.puzzle-detail__tabs {
  margin-top: var(--sp-8);
}

.tabs-nav {
  display: flex;
  gap: var(--sp-1);
  overflow-x: auto;
  scrollbar-width: none;
  border-bottom: 1px solid var(--line-hi);
}

.tabs-nav::-webkit-scrollbar {
  display: none;
}

.tabs-nav__btn {
  position: relative;
  flex: none;
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-400);
  white-space: nowrap;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color var(--dur-1) var(--ease),
    border-color var(--dur-1) var(--ease);
}

.tabs-nav__btn:hover {
  color: var(--ink-100);
}

.tabs-nav__btn.active {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

.tabs-content {
  padding-top: var(--sp-6);
}

/* Loading: one determinate-looking bar, not a spinner farm. */
.tab-loading {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-10) 0;
  align-items: center;
  color: var(--ink-400);
  font-size: var(--fs-sm);
}

.tab-loading__bar {
  width: 160px;
  height: 2px;
  border-radius: var(--r-full);
  background: var(--ink-750);
  overflow: hidden;
  position: relative;
}

.tab-loading__bar::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: -40%;
  width: 40%;
  background: var(--accent);
  animation: tab-loading 1.1s var(--ease) infinite;
}

@keyframes tab-loading {
  to {
    inset-inline-start: 100%;
  }
}

/* ---------------------------------------------------------------------------
   Tab content sections
   ------------------------------------------------------------------------ */

.content-section {
  margin-bottom: var(--sp-8);
}

.content-section__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  margin-bottom: var(--sp-4);
}

.content-section h2 {
  font-size: var(--fs-lg);
  margin-bottom: var(--sp-4);
}

.content-section__header h2 {
  margin-bottom: 0;
}

.content-section__body {
  color: var(--ink-200);
  font-size: var(--fs-md);
  line-height: var(--lh-body);
  max-width: var(--w-prose);
  overflow-wrap: anywhere;
}

.content-section__body p + p {
  margin-top: var(--sp-4);
}

.steps-list {
  list-style: none;
  counter-reset: step;
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  max-width: var(--w-prose);
}

.steps-list li {
  counter-increment: step;
  position: relative;
  padding-inline-start: var(--sp-10);
}

.steps-list li::before {
  content: counter(step);
  position: absolute;
  inset-inline-start: 0;
  top: 0;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--accent);
  background: var(--accent-wash);
  border: 1px solid var(--accent-dim);
  border-radius: var(--r-full);
}

.steps-list strong {
  display: block;
  color: var(--ink-050);
  font-size: var(--fs-md);
  margin-bottom: var(--sp-1);
}

.steps-list p {
  color: var(--ink-400);
  font-size: var(--fs-base);
}

/* ---------------------------------------------------------------------------
   Responsive
   ------------------------------------------------------------------------ */

@media (min-width: 768px) {
  .puzzles-grid {
    gap: var(--sp-6);
  }

  .puzzle-detail__title {
    font-size: var(--fs-4xl);
  }
}

@media (min-width: 1024px) {
  .puzzle-detail__grid {
    grid-template-columns: minmax(0, 1fr) 340px;
    grid-template-areas:
      "head head"
      "main side";
    column-gap: var(--sp-10);
    row-gap: 0;
  }

  .puzzle-detail__head {
    grid-area: head;
  }
  .puzzle-detail__main {
    grid-area: main;
    min-width: 0;
  }
  .puzzle-detail__side {
    grid-area: side;
    position: sticky;
    top: calc(var(--h-header) + var(--sp-6));
  }
}

@media (max-width: 1023px) {
  /* Phone reading order: what the challenge is, then the stake and its
     action, then the image and the discussion. */
  .puzzle-detail__grid {
    display: flex;
    flex-direction: column;
    /* The base rule aligns grid items to `start`; in a column flex container
       that makes each child size to its max-content and overflow the page.
       Stretch is what the single-column layout actually wants. */
    align-items: stretch;
  }

  .puzzle-detail__head {
    order: 0;
    margin-bottom: var(--sp-2);
  }
  .puzzle-detail__side {
    order: 1;
  }
  .puzzle-detail__main {
    order: 2;
  }
}

/* ===========================================================================
   REVIEWS TAB (Avaliações)

   Recomposed onto the panel primitive. What was here before was a stack of
   three unrelated 12px cards with a comment panel nested inside a review
   panel; the summary spent 292px of height on one number, and the average
   said 4.07 without ever showing that two of the fourteen scores were a 1.
   ======================================================================== */

.reviews {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding-block: var(--sp-4);
}

/* --- Summary: the average, and the shape behind it ---------------------- */

.reviews__summary {
  display: grid;
  gap: var(--sp-5) var(--sp-8);
  padding: var(--sp-5);
}

@media (min-width: 560px) {
  .reviews__summary {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
  }
}

.reviews__score {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
}

/* "4,07" and "/5" are one numeric expression, so the pair is laid out LTR and
   isolated: left to the bidi algorithm, Hebrew reversed the flex order and the
   average read "5/4,07". */
.reviews__average {
  display: flex;
  align-items: baseline;
  gap: 0.1em;
  direction: ltr;
  unicode-bidi: isolate;
}

.reviews__average-value {
  font-size: var(--fs-3xl);
  font-weight: 700;
  line-height: 1;
  color: var(--accent);
}

.reviews__average-scale {
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  color: var(--ink-400);
}

.reviews__total {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-sm);
  color: var(--ink-300);
}

/* The histogram: one row per score, newest convention first (5 at the top). */
.reviews__dist {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--sp-1) var(--sp-3);
  margin: 0;
}

.reviews__dist-score {
  display: inline-flex;
  align-items: center;
  gap: 0.25em;
  font-size: var(--fs-sm);
  color: var(--ink-300);
}

.reviews__dist-score .icon {
  color: var(--ink-400);
}

.reviews__dist-value {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: 0;
}

/* The proportion lives in the SVG's own geometry, not in a width: production
   emits no style-src, so an inline style attribute is dropped by the CSP and
   every bar would silently render at zero. Under RTL the whole bar flips so
   the fill still grows from the inline start. */
.reviews__bar {
  flex: 1;
  min-inline-size: 0;
  display: block;
  block-size: 5px;
  transform: scaleX(var(--bar-dir, 1));
}

[dir="rtl"] .reviews__bar {
  --bar-dir: -1;
}

.reviews__bar-track {
  fill: var(--ink-800);
}

.reviews__bar-fill {
  fill: var(--accent);
}

.reviews__dist-count {
  min-inline-size: 3ch;
  font-size: var(--fs-sm);
  color: var(--ink-300);
  text-align: end;
}

/* --- Your rating -------------------------------------------------------- */

.reviews__fields {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

.reviews__score-input {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.reviews__readout {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-sm);
  color: var(--ink-300);
}

.reviews__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
}

/* button_to wraps its control in a form; unwrap it so the button is a direct
   child of the action row instead of a block that breaks the flex line. */
.reviews__remove {
  display: contents;
}

/* Star rating input.
   Real buttons in a radiogroup: the previous spans were mouse-only, which
   made the review form impossible to complete from a keyboard. The fill
   runs from the first star up to the hovered or focused one, expressed as a
   single :has() pair rather than six overlapping blocks. */
/* Each button is 44px square (28px glyph in an sp-2 pad), so the control is
   tappable on a phone; the group is pulled back by that pad so the first star
   still lines up with the field label above it. */
.star-rating-input {
  display: flex;
  margin-inline-start: calc(var(--sp-2) * -1);
}

.star-input {
  display: inline-flex;
  padding: var(--sp-2);
  background: none;
  border: none;
  border-radius: var(--r-sm);
  color: var(--ink-500);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease),
    transform var(--dur-1) var(--ease-out);
}

/* Committed state: every star up to and including the selection. Solid, so a
   chosen star matches the filled star the review will be displayed with. */
.star-input.selected {
  color: var(--accent);
}

.star-input.selected .icon,
.star-rating-input .star-input:hover .icon,
.star-rating-input .star-input:has(~ .star-input:hover) .icon,
.star-rating-input .star-input:focus-visible .icon,
.star-rating-input .star-input:has(~ .star-input:focus-visible) .icon {
  fill: currentColor;
}

/* The preview overrides the committed fill too: pointing at star 2 must empty
   stars 3 to 5, not leave them solid from an earlier choice. */
.star-rating-input:hover .star-input .icon,
.star-rating-input:focus-within .star-input .icon {
  fill: none;
}

/* Preview state: hovering or focusing a star fills it and everything before
   it, and empties everything after it. */
.star-rating-input:hover .star-input,
.star-rating-input:focus-within .star-input {
  color: var(--ink-500);
}

.star-rating-input .star-input:hover,
.star-rating-input .star-input:has(~ .star-input:hover),
.star-rating-input .star-input:focus-visible,
.star-rating-input .star-input:has(~ .star-input:focus-visible) {
  color: var(--accent);
}

.star-input:hover {
  transform: scale(1.08);
}

/* --- The reviews themselves --------------------------------------------- */

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

/* A score with no comment is one line, not a card with an empty body: four
   of the fourteen reviews on a typical challenge carry no text at all. */
.review {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr);
  column-gap: var(--sp-3);
  row-gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-5);
}

.review--mine {
  background: var(--ink-900);
}

.review__avatar {
  position: relative;
  grid-column: 1;
  grid-row: 1;
  display: block;
  inline-size: 28px;
  block-size: 28px;
  overflow: hidden;
  background: var(--ink-800);
  border-radius: var(--r-full);
}

.review__avatar img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
}

.review__initial {
  display: flex;
  align-items: center;
  justify-content: center;
  block-size: 100%;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-200);
}

.review__head {
  grid-column: 2;
  grid-row: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-3);
  min-block-size: 28px;
}

.review__who,
.review__meta {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

.review__name {
  font-weight: 600;
  color: var(--ink-050);
  transition: color var(--dur-1) var(--ease);
}

.review__name:hover {
  color: var(--accent);
}

/* Isolated for the same reason .mono is: under RTL the bidi algorithm splits
   "30/04 04:00" on its runs and prints the time before the date. */
.review__date {
  font-size: var(--fs-sm);
  color: var(--ink-400);
  white-space: nowrap;
  direction: ltr;
  unicode-bidi: isolate;
}

.review__body {
  grid-column: 2;
  grid-row: 2;
  max-inline-size: var(--w-prose);
  color: var(--ink-200);
}

.review__body p {
  margin: 0;
}

.review__body p + p {
  margin-block-start: var(--sp-2);
}

/* ===========================================================================
   HINTS TAB (Dicas) — the hint market

   A hint is a market position: its price rises 10% with every purchase, 40%
   of each sale is added to the challenge prize and 40% goes to the author.
   What was here before hid all of that. Six identical 250px cards stated the
   same price three times each (badge, "preço inicial", button), spent 190px
   apiece on a centred "Dica bloqueada", and printed "0 0" under every hint
   that had never been voted on — 1,900px of scroll carrying no evidence a
   buyer could act on, with a 40x price spread invisible unless you read
   every card.

   It is composed as a ledger of listings instead: the strip states the shape
   of the market, and each hint is one row whose price sits in a single
   right-aligned tabular column, so the spread reads on one glance down it.
   ======================================================================== */

.hmkt {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding-block: var(--sp-4);
}

.hmkt__market {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

/* The page-level strip carries a 2rem tail for the grid beneath it; inside a
   tab it is a header for the note directly below, so the gap comes from the
   flex column instead. */
.hmkt__strip {
  margin: 0;
}

.hmkt__mechanic {
  display: flex;
  align-items: start;
  gap: var(--sp-2);
  max-inline-size: 72ch;
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--ink-300);
}

.hmkt__mechanic .icon {
  margin-block-start: 0.15em;
  color: var(--ink-400);
}

/* --- Owner's form ------------------------------------------------------- */

.hmkt__fields {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

/* A price is three or four digits; a full-bleed field promises a paragraph.
   The cap goes on the control alone — capping the whole field also wrapped
   its hint into a narrow two-line column. */
.hmkt__price-field input {
  max-inline-size: 10rem;
}

.hmkt__price-field .field__hint {
  max-inline-size: 48ch;
}

.hmkt__label-unit {
  font-family: var(--font-mono);
  color: var(--ink-400);
}

.hmkt__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
}

/* --- The listings ------------------------------------------------------- */

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

/* Explicit columns for the ordinal and the price, auto rows for everything
   else: a locked row is one line, an open row grows a body and a vote strip
   beneath the same two columns without either case needing its own grid. */
.hmkt__row {
  display: grid;
  grid-template-columns: 1.75rem minmax(0, 1fr) auto;
  column-gap: var(--sp-4);
  row-gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
}

.hmkt__row--open {
  grid-template-columns: 1.75rem minmax(0, 1fr);
}

.hmkt__ord {
  grid-column: 1;
  grid-row: 1;
  align-self: center;
  font-size: var(--fs-sm);
  color: var(--ink-400);
}

.hmkt__head {
  grid-column: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  align-self: center;
  min-block-size: 24px;
}

.hmkt__row--open .hmkt__ord,
.hmkt__row--open .hmkt__head {
  align-self: start;
}

.hmkt__head .chip .icon {
  margin-inline-end: 0.125em;
}

/* The row's evidence: buyers, the community's verdict, the price it opened
   at. Dots are drawn between siblings rather than typed into the markup so a
   wrapped row never starts a line with a separator. */
.hmkt__facts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1) var(--sp-3);
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--fs-sm);
  color: var(--ink-300);
}

.hmkt__facts > li {
  display: inline-flex;
  align-items: center;
  gap: 0.375em;
}

.hmkt__facts > li + li::before {
  content: "";
  inline-size: 3px;
  block-size: 3px;
  margin-inline-end: calc(var(--sp-3) - 0.375em);
  border-radius: var(--r-full);
  background: var(--ink-600);
}

/* Both verdicts stay neutral. The thumb and the word carry the direction, so
   the colour budget is not spent weighting one signal over the other — a
   dislike is the more decision-relevant number here, not an error. */
.hmkt__fact--vote .icon {
  color: var(--ink-400);
}

/* --- Price and the buy control ------------------------------------------ */

.hmkt__deal {
  grid-column: 3;
  grid-row: 1;
  display: flex;
  align-items: center;
  justify-content: end;
  gap: var(--sp-4);
}

.hmkt__price {
  margin: 0;
  font-size: var(--fs-xl);
  font-weight: 600;
  line-height: 1.1;
  color: var(--accent);
  /* ru delimits thousands with U+0020; without this a price breaks in half. */
  white-space: nowrap;
}

.hmkt__unit {
  margin-inline-start: 0.35em;
  font-size: var(--fs-xs);
  font-weight: 600;
  opacity: 0.7;
}

.hmkt__buy {
  display: contents;
}

/* --- An unlocked hint --------------------------------------------------- */

.hmkt__body {
  grid-column: 2 / -1;
  max-inline-size: var(--w-prose);
  color: var(--ink-100);
}

.hmkt__body p {
  margin: 0;
}

.hmkt__body p + p {
  margin-block-start: var(--sp-2);
}

.hmkt__vote {
  grid-column: 2 / -1;
}

.hvote {
  display: flex;
  gap: var(--sp-2);
}

.hvote__form {
  display: contents;
}

.hvote__btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 0.25rem 0.5rem;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-300);
  background: var(--ink-800);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease),
    border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}

.hvote__btn:hover {
  background: var(--ink-750);
  border-color: var(--line-hard);
  color: var(--ink-050);
}

.hvote__btn--on {
  color: var(--ink-050);
  border-color: var(--line-hi);
  background: var(--ink-700);
}

/* --- Narrow ------------------------------------------------------------- */

/* Below the row's comfortable width the price and its control drop under the
   facts rather than squeezing them: the amount is money and must not wrap. */
@media (max-width: 599px) {
  .hmkt__row {
    grid-template-columns: 1.75rem minmax(0, 1fr);
  }

  .hmkt__ord,
  .hmkt__head {
    align-self: start;
  }

  .hmkt__deal {
    grid-column: 2;
    grid-row: auto;
    justify-content: space-between;
  }
}

/* ===========================================================================
   DIALOGS — the lightbox and the solution submission

   Two overlays live on this page, so the overlay is one block and each dialog
   is only its own content. They were two hand-copied 90%-black layers before,
   with two close buttons and two scale transitions, and only one of the copies
   ever received the visibility fix below.
   ======================================================================== */

.pz-dialog {
  position: fixed;
  inset: 0;
  z-index: var(--z-flash);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-4);
  background: rgba(0, 0, 0, 0.9);
  opacity: 0;
  visibility: hidden;
  /* Visibility is a discrete property: transitioning it means the element is
     still `hidden` when the open handler runs, so focus() silently fails and
     the dialog opens with focus stranded behind the overlay. Flip it
     instantly on open and delay it only on close. */
  transition: opacity var(--dur-3) var(--ease), visibility 0s linear var(--dur-3);
}

.pz-dialog.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur-3) var(--ease);
}

/* One close control for both dialogs. It carries a sunken plate and a hairline
   rather than sitting bare, because in the lightbox it lands over artwork of
   unknown brightness and a bare glyph there is invisible half the time. */
.pz-dialog__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 32px;
  block-size: 32px;
  flex: none;
  padding: 0;
  background: var(--ink-900);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  color: var(--ink-300);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease),
    border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}

.pz-dialog__close:hover {
  background: var(--ink-800);
  border-color: var(--line-hard);
  color: var(--ink-050);
}

/* In the lightbox there is no header row to seat it in — the image is the whole
   dialog — so the control floats in the frame's inline-end corner. */
.pz-dialog__close--float {
  position: absolute;
  z-index: 1;
  inset-block-start: var(--sp-3);
  inset-inline-end: var(--sp-3);
}

/* --- Lightbox ----------------------------------------------------------- */

.pz-lightbox {
  position: relative;
  display: flex;
  max-inline-size: 95vw;
  max-block-size: 95vh;
}

.pz-lightbox__img {
  max-block-size: 90vh;
  inline-size: auto;
  object-fit: contain;
  border-radius: var(--r-lg);
  box-shadow: var(--sh-4);
  transform: scale(0.97);
  transition: transform var(--dur-3) var(--ease-out);
}

.pz-dialog.is-open .pz-lightbox__img {
  transform: scale(1);
}

@media (max-width: 767px) {
  .pz-dialog {
    padding: var(--sp-2);
  }

  .pz-lightbox {
    max-inline-size: 100%;
  }

  .pz-lightbox__img {
    max-block-size: 85vh;
    border-radius: var(--r-md);
  }
}

/* --- Solution submission ------------------------------------------------
   A private key entered against a staked wallet, so the dialog is the panel
   primitive with the uppercase header every other panel in the app carries,
   and the per-attempt price is a band rather than a line of orange prose.
   ---------------------------------------------------------------------- */

.pz-sol {
  inline-size: 100%;
  max-inline-size: 460px;
  max-block-size: calc(100vh - var(--sp-8));
  overflow-y: auto;
  box-shadow: var(--sh-4);
  transform: scale(0.97);
  transition: transform var(--dur-3) var(--ease-out);
}

.pz-dialog.is-open .pz-sol {
  transform: scale(1);
}

/* Every attempt costs a Caçatal, and a Caçatal is money — so this is the one
   place in the dialog the accent is spent, in the wash band the system
   reserves for a figure the visitor is about to be charged. The header above
   already draws a hairline, so the band closes itself only at the bottom. */
.pz-sol__cost {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-5);
  background: var(--accent-wash);
  border-block-end: 1px solid var(--accent-dim);
  color: var(--accent);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.pz-sol__cost .icon {
  flex: none;
}

.pz-sol__body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-5);
}

.pz-sol__lede {
  color: var(--ink-200);
}

/* The frame that receives the verdict on an attempt. `contents` so an empty
   frame costs no box and no gap: it sits between the lede and the field for its
   whole life and only holds something once something has been submitted. */
.pz-sol__flash {
  display: contents;
}

/* Not `.flash-container`, which is the fixed toast rail in the page's own
   corner. In here the message belongs to the dialog it answers. */
.pz-sol__flashes {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.pz-sol__form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

/* Sixty-four hexadecimal characters that own a wallet: an identifier, so it is
   set in the machine face and isolated from the bidi algorithm, exactly as the
   create-challenge form sets the key it generates from. The prompt is prose. */
.pz-sol__form input[type="text"] {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  letter-spacing: 0;
  direction: ltr;
  unicode-bidi: isolate;
  text-align: start;
}

.pz-sol__form input[type="text"]::placeholder {
  font-family: var(--font-ui);
}

.pz-sol__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--sp-3);
}

/* Below this the two labels no longer share a line in every locale, and a
   half-wrapped pair of money controls reads as broken rather than as narrow. */
@media (max-width: 479px) {
  .pz-sol__actions {
    flex-direction: column-reverse;
  }

  .pz-sol__actions .btn {
    inline-size: 100%;
  }
}

/* ===========================================================================
   SOLUTIONS TAB (Soluções)

   The write-up a creator or solver publishes once a challenge is claimed.
   Composed as the reviews list next to it — one panel, one hairline-separated
   row per solution, the author in a 28px gutter and the prose aligned under
   their name. What was here before was a stack of 12px cards, each ringed in
   2px of orange and headed by its own black band: three surfaces deep for two
   lines of text, and the tab printed the word "Soluções" under the tab
   labelled Soluções.
   ======================================================================== */

.sols {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding-block: var(--sp-4);
}

.sols__fields {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

.sols__fields textarea {
  min-block-size: 132px;
  resize: vertical;
}

.sols__actions {
  display: flex;
  justify-content: flex-end;
}

/* The list is the panel: one <ul> carrying the surface, so a Turbo Stream can
   prepend a published solution straight into it. */
.sols__list {
  list-style: none;
}

.sol {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr);
  column-gap: var(--sp-3);
  row-gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-5);
}

.sol__face {
  grid-column: 1;
  grid-row: 1;
}

/* Stacked below 768px for the same reason the comment head is: a short name
   keeps its date on the line and a long one pushes it below, so two
   neighbouring rows read as two different layouts. */
.sol__head {
  grid-column: 2;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-1);
  min-block-size: 28px;
}

@media (min-width: 768px) {
  .sol__head {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-1) var(--sp-3);
  }
}

.sol__who,
.sol__meta {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-inline-size: 0;
}

.sol__name {
  font-weight: 600;
  color: var(--ink-050);
  overflow-wrap: anywhere;
  transition: color var(--dur-1) var(--ease);
}

.sol__name:hover {
  color: var(--accent);
}

/* Isolated for the same reason .mono is: under RTL the bidi algorithm splits
   "30/04 04:00" on its runs and prints the time before the date. */
.sol__date {
  font-size: var(--fs-sm);
  color: var(--ink-400);
  white-space: nowrap;
  direction: ltr;
  unicode-bidi: isolate;
}

.sol__body {
  grid-column: 2;
  grid-row: 2;
  max-inline-size: var(--w-prose);
  color: var(--ink-200);
  overflow-wrap: anywhere;
}

.sol__body p {
  margin: 0;
}

.sol__body p + p {
  margin-block-start: var(--sp-2);
}

/* ===========================================================================
   COMMENTS TAB (Comentários)

   A thread is a comment, the replies under it and the one control that answers
   it — one compound record, so it is one row of a list rather than a card in a
   stack of cards. Same silhouette as the reviews beside it: the author in a
   gutter, their text aligned under their name.

   What was here before spent an orange glow on the submit button's hover, a
   second animated orange ring on the deep-link highlight, a 2px orange ring on
   every avatar, and drew each reply as an ink-850 box sitting on an ink-850
   ground — where the fill did nothing and a 10px orange dot in the gutter was
   left carrying the whole parent-child relationship.
   ======================================================================== */

.cmts {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding-block: var(--sp-4);
}

/* --- The box that starts a thread, and the two that answer or amend one --- */

.cmt-form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

.cmt-form textarea {
  resize: vertical;
}

.cmt-form__actions {
  display: flex;
  justify-content: flex-end;
}

/* --- The list ------------------------------------------------------------ */

/* One panel holding every thread, and no `overflow` on it: a reply's `@` picker
   is absolutely positioned and has to be able to leave the list it was opened
   inside. */
.cmts__list {
  display: flex;
  flex-direction: column;
}

/* Infinite-scroll pagination frames hold the next batch of comments. Once a
   frame has loaded (Turbo sets [complete]) `display: contents` lets its child
   .cmt elements join this list's own flex layout, and the hairline rule below
   resolves straight through it.

   The pending frame keeps its own box, which is where the feature currently
   stops: an empty lazy frame is 0px tall, so scrolling it into view never
   satisfies Turbo's IntersectionObserver and page two is never requested. That
   is true of the markup this pass replaced as well — see Known Gaps. */
.cmts__list turbo-frame[id^="comments_page_"][complete] {
  display: contents;
}

/* --- One comment, and one reply: the same record at two depths ----------- */

/* The rule between threads is drawn on the thread's own leading edge and
   suppressed on the first one, not with `.cmt + .cmt`: the next page of
   comments arrives inside a turbo-frame, and a frame at `display: contents` is
   not a DOM sibling of the thread above it — an adjacent-sibling rule would
   silently drop the hairline at every page boundary. Every thread that follows
   something draws its own, and only the list's true first child, wherever a
   prepend puts it, has none. */
.cmt {
  --cmt-face: 28px;
  display: grid;
  grid-template-columns: var(--cmt-face) minmax(0, 1fr);
  column-gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-block-start: 1px solid var(--line);
}

.cmts__list > .cmt:first-child {
  border-block-start: 0;
}

.cmt__face {
  grid-column: 1;
  display: block;
}

.cmt__face .avatar {
  --avatar-size: var(--cmt-face);
}

/* Everything but the face sits in the text column, so the body, the replies
   and the reply control all hang off the name rather than off the gutter. */
.cmt__main {
  grid-column: 2;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

/* Stacked at this width, and only restored to a row at 768px: left to
   `space-between` alone, a short name keeps its timestamp on the line and a
   long one pushes it below, so two neighbouring threads read as two different
   layouts. */
.cmt__head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-1);
  min-block-size: var(--cmt-face);
}

.cmt__who,
.cmt__meta {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-inline-size: 0;
}

.cmt__name {
  font-weight: 600;
  color: var(--ink-050);
  overflow-wrap: anywhere;
  transition: color var(--dur-1) var(--ease);
}

.cmt__name:hover {
  color: var(--accent);
}

/* Isolated for the same reason .mono is: under RTL the bidi algorithm splits
   "30/04 04:00" on its runs and prints the time before the date. */
.cmt__date {
  font-size: var(--fs-sm);
  color: var(--ink-400);
  white-space: nowrap;
  direction: ltr;
  unicode-bidi: isolate;
}

.cmt__edited {
  font-size: var(--fs-xs);
  color: var(--ink-400);
}

/* Comments carry pasted keys and hex ranges with no break opportunity, so
   without `anywhere` a single token widens the row past the viewport. */
.cmt__body {
  max-inline-size: var(--w-prose);
  color: var(--ink-200);
  overflow-wrap: anywhere;
}

.cmt__body p {
  margin: 0;
}

.cmt__body p + p {
  margin-block-start: var(--sp-2);
}

.cmt__original {
  display: block;
  margin-block-start: var(--sp-2);
  padding: 0;
  background: none;
  border: 0;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-400);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease);
}

.cmt__original:hover {
  color: var(--ink-200);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* Edit history. A native disclosure, so a signed-out reader gets the same
   thing an author does and the browser handles keyboard and find-in-page. */
.cmt__history {
  margin-block-start: var(--sp-2);
  max-inline-size: var(--w-prose);
}

/* Styled off .cmt__original: the two sit under the same body and both ask the
   reader to see other words for the same comment. Display is left alone —
   `summary` is a list-item, and anything else drops the triangle. */
.cmt__history-toggle {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-400);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease);
}

.cmt__history-toggle:hover,
.cmt__history[open] > .cmt__history-toggle {
  color: var(--ink-200);
}

/* The disclosure triangle is the only affordance saying this opens, so it is
   kept rather than hidden, and only tinted to match the label. */
.cmt__history-toggle::marker {
  color: currentColor;
}

.cmt__history-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  margin: var(--sp-2) 0 0;
  padding-inline-start: var(--sp-3);
  list-style: none;
  border-inline-start: 1px solid var(--line);
}

.cmt__version-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-block-end: var(--sp-1);
}

.cmt__version-tag {
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--ink-400);
}

/* Past wording reads quieter than the live comment above it; the entry marked
   current is the one that matches what is already on the page. */
.cmt__version-body {
  color: var(--ink-400);
}

.cmt__version.is-current .cmt__version-body {
  color: var(--ink-200);
}

/* Replies hang off one continuous hairline in the gutter — the thread line a
   transcript draws — instead of each carrying its own box. */
.cmt__replies {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  margin-block-start: var(--sp-1);
  padding-inline-start: var(--sp-4);
  border-inline-start: 1px solid var(--line);
}

/* Collapse the container when there are no replies yet; it must stay in the
   DOM as the Turbo Stream append target, so it can't be omitted server-side. */
.cmt__replies:not(:has(.cmt--reply)) {
  display: none;
}

/* A reply is the same record one level down: same grid, smaller face, and no
   rule of its own — the gutter line above already says where it belongs. */
.cmt--reply {
  --cmt-face: 22px;
  padding: 0;
  border-block-start: 0;
}

.cmt--reply .cmt__name {
  font-size: var(--fs-sm);
}

.cmt--reply .cmt__date {
  font-size: var(--fs-xs);
}

/* Arriving from a notification deep-link. A fill step, not a ring: the one
   zero-offset shadow in this system is the focus ring, and the row being
   pointed at here is not focused. It ends transparent rather than on a named
   fill, so the same animation reads correctly on a thread sitting on the list
   panel and on a reply sitting inside a thread. */
.cmt.is-linked {
  animation: cmt-linked-flash 2.5s var(--ease);
}

@keyframes cmt-linked-flash {
  0%,
  30% {
    background: var(--ink-750);
  }
  100% {
    background: transparent;
  }
}

/* --- What you can do about a thread ------------------------------------- */

.cmt__actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-3);
  margin-block-start: var(--sp-1);
}

.cmt__buttons {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

.cmt__reply-form {
  inline-size: 100%;
}

/* --- Wider ------------------------------------------------------------- */

@media (min-width: 768px) {
  .cmt {
    --cmt-face: 32px;
    padding: var(--sp-5);
  }

  .cmt--reply {
    --cmt-face: 24px;
    padding: 0;
  }

  /* Wide enough for the timestamp to share the row with the name and still
     leave both room; it wraps back underneath if the name runs long. */
  .cmt__head {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-1) var(--sp-3);
  }
}

/* --- On-chain balance states -------------------------------------------
   Only what the puzzle controller actually injects. The wallet balance is
   read live from a third-party explorer, so "couldn't reach it" is an
   unavailability, not a failure of the challenge: it reads as a muted note,
   never as red alarm text.
   ---------------------------------------------------------------------- */

.balance-loading {
  color: var(--ink-400);
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
}

.balance-loading::after {
  content: "";
  display: inline-block;
  width: 3px;
  height: 3px;
  margin-inline-start: 0.4em;
  border-radius: var(--r-full);
  background: currentColor;
  animation: chip-pulse 1.4s var(--ease) infinite;
  vertical-align: middle;
}

.balance-amount {
  color: var(--ink-100);
}

.balance-amount.has-balance {
  color: var(--solved);
}

.balance-error {
  color: var(--ink-400);
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
}

/* ---------------------------------------------------------------------------
   Display-only star ratings (averages and per-review scores).
   The drawn star carries all three states: on takes an accent fill, off stays
   a hairline outline, half draws the filled glyph clipped over the outline.
   CSS beats the SVG's fill presentation attribute.

   The modifiers are --on / --off rather than .filled / .empty: a bare .empty
   here was matching the global `.empty` state block in base.css, which padded
   every unfilled star out to 64x144 and left the summary 292px tall.
   ------------------------------------------------------------------------ */

.stars {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}

.stars--lg {
  gap: var(--sp-1);
}

.star {
  display: inline-flex;
  line-height: 0;
}

.star--on {
  color: var(--accent);
}

.star--on .icon {
  fill: currentColor;
}

.star--off {
  color: var(--ink-600);
}

/* Half star: the outline glyph, with a filled copy clipped to the inline-start
   half laid over it. Logical properties, so under RTL the clip lands on the
   other side and the star fills from the direction the row is read from. */
.star--half {
  position: relative;
}

.star__half {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 50%;
  overflow: hidden;
  line-height: 0;
  color: var(--accent);
}

.star__half .icon {
  fill: currentColor;
}
