/* ── The kit's second file: the sidebar, the signed-in page layout and the
   component families only signed-in pages use (loaded after tokens.css and
   components.css). Like components.css, it is the only place anything in
   it is defined; a page may position a component here, never repaint it. ── */

:root {
  --sidebar-width: 240px;
}

/* ── Layout ── */
.layout {
  display: flex;
  min-height: 100vh;
}

.main {
  margin-left: var(--sidebar-width);
  flex: 1;
  /* Without this, .main's default min-width:auto refuses to shrink below the
     min-content size of whatever's inside it — including deeply nested
     white-space:nowrap text (e.g. .card-label, .card-score on a long
     paper/quiz title + status combo) even though those descendants already
     have their own min-width:0. That forces the whole page wider than the
     viewport instead of letting the existing ellipsis truncation do its job.
     Found + fixed in the 2026-07 UI review. */
  min-width: 0;
}

/* ── Sidebar ── */
.sidebar {
  width: var(--sidebar-width);
  background: var(--surface-raised);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  position: fixed;
  top: 0;
  left: 0;
  height: 100vh;
  z-index: 100;
}

/* The kit's .logo; only where it sits in the sidebar is here. */
.sidebar-logo {
  height: 56px;
  padding: 0 1.25rem;
  border-bottom: 1px solid var(--border);
  display: flex;
  flex-shrink: 0;
}

/* The phone drawer's ✕, where the hamburger that opened it sits. */
.sidebar-close { display: none; position: absolute; top: 6px; right: 0.5rem; }

.sidebar-nav { flex: 1; padding: 1rem 0; }

.nav-link {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 1.25rem;
  /* The touch-target floor is stated, not left to padding + line-height:
     the type-size sweep took the label to --fs-small and the row measured
     43px (Cluster 3). */
  min-height: 44px;
  color: var(--text-muted);
  text-decoration: none;
  font-size: var(--fs-small);
  transition: background 0.15s, color 0.15s;
  width: 100%;
  background: none;
  border: none;
  cursor: pointer;
  text-align: left;
}
@media (hover: hover) {
  .nav-link:hover { background: var(--accent-subtle); color: var(--text-accent); }
}
.nav-link:active { background: var(--accent-subtle); }
.nav-link.active { background: var(--accent-subtle); color: var(--text-accent); font-weight: 600; border-left: 3px solid var(--text-accent); }
.nav-link svg { width: 18px; height: 18px; flex-shrink: 0; }
/* The unread-notifications dot on the Notifications link. */
.nav-badge { display: inline-block; width: 8px; height: 8px; margin-left: auto; flex-shrink: 0; border-radius: 50%; background: var(--fill-danger); }

/* ── Credit balance chip ── */
.sidebar-credit-chip {
  margin: 0 0.875rem 0.75rem;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.55rem 0.75rem;
  font-size: var(--fs-small);
  color: var(--text-secondary);
  line-height: 1.45;
  display: block;
}
.sidebar-credit-chip:empty { display: none; }
.chip-plan {
  font-weight: 700;
  display: block;
  margin-bottom: 0.1rem;
}
.chip-detail {
  color: var(--text-muted);
  font-size: var(--fs-small);
  display: block;
}
.chip-detail--packs {
  margin-top: 0.2rem;
}
/* Shown only while a card payment is being retried. Amber, not red: the plan
   still works, and the red treatment below means "nothing left". */
.chip-detail--warning {
  margin-top: 0.3rem;
  font-weight: 600;
  color: var(--text-warning);
  text-decoration: none;
}
/* The chip is the credit box's last line; its 44px area extends over the
   caption above it, which is not tappable, so nothing competes for a tap. */
.chip-topup {
  display: inline-block;
  position: relative;
  margin-top: 0.3rem;
  font-weight: 600;
  color: var(--text-accent);
  text-decoration: none;
  font-size: var(--fs-small);
}
.chip-topup::before { content: ''; position: absolute; inset: -14px 0; }
.sidebar-credit-chip--low {
  background: var(--bg-warning);
  border-color: var(--border-warning);
  color: var(--text-warning);
}
.sidebar-credit-chip--low .chip-detail { color: var(--text-warning); }
.sidebar-credit-chip--low .chip-topup { color: var(--text-accent); font-weight: 700; }
.sidebar-credit-chip--empty {
  background: var(--bg-danger);
  border-color: var(--border-danger);
  color: var(--text-danger);
}
.sidebar-credit-chip--empty .chip-detail { color: var(--text-danger); }
.sidebar-credit-chip--empty .chip-topup { color: var(--text-accent); font-weight: 700; }

