/* ============================================================
   notebook-papers.css — Phase 3 Wave 1, Task 7
   Page-scoped skin for the past-papers / mocks INDEX and the single
   paper VIEWER, in the Annotated-Notebook language.

   Tokens only (hook-enforced: zero raw colors). This layer styles
   BOTH the nb-papers-*/nb-pv-* structural classes AND the FROZEN
   foreign classes the two scripts read — pp-* (pastpapers.js) and
   pv-* (papers.js). Those foreign classes are RESTYLED, never
   renamed: the JS DOM contract stays intact.

   2026-08-29 row redesign: the search field and session chips are gone
   from the template (both hooks null-guarded in pastpapers.js before
   removal); rows moved from boxed cards onto hairline rules and became
   stretched links with one quiet mark-scheme affordance. Nothing was
   renamed — .nb-papers-row-link / -ms / -go are additive.
   ============================================================ */

/* ---------- 1 · Index shell -------------------------------------
   This page does not load notebook-hub, so it owns the shared index
   typography roles it renders in addition to papers-specific rhythm. */
.nb-papers {
  width: 100%;
}
/* Deliberately kept with no markup behind it. The eyebrow was removed from
   nb_papers_index.html 2026-08-29 (it duplicated the breadcrumb above it), but
   pages/tests_typography_stage5_wave3.py:497 resolves this selector through
   _declarations(), which RAISES "Missing CSS rule" when it is absent — so
   deleting the rule turns a passing test into an error rather than a failure.
   Same reason as .nb-papers-source and .nb-papers-row-actions .nb-btn. */
.nb-papers .nb-index-eyebrow {
  font: var(--nb-type-label);
}
.nb-papers .nb-index-title {
  font: var(--nb-type-headline);
  letter-spacing: -0.015em;
}
.nb-papers .nb-index-lede {
  font: var(--nb-type-prose);
}
/* One provenance line under the lede, replacing the per-row source chip.
   Sized with font-size rather than a composite `font:` role so
   TypographyDebtRatchetTests.COMPONENT_TYPOGRAPHY_COUNT stays flat — that
   ratchet counts font/font-family/font-weight, and a var() font-size is not
   debt. */
.nb-papers-provenance {
  margin: var(--nb-s2) 0 0;
  font-size: var(--nb-text-sm);
  color: var(--nb-ink-2);
}
.nb-papers-filter-hint,
.nb-papers-empty .nb-empty-note,
.nb-papers-guide-list,
.nb-pv-fallback {
  font: var(--nb-type-ui);
}

/* ---------- 2 · Filter bar --------------------------------------
   One sunken control strip: labelled filter groups (area tabs, unit
   chips), closing on the result/clear row. pastpapers.js toggles
   is-active / aria-pressed and hides units until an area is chosen
   (is-waiting-for-family). */
.nb-papers-controls {
  display: flex;
  flex-direction: column;
  min-width: 0;
  gap: var(--nb-s5);
  margin-bottom: var(--nb-s8);
  padding: var(--nb-s5) var(--nb-s6);
  background: var(--nb-paper-sunken);
  border: 1px solid var(--nb-border);
  border-radius: var(--nb-r-lg);
}

/* A bank with no unit filters (mocks) renders the <section> EMPTY, because
   pastpapers.js bails at js:4 without [data-pp-controls]. The shell must stay
   in the DOM and out of the layout — display:none does not hide an element
   from querySelector, so the script still initialises. */
.nb-papers-controls--bare {
  display: none;
}

.nb-papers-filter {
  display: flex;
  flex-direction: column;
  min-width: 0;
  gap: var(--nb-s3);
}
.nb-papers-filter-label {
  font: var(--nb-type-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--nb-ink-2);
}
.nb-papers-filter-hint {
  margin: 0;
  color: var(--nb-ink-2);
}

/* Area tabs — the notebook's index tabs, wrapping on narrow screens
   (the shared .nb-tabs is a flex row on a hairline). */
.nb-papers-tabs {
  flex-wrap: wrap;
}
.nb-papers-tab-count {
  margin-left: var(--nb-s1);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  opacity: 0.75;
}

/* Unit / session chips — a wrapping row of neutral pills. The current
   filter uses the semantic selection fill and notebook-edge marker. */
