/* ============================================================
   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;
}
/* The header rhythm the hub pages get from notebook-hub.css, which this page
   does not load (mobile audit 2026-09-25, S-025): without it the breadcrumb
   sat on the title and the header on the first year group, 0px apart at every
   width. Same two steps as the hub. Scoped to .nb-papers, so the paper
   viewer (.nb-paper-viewer) keeps its own header spacing. */
.nb-papers .nb-index-header {
  margin-bottom: var(--nb-s8);
}
.nb-papers .nb-index-header .nb-breadcrumb {
  margin-bottom: var(--nb-s5);
}
/* The subject library's header (owner 2026-10-01: the new look site wide):
   the resource word in blue, and the way back up in blue links. */
.nb-papers .nb-index-title-accent {
  color: var(--nb-blue);
}
.nb-papers .nb-index-header .nb-breadcrumb a {
  color: var(--nb-blue);
}
.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);
  /* One ink (owner, 2026-09-26: "are these white or what???"): full ink,
     and at 13px a 400 stroke is smoothed into the night paper and reads
     grey, so the line takes 500, the second weight, as the desk does. */
  font-weight: 500;
  color: var(--nb-ink);
}
.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 / data-ui-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;
  /* one ink (owner, 2026-09-26): the count is full strength, never faded;
     its 600 against the label's weight is the step */
  opacity: 1;
}

/* 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: 1;  /* one ink: the code is full strength, its 700 is the step */
}

/* 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;
}
/* While the row waits, pastpapers.js disables its chips too, so a key
   cannot press what a pointer cannot. The row's opacity stays the one cue:
   no user-agent disabled fade or text fill stacks on top of it. */
.nb-papers-filter.is-waiting-for-family .nb-papers-chip:disabled {
  opacity: 1;
  -webkit-text-fill-color: currentColor;
}
.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);
}
/* The count is rendered once, by the server, for the whole year; a filter
   then hides rows (pastpapers.js sets [hidden] on them) and the count went on
   saying "4 papers" over one visible row (mobile audit 2026-09-25, S-047: 15
   of 15 years wrong under the Unit 4 filter). While any of a year's rows is
   filtered out its count stands down; "Showing N" above the list still gives
   the total. With no filter every count is shown and right, as before. */
.nb-papers-group:has([data-pp-card][hidden]) .nb-papers-group-count {
  display: none;
}

/* ---------- 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;
}
/* Under a filter the first rows of a year can be hidden, and the first one
   left showing drew its hairline straight under the heading's rule: a double
   line (seen while checking S-047). The first VISIBLE row drops it instead.
   Unfiltered it is the same row as :first-child. */
.nb-papers-row:nth-child(1 of :not([hidden])) {
  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);
  /* one ink: full ink at 500 (a 13px 400 link read grey by night). The
     Question paper link beside it takes the same 500, so the pair stays
     level and the blue alone says which is the paper. */
  font-weight: 500;
  color: var(--nb-ink);
  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);
  /* 500, the second weight, like the Mark scheme link beside it: a 13px
     word at 400 reads thin by night (The One-Ink Rule, owner 2026-09-26),
     and the primary link must not read lighter than the secondary one. */
  font-weight: 500;
  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%;
  }
}

/* A PHONE OPENS THE PAPER FROM THE FIRST SCREEN (mobile audit 2026-09-25,
   S-021). A phone browser either leaves an inline PDF blank (Chrome on
   Android) or paints its first page only (Safari on iPhone), so on a phone
   the real way to read the paper is the browser's own viewer. That route was
   a 19px inline link under a frame at least 560px tall: at 390x844 it sat at
   y=996, and at 844x390 the frame alone was taller than the screen. Here the
   link becomes a full-width blue button straight under Download, and the
   frame is never taller than the window under the topbar. The button goes
   ABOVE the frame rather than replacing it, so Safari keeps its preview.
   #pv-open keeps its id and its place in the markup: papers.js rewrites its
   href on the Question paper / Mark scheme toggle, so the button follows the
   toggle. The button keeps the house button geometry (DESIGN.md section 5:
   --nb-r-sm, 10px 16px, and the 44px of the Download button above it), and
   font-size only (no font or font-weight), so the typography ratchet stays
   flat. The 100vh line is the fallback for a browser without svh.
   A TOUCH TABLET GETS THE SAME BUTTON (mobile audit round 2, S-R1-6). iPad
   Safari paints an embedded PDF as a static first page and Android Chrome
   leaves it blank, so on a tablet too the native viewer is the real reading
   route, and it was the same 19px link under a frame of 560 to 940px, below
   the first screen (y 1041 of 1024 at 768x1024). (pointer: coarse) brings the
   reorder and the button to tablets; the frame keeps its taller tablet height,
   because the clamp below stays in a block of its own with the phone query.
   A screen wider than 720px and taller than 500px with a fine pointer (every
   desktop) is untouched. */