.sidebar-footer {
  padding: 1rem 1.25rem;
  border-top: 1px solid var(--border);
}

/* Every footer control is a 44px row: the rows stack, so the box grows
   rather than taking a ::before hit-area that would overlap the next one. */
.sidebar-legal {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
}
.sidebar-legal a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--fs-eyebrow);
  color: var(--text-muted);
  text-decoration: none;
}

.signout-btn {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: var(--fs-small);
  cursor: pointer;
  padding: 0;
  min-height: 44px;
  text-align: left;
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
@media (hover: hover) {
  .signout-btn:hover { color: var(--text-danger); }
}
.signout-btn .kit-icon, .sidebar-settings-link .kit-icon { width: 16px; height: 16px; }

.sidebar-settings-link {
  color: var(--text-muted);
  font-size: var(--fs-small);
  text-decoration: none;
  padding: 0;
  min-height: 44px;
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
@media (hover: hover) {
  .sidebar-settings-link:hover { color: var(--text-accent); }
}

/* ── Mobile top bar ── */
/* Three slots: Back (or nothing) | logo | hamburger. The hamburger sits on the
   RIGHT on every page so it never moves, and Back takes the left, which is
   where every phone convention puts it (owner, 2026-09-05). The height is
   published as a custom property so any sticky element on the page can offset
   itself by it instead of hiding underneath. */
:root { --mobile-top-bar-h: 56px; }
/* An in-page link (#submission-card) stops below the sticky bar instead of
   under it: the phone top bar and .page-header-bar are both this height, and
   the document is the scroller at every width. */
html { scroll-padding-top: calc(var(--mobile-top-bar-h) + 0.5rem); }

.mobile-top-bar {
  display: none;
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border);
  padding: 0 0.5rem;
  height: var(--mobile-top-bar-h);
  align-items: center;
  gap: 0.5rem;
}
/* Keeps the logo optically centred whether or not a Back link is present. */
.mobile-top-bar .mobile-logo { margin-inline: auto; }
.mobile-top-bar .hamburger-btn { order: 3; }
.mobile-top-bar .mobile-bar-spacer { width: 44px; flex: 0 0 44px; }

/* The kit's .logo.logo--compact; only where it sits in the top bar is here. */
.mobile-logo {
  flex: 1;
  justify-content: center;
}

.hamburger-btn {
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.5rem;
  color: var(--text);
  min-height: 44px;
  min-width: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.hamburger-btn .kit-icon { width: 24px; height: 24px; }

/* ── Sidebar backdrop ── */
.sidebar-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: var(--scrim);
  z-index: 200;
}
.sidebar-backdrop.open { display: block; }

/* ── Vote widget ──────────────────────────────────────────────────────────
   The thumbs up/down control on a quiz question, a mock paper and a revision
   note. It lived in three page <style> blocks and three page scripts, and had
   already drifted (paper used .paper-vote-btn) before revision notes made it a
   third copy. Markup and behaviour are now built by initVoteWidget() in
   utils.js — the same rule CLAUDE.md states for the sidebar and report modal.

   The container is deliberately plain: quiz and notes separate it from the
   content above with a rule, the paper sits it inside its own padded card, so
   spacing stays with whoever owns the surrounding layout. */
.vote-widget {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
}
.vote-label {
  font-size: var(--fs-small);
  color: var(--text-muted);
  font-weight: 500;
  flex-shrink: 0;
}
.vote-btn {
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.25rem;
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  transition: background 0.15s;
  color: var(--text-muted);
}
@media (hover: hover) {
  .vote-btn:hover { background: var(--accent-subtle); color: var(--text-accent); }
}
.vote-btn .kit-icon { width: 22px; height: 22px; }
/* The chosen thumb is filled. */
.vote-btn.voted-up .kit-icon, .vote-btn.voted-down .kit-icon { fill: currentColor; }
.vote-btn.voted-up { color: var(--text-accent); }
.vote-btn.voted-down { color: var(--text-accent); }

/* Separates the control from the content it rates. Used by quiz and notes; the
   paper's own card already provides that separation. */
.vote-widget.vote-widget-divided {
  margin-top: 1.25rem;
  padding-top: 1.125rem;
  border-top: 1px solid var(--border);
}

/* ── Downvote reason modal ────────────────────────────────────────────────
   Shown only where a surface supplies a reason list (papers today). Moved here
   with the widget; paper.html's image-limit dialog reuses the kit's own
   .modal-overlay / .modal-box / .modal-title (components.css), opened through
   openDialog, so those stay generally available rather than becoming
   vote-only. Only the options list is its own; each option is the kit's
   .choice-card (.vote-modal-option is the behaviour hook) and its buttons
   are the kit's .modal-actions. */
.vote-modal-options { display: flex; flex-direction: column; gap: 0.625rem; }
.limit-modal-body { font-size: var(--fs-body); color: var(--text-secondary); line-height: 1.5; }

/* ── Report issue modal: the kit dialog; only its address row is its own ──
   The report form's fallback: the support address as selectable text with a
   copy button, under the form. A bare mailto: only works where a mail client
   is configured — on a school PC or a phone using webmail it silently does
   nothing, and the student believes they have reported something. */
.report-modal-address {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  background: var(--accent-subtle);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.65rem 0.75rem;
  margin-top: 1rem;
}
.report-modal-address .form-hint { margin: 0; }
.report-modal-email {
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--text);
  user-select: all;
  word-break: break-all;
  flex: 1 1 auto;
  min-width: 0;
}
/* .report-modal-copy → components.css .btn-secondary.btn-sm.
   .report-modal-cta → components.css .btn-primary.btn-block. */