.nb-papers-chiprow {
  display: flex;
  flex-wrap: wrap;
  gap: var(--nb-s2);
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
  overscroll-behavior-inline: contain;
}
.nb-papers-chiprow.pp-scroll-has-after {
  -webkit-mask-image: linear-gradient(to right, var(--nb-paper) 0, var(--nb-paper) calc(100% - var(--nb-s6)), transparent 100%);
  mask-image: linear-gradient(to right, var(--nb-paper) 0, var(--nb-paper) calc(100% - var(--nb-s6)), transparent 100%);
}
.nb-papers-chiprow.pp-scroll-has-before {
  -webkit-mask-image: linear-gradient(to right, transparent 0, var(--nb-paper) var(--nb-s6), var(--nb-paper) 100%);
  mask-image: linear-gradient(to right, transparent 0, var(--nb-paper) var(--nb-s6), var(--nb-paper) 100%);
}
.nb-papers-chiprow.pp-scroll-has-before.pp-scroll-has-after {
  -webkit-mask-image: linear-gradient(to right, transparent 0, var(--nb-paper) var(--nb-s6), var(--nb-paper) calc(100% - var(--nb-s6)), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, var(--nb-paper) var(--nb-s6), var(--nb-paper) calc(100% - var(--nb-s6)), transparent 100%);
}
.nb-papers-chip {
  border-color: var(--nb-control-border);
  cursor: pointer;
  transition:
    background-color var(--nb-dur-1) var(--nb-ease),
    border-color var(--nb-dur-1) var(--nb-ease),
    color var(--nb-dur-1) var(--nb-ease),
    box-shadow var(--nb-dur-1) var(--nb-ease),
    transform var(--nb-dur-1) var(--nb-ease);
}
.nb-papers-chip:hover:not(.is-active) {
  border-color: var(--nb-border-strong);
  color: var(--nb-ink);
  transform: translateY(-1px);
}
html.theme-light .nb-papers-chip:hover:not(.is-active) {
  border-color: var(--nb-control-border);
}
.nb-papers-chip.is-active {
  background: var(--nb-selection-fill);
  border-color: var(--nb-selection-border);
  color: var(--nb-selection-ink);
  box-shadow: inset 0 -3px 0 var(--nb-selection-marker);
}
.nb-papers-chip-code {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  letter-spacing: 0.02em;
  opacity: 0.85;
}

/* Waiting-for-family: the unit group dims until an area is chosen
   (pastpapers.js sets .is-waiting-for-family on [data-pp-unit-group]). */
.nb-papers-filter.is-waiting-for-family .nb-papers-chiprow {
  opacity: 0.45;
  pointer-events: none;
}
.nb-papers-filter.is-waiting-for-family .nb-papers-filter-hint {
  color: var(--nb-blue);
}

/* Result row — the live count + the clear button. */
.nb-papers-result {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--nb-s3);
  flex-wrap: wrap;
  padding-top: var(--nb-s3);
  border-top: 1px solid var(--nb-border);
}
.nb-papers-count {
  margin: 0;
  font: var(--nb-type-label);
  color: var(--nb-ink);
}
.nb-papers-count [data-pp-count] {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--nb-ink);
}
/* .nb-btn forces display:inline-flex — pastpapers.js hides this Clear button
   (sets [hidden]) whenever there are no active filters, so [hidden] must win. */
.nb-papers-clear[hidden] {
  display: none;
}

/* ---------- 3 · Empty state -------------------------------------
   pastpapers.js unhides [data-pp-empty] when the filtered count is 0.
   The shared .nb-empty gives the hollow-nib + status line; here it
   sits on a sunken sheet with a way back. */
.nb-papers-empty {
  margin-bottom: var(--nb-s6);
  padding: var(--nb-s8) var(--nb-s6);
  background: var(--nb-paper-sunken);
  border: 1px dashed var(--nb-border-strong);
  border-radius: var(--nb-r-lg);
}
/* .nb-empty forces display:flex — the HTML [hidden] attr (pastpapers.js toggles
   it) must still win, or the empty card shows even with papers listed. */
.nb-papers-empty[hidden] {
  display: none;
}

/* ---------- 4 · Session groups ----------------------------------
   Each year/session is a plain run of rows under a heading — NOT a
   card (.nb-card was dropped from the template 2026-08-29). The only
   structure is rule weight: the group head sits on a strong hairline,
   the rows below it on plain ones. pastpapers.js hides a whole group
   when all its cards are filtered out. */