@media (max-width: 720px), (orientation: landscape) and (max-height: 500px), (pointer: coarse) {
  .nb-pv-inner {
    display: flex;
    flex-direction: column;
  }
  .nb-pv-fallback {
    order: 1;
    margin: 0 0 var(--nb-s4);
  }
  .nb-pv-frame-wrap {
    order: 2;
    margin-top: 0;
  }
  /* The back link wraps inside the column (S-R1-2): "← All mock exams" kept
     .nb-btn's nowrap and made the page 176px wide at 160 CSS px (a 320px
     phone at 200% zoom). One line at default text, as before. */
  .nb-pv-back {
    order: 3;
    white-space: normal;
    text-wrap: balance;
    text-align: center;
    max-inline-size: 100%;
  }
  .nb-pv-fallback-lead,
  .nb-pv-fallback-end {
    display: none;
  }
  /* (0,2,0), after notebook-components.css: beats .nb-link and .nb-link:hover,
     so a tap can never leave blue ink on the blue button. */
  .nb-pv-fallback .nb-pv-open {
    display: flex;
    align-items: center;
    justify-content: center;
    inline-size: 100%;
    min-block-size: 44px;
    padding: 10px var(--nb-s4);
    border-radius: var(--nb-r-sm);
    background: var(--nb-blue);
    color: var(--nb-on-blue);
    font-size: var(--nb-text-base);
    text-align: center;
    text-decoration: none;
    transition: background-color var(--nb-dur-1) var(--nb-ease);
  }
  .nb-pv-fallback .nb-pv-open:active {
    background: var(--nb-blue-strong);
  }
}
/* The frame is never taller than the window under the topbar: phones and a
   phone on its side only (a tablet keeps its taller preview, S-R1-6). */
@media (max-width: 720px), (orientation: landscape) and (max-height: 500px) {
  .nb-pv-frame {
    height: clamp(280px, calc(100vh - var(--nb-chrome-h) - var(--nb-s9)), 560px);
    height: clamp(280px, calc(100svh - var(--nb-chrome-h) - var(--nb-s9)), 560px);
  }
}
@media (max-width: 720px) and (hover: hover), (orientation: landscape) and (max-height: 500px) and (hover: hover) {
  .nb-pv-fallback .nb-pv-open:hover {
    background: var(--nb-blue-strong);
    color: var(--nb-on-blue);
  }
}

/* Below 480px the row stacks (below), so the meta has the row's full width
   and "Unit 1" keeps its place beside the code badge, as on the desktop
   (mobile audit 2026-09-25, S-046: it used to be hidden here to keep the meta
   on one line, which the stacked row now does with it shown). */
@media (max-width: 480px) {
  /* 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 {
    /* With large text the two labels cannot share one line (at 2x text they
       need 433px of a 350px row at 390, which pushed the page sideways), so
       Mark scheme drops under Question paper instead. At default text they
       share the line as before. */
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: var(--nb-s2);
    padding-inline-start: var(--nb-s8);
  }
  /* With large text or page zoom nothing on these rows may push the page
     sideways (mobile audit round 2, S-R1-3). At 320px and 2x text, or at 195
     CSS px, "4 papers" could not leave the year's line and ran past the right
     edge (the page was 348px in a 320px window), the two nowrap labels could
     neither shrink nor wrap, and at 160 CSS px a title word ("Marketing") was
     wider than its 68px column. The count now takes a line of its own under
     the year, a label wraps inside its own box, and a title word breaks only
     when it is wider than the column. At default text every one of them still
     fits on its line, so nothing moves. */
  .nb-papers-group-head {
    flex-wrap: wrap;
    row-gap: var(--nb-s1);
  }
  .nb-papers-row-go,
  .nb-papers-row-ms {
    min-block-size: 44px;
    padding-inline: var(--nb-s2);
    flex: 0 1 auto;
    min-inline-size: 0;
    white-space: normal;
  }
  .nb-papers-row-title {
    overflow-wrap: break-word;
  }
}

/* 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;
  }
}