/* Below ~380px the address and the copy button no longer fit on one line.
   Rather than leave the button stranded at the left of its own row, stack the
   pair and let it span the width — the address stays selectable either way. */
@media (max-width: 379px) {
  .report-modal-address { flex-direction: column; align-items: stretch; }
  .report-modal-address .btn-secondary { width: 100%; }
}

/* ── Teacher banner ── the kit's warning notice (sidebar.js), hidden until
   the account turns out to be a teacher's. */
.teacher-banner { display: none; margin-bottom: 1.25rem; }

/* ── Shared history card ── */
.card-list { display: flex; flex-direction: column; gap: 0.75rem; }

/* The wrap is the visual white box; the <a> inside it is just the link area */
/* The look is the kit's .card.card--elevated (in the markup); this is only
   the row's layout. */
.history-card-wrap {
  padding: 1rem 1.25rem;
  display: flex;
  /* Wraps so a refused hide's .action-error (flex-basis 100%) takes its own
     line under the card; the card link's basis is 0, so it never wraps. */
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
  transition: box-shadow 0.15s, transform 0.15s, border-color 0.15s;
  min-height: 44px;
}
/* The <a> is transparent — just the clickable text area */
.history-card {
  flex: 1;
  min-width: 0;
  text-decoration: none;
  color: inherit;
}
.card-left { flex: 1; min-width: 0; }
.card-type { margin-bottom: 0.2rem; }
.card-label {
  font-size: var(--fs-small);
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.card-date {
  font-size: var(--fs-small);
  color: var(--text-muted);
  margin-top: 0.15rem;
}
.card-right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.15rem;
  flex-shrink: 0;
}
.card-score {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--text-accent);
  white-space: nowrap;
}
.card-score.status-marked  { color: var(--text-success); }
.card-score.status-uploaded  { color: var(--text-warning); }
.card-score.status-generated { color: var(--text-accent); }


/* ── Generating / failed card states ── */
/* A generating or failed card is non-clickable (no .card--interactive): a
   subtle shadow and a muted/red rail. */
.history-card-wrap.is-generating,
.history-card-wrap.is-failed {
  box-shadow: var(--shadow-card-subtle);
}
.history-card-wrap.is-generating { border-left-color: var(--text-muted); }
.history-card-wrap.is-failed     { border-left-color: var(--text-danger); }

/* The static card is a non-clickable <div> — no link affordance. */
.history-card-static { cursor: default; }

/* "Generating…" line: the kit's .btn-spinner, in the line's own colour. */
.card-generating-text {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--text-accent);
  font-weight: 500;
}

/* Subtle shimmer over the generating card so it reads as "working", not broken. */
.history-card-wrap.is-generating { position: relative; overflow: hidden; }
.history-card-wrap.is-generating::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent 0%, var(--shimmer-band) 50%, transparent 100%);
  transform: translateX(-100%);
  animation: gen-shimmer 1.6s ease-in-out infinite;
  pointer-events: none;
}
@keyframes gen-shimmer { to { transform: translateX(100%); } }

.card-failed-text { color: var(--text-danger); font-weight: 500; }

/* .card-retry-btn → components.css .btn-primary. */


/* ── Responsive ──
   767, not 639: the fixed 240px sidebar and the multi-column page grids both
   used to switch on at 640, so the 640-767 band got LESS usable width than a
   375px phone (measured 2026-09-05: dashboard action cards 164px wide with
   three-line titles, against 343px on a phone). Everything below 768 is now
   the single-column layout with the drawer. */