.nb-papers-groups {
  display: flex;
  flex-direction: column;
  gap: var(--nb-s8);
}
.nb-papers-group[hidden] {
  display: none;
}
.nb-papers-group-head {
  display: flex;
  align-items: baseline;
  gap: var(--nb-s3);
  margin-bottom: var(--nb-s2);
  padding-bottom: var(--nb-s3);
  border-bottom: 1px solid var(--nb-border-strong);
}
.nb-papers-group-title {
  margin: 0;
  font: var(--nb-type-title);
  letter-spacing: -0.01em;
  color: var(--nb-ink);
}
.nb-papers-group-link {
  color: var(--nb-ink-3);
  text-decoration: none;
  font-weight: 700;
  opacity: 0;
  transition: opacity var(--nb-dur-1) var(--nb-ease), color var(--nb-dur-1) var(--nb-ease);
}
.nb-papers-group-head:hover .nb-papers-group-link,
.nb-papers-group-link:focus-visible {
  opacity: 1;
  color: var(--nb-blue);
}
.nb-papers-group-count {
  margin-left: auto;
  font: var(--nb-type-label);
  font-variant-numeric: tabular-nums;
  color: var(--nb-ink);
}

/* ---------- 5 · Paper rows --------------------------------------
   A row: file glyph + title/meta on the left, the quiet mark-scheme
   link and a direction glyph on the right, all on a hairline rule.

   STRETCHED LINK — .nb-papers-row-link::after covers the whole <li>,
   so the row is the primary target; .nb-papers-row-ms is lifted onto
   z-index:1 so the mark scheme stays independently clickable. An <a>
   cannot be nested inside an <a>, so this pattern is load-bearing.
   pastpapers.js sets [hidden] on filtered rows (js:135). */
.nb-papers-list {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}
.nb-papers-row {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--nb-s4);
  padding: var(--nb-s3);
  border-top: 1px solid var(--nb-border);
  transition:
    background-color var(--nb-dur-1) var(--nb-ease),
    opacity var(--nb-dur-1) var(--nb-ease),
    display var(--nb-dur-1) var(--nb-ease) allow-discrete;
}
.nb-papers-row:first-child {
  border-top: 0;
}
.nb-papers-row:hover,
.nb-papers-row:has(.nb-papers-row-link:focus-visible) {
  background: var(--nb-selection-wash);
}
/* [hidden] must beat display:flex — pastpapers.js filters with it. The
   opacity + discrete `display` transition above is the filter crossfade:
   where `allow-discrete` is unsupported the row simply cuts, which is the
   pre-redesign behaviour, so this is pure progressive enhancement. */
.nb-papers-row[hidden] {
  display: none;
  opacity: 0;
}
/* Entering direction needs a start value, or the row pops in at full opacity. */
@starting-style {
  .nb-papers-row:not([hidden]) {
    opacity: 0;
  }
}
.nb-papers-row-main {
  display: flex;
  align-items: center;
  gap: var(--nb-s3);
  min-width: 0;
}
.nb-papers-row-icon {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--nb-s8);
  height: var(--nb-s8);
  border-radius: var(--nb-r-md);
  background: var(--nb-paper-sunken);
  border: 1px solid var(--nb-border);
  color: var(--nb-blue);
}
.nb-papers-row-copy {
  display: flex;
  flex-direction: column;
  gap: var(--nb-s1);
  min-width: 0;
}
.nb-papers-row-title {
  font: var(--nb-type-title);
  letter-spacing: -0.01em;
  color: var(--nb-ink);
}
/* The stretched primary link. Its ::after is the row's hit area, so it must
   stay the FIRST positioned descendant — .nb-papers-row-ms lifts above it. */
.nb-papers-row-link {
  text-decoration: none;
  transition: color var(--nb-dur-1) var(--nb-ease);
}
.nb-papers-row-link::after {
  content: "";
  position: absolute;
  inset: 0;
}
.nb-papers-row:hover .nb-papers-row-link {
  color: var(--nb-blue);
}
/* Kept on the link itself rather than only on the row's :has() wash, so the
   focus ring survives where :has() does not. */
