/* /catalog-updates layout. No gradient literals here -- those live in
 * gradients.css (docs/features/site-gradients.md); this page ships neutral
 * headers. Chrome primitives (cc-toolbar, cc-chip, cc-btn, cc-empty) come
 * from theme.css -- this file only adds the timeline/date/group/row layout and the
 * per-kind chip colors that don't exist as a shared primitive yet.
 */

.cu-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 1.25rem;
}
.cu-head h1 {
  font-size: 1.5rem;
  margin: 0;
  color: var(--text);
}

/* The open panel is a CARD (a labelled form dropped below the search row), not
 * the bare wrapping toolbar strip it used to be: .cc-toolbar (theme.css) still
 * supplies the border, and this stacks its sections vertically and gives the
 * whole thing a surface + shadow so five decisions read as five rows rather
 * than one ~25-chip wall. width:100% drops it onto its own line inside the
 * wrapping .cu-head flex row. The mobile block below re-purposes the same
 * element as the bottom sheet and overrides all of this. */
.cu-controls {
  flex-direction: column;
  align-items: stretch;
  row-gap: 0;
  width: 100%;
  padding: 0.9rem 1.1rem;
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: 0 10px 28px color-mix(in srgb, var(--shadow-ink) 16%, transparent);
}

/* One labelled row per filter axis: fixed label column, controls beside it, a
 * hairline between rows. align-items start (not center) so a wrapped publisher
 * group keeps its label at the top of the row. */
.cu-filter-section {
  display: grid;
  grid-template-columns: 6.5rem minmax(0, 1fr);
  align-items: start;
  column-gap: 1rem;
  padding: 0.55rem 0;
}
.cu-filter-section + .cu-filter-section {
  border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}

/* The controls of one multi-select axis (publisher, change type). Each is a
 * role="group" with its own label for screen readers. */
.cu-control-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
}

/* Single-choice axes render as one segmented control each (shared cc-segment
 * primitive), which is what visually separates "pick one" from the additive
 * check-chips around them. fit-content: the segment must not stretch to the
 * grid track like a toolbar. */
.cu-filter-seg { width: fit-content; }

/* The check an ACTIVE multi-select chip carries. Server-rendered only on the
 * active state, so the icon itself is the affordance -- no JS involved. */
.cu-check {
  font-size: 0.9em;
  margin-right: 0.3rem;
}

/* The widen escape hatch ("All publishers") is a different KIND of action from
 * the additive chips before it -- a quiet link, not another identical pill. */
.cu-pub-all {
  align-self: center;
  margin-left: 0.25rem;
  font-size: 0.78rem;
  color: var(--text-dim);
  text-decoration: underline;
}
.cu-pub-all:hover { color: var(--text); }
.cu-pub-all.is-active {
  color: var(--text);
  font-weight: 600;
}

/* The chips and segment options are links now (filters live in the query
 * string), so undo the underline/colour a bare <a> would otherwise bring into
 * them. The segment rule is scoped to this page's two segment homes rather
 * than restyling .cc-tab globally. */
.cu-toggle,
.cu-toggle:hover,
.cu-searchbar .cc-tab,
.cu-controls .cc-tab {
  text-decoration: none;
}

.cu-count {
  margin: 0;
  width: 100%;
  color: var(--text-muted);
  font-size: 0.82rem;
}

.cu-more {
  display: block;
  width: fit-content;
  margin: 0.5rem auto 0;
}

.cu-timeline {
  margin-top: 1.25rem;
}

.cu-timeline-day {
  position: relative;
  padding-left: 2.25rem;
  padding-bottom: 1.5rem;
}
.cu-timeline-day::before {
  content: "";
  position: absolute;
  left: 0.35rem;
  top: 0.35rem;
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 50%;
  background: var(--brand);
  box-shadow: 0 0 0 4px var(--surface);
}
.cu-timeline-day::after {
  content: "";
  position: absolute;
  left: 0.67rem;
  top: 1.2rem;
  height: 1rem;
  width: 1px;
  background: var(--border);
}
/* Each marker owns a short rail segment. This avoids relying on the last DOM
 * day to terminate a continuous rail, so filtered-out days cannot leave a
 * dangling line or extra final-stop spacing. */