@media (max-width: 767px) {
  .mobile-top-bar { display: flex; }

  /* Slides in from the RIGHT, attached to the hamburger that opens it. */
  .sidebar {
    left: auto;
    right: 0;
    border-right: none;
    border-left: 1px solid var(--border);
    transform: translateX(100%);
    /* Closed is hidden, not only off screen, so its links leave the tab
       order and the accessibility tree; the hide waits for the slide-out. */
    visibility: hidden;
    transition: transform 0.25s ease, visibility 0s linear 0.25s;
    z-index: 250;
    height: 100%;
    overflow-y: auto;
  }
  .sidebar.open { transform: translateX(0); visibility: visible; transition: transform 0.25s ease, visibility 0s linear 0s; }
  .sidebar-close { display: inline-flex; }

  .main { margin-left: 0; }
}

/* Tablet/desktop: sidebar is a fixed-height column that can't grow with the
   viewport, so if nav links + footer exceed 100vh (short laptop screens),
   the nav-links region gets its own scrollbar. Logo and footer stay pinned —
   min-height:0 overrides the flex-item default that would otherwise refuse
   to shrink below content size (same issue as .main's min-width, see above). */
@media (min-width: 768px) {
  .sidebar-nav {
    overflow-y: auto;
    min-height: 0;
  }
}

/* Inline button spinner (.btn-spinner) → components.css */

/* ── Shared answer-image upload buttons (paper.html, class-submit.html) ──
 * The two buttons each trigger one of two file inputs (camera vs gallery —
 * Android needs both). The dropzone body itself is deliberately inert: only
 * the buttons and drag-drop act, so no :hover affordance on the body. */
.upload-dropzone .dz-actions {
  display: flex; flex-wrap: wrap; gap: 10px;
  justify-content: center; margin-top: 6px;
}
/* .dz-btn → components.css .btn-outline. */
.upload-dropzone .dz-btn-gallery .dz-lbl-desktop { display: none; }
/* Desktop: no live camera path, so hide "Take photos" and relabel gallery. */
@media (hover: hover) and (pointer: fine) {
  .upload-dropzone .dz-btn-camera { display: none; }
  .upload-dropzone .dz-btn-gallery .dz-lbl-mobile { display: none; }
  .upload-dropzone .dz-btn-gallery .dz-lbl-desktop { display: inline; }
}
/* Sparkle icon inside AI-action buttons */
.btn-sparkle { flex-shrink: 0; }

/* Form controls (.form-group/.form-label/.form-input/.form-select) →
   components.css */

/* Toast (.app-toast and its celebrate variant) → components.css */

/* ── Shared secondary-page header bar (back link) ──
   Was byte-identical, copy-pasted into paper.html, quizzes.html, papers.html,
   settings.html, notifications.html and class-submit.html; consolidated here
   in the 2026-07 UI review. quiz.html is NOT one of these — it uses its own
   distinct .header (drawer toggle, progress bar, counters), not this pattern. */
.page-header-bar {
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border);
  padding: 0 1rem;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  position: sticky;
  top: 0;
  z-index: 10;
}
/* The bar's back control is the kit's .link-back. */

/* ── Shared list-page chrome (quizzes.html / papers.html) ──
   Consolidated in the 2026-07 UI review — both pages loaded a near-identical
   copy of this block. One documented divergence remains page-specific: see
   known-findings.md UI26-01 — .filter-clear's mobile grid-column differs
   between the two pages (3 vs 4 filter dropdowns changes the 2-col mobile
   grid layout), so each page keeps its own small override for that one
   property after this shared block. */
.page {
  padding: 1.5rem 2rem 2rem;
  max-width: 900px;
}
.page-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.75rem;
}
.page-header .btn-primary { flex-shrink: 0; }
/* .page-title → components.css (type scale) */
/* The Study Zone pages' title and subtitle, above each page's own content. */
.study-page-header { display: block; margin-bottom: 1.5rem; }
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-bottom: 1.25rem;
  align-items: center;
}
.select-wrapper {
  position: relative;
  display: inline-flex;
  align-items: center;
  background: var(--surface-raised);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-pill);
  box-shadow: var(--shadow-card);
  transition: border-color 0.15s, box-shadow 0.15s;
  min-height: 44px;
}
.select-wrapper.active {
  border-color: var(--text-accent);
  background: var(--accent-subtle);
}
.select-wrapper select {
  background: transparent;
  border: none;
  padding: 0.5rem 1rem;
  font-size: var(--fs-small);
  color: var(--text);
  cursor: pointer;
  min-height: 44px;
}
/* Targets the TRIGGER as well: enhanceSelect() hides the native <select>, so
   styling it alone was invisible (2026-09-05 UI review). */