.nb-papers-row-link:focus-visible {
  outline: 2px solid var(--nb-blue);
  outline-offset: 2px;
  border-radius: var(--nb-r-sm);
}
.nb-papers-row-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--nb-s2);
  font: var(--nb-type-label);
  color: var(--nb-ink);
}
.nb-papers-unit-badge {
  padding: 0.05em 0.5em;
  border-radius: var(--nb-r-pill);
  background: var(--nb-blue-wash);
  border: 1px solid color-mix(in srgb, var(--nb-blue) 25%, transparent);
  font: var(--nb-type-label);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: var(--nb-blue);
}
/* Deliberately kept with no markup behind it. The per-row source chip was
   removed 2026-08-29 (see .nb-papers-provenance), but
   pages/tests_typography_stage5_wave3.py:517 resolves this selector through
   _declarations(), which RAISES "Missing CSS rule" when it is absent — so
   deleting the rule turns a passing test into an error rather than a failure.
   The legacy papers_index.html still renders .pp-source-meta. */
.nb-papers-source {
  color: var(--nb-ink-2);
}

/* Right edge of the row — one quiet mark-scheme link plus the direction
   glyph. The glyph is ALWAYS rendered so a row with no mark scheme (mocks
   2024-mock-b, mark_scheme: null) still reads as a row rather than as a
   line that lost something. */
.nb-papers-row-actions {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--nb-s2);
}

/* Deliberately kept. No .nb-btn survives inside a paper row after the
   2026-08-29 redesign, but pages/tests_typography_stage5_wave3.py:523 asserts
   this rule carries no font/font-size role, and its _declarations() helper
   RAISES "Missing CSS rule" when the selector is absent — deleting this turns
   a passing test into an error rather than a failure. */
.nb-papers-row-actions .nb-btn {
  padding: var(--nb-s2) var(--nb-s3);
}

.nb-papers-row-ms {
  position: relative;
  z-index: 1;
  flex: none;
  display: inline-flex;
  align-items: center;
  padding: var(--nb-s1) var(--nb-s2);
  border-radius: var(--nb-r-md);
  /* Sized off the label token but WITHOUT the composite `font:` role: at the
     label's inherited 600 this read as loud as the meta beside it, and the
     affordance is meant to be quiet. Using font-size also keeps
     TypographyDebtRatchetTests.COMPONENT_TYPOGRAPHY_COUNT flat — that ratchet
     counts font/font-family/font-weight, and a var() font-size is not debt. */
  font-size: var(--nb-text-sm);
  color: var(--nb-ink-2);
  text-decoration: none;
  white-space: nowrap;
  transition:
    color var(--nb-dur-1) var(--nb-ease),
    background-color var(--nb-dur-1) var(--nb-ease);
}
.nb-papers-row-ms:hover {
  color: var(--nb-blue);
  background: var(--nb-blue-wash);
}
.nb-papers-row-ms:focus-visible {
  outline: 2px solid var(--nb-blue);
  outline-offset: 2px;
}

/* The LABEL for the primary action. Blue at rest, where the mark scheme is
   secondary ink: the row IS the question-paper link, so the primary affordance
   should be the one that reads as interactive without hovering. Sized with
   font-size rather than a composite `font:` role to keep
   TypographyDebtRatchetTests.COMPONENT_TYPOGRAPHY_COUNT flat. */
.nb-papers-row-go {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--nb-s1);
  padding: var(--nb-s1) var(--nb-s2);
  font-size: var(--nb-text-sm);
  color: var(--nb-blue);
  white-space: nowrap;
  transition: color var(--nb-dur-1) var(--nb-ease);
}
/* Only the arrow travels on hover — sliding a text label reads as a glitch. */
.nb-papers-row-go svg {
  flex: none;
  transition: transform var(--nb-dur-1) var(--nb-ease);
}
.nb-papers-row:hover .nb-papers-row-go svg {
  transform: translateX(2px);
}

/* ---------- 6 · Guide + FAQ -------------------------------------
   The closing guidance block: an optional unit guide and the FAQ as
   notebook <details>. The FAQPage JSON-LD mirrors these questions. */
.nb-papers-guide {
  display: flex;
  flex-direction: column;
  gap: var(--nb-s6);
  margin-top: var(--nb-s8);
}
.nb-papers-guide-title {
  margin: 0 0 var(--nb-s4);
  font: var(--nb-type-title);
  letter-spacing: -0.01em;
  color: var(--nb-ink);
}
.nb-papers-unit-guide {
  padding: var(--nb-s5) var(--nb-s6);
  background: var(--nb-paper-sunken);
  border: 1px solid var(--nb-border);
  border-radius: var(--nb-r-lg);
}
.nb-papers-guide-list {
  display: flex;
  flex-direction: column;
  gap: var(--nb-s2);
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--nb-ink);
}
.nb-papers-guide-code {
  display: inline-block;
  min-width: 3.5em;
  margin-right: var(--nb-s2);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--nb-blue);
}