/* Day and batch headings are <summary>, so the whole heading is the collapse
 * control. list-style:none kills the default disclosure triangle in Firefox;
 * ::-webkit-details-marker does the same in Safari. A chevron of our own is
 * drawn from the open state so it points the right way in both. */
.cu-timeline-date,
.cu-batch-label {
  cursor: pointer;
  list-style: none;
}
.cu-timeline-date::-webkit-details-marker,
.cu-batch-label::-webkit-details-marker { display: none; }
.cu-timeline-date:focus-visible,
.cu-batch-label:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

.cu-timeline-date {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0 0 0.75rem;
  color: var(--text);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}
.cu-timeline-date h2 {
  margin: 0;
  font: inherit;
  color: inherit;
}
.cu-timeline-date::after,
.cu-batch-label::after {
  content: "";
  width: 0.42rem;
  height: 0.42rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-0.1rem, -0.1rem);
  opacity: 0.55;
  transition: transform 120ms ease;
}
[open] > .cu-timeline-date::after,
[open] > .cu-batch-label::after {
  transform: rotate(-135deg) translate(-0.14rem, -0.14rem);
}
@media (prefers-reduced-motion: reduce) {
  .cu-timeline-date::after,
  .cu-batch-label::after { transition: none; }
}

.cu-day-count,
.cu-batch-count {
  margin-left: auto;
  color: var(--text-muted);
  font-size: 0.78rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

.cu-timeline-groups {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.cu-timeline-group:not(.is-loose) {
  border-left: 3px solid var(--border);
  padding-left: 0.8rem;
}

.cu-batch-label {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  margin-bottom: 0.45rem;
}
.cu-batch-label-title {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.85rem;
  font-weight: 700;
}
/* Sits under a truncated batch's visible slice, leading to that batch's own
 * page rather than trying to render thousands of rows inline. */
.cu-group-more {
  display: inline-block;
  margin-top: 0.5rem;
  font-size: 0.8rem;
  color: var(--brand);
}

.cu-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

/* Grid shape. Auto-fill rather than auto-fit so a group holding one card does
 * not stretch it across the whole row. `align-items: start` keeps a card whose
 * cover/description panel is open from stretching its neighbours to match. */
.cu-cards {
  list-style: none;
  margin: 0;
  padding: 0.35rem 0.6rem 0.6rem;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  align-items: start;
  gap: 0.9rem 0.75rem;
}

.cu-card-item {
  /* Anchors the heart pin: the cover is the first child and spans the full
   * card width, so the card's top-right IS the cover's top-right. */
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  min-width: 0;
}

/* 2:3, the standard trade-dress ratio. object-fit: cover rather than contain so
 * a source supplying a squarer scan does not letterbox against the surface. */
.cu-card-cover {
  width: 100%;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

/* The list clamps its title to one line inside a grid cell; a card has the
 * width to give it three, which is what makes an unlinkable ALL-CAPS
 * distributor title readable rather than truncated to nothing.
 *
 * white-space: normal is what MAKES that true, and its absence is silent:
 * the base .cu-title below is `white-space: nowrap` (right for the list's
 * 1fr title column), and -webkit-line-clamp cannot break a line that is
 * never allowed to wrap. Measured without it, a card title computed to
 * one 17px line with scrollWidth 442 against clientWidth 160 -- clipped
 * HARDER than the list, with the clamp rule inert. */
.cu-card-item .cu-title {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  white-space: normal;
  font-size: 0.82rem;
  line-height: 1.3;
}

/* Date changes stay on one line. Grids carrying them need wider tracks,
 * capped at the container width so narrow phones never overflow the page. */
.cu-cards:has(.cu-tag-foc, .cu-tag-onsale) {
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
}
.cu-card-item .cu-tag { white-space: normal; }
.cu-card-item .cu-tag-foc,
.cu-card-item .cu-tag-onsale { white-space: nowrap; }

.cu-card-item .cu-tags {
  flex-wrap: wrap;
  justify-content: flex-start;
  /* Redundant against the base rule today, declared here so a future
   * card-scope override cannot reintroduce the automatic minimum size. */
  min-width: 0;
}

/* Cards pin the heart on the cover, matching the front page's card tile
 * (.omni-card-interest-heart in omnis.css): a translucent circle at the
 * cover's top-right, filling in the interest accent once shelved. Same
 * treatment, this page's tokens -- omnis.css and its --omni-* vars are not
 * loaded here. Absolute positioning takes the button out of flow, so the
 * card-scope rules below override the base .cu-heart states (quiet outline,
 * muted stroke) that are sized for the list's inline column. `.is-filed`
 * still only hides it (visibility, from the base rule), and the blank slot
 * is server-rendered -- neither ever reflows the card after paint. */
.cu-card-item .cu-heart {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  width: 32px;
  height: 32px;
  background: color-mix(in srgb, #000 62%, transparent);
  border-color: color-mix(in srgb, var(--on-image) 18%, transparent);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.cu-card-item .cu-heart svg { stroke: var(--accent-interest); }
.cu-card-item .cu-heart:hover {
  background: var(--accent-interest);
  border-color: var(--accent-interest);
}
.cu-card-item .cu-heart:hover svg {
  fill: var(--on-image);
  stroke: var(--on-image);
}
.cu-card-item .cu-heart.is-active {
  background: var(--accent-interest);
  border-color: var(--accent-interest);
  box-shadow: 0 2px 8px color-mix(in srgb, var(--accent-interest) 50%, transparent);
}
.cu-card-item .cu-heart.is-active svg {
  fill: var(--on-image);
  stroke: var(--on-image);
}
/* An unshelvable card needs no placeholder: the pin sits outside the flow, so
 * there is no column to hold open (unlike the list, where the blank span keeps
 * the grid tracks identical). */
.cu-card-item .cu-heart-blank { display: none; }

.cu-row {
  display: grid;
  /* cover | title | tags | heart */
  grid-template-columns: 48px 1fr auto auto;
  align-items: center;
  gap: 0.6rem;
  padding: 0.35rem 0.4rem;
  border-radius: var(--radius-sm);
}
.cu-row:hover { background: color-mix(in srgb, var(--surface-2) 60%, transparent); }

.cu-cover {
  width: 48px;
  height: 48px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

/* Rendered instead of <img class="cu-cover"> when event.cover_url is blank,
 * so a row with no cover doesn't self-request the page (empty src resolves
 * to the current URL). display:inline-block is required here because a
 * plain <span> ignores width/height -- <img> gets that for free as a
 * replaced element. */
.cu-cover-blank { display: inline-block; }

/* The favorite heart. Outline at rest so a feed of unfiled rows stays quiet;
 * filled in the interest accent once the row is on the shelf. */
.cu-heart {
  width: 30px;
  height: 30px;
  padding: 0;
  display: grid;
  place-items: center;
  background: none;
  border: 1px solid transparent;
  border-radius: 50%;
  cursor: pointer;
  transition: transform 0.15s ease, border-color 0.15s ease;
}
.cu-heart svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: var(--text-muted);
  stroke-width: 2;
  transition: fill 0.15s ease, stroke 0.15s ease;
}
.cu-heart:hover { transform: scale(1.08); border-color: var(--border); }
.cu-heart:hover svg { stroke: var(--accent-interest); }
.cu-heart:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}
.cu-heart.is-active svg {
  fill: var(--accent-interest);
  stroke: var(--accent-interest);
}
/* Owned editions show no heart on the front page either. The slot is kept, not
 * removed, because this state lands after paint and a display:none would
 * reflow the row under the reader's cursor. */
.cu-heart.is-filed { visibility: hidden; }
/* Rendered instead of a button for a row that cannot be shelved; a bare <span>
 * ignores width/height without this. */
.cu-heart-blank {
  display: inline-block;
  cursor: default;
}

.cu-title {
  color: var(--text);
  text-decoration: none;
  font-size: 0.92rem;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cu-title:hover { text-decoration: underline; }

/* A row carries one tag per field that changed for its edition that day, so
 * the third grid column holds a list, not a single label. Wraps rather than
 * stretching the column, keeping the title column's width stable down the
 * page; justify-content keeps a wrapped second line right-aligned with the
 * first. */
.cu-tags {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  gap: 0.3rem;
  min-width: 0;
}

/* Named cu-tag, not "cu-ch" + "ip": this is a static per-kind status label,
 * not the interactive toggle cc-chip already provides (no hover/active
 * state, no cursor:pointer) -- that literal suffix here would also trip the
 * CSS building-block audit's local-primitive ratchet
 * (tests/test_tta79_frontend_gate_closeout.py, scripts/audit_css_building_blocks.py,
 * which scans this file's raw text including comments, so even a mention of
 * the avoided name must not appear as a dotted selector-shaped token). */
.cu-tag {
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.2rem 0.55rem;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  color: var(--text-muted);
  white-space: nowrap;
}
/* An expandable chip. The <summary> IS the chip, so an expandable and a plain
 * chip are the same size and sit on the same baseline in the tag row. */
.cu-change { display: inline-block; }
.cu-change > summary {
  cursor: pointer;
  list-style: none;
  display: inline-block;
}
.cu-change > summary::-webkit-details-marker { display: none; }
.cu-change > summary:hover { color: var(--text); border-color: var(--border-strong); }
.cu-change > summary:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}
/* An open panel needs the full row width, but it lives inside the tag cell of a
 * 3-column grid. Rather than trying to escape that cell, the ROW reflows: cover
 * and title keep the first line, and the tag cell (chips + the open panel) drops
 * to its own full-width line beneath them. Closed rows are untouched. */
.cu-row:has(.cu-change[open]) {
  grid-template-columns: 48px 1fr auto;
  row-gap: 0.35rem;
}
.cu-row:has(.cu-change[open]) .cu-tags {
  grid-column: 1 / -1;
  justify-content: flex-start;
}
/* Explicit, not auto-placed: the tag cell above spans every column and moves
 * the auto-placement cursor past row 1, which would drop the heart onto a row
 * of its own. */
.cu-row:has(.cu-change[open]) .cu-heart {
  grid-row: 1;
  grid-column: 3;
}
.cu-change[open] {
  display: block;
  flex-basis: 100%;
  margin-top: 0.1rem;
}

.cu-cover-diff {
  display: flex;
  align-items: flex-end;
  justify-content: flex-start;
  gap: 0.6rem;
  margin-top: 0.5rem;
}
.cu-cover-diff figure { margin: 0; text-align: center; }
.cu-cover-diff img {
  width: 72px;
  height: 72px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  /* A source's previous URL is often dead (every previewsworld thumb 404s), so
   * a broken before-image must read as an empty slot, not a broken page. */
  background: var(--surface-2);
}
.cu-cover-diff figcaption {
  color: var(--text-muted);
  font-size: 0.68rem;
  margin-top: 0.15rem;
}
.cu-cover-diff-arrow { color: var(--text-muted); padding-bottom: 1.1rem; }

.cu-desc-diff {
  margin: 0.5rem 0 0;
  text-align: left;
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--text-muted);
  white-space: normal;
}
.cu-desc-diff del {
  text-decoration: line-through;
  color: var(--status-danger-fg);
  opacity: 0.85;
}
.cu-desc-diff ins {
  text-decoration: none;
  color: var(--success);
  font-weight: 600;
}
.cu-desc-gap { opacity: 0.5; }

.cu-tag-added { color: var(--accent-canonical, var(--brand)); border-color: currentColor; }
.cu-tag-backfill { color: var(--text-dim); }
.cu-tag-onsale { color: var(--brand); border-color: currentColor; }
.cu-tag-foc { color: var(--text-dim); }
.cu-tag-price { color: var(--status-danger-fg); border-color: currentColor; }
.cu-tag-pages { color: var(--text-muted); }
.cu-tag-cover { color: var(--text-muted); }
.cu-tag-description { color: var(--text-muted); }

/* NMC video badge (Task 11). Rendered only for the rare row carrying
 * event.video, so rather than reshaping the 3-column .cu-row grid for a
 * column almost no row uses, it spans the full row on a new implicit line
 * beneath cover/title/tags -- the same reflow-not-reshape approach
 * .cu-row:has(.cu-change[open]) already uses above for the change-detail
 * panel. editionVideoFacade() (edition_video_facade.js) drives it -- the
 * same Alpine component the edition dossier uses -- but this page's markup
 * uses its own cu-video__* classnames rather than the dossier's ev-video__*,
 * so the sizing lives here rather than being pulled in from
 * edition_video.css (this page has no bundled CSS entrypoint to @import
 * into; catalog_updates.css is linked directly, like every other CSS file
 * on this page). */
.cu-video {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.3rem;
}

.cu-video__badge {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.15rem 0.55rem 0.15rem 0.15rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  color: var(--text);
  cursor: pointer;
  font-size: 0.75rem;
  font-weight: 700;
}
.cu-video__badge:hover { border-color: var(--border-strong); }
.cu-video__badge:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.cu-video__thumb {
  display: block;
  width: 80px;
  height: 45px;
  object-fit: cover;
  border-radius: var(--radius-sm);
}

.cu-video__label {
  color: var(--status-danger-fg);
  letter-spacing: 0.04em;
}

.cu-video__player {
  width: min(320px, 100%);
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--shadow-ink);
}
.cu-video__player iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

.cu-video__fallback {
  color: var(--text-muted);
  font-size: 0.82rem;
  text-decoration: none;
}
.cu-video__fallback:hover { text-decoration: underline; }

/* Named cu-empty-gap, not "cu-em" + "pty": the shared cc-empty primitive
 * (theme.css) already supplies the box; this only adds the top margin
 * needed to separate it from the page header and controls. That literal
 * suffix here would trip the same local-primitive
 * ratchet as cu-tag above (see the comment there). */
.cu-empty-gap { margin-top: 1.5rem; }

/* Smooth height animation for every disclosure in the timeline -- day headers,
 * batch groups, and the inline cover/description diff chips -- driven by
 * js/dossier_disclosure.js. Keyed to the animating state rather than to any
 * body class, so it covers .cu-timeline-groups, .cu-group-body, .cu-cover-diff
 * and .cu-desc-diff alike.
 *
 * The rule in source-dossier.css is scoped to .dossier-content/.dossier-manage
 * and does not reach this page, hence the second declaration rather than a
 * shared one.
 *
 * Only active during open/close (the JS adds and removes the class), so the
 * description panel's async fetch does not animate when its text lands.
 * prefers-reduced-motion needs no rule here: --motion-slow collapses to 1ms. */
.cu-timeline .cc-disclosure-animating {
  overflow: hidden;
  transition: height var(--motion-slow) var(--ease-standard);
}

/* --- Filter chrome (search bar, sheet, active filters) ------------------- */

.cu-searchbar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0.6rem auto 0.5rem;
  max-width: 34rem;
}

.cu-search {
  position: relative;
  display: flex;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
}

.cu-search-icon {
  position: absolute;
  left: 0.6rem;
  color: var(--text-dim);
  pointer-events: none;
}

.cu-search-input {
  width: 100%;
  padding: 0.4rem 2rem 0.4rem 2rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  /* --surface-2, not --surface: this sits directly on the page background
   * (which IS --surface), so an input filled with --surface would be
   * invisible against it. Matches .roe-text-input's identical
   * border+radius+background pairing (reading_order_editor.css:110). */
  background: var(--surface-2);
  color: var(--text);
  font-size: 0.85rem;
}
.cu-search-input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* The submit affordance exists for keyboard/no-JS users; Enter in the input
 * already submits, so it is visually quiet rather than a call to action. */
.cu-search-go,
.cu-search-clear {
  position: absolute;
  right: 0.5rem;
  border: 0;
  background: none;
  color: var(--text-dim);
  padding: 0.15rem;
  line-height: 1;
}
.cu-search-clear { position: static; }
.cu-search-go:hover,
.cu-search-clear:hover { color: var(--text); }

.cu-filter-trigger {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  flex: 0 0 auto;
  /* Computes to 0 while the search box flex-grows, but is the entire
   * right-align mechanism if the box is ever conditionally hidden. */
  margin-left: auto;
}
.cu-filter-trigger-count {
  display: inline-grid;
  place-items: center;
  min-width: 1.15rem;
  height: 1.15rem;
  padding: 0 0.25rem;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--on-brand);
  font-size: 0.68rem;
}