.select-wrapper.active select,
.select-wrapper.active .cs-trigger-label { font-weight: 600; color: var(--text-accent); }
/* The enhanced trigger (.cs-trigger, injected by enhanceSelect() in utils.js)
   draws its own background/border by default — now that it fills
   .select-wrapper (2026-07 fix), that doubled up with .select-wrapper's own
   pill border into a visible double-line. .select-wrapper already draws the
   pill (same treatment the native <select> got above), so the trigger goes
   transparent here, same as the native select always did. Hover/open border
   feedback moves to the wrapper via :has() (already used elsewhere in this
   codebase, e.g. paper.html's .ms-header:has(.ms-body.open)) so it isn't
   silently lost. */
.select-wrapper .cs-trigger {
  background: transparent;
  border: none;
  padding: 0.5rem 1rem;
}
.select-wrapper:has(.cs-trigger[aria-expanded="true"]) {
  border-color: var(--fill-accent);
}
@media (hover: hover) {
  .select-wrapper:has(.cs-trigger:hover) { border-color: var(--fill-accent); }
}
.filter-clear {
  font-size: var(--fs-small);
  color: var(--text-muted);
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.3rem 0.5rem;
  border-radius: var(--radius-pill);
  display: none;
  min-height: 44px;
  align-items: center;
}
@media (hover: hover) {
  .filter-clear:hover { color: var(--text); }
}
.filter-clear.visible { display: inline-flex; }
.filter-count {
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--text-accent);
  background: var(--accent-subtle);
  border-radius: var(--radius-pill);
  padding: 0.2rem 0.75rem;
  margin-left: auto;
}
/* Empty until the list's first successful load fills it. */
.filter-count:empty { display: none; }
/* .empty-state / .error-state → components.css (which also carries the
   loading and success states and lets each hold an inline action button) */
.empty-state.good { color: var(--text-success); }
@media (max-width: 767px) {
  .page {
    padding: 1.25rem 1rem;
    max-width: 100%;
  }

  /* The page's own Back bar is redundant: Back lives in the top bar's left
     slot now. Leaving it stacked cost 112px of chrome and, once scrolled, put
     the only Back link underneath the top bar where it could not be tapped. */
  .page-header-bar { display: none; }

  /* Title and its action stack instead of fighting over one row. */
  .page-header {
    flex-direction: column;
    align-items: stretch;
    gap: 0.75rem;
  }
  .page-header .btn-primary,
  .page-header .btn-secondary,
  .page-header .btn-outline { width: 100%; }

  /* Anything else that sticks must clear the bar rather than slide under it. */
  .page .banner { top: calc(var(--mobile-top-bar-h) + 0.5rem); }

  .filter-bar {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem;
  }
  .select-wrapper { width: 100%; min-width: 0; }
  .select-wrapper select { width: 100%; min-width: 0; }
  .filter-clear { justify-content: center; }
  .filter-count { grid-column: 1 / -1; width: 100%; margin-left: 0; text-align: center; }
}
@media (min-width: 768px) and (max-width: 1023px) {
  .page { padding: 1.5rem; }
}

/* Inline text-link (.link-inline and its quiet/danger variants) →
   components.css, which also gives every one the 44px hit-area that each
   usage used to add by hand. */

/* ── Shared weak-topic pill (dashboard.html / study-strengths.html) ──
   A clickable pill for a topic the student is weak on — tapping it deep-
   links into the quiz-generation modal pre-filled for that exact topic
   (see openQuizModalWithTopic in dashboard.js). Moved here 2026-07-08 so
   both pages share one definition instead of two that can drift out of
   sync (e.g. hover feedback previously differed between the two copies). */
/* Warning tint, not danger: these are tappable "practise this" chips inside a
   section whose rail is already --accent-warm; red read as "you failed these"
   and matched the error banner (dashboard-18). */
/* .topic-chip → components.css .pill.pill-btn.pill--warning — a
   permanently-tinted tappable chip (see the .pill-btn.pill--* rules there),
   not a state that toggles. */

/* .topic-chip-passive → components.css: a topic no question set can be made
   on reads as a fact (dashed, with its source icon), not a 44px look-alike of
   the tappable chip. */

/* ── Answer-image dropzone ──
   The base the two submission pages each carried (F498); a page keeps only
   its own gap, margin and height. The body is not clickable — only the
   buttons are — so no pointer cursor and no press flash on a phone. */
.upload-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border: 2px dashed var(--border);
  border-radius: var(--radius-md);
  cursor: default;
  -webkit-tap-highlight-color: transparent;
  padding: 1.5rem 1rem;
  text-align: center;
  background: var(--surface-sunken);
  transition: border-color 0.15s, background 0.15s;
}