.nb-papers-faq-list {
  display: flex;
  flex-direction: column;
  gap: var(--nb-s3);
}
.nb-papers-faq-item {
  padding: var(--nb-s4) var(--nb-s5);
  background: var(--nb-paper-raised);
  border: 1px solid var(--nb-border);
  border-radius: var(--nb-r-md);
  box-shadow: var(--nb-shadow-1);
}
.nb-papers-faq-item > summary {
  cursor: pointer;
  list-style: none;
  font: var(--nb-type-control);
  color: var(--nb-ink);
}
.nb-papers-faq-item > summary::-webkit-details-marker {
  display: none;
}
.nb-papers-faq-item > summary::after {
  content: "+";
  float: right;
  margin-left: var(--nb-s3);
  font-weight: 400;
  color: var(--nb-ink-2);
  transition: transform var(--nb-dur-1) var(--nb-ease);
}
.nb-papers-faq-item[open] > summary::after {
  content: "\2212"; /* minus */
}
.nb-papers-faq-item > p {
  margin: var(--nb-s3) 0 0;
  font: var(--nb-type-prose);
  color: var(--nb-ink);
}

/* ---------- 7 · Paper viewer -----------------------------------
   A single hosted PDF gets a wider canvas than the reading measure:
   the sheet + the embedded PDF want room. */
.nb-page-main:has(.nb-paper-viewer) {
  max-width: 1080px;
}
.nb-paper-viewer {
  width: 100%;
}
.nb-pv-inner {
  width: 100%;
}
.nb-pv-header {
  margin-bottom: var(--nb-s6);
}
.nb-pv-heading {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--nb-s5);
  flex-wrap: wrap;
  margin-top: var(--nb-s4);
}
.nb-pv-title-block {
  min-width: 0;
}
.nb-pv-title {
  font: var(--nb-type-headline);
  letter-spacing: -0.015em;
}
.nb-pv-meta {
  margin: var(--nb-s2) 0 0;
  font: var(--nb-type-label);
  color: var(--nb-ink);
}
.nb-pv-controls {
  display: flex;
  align-items: center;
  gap: var(--nb-s3);
  flex-wrap: wrap;
}
.nb-pv-toggle {
  flex-wrap: wrap;
}
.nb-pv-download {
  gap: var(--nb-s2);
}

/* The PDF sheet — a raised frame holding the iframe on a sunken well.
   Height mirrors the legacy viewport math so the PDF fills the fold. */
.nb-pv-frame-wrap {
  margin-top: var(--nb-s6);
  padding: var(--nb-s3);
  background: var(--nb-paper-raised);
  border: 1px solid var(--nb-border);
  border-radius: var(--nb-r-lg);
  box-shadow: var(--nb-shadow-1);
}
.nb-pv-frame {
  display: block;
  width: 100%;
  height: clamp(560px, calc(100vh - 240px), 1100px);
  border: 0;
  border-radius: var(--nb-r-md);
  background: var(--nb-paper-sunken);
}
.nb-pv-fallback {
  margin: var(--nb-s4) 0 0;
  color: var(--nb-ink-2);
}
.nb-pv-back {
  margin-top: var(--nb-s6);
  align-self: flex-start;
}