/* Every width, not mobile-only: the toolbar is collapsed behind the Filters
 * button at all widths now, so this chip row is the one always-visible
 * statement of what is narrowing the feed. width:100% drops it onto its own
 * line inside the wrapping .cu-head flex row. */
.cu-active-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  width: 100%;
  margin: 0 0 0.5rem;
}

.cu-active-filter {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
}
.cu-active-filter-x { font-size: 0.8rem; opacity: 0.7; }
.cu-active-clear {
  border: 0;
  background: none;
  color: var(--text-dim);
  font-size: 0.78rem;
  text-decoration: underline;
}

.cu-controls-label {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  /* Optically centers the label against the FIRST line of its controls; the
   * section grid is align-items:start so a wrapped chip group grows downward
   * from there rather than re-centering the label. */
  padding-top: 0.5rem;
}

.cu-shape-segment { flex: 0 0 auto; }

/* Expand/collapse all. Icon-only at every width, like the shape toggle beside
 * it, so the search row stays one line on a phone. */
.cu-collapse-all {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}
/* The one control on this page that does nothing unscripted -- every filter,
 * the search box and the shape toggle are ordinary links. base.html stamps
 * `.js` on <html> from a head script, so this hides the button exactly when it
 * cannot work; native <details> still collapses each section by hand there. */