/* ── Shared upload/checklist chrome (paper.html / class-submit.html) ──
   Consolidated in the 2026-07 UI review — these blocks were byte-identical
   (both pages even carried "mirrors X.html, same tokens for both" comments).
   Each page keeps small local overrides for the few properties that
   genuinely differ (the photo-count notice's margin-bottom, upload-dropzone
   sizing) — see the comment left in each page's own <style> block. The
   photo-count line itself is the kit's .notice.notice--warning;
   .image-count-status is only its hook. */
.guidance-box {
  padding: 12px 14px;
  font-size: var(--fs-small);
  color: var(--text-accent);
  line-height: 1.55;
}
.guidance-box strong { display: block; margin-bottom: 4px; color: var(--text-accent); }
.guidance-divider { border: none; border-top: 1px solid var(--border-accent); margin: 8px 0; }
.guidance-note { color: var(--text-accent); }
/* The upload and review steps (SubmissionMarkup in submission-flow.js),
   the same on the paper page and the class page. .card-title / .card-sub are
   also the class page's other cards' heading and line. */
.card-title { font-size: var(--fs-heading); font-weight: 700; margin-bottom: 1rem; }
.card-sub { font-size: var(--fs-body); color: var(--text-muted); margin-bottom: 1.25rem; }
.extracted-hint { font-size: var(--fs-small); color: var(--text-muted); margin-top: 0.5rem; }
.phase-error { margin-bottom: 12px; }
.submission-upload-btn { margin-top: 1rem; }
.phase-actions { display: flex; flex-direction: column; gap: 10px; margin-top: 16px; }
@media (min-width: 480px) {
  .phase-actions { flex-direction: row; }
  .phase-actions .btn { flex: 1; }
}
.preview-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 14px;
}
.preview-thumb {
  position: relative;
  width: 80px;
  height: 80px;
  border-radius: var(--radius-xs);
  overflow: hidden;
  border: 2px solid var(--border);
  flex-shrink: 0;
}
.preview-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.preview-thumb .page-label {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  /* The dimming scrim under white text on a photo. */
  background: var(--scrim);
  color: var(--text-on-accent);
  font-size: var(--fs-eyebrow);
  text-align: center;
  padding: 2px 0;
}
.preview-thumb .thumb-remove {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--scrim);
  color: var(--text-on-accent);
  font-size: var(--fs-body);
  line-height: 1;
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  z-index: 2;
}
@media (hover: hover) {
  .preview-thumb .thumb-remove:hover { background: var(--fill-danger); }
}
.preview-thumb .thumb-remove .kit-icon { width: 14px; height: 14px; }
/* Invisible 44x44px hit-area — keeps the visible ✕ at 22px (an 80x80px
   thumbnail can't take a bigger badge without covering the photo) while
   meeting the 44px touch-target minimum. It grows down and left, into the
   thumbnail, because .preview-thumb clips anything past its edge and a
   clipped area takes no taps: 4 + 22 + 18 = 44 each way, flush with the
   thumbnail's top-right corner. */
.preview-thumb .thumb-remove::before {
  content: '';
  position: absolute;
  inset: -4px -4px -18px -18px;
}
.upload-dropzone .dz-icon { display: inline-flex; }
.upload-dropzone .dz-icon .kit-icon { width: 32px; height: 32px; }
.upload-dropzone .dz-label { font-size: var(--fs-small); font-weight: 600; color: var(--text-secondary); }
.upload-dropzone .dz-sub { font-size: var(--fs-eyebrow); color: var(--text-muted); }
.upload-dropzone.drag-over { border-color: var(--text-accent); background: var(--accent-subtle); }
.upload-dropzone input[type="file"] { display: none; }

/* .extracted-textarea → components.css. */

.checklist-section { margin-top: 14px; }
.checklist-heading { margin-bottom: 4px; }
.checklist-intro {
  font-size: var(--fs-eyebrow);
  color: var(--text-muted);
  margin-bottom: 8px;
  line-height: 1.4;
}
/* ── Marked-question cards ──
   The per-question feedback card, score and band pills, feedback lists and
   the overall-feedback card. One look on every surface: rendered by
   questionCard (paper-view.js) for the student's paper page, the class page
   and the teacher's view, and written out by the marked sample page. The
   card is the kit's .card.card--elevated; the label and pills sit on it as
   a plain row. */