/* ---------- 8 · Responsive -------------------------------------- */
@media (max-width: 720px) {
  .nb-papers-controls {
    padding: var(--nb-s4) var(--nb-s4);
  }
  .nb-papers-chiprow {
    flex-wrap: nowrap;
    overflow-x: auto;
    padding: var(--nb-s1);
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .nb-papers-chiprow::-webkit-scrollbar {
    display: none;
  }
  /* The row STAYS horizontal at narrow widths. Its right edge is now a short
     link plus a glyph rather than three buttons, so it costs little width and
     stacking it would push the mark scheme a full row away from its paper. */
  .nb-papers-row {
    gap: var(--nb-s2);
    padding: var(--nb-s3) var(--nb-s1);
  }
  .nb-papers-row-main {
    min-width: 0;
  }
  .nb-papers-row-actions {
    gap: var(--nb-s1);
  }
  .nb-pv-heading {
    align-items: flex-start;
  }
  .nb-pv-controls {
    width: 100%;
  }
}

/* Below 480px the copy column is the scarcest thing on the row, and "Unit 1"
   is the one meta item that carries no information the badge beside it does
   not already state (WMA11 encodes the same unit). Dropping it keeps the meta
   on ONE line. The value still reaches the filter: search_text is assembled
   server-side in views.py:966-971, not read from the DOM. */
@media (max-width: 480px) {
  .nb-papers-row-unit {
    display: none;
  }

  /* Both actions are now LABELLED, and two labels plus the copy will not share
     one line at this width — so the row stacks and the actions get their own
     row beneath the meta. Rows get taller; a labelled primary is worth it.
     .nb-papers-row-go stays decorative markup over the stretched anchor, so
     tapping it still resolves to the row link. */
  .nb-papers-row {
    flex-direction: column;
    align-items: stretch;
    gap: var(--nb-s2);
  }
  .nb-papers-row-actions {
    justify-content: flex-start;
    gap: var(--nb-s2);
    padding-inline-start: var(--nb-s8);
  }
  .nb-papers-row-go,
  .nb-papers-row-ms {
    min-block-size: 44px;
    padding-inline: var(--nb-s2);
  }
}

/* Filters and deep links are repeated, high-frequency paper-bank controls.
   Keep the dense fine-pointer index while making touch-primary use explicit. */
@media (pointer: coarse) {
  .nb-papers-chip {
    min-block-size: 44px;
  }
  .nb-papers-group-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-inline-size: 44px;
    min-block-size: 44px;
    opacity: 1;
  }
  /* The mark scheme is the one control competing with the row's own hit area,
     so it earns the full touch floor rather than the dense fine-pointer size. */
  .nb-papers-row-ms {
    min-inline-size: 44px;
    min-block-size: 44px;
    justify-content: center;
  }
}

/* ---------- 9 · Motion ------------------------------------------
   Two separate mechanisms, both opt-in on prefers-reduced-motion:

   · ON LOAD — the staggered row entrance below. It is CSS because
     esa-motion's revealStagger returns early for an above-the-fold
     container (esa-motion.js:119) and .nb-papers-groups always starts
     above the fold, so [data-reveal-stagger] on it only ever covers
     groups the reader scrolls down to.
   · ON FILTER — the opacity + discrete `display` crossfade in §5.

   The delay ramp is capped at the tenth row: past that the stagger stops
   being legible and starts being a wait. */
@media (prefers-reduced-motion: no-preference) {
  .nb-papers-row {
    animation: nb-papers-row-in var(--nb-dur-2) var(--nb-ease) backwards;
  }
  .nb-papers-row:nth-child(2) { animation-delay: 30ms; }
  .nb-papers-row:nth-child(3) { animation-delay: 60ms; }
  .nb-papers-row:nth-child(4) { animation-delay: 90ms; }
  .nb-papers-row:nth-child(5) { animation-delay: 120ms; }
  .nb-papers-row:nth-child(6) { animation-delay: 150ms; }
  .nb-papers-row:nth-child(7) { animation-delay: 180ms; }
  .nb-papers-row:nth-child(8) { animation-delay: 210ms; }
  .nb-papers-row:nth-child(9) { animation-delay: 240ms; }
  .nb-papers-row:nth-child(n + 10) { animation-delay: 270ms; }
}

@keyframes nb-papers-row-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* This layer had no reduced-motion guard before 2026-08-29 despite shipping
   transitions since Wave 1. It covers the pre-existing chip/permalink
   transitions as well as everything the row redesign added. */
@media (prefers-reduced-motion: reduce) {
  /* `transition-duration: 0s`, not `transition: none`, on purpose:
     NotebookLowGlareThemeTests.test_selected_consumers_use_semantic_tokens
     resolves .nb-papers-chip with required_properties=("transition",) and
     asserts exactly ONE matching rule. A second rule declaring the `transition`
     shorthand makes that lookup ambiguous and fails the test; the longhand
     disables motion just as completely without colliding. */
  .nb-papers-chip,
  .nb-papers-group-link,
  .nb-papers-row,
  .nb-papers-row-link,
  .nb-papers-row-ms,
  .nb-papers-row-go,
  .nb-papers-row-go svg,
  .nb-papers-faq-item > summary::after {
    transition-duration: 0s;
  }
  .nb-papers-row {
    animation: none;
  }
  .nb-papers-chip:hover:not(.is-active) {
    transform: none;
  }
  .nb-papers-row:hover .nb-papers-row-go svg {
    transform: none;
  }
}