html:not(.js) .cu-collapse-all { display: none; }

.cu-publisher-group { flex-wrap: wrap; gap: 0.3rem; }
.cu-pub-toggle { display: inline-flex; align-items: center; gap: 0.3rem; }
.cu-pub-logo { height: 18px; width: auto; }
.cu-pub-count {
  font-size: 0.68rem;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* Sheet chrome: inert above 640px, where the open toolbar is a plain inline
 * row rather than a sheet. */
.cu-sheet-backdrop,
.cu-sheet-head,
.cu-sheet-foot { display: none; }

/* Desktop: the toolbar is COLLAPSED behind the Filters button, exactly the
 * model mobile already uses (there it is the bottom sheet below). The header
 * a reader lands on is title + search + Filters + the active-filter chips;
 * ~25 chips across five labelled groups only appear on request. min-width so
 * this can never fight the max-width sheet block below, and 640.1px rather
 * than 641px so no fractional-width viewport falls between the two. The
 * template's <noscript> rule (`display: flex`, later in the cascade) restores
 * the toolbar for a no-JS reader, who has no way to set .is-open. */
@media (min-width: 640.1px) {
  /* Same open/close motion as theme.css's .cc-filters: display transitions
     discretely, so the closed toolbar still leaves the layout. */
  .cu-controls {
    transition: opacity var(--motion-med) var(--ease-standard),
                transform var(--motion-med) var(--ease-standard),
                display var(--motion-med) allow-discrete;
  }
  .cu-controls:not(.is-open) { display: none; opacity: 0; transform: translateY(-6px); }
  @starting-style {
    .cu-controls.is-open { opacity: 0; transform: translateY(-6px); }
  }
}

/* ONE mobile block, last in the file, on purpose.
 *
 * omnis.css has several separate max-width:640px blocks (three today) and that
 * split caused a series of silent failures -- most sharply a base rule
 * appended after the block
 * that enabled it, which wins on source order because media queries add no
 * specificity, and which a test asserting "the selector is inside the mobile
 * block" still passes. One block, appended last, makes the whole class
 * unreachable. Pinned by tests/test_catalog_updates_filter_chrome.py. */
@media (max-width: 640px) {
  /* The label column is desktop rhythm; in the sheet each section stacks its
   * label above its controls. */
  .cu-filter-section { grid-template-columns: 1fr; }
  .cu-controls-label { padding-top: 0; }

  .cu-timeline-day { padding-left: 1.65rem; }
  .cu-timeline-day::before { left: 0.2rem; }
  .cu-timeline-day::after { left: 0.52rem; }
  /* cover | title | tags | heart -- four columns, matching the base rule. The
   * heart landed on dev while this branch was in flight; a 3-column value here
   * would silently drop it off the mobile row. */
  .cu-row { grid-template-columns: 40px 1fr auto auto; }
  .cu-cover { width: 40px; height: 40px; }
  .cu-cards:not(:has(.cu-tag-foc, .cu-tag-onsale)) { grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); }

  /* .cu-controls stops being an inline row and BECOMES the bottom sheet.
   * Every secondary control already lives inside it, so nothing is
   * duplicated -- which is exactly why .cu-searchbar must stay outside it. */
  .cu-controls {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* 1050/1040, matching the front page's sheet (omnis.css) rather than the
     * 40/39 this started at. Two site-wide floating widgets sit exactly on
     * this sheet's footer -- .lc-fab (log_chat.css) at 1049 and
     * .discord-cta-floating (discord_cta.css) at 1030, both
     * `position: fixed; bottom: ~18px; right: ~18px`. Below them, a tap near
     * "Show N results" landed on the chat FAB and the backdrop never dimmed
     * it. The pairing is deliberate: the sheet clears both, the backdrop
     * clears the Discord CTA, and the FAB stays above the scrim exactly as it
     * does on the front page. */
    z-index: 1050;
    display: flex;
    flex-direction: column;
    /* Explicit: wrap on a COLUMN container with a definite max-height wraps
     * into a second COLUMN instead of scrolling, so the sheet would never
     * scroll and its lower controls would be unreachable. */
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 0.4rem;
    max-height: 78vh;
    overflow-y: auto;
    /* The sheet is this page's own scrollport; without this a flick past the
     * last chip chains into scrolling the feed behind it. */
    overscroll-behavior: contain;
    padding: 0 1rem;
    background: var(--surface);
    /* Reset the .cc-toolbar base rule's all-round border and border-radius
     * (theme.css) -- left as-is the sheet would carry a hairline border on
     * all four sides and bottom corners flush-square with the radii below
     * only rounding the top. */
    border: 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    box-shadow: 0 -8px 30px color-mix(in srgb, var(--shadow-ink) 40%, transparent);
    transform: translateY(100%);
    visibility: hidden;
    transition: transform var(--motion-slow) var(--ease-standard),
                visibility var(--motion-slow) var(--ease-standard);
  }
  .cu-controls.is-open {
    transform: translateY(0);
    visibility: visible;
  }

  /* CSS-driven, not x-show: this page has no x-cloak (deliberately -- the
   * rendered DOM already equals the filtered result) and Alpine is deferred,
   * so an x-show'd backdrop paints at first mobile paint and stays until
   * Alpine's first tick writes display:none, dimming/blocking the feed on
   * every load. Instead it defaults hidden and is revealed by the same
   * .is-open state that opens the sheet -- a descendant selector, not a
   * sibling one, because the backdrop precedes .cu-controls in the DOM. */
  .cu-sheet-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 1040;
    background: color-mix(in srgb, var(--shadow-ink) 55%, transparent);
    /* NOT body{overflow:hidden}: the global ::-webkit-scrollbar makes that
     * cost 8px of layout width, and showFilters is reachable on desktop. */
    overscroll-behavior: contain;
    touch-action: none;
    visibility: hidden;
    opacity: 0;
    transition: opacity var(--motion-slow) var(--ease-standard),
                visibility var(--motion-slow) var(--ease-standard);
  }
  #catalog-updates-app:has(.cu-controls.is-open) .cu-sheet-backdrop {
    visibility: visible;
    opacity: 1;
  }

  .cu-sheet-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    position: sticky;
    top: 0;
    z-index: 1;
    padding: 0.75rem 0 0.5rem;
    background: var(--surface);
  }
  .cu-sheet-handle {
    position: absolute;
    left: 50%;
    top: 0.35rem;
    transform: translateX(-50%);
    width: 2.25rem;
    height: 0.25rem;
    border-radius: var(--radius-pill);
    background: var(--border);
  }
  .cu-sheet-title { font-weight: 600; }
  .cu-sheet-close { border: 0; background: none; color: var(--text-dim); }

  .cu-sheet-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    position: sticky;
    bottom: 0;
    background: var(--surface);
    /* The safe-area allowance lives HERE, not on .cu-controls: a sticky
     * bottom pins to the scrollport's PADDING box, so padding on the
     * container bleeds content under this footer. */
    padding: 0.6rem 0 calc(0.6rem + env(safe-area-inset-bottom));
  }
  .cu-sheet-apply {
    flex: 1 1 auto;
    border: 0;
    border-radius: var(--radius-pill);
    padding: 0.5rem 1rem;
    background: var(--accent);
    color: var(--on-brand);
  }
  .cu-sheet-clear {
    border: 0;
    background: none;
    color: var(--text-dim);
    text-decoration: underline;
  }

  .cu-filter-trigger-label { display: none; }
  .cu-searchbar { margin-top: 0.4rem; }
}