.q-card {
  padding: 1rem 1.125rem;
  margin-bottom: 10px;
}
.q-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 0.4rem;
}
.q-label {
  font-weight: 700;
  font-size: var(--fs-small);
  color: var(--text-accent);
}
/* .q-score-pill(-full/-partial/-zero) → components.css .pill.pill--success /
   .pill--warning / .pill--danger.
   .q-band-pill → components.css .pill.pill--accent. */
.q-feedback {
  /* Examiner feedback per question - the whole point of the page. */
  font-size: var(--fs-body);
  color: var(--text-secondary);
  line-height: var(--lh-body);
  margin-bottom: 10px;
}
.q-list-heading { margin-bottom: 4px; }
.q-list-heading.improvements { color: var(--text-warning); }
.q-list {
  list-style: none;
  padding: 0;
  margin: 0 0 8px;
  font-size: var(--fs-body);
  color: var(--text-secondary);
  line-height: 1.5;
}
.q-list li {
  padding: 3px 0 3px 18px;
  position: relative;
}
.q-list li::before {
  position: absolute;
  left: 0;
}
.q-list.strengths li::before { content: "✓"; color: var(--text-success); }
.q-list.improvements li::before { content: "→"; color: var(--text-warning); }

/* The band and score pills in a card header. */
.q-card-pills { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }

/* A question marked on scorecards: one row per scorecard above the feedback. */
.q-scorecards {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  margin: 0.25rem 0 0.75rem;
}
.q-scorecard {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 10px;
  padding: 0.55rem 0.75rem;
}
.q-scorecard + .q-scorecard { border-top: 1px solid var(--border); }
.q-scorecard-name { font-size: var(--fs-body); font-weight: 600; color: var(--text); }
/* The feedback opens on the questions that LOST marks; "Read all feedback"
   adds the rest back (Cluster 20). #results-body is the shared results
   surface's body on both submission pages (renderMarkedSubmission). */
#results-body:not(.show-all) .q-card--full { display: none; }
/* The teacher's read-only checklist: the student's dispute note under the
   criterion it was written against. The note is the kit's
   .notice.notice--warning; this places it. */
.dispute-callout { margin: 6px 0 0 20px; }
.dispute-note-text { font-style: italic; }
.dispute-note-empty { color: var(--text-muted); }

/* Overall feedback */
.overall-feedback-card {
  padding: 14px 16px;
  margin-top: 12px;
  margin-bottom: 12px;
}
.overall-feedback-label { margin-bottom: 6px; }
.overall-feedback-text {
  font-size: var(--fs-body);
  color: var(--text-secondary);
  line-height: var(--lh-body);
}

.checklist-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.checklist-row {
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  padding: 8px 10px;
}
.checklist-row-disputed { border-color: var(--text-warning); }
.checklist-icon {
  font-size: var(--fs-small);
  font-weight: 700;
  margin-right: 6px;
}
.checklist-icon-pass { color: var(--text-success); }
.checklist-icon-fail { color: var(--text-danger); }
.checklist-text { font-size: var(--fs-small); color: var(--text-secondary); }
.btn-dispute {
  display: inline-block;
  margin-top: 6px;
  padding: 4px 10px;
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  background: var(--surface-raised);
  color: var(--text-secondary);
  cursor: pointer;
  min-height: 34px;
  /* 34px to the eye, 44px to the finger (known-findings UI26-02): the kit's
     invisible ::before hit-area (as on .btn-sm), so the dense feedback row
     keeps its shape and the tap target still meets the minimum. -6px, not
     -5px: inset is measured from the padding box, inside the 1px border. */
  position: relative;
}
.btn-dispute::before { content: ''; position: absolute; inset: -6px 0; }
@media (hover: hover) {
  .btn-dispute:hover { border-color: var(--text-warning); color: var(--text-warning); }
}
.btn-dispute-done { background: var(--bg-warning); border-color: var(--text-warning); color: var(--text-warning); }
.dispute-area {
  display: none;
  margin-top: 8px;
}
.dispute-area.open { display: block; }
.dispute-label {
  display: block;
  font-size: var(--fs-eyebrow);
  color: var(--text-muted);
  margin-bottom: 4px;
}
/* 1rem, not the 0.82rem this used to be: iOS auto-zooms any form control it can
   focus below 16px, and this is a field the student types into on a phone.
   Explicit colour/background for the same reason every other control has them —
   see tokens.css's color-scheme note. Note this is the dispute *textarea*; the
   .btn-dispute button above keeps its 34px look and reaches 44px through its
   ::before hit-area (known-findings UI26-02). */
.dispute-note {
  width: 100%;
  font-size: var(--fs-body);
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  padding: 6px 8px;
  resize: vertical;
  line-height: 1.4;
  color: var(--text);
  background: var(--surface-raised);
}
.dispute-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 6px;
}
/* .btn-dispute-submit → components.css .btn-primary.btn-sm (kept compact,
   deliberately not the full standard button — it sits in the same dense
   per-question feedback row as .btn-dispute, whose own 34px visual size is a
   hand-tuned exception, UI26-02, .claude/ui/known-findings.md). */
.dispute-status { font-size: var(--fs-eyebrow); }
.dispute-status-ok { color: var(--text-success); }
.dispute-status-error { color: var(--text-danger); }

/* ── The on-page paper ──
   Rendered by paper-view.js from the same stored paper_content the PDF is
   printed from, on the class-assignment page; scoped to .paper-view so the
   generic names (section-label, question-text) cannot reach another page's
   own use of them. MATCHED PAIR with app/templates/paper_pdf.html: every
   construct the template prints has a rule here. */
.paper-view .paper-cover-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1rem;
  font-size: var(--fs-small);
  color: var(--text-secondary);
  margin-bottom: 0.75rem;
}
.paper-view .paper-cover-strip strong { color: var(--text); }
.paper-view .paper-instructions {
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
  font-size: var(--fs-small);
  line-height: var(--lh-body);
}
.paper-view .paper-instructions h3 { margin: 0 0 0.4rem; }
.paper-view .paper-instructions ul { margin: 0; padding-left: 1.1rem; }
.paper-view .paper-instructions li { margin-bottom: 0.25rem; }
.paper-view .paper-instructions p { margin: 0; }
.paper-view .section-label {
  padding: 0.5rem 0 0.25rem;
  margin-top: 1rem;
  border-top: 1px solid var(--border);
}
/* Ours, not the model's: the instruction under every heading, "Answer TWO of
   the following three questions." or "Answer all questions in this section." */
.paper-view .section-choice {
  font-size: var(--fs-small);
  font-weight: 700;
  color: var(--text);
  margin: 0 0 0.5rem;
}
/* A section's title under its heading, on the paper and in the mark scheme
   (paper.js renderMarkScheme), and the same treatment for the question's
   reminder line under an answer's heading (renderAnswer), as the mark scheme
   PDF sets them. */
:is(.paper-view, #msContent, .ms-answer) .section-desc {
  font-size: var(--fs-small);
  font-style: italic;
  color: var(--text-secondary);
  margin: 0 0 0.75rem;
  line-height: var(--lh-body);
}
:is(.paper-view, #msContent, .ms-answer) .section-desc p { margin: 0; }
/* The Source Sheet: one numbered line per entry, before the questions that
   read it. pre-wrap keeps each entry one printed line, so "lines 10 to 20"
   points at the same text on screen as on paper. */
.paper-view .source-sheet {
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
}
.paper-view .source-sheet-title { font-weight: 700; font-size: var(--fs-small); margin-bottom: 0.5rem; }
.paper-view .source-line { display: flex; gap: 0.75rem; font-size: var(--fs-body); line-height: var(--lh-body); }
.paper-view .source-line-no {
  flex: 0 0 2em;
  text-align: right;
  color: var(--text-muted);
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
  padding-top: 0.1em;
}
.paper-view .source-line-text { white-space: pre-wrap; }
.paper-view .question-block {
  margin-bottom: 1.25rem;
  padding: 1rem;
  background: var(--surface-sunken);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
}
.paper-view .question-label { font-weight: 700; font-size: var(--fs-small); color: var(--text-accent); margin-bottom: 0.35rem; }
.paper-view .question-text { font-size: var(--fs-body); line-height: var(--lh-body); margin-bottom: 0.4rem; }
.paper-view .mcq-option { font-size: var(--fs-body); line-height: var(--lh-body); margin-bottom: 0.4rem; }
.paper-view .question-marks { font-size: var(--fs-small); color: var(--text-muted); font-weight: 600; }
.paper-view .question-part { margin-top: 0.75rem; padding-left: 1rem; border-left: 3px solid var(--border); }
.paper-view .question-stimulus { margin: 0.75rem 0; padding: 0.75rem 1rem; background: var(--accent-subtle); border-radius: var(--radius-sm); }
/* A figure carries its own name, as in the PDF. The line under it is chosen
   by the figure's scale (DIAGRAM_CAPTION in utils.js); the PDF prints none. */
.paper-view .question-diagram { margin: 0.75rem 0 0.25rem; }
/* Capped so a tall figure cannot push the question text off screen. */
.paper-view .question-diagram svg { max-width: 100%; max-height: 45vh; width: auto; height: auto; }
.paper-view .figure-label { font-size: var(--fs-small); font-weight: 700; color: var(--text-secondary); margin: 0; }
