/* ============================================================================
 * Content Provenance — Admin
 *
 * Self-contained. apps/ pages get no platform stylesheet, so the flat design
 * tokens from style/iotclass.css + style/design-system.css are mirrored here
 * rather than imported: the same ink/line/surface ramp, the same accent, the
 * same "no shadow, no gradient, hairline frames" Khan-flat discipline.
 *
 * If the platform ramp changes, mirror it here (this file is the only place
 * these values appear in the app).
 * ========================================================================== */

:root {
  /* Mirrored from style/iotclass.css :root — flat interaction system */
  --pv-surface: #ffffff;
  --pv-surface-muted: #f7f9fb;
  --pv-surface-sunken: #f1f4f7;
  --pv-surface-hover: #fbfcfe;
  --pv-line: #e6e8eb;
  --pv-line-strong: #d2d6dc;
  --pv-ink: #1f2933;
  --pv-ink-soft: #52606d;
  --pv-ink-faint: #7b8794;
  --pv-accent: #1865f2;
  --pv-accent-ink: #0f47ad;
  --pv-accent-soft: #eef3ff;

  /* Semantic states, matched to admin-unit-reviews.css */
  --pv-green: #16784b;
  --pv-green-soft: #e7f6ee;
  --pv-amber: #9a6500;
  --pv-amber-soft: #fff4d5;
  --pv-red: #b42318;
  --pv-red-soft: #feeceb;
  --pv-gray: #667085;
  --pv-gray-soft: #f2f4f7;

  --pv-radius: 10px;
  --pv-radius-sm: 7px;
  --pv-radius-pill: 999px;

  --pv-font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --pv-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --pv-topbar-h: 5.25rem;
  --pv-homebar-h: 2.35rem;
}

/* Founder-facing editorial audit trail. Database strings are escaped in
   app.js; these rules are presentation only and never interpret row data. */
.pv-audit-toolbar {
  align-items: end;
}

.pv-audit-toolbar .pv-sources-lede {
  flex-basis: 100%;
}

.pv-audit-detail-head {
  display: flex;
  gap: 1rem;
  align-items: flex-start;
  margin-bottom: 1rem;
}

.pv-audit-detail-head h2,
.pv-audit-detail-head p {
  margin: 0 0 .35rem;
  overflow-wrap: anywhere;
}

.pv-audit-list {
  display: grid;
  gap: .7rem;
}

.pv-audit-row {
  border: 1px solid var(--pv-border, #d7dce3);
  border-radius: .75rem;
  background: var(--pv-card, #fff);
  overflow: hidden;
}

.pv-audit-row--baseline {
  border-left: 4px solid #667085;
}

.pv-audit-row-main {
  width: 100%;
  display: grid;
  grid-template-columns: minmax(9rem, .8fr) minmax(10rem, 1fr) minmax(9rem, .8fr) minmax(12rem, 1.4fr);
  gap: .35rem 1rem;
  padding: 1rem;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.pv-audit-row-main:hover,
.pv-audit-row-main:focus-visible {
  background: #f8fafc;
}

.pv-audit-row-main strong {
  grid-column: 1 / -1;
  font-size: 1rem;
}

.pv-audit-row-main time,
.pv-audit-actor,
.pv-audit-surface,
.pv-audit-entity {
  color: #475467;
  font-size: .82rem;
  overflow-wrap: anywhere;
}

.pv-audit-surface {
  font-weight: 650;
}

.pv-audit-raw {
  border-top: 1px solid var(--pv-border, #d7dce3);
  padding: .65rem 1rem;
}

.pv-audit-raw summary {
  cursor: pointer;
  color: #475467;
  font-size: .82rem;
}

.pv-audit-raw pre {
  max-height: 24rem;
  overflow: auto;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: .78rem;
}

.pv-history-audit-link {
  margin-bottom: .75rem;
}

@media (max-width: 760px) {
  .pv-audit-row-main {
    grid-template-columns: 1fr;
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  max-width: 100%;
}

body {
  background: var(--pv-surface-muted);
  color: var(--pv-ink);
  font-family: var(--pv-font);
  font-size: 15px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

[hidden] {
  display: none !important;
}

a {
  color: var(--pv-accent-ink);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

:focus-visible {
  outline: 2px solid var(--pv-accent);
  outline-offset: 2px;
  border-radius: 3px;
}

.pv-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- Home bar ------------------------------------------------------------
 * Present in every gate state (loading/denied/app) and above all of them,
 * because it lives outside the three conditional sections rather than
 * inside any one of them -- a signed-out visitor sees it as readily as an
 * admin. Sticky at the very top so it survives scrolling the app pane too;
 * .pv-topbar below stacks its own sticky offset under it (see --pv-homebar-h). */

.pv-homebar {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  height: var(--pv-homebar-h);
  padding: 0 clamp(1rem, 3vw, 2rem);
  background: var(--pv-surface);
  border-bottom: 1px solid var(--pv-line);
}

.pv-homebar-link {
  color: var(--pv-ink-faint);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.01em;
}

.pv-homebar-link:hover {
  color: var(--pv-accent-ink);
  text-decoration: none;
}

/* ---- Gate states -------------------------------------------------------- */

.pv-gate {
  display: grid;
  place-content: center;
  min-height: calc(100vh - var(--pv-homebar-h));
  padding: 2rem;
}

.pv-gate-card {
  max-width: 34rem;
  padding: 2.5rem 2.25rem;
  border: 1px solid var(--pv-line);
  border-top: 4px solid var(--pv-accent);
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  text-align: center;
}

.pv-gate-card--denied {
  border-top-color: var(--pv-red);
}

.pv-gate-icon {
  width: 2.1rem;
  height: 2.1rem;
  margin-bottom: 0.85rem;
  color: var(--pv-accent);
}

.pv-gate-card--denied .pv-gate-icon {
  color: var(--pv-red);
}

.pv-gate-card h1 {
  margin: 0 0 0.6rem;
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: -0.015em;
}

.pv-gate-card p {
  margin: 0;
  color: var(--pv-ink-soft);
}

.pv-gate-actions {
  display: flex;
  gap: 0.6rem;
  justify-content: center;
  margin: 1.5rem 0 1.25rem;
  flex-wrap: wrap;
}

.pv-gate-foot {
  padding-top: 1.1rem;
  border-top: 1px solid var(--pv-line);
  color: var(--pv-ink-faint) !important;
  font-size: 0.8rem;
}

/* ---- Buttons ------------------------------------------------------------ */

.pv-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.95rem;
  border: 1px solid var(--pv-line-strong);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface);
  color: var(--pv-ink);
  font: inherit;
  font-size: 0.86rem;
  font-weight: 600;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease,
    color 0.15s ease;
}

.pv-btn:hover {
  background: var(--pv-surface-sunken);
  text-decoration: none;
}

.pv-btn--primary {
  border-color: var(--pv-accent);
  background: var(--pv-accent);
  color: #fff;
}

.pv-btn--primary:hover {
  background: var(--pv-accent-ink);
  border-color: var(--pv-accent-ink);
}

/* Images-mode approval toggle: after an approval is saved/restored, the
 * same action reads as a clear success-green Approved control. */
.pv-btn--approved {
  border-color: var(--pv-green);
  background: var(--pv-green);
  color: #fff;
}

.pv-btn--approved:hover {
  border-color: #105f3b;
  background: #105f3b;
}

.pv-btn--ghost {
  border-color: transparent;
  background: transparent;
  color: var(--pv-ink-soft);
}

.pv-btn--ghost:hover {
  background: var(--pv-gray-soft);
  color: var(--pv-ink);
}

.pv-linkbtn {
  padding: 0;
  border: 0;
  background: none;
  color: var(--pv-accent-ink);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
}

.pv-linkbtn:hover {
  text-decoration: underline;
}

/* ---- Top bar ------------------------------------------------------------ */

.pv-topbar {
  position: sticky;
  /* Stacks directly below the always-present home bar, both sticky. */
  top: var(--pv-homebar-h);
  z-index: 30;
  background: var(--pv-surface);
  border-bottom: 1px solid var(--pv-line);
}

.pv-topbar-inner {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  max-width: 1680px;
  margin-inline: auto;
  padding: 1.1rem clamp(1rem, 3vw, 2rem) 1rem;
}

.pv-eyebrow {
  margin: 0 0 0.2rem;
  color: var(--pv-ink-faint);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.pv-brand h1 {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0;
  font-size: clamp(1.4rem, 2.4vw, 1.85rem);
  font-weight: 700;
  letter-spacing: -0.022em;
}

.pv-chip {
  display: inline-block;
  padding: 0.2rem 0.5rem;
  border-radius: var(--pv-radius-pill);
  background: var(--pv-gray-soft);
  color: var(--pv-ink-soft);
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  vertical-align: middle;
}

.pv-chip--admin {
  background: var(--pv-accent-soft);
  color: var(--pv-accent-ink);
}

.pv-coverage {
  display: flex;
  gap: 1.6rem;
  flex-wrap: wrap;
  padding: 0.9rem 1rem 0;
}

.pv-cov {
  min-width: 5rem;
}

.pv-cov strong {
  display: block;
  font-size: 1.3rem;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.pv-cov span {
  display: block;
  margin-top: 0.15rem;
  color: var(--pv-ink-faint);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

.pv-cov--good strong {
  color: var(--pv-green);
}

.pv-cov--gap strong {
  color: var(--pv-amber);
}

/* ---- Intro band --------------------------------------------------------- */

.pv-intro {
  background: var(--pv-surface-muted);
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
}

.pv-intro-inner {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: 1.25rem 2.25rem;
  align-items: start;
  padding: 0.85rem 1rem 1rem;
}

.pv-intro-lede {
  margin: 0;
  max-width: 60ch;
  color: var(--pv-ink-soft);
  font-size: 0.95rem;
}

.pv-note {
  margin: 0;
  padding: 0.7rem 0.95rem;
  border-left: 3px solid var(--pv-amber);
  border-radius: 0 var(--pv-radius-sm) var(--pv-radius-sm) 0;
  background: var(--pv-amber-soft);
  color: #6b4700;
  font-size: 0.83rem;
  line-height: 1.55;
}

.pv-note strong {
  color: #5a3c00;
}

.pv-error {
  margin: 0;
  padding: 0.75rem 1rem;
  border: 1px solid #f3b6b1;
  border-radius: var(--pv-radius-sm);
  background: var(--pv-red-soft);
  color: var(--pv-red);
  font-size: 0.88rem;
}

/* ---- Stable shell + swappable content region -----------------------------
 * .pv-main owns the page-width column. Its first child, .pv-shell, contains
 * only the persistent mode nav; its second child, .pv-content-region, is the
 * sole mode-swapped surface. Neither node is ever replaced by JavaScript. */

.pv-main {
  display: flex;
  flex-direction: column;
  gap: 1.15rem;
  max-width: 1680px;
  margin-inline: auto;
  padding: 1.15rem clamp(1rem, 3vw, 2rem) 2.5rem;
}

.pv-shell,
.pv-content-region {
  min-width: 0;
}

.pv-content-region {
  display: flex;
  flex-direction: column;
  gap: 1.15rem;
}

.pv-shell-grid {
  display: grid;
  grid-template-columns: minmax(20rem, 25rem) minmax(0, 1fr);
  gap: 1.15rem;
  align-items: start;
}

/* Toolbar-mode layout: a plain single-width column -- the active
 * .pv-toolbar bar (sticky, see below) followed by that mode's own
 * full-width content pane (.pv-detail--full, see the Detail-pane section). */
.pv-shell-toolbar {
  display: flex;
  flex-direction: column;
  gap: 1.15rem;
}

.pv-oversight-region {
  min-width: 0;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  overflow: hidden;
}

.pv-oversight-frame {
  display: block;
  width: 100%;
  min-height: max(54rem, calc(100vh - var(--pv-homebar-h) - var(--pv-topbar-h) - 3rem));
  border: 0;
  background: var(--pv-surface);
}

/* ---- Rail --------------------------------------------------------------- */

.pv-rail {
  position: sticky;
  top: calc(var(--pv-homebar-h) + var(--pv-topbar-h) + 0.6rem);
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - var(--pv-homebar-h) - var(--pv-topbar-h) - 1.6rem);
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  overflow: hidden;
}

.pv-rail-controls {
  padding: 0.85rem 0.9rem 0.7rem;
  border-bottom: 1px solid var(--pv-line);
  background: var(--pv-surface);
}

.pv-field {
  display: block;
}

.pv-field > span {
  display: block;
  margin-bottom: 0.3rem;
  color: var(--pv-ink-faint);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

.pv-field input,
.pv-field select {
  width: 100%;
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--pv-line-strong);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface);
  color: var(--pv-ink);
  font: inherit;
  font-size: 0.9rem;
}

.pv-field input::placeholder {
  color: var(--pv-ink-faint);
}

.pv-segmented {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 2px;
  margin-top: 0.65rem;
  padding: 2px;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface-sunken);
}

.pv-seg {
  padding: 0.34rem 0.3rem;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--pv-ink-soft);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
}

.pv-seg:hover {
  color: var(--pv-ink);
}

.pv-seg.is-active {
  background: var(--pv-surface);
  color: var(--pv-ink);
  box-shadow: inset 0 0 0 1px var(--pv-line-strong);
}

/* ---- Shared decision-status toggle (founder directive 2026-08-08) --------
 * ONE chip-row component reused by Figures/Gaps/Images/the Sources triage
 * queue/the Chapters review rail -- see app.js renderStatusToggle(). Same
 * visual language as .pv-segmented/.pv-seg above, but a flex-wrap row
 * rather than a fixed-column grid: the option COUNT and label length both
 * vary mode to mode (Images alone runs to 7 options, one of them
 * "Placement-approved"), so a grid would either squash text or leave dead
 * cells the way a fixed .pv-segmented would. */

.pv-status-toggle {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 0.65rem;
  padding: 2px;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface-sunken);
}

.pv-status-chip {
  padding: 0.34rem 0.55rem;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--pv-ink-soft);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

.pv-status-chip:hover {
  color: var(--pv-ink);
}

.pv-status-chip.is-active {
  background: var(--pv-surface);
  color: var(--pv-ink);
  box-shadow: inset 0 0 0 1px var(--pv-line-strong);
}

.pv-rail-status {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 0.6rem;
  color: var(--pv-ink-faint);
  font-size: 0.78rem;
}

/* ---- Mode navigation: grouped, wrapping chip clusters ---------------------
 * Replaces the old flat single-row tab strip (founder-reported bug,
 * 2026-08-07: eleven tabs no longer fit and were unreadable). Three labeled
 * groups (Content / Sources & media / Operations, see index.html) each hold
 * a small role="tablist" of chips; .pv-mode-chips flex-wraps them onto as
 * many rows as the viewport needs -- NEVER a horizontal scroll or clip.
 * Adding a mode means joining a group's markup, nothing here changes: this
 * is what retires the old column-count modifier ladder (.pv-mode-tabs--three
 * through --eleven, one new class per tab ever added) entirely.
 *
 * Own card now (hybrid layout, 2026-08-08): previously the top section of
 * .pv-rail's own bordered box, sharing its border/background/radius. Moved
 * out to sit above BOTH .pv-shell-grid and .pv-shell-toolbar (a shared-
 * markup move the split forces -- every mode needs it, not just
 * Chapters/Sources), so it needs that same card look of its own now. */

.pv-mode-nav {
  padding: 0.6rem 0.6rem 0.35rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
}

.pv-mode-groups {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem 1.15rem;
}

.pv-mode-group {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
}

.pv-mode-group-label {
  margin: 0;
  color: var(--pv-ink-faint);
  font-size: 0.64rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.pv-mode-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}

/* Compact chip: smaller padding/font than the old tab strip so all eleven
 * comfortably fit in one or two rows on a laptop. */
.pv-modetab {
  display: inline-flex;
  align-items: center;
  padding: 0.32rem 0.62rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius-pill);
  background: var(--pv-surface-muted);
  color: var(--pv-ink-soft);
  font: inherit;
  font-size: 0.74rem;
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}

.pv-modetab:hover {
  color: var(--pv-ink);
  border-color: var(--pv-line-strong);
}

.pv-modetab.is-active {
  border-color: var(--pv-accent);
  background: var(--pv-accent-soft);
  color: var(--pv-accent-ink);
}

/* Trust: the governance programme's capstone dashboard -- a subtle distinct
 * (amber, matched to the existing semantic-amber ramp above) accent even at
 * rest, so it reads as different in kind from the other ten review queues.
 * Still uses the SAME .is-active highlight language as every other chip
 * once selected, just in its own hue. */
.pv-modetab--trust {
  border-color: var(--pv-amber-soft);
  background: var(--pv-amber-soft);
  color: var(--pv-amber);
}

.pv-modetab--trust.is-active {
  border-color: var(--pv-amber);
  background: var(--pv-amber);
  color: #fff;
}

/* Narrow-viewport fallback -- hidden by default; unhidden below the
 * breakpoint at which .pv-mode-groups hides in turn (see the Responsive
 * section further down). Reuses the standard .pv-field label+control look. */
.pv-mode-select-field {
  display: none;
  padding: 0 0.6rem 0.6rem;
}

.pv-rail-panel {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

/* ---- Toolbar (hybrid layout, 2026-08-08) ----------------------------------
 * The ten "toolbar" modes' former rail-only controls (status toggle, kind/
 * type filters, module <select>, checkboxes, captions/banners), relocated
 * verbatim -- same ids/classes/handlers -- into a horizontal flex-wrap bar
 * sitting directly under the mode nav, mode content spanning the full
 * container width below it (see .pv-detail--full further down). ONE shared
 * component pattern for all ten, per the brief; only content COUNT differs
 * mode to mode, never the pattern itself.
 *
 * Sticky at the SAME top offset .pv-rail/.pv-detail already stick to (right
 * below the homebar+topbar sticky stack) -- verified against the real DOM:
 * neither the toolbar itself nor any ancestor between it and <body>
 * (.pv-shell-toolbar, .pv-shell, #pv-app, .pv-root) sets an overflow other
 * than the initial `visible`, so the sticky offset resolves against the
 * document/viewport scroll exactly like .pv-rail's already does -- a sticky
 * element one overflow:hidden/auto ancestor away from the scrolling root
 * would silently stop sticking, which is why this is called out explicitly
 * rather than assumed. Solid background + a full bordered/rounded card (the
 * same "no shadow, hairline frame" language every other region of this app
 * uses, not a bare bottom-rule -- .pv-toolbar sits in the same padded
 * max-width column as the rail/detail cards, so a full-bleed topbar-style
 * bar would look orphaned there) so page content never shows through as it
 * scrolls underneath. */

.pv-toolbar {
  position: sticky;
  top: calc(var(--pv-homebar-h) + var(--pv-topbar-h) + 0.6rem);
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 1.1rem;
  row-gap: 0.6rem;
  padding: 0.85rem 0.9rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
}

/* Group order within the bar (founder brief): status toggle first, then
 * secondary (kind/type) filters, then selects/checkboxes; informational
 * captions/banners last, pushed to the right (or wrapped to their own line
 * on narrow widths, via the same flex-wrap the whole bar already uses) --
 * generic selectors, not one rule per mode, since every mode reuses the SAME
 * few control shapes. Every rule here only reorders/repositions controls
 * that already exist; none of it changes what a control does. */
.pv-toolbar [id$="-status"] {
  order: 1;
}

.pv-toolbar .pv-segmented {
  order: 2;
  margin-top: 0;
}

.pv-toolbar .pv-field,
.pv-toolbar .pv-checkbox-field {
  order: 3;
  margin-top: 0;
}

.pv-toolbar .pv-field {
  min-width: 12rem;
}

.pv-toolbar .pv-field input,
.pv-toolbar .pv-field select {
  width: auto;
}

.pv-toolbar .pv-sources-lede,
.pv-toolbar .pv-triage-banner,
.pv-toolbar .pv-link-report-banner,
.pv-toolbar .pv-rail-status {
  order: 4;
}

/* The informational/caption group: pushed to the end of the current flex
 * line. ONLY the caption (.pv-sources-lede) carries the push (a single
 * `margin-left: auto`) -- giving every order-4 item its own auto margin
 * would split the free space between EACH of them, prying them apart with
 * daylight in between instead of keeping the group snug together at the
 * right edge; one auto margin on the first prose element of the group is
 * enough, everything else in the group (the count readout, and -- on the
 * rare mode/state where both render -- a banner) simply trails it at the
 * container's own `column-gap`, which (unlike a `+` adjacent-sibling
 * selector) is computed off VISUAL flex order, not DOM adjacency, so it
 * still applies correctly even where a banner or status container sits
 * between them in the markup. */
.pv-toolbar .pv-sources-lede {
  flex: none;
  max-width: 34rem;
  margin-left: auto;
}

.pv-toolbar .pv-rail-status {
  flex: none;
}

/* Banners (the Figures unit-scoped-filter banner, the Links report-age
 * banner) always take their own full-width row when present -- never
 * competing for inline space with the lede/status-toggle/selects beside
 * them, and (since a 100%-basis item cannot share a line) this is also what
 * keeps a visible banner from splitting the lede+count auto-margin push
 * above: the banner forces a line break, so the free-space math for
 * `.pv-sources-lede`'s margin only ever considers its OWN line. */
.pv-toolbar .pv-triage-banner,
.pv-toolbar .pv-link-report-banner {
  flex-basis: 100%;
  margin: 0;
}

/* ---- Sources list --------------------------------------------------------- */

.pv-sources-lede {
  margin: 0 0 0.5rem;
  color: var(--pv-ink-faint);
  font-size: 0.8rem;
  line-height: 1.5;
}

.pv-sources-list {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0.35rem 0.35rem 0.9rem;
}

.pv-row--source {
  flex-wrap: wrap;
  row-gap: 0.32rem;
  padding: 0.55rem 0.6rem;
}

.pv-row--source .pv-row-label {
  flex-basis: 100%;
  overflow: visible;
  white-space: normal;
  font-weight: 600;
}

/* Reverse-provenance usage summary, e.g. "used by 6 chapters · 4 slides
   mapped" -- only rendered at all when the deck has nonzero usage (see
   app.js renderSourcesList()), so a deck with none shows no third line. */
.pv-row-usage {
  flex-basis: 100%;
  color: var(--pv-ink-faint);
  font-size: 0.74rem;
}

/* Curation rollup, e.g. "3 marked not useful" -- same honest-absence rule as
   .pv-row-usage above: only rendered for a deck with a nonzero count. */
.pv-row-usage--curated {
  color: var(--pv-gray);
  font-weight: 600;
}

.pv-idchip {
  flex: none;
  padding: 0.1rem 0.4rem;
  border: 1px solid var(--pv-line);
  border-radius: 4px;
  background: var(--pv-surface-sunken);
  color: var(--pv-ink-soft);
  font-family: var(--pv-mono);
  font-size: 0.72rem;
}

.pv-idchip--lg {
  padding: 0.18rem 0.55rem;
  font-size: 0.8rem;
}

.pv-badge--kind {
  background: var(--pv-accent-soft);
  color: var(--pv-accent-ink);
  font-size: 0.64rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.pv-tree {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0.35rem 0.35rem 0.9rem;
}

/* Tree rows */

.pv-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.34rem 0.5rem;
  border: 0;
  border-radius: 6px;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.pv-row:hover {
  background: var(--pv-surface-sunken);
}

.pv-row-caret {
  flex: none;
  width: 0.8rem;
  color: var(--pv-ink-faint);
  font-size: 0.62rem;
  line-height: 1;
  transition: transform 0.14s ease;
}

.pv-row.is-open > .pv-row-caret {
  transform: rotate(90deg);
}

.pv-row-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pv-row--book {
  margin-top: 0.35rem;
  font-size: 0.86rem;
  font-weight: 700;
  letter-spacing: 0.005em;
}

.pv-row--book .pv-row-swatch {
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 2px;
  background: var(--pv-row-color, var(--pv-gray));
}

.pv-row--part {
  padding-left: 1.25rem;
  font-size: 0.83rem;
  font-weight: 600;
  color: var(--pv-ink-soft);
}

.pv-section-label {
  padding: 0.5rem 0.5rem 0.2rem 2.55rem;
  color: var(--pv-ink-faint);
  font-size: 0.63rem;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

.pv-row--chapter {
  padding-left: 2.55rem;
  font-size: 0.84rem;
  color: var(--pv-ink);
}

.pv-row--chapter.is-selected {
  background: var(--pv-accent-soft);
  color: var(--pv-accent-ink);
  font-weight: 600;
}

.pv-row--chapter.is-selected:hover {
  background: var(--pv-accent-soft);
}

/* Badges */

.pv-badge {
  flex: none;
  padding: 0.08rem 0.42rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.68rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.pv-badge--slides {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-badge--none {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
  font-weight: 600;
}

.pv-badge--roll {
  background: transparent;
  color: var(--pv-ink-faint);
  font-weight: 600;
  font-size: 0.7rem;
}

/* Deck-header curation rollup, e.g. "3 marked not useful" -- only rendered
   when nonzero (see app.js renderSourceDetail()). */
.pv-badge--curated {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
}

.pv-tree-empty,
.pv-tree-more {
  padding: 1.4rem 0.9rem;
  color: var(--pv-ink-faint);
  font-size: 0.84rem;
  text-align: center;
}

.pv-tree-more {
  padding: 0.7rem 0.9rem;
  border-top: 1px dashed var(--pv-line-strong);
  margin-top: 0.4rem;
}

/* ---- Detail pane -------------------------------------------------------- */

.pv-detail {
  position: sticky;
  top: calc(var(--pv-homebar-h) + var(--pv-topbar-h) + 0.6rem);
  min-height: 26rem;
  max-height: calc(100vh - var(--pv-homebar-h) - var(--pv-topbar-h) - 1.6rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  padding: clamp(1.1rem, 2.4vw, 1.8rem);
}

/* ---- Full-width content (hybrid layout, 2026-08-08) ----------------------
 * The ten toolbar modes' own content pane, sitting below their sticky
 * .pv-toolbar bar inside .pv-shell-toolbar (a plain single-column stack, not
 * the rail/detail two-column grid -- see .pv-shell-grid vs .pv-shell-toolbar
 * above). Overrides .pv-detail's rail-detail-pane behavior: that pane is
 * sticky with its OWN internal scroll region because it sits beside a
 * same-height sticky rail and must not grow past it; a full-width pane below
 * a sticky toolbar has no such neighbor to match, so it scrolls with the
 * page instead -- literally "content scrolls under [the toolbar]", the
 * founder brief's own phrase for the point of this split. Width is already
 * 100% of its flex-column parent; stated here for clarity, not because
 * anything would otherwise constrain it. */
.pv-detail--full {
  position: static;
  width: 100%;
  max-height: none;
  overflow: visible;
}

.pv-placeholder {
  display: grid;
  place-content: center;
  justify-items: center;
  min-height: 22rem;
  text-align: center;
  color: var(--pv-ink-faint);
}

.pv-placeholder h2 {
  margin: 0.5rem 0 0.35rem;
  color: var(--pv-ink-soft);
  font-size: 1.05rem;
  font-weight: 700;
}

.pv-placeholder p {
  margin: 0;
  max-width: 34ch;
  font-size: 0.88rem;
}

.pv-placeholder-icon {
  width: 2.4rem;
  height: 2.4rem;
  color: var(--pv-line-strong);
}

.pv-srcpath {
  display: inline-flex;
  align-items: center;
  padding: 0.5rem 0.7rem;
  border: 1px dashed var(--pv-line-strong);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface-muted);
  color: var(--pv-ink-faint);
  font-family: var(--pv-mono);
  font-size: 0.78rem;
}

/* ---- Source detail (Sources mode right pane) ------------------------------ */

.pv-source-dir {
  display: inline-flex;
  margin: 0 0 0.7rem;
  padding: 0.35rem 0.6rem;
  border: 1px dashed var(--pv-line-strong);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface-muted);
  color: var(--pv-ink-faint);
  font-family: var(--pv-mono);
  font-size: 0.76rem;
}

.pv-source-pagebar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}

.pv-source-pagenav {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.pv-source-pageindicator {
  color: var(--pv-ink-soft);
  font-size: 0.82rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Three-state curation filter (All / Useful only / Not-useful only), in the
   source page bar between the pager and the jump form. Reuses the same
   .pv-segmented/.pv-seg pieces the Chapters-mode rail filter already uses --
   --inline only overrides the block-stacked spacing/width that control was
   built for, not the look. */
.pv-source-filter {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.pv-source-filter-label {
  color: var(--pv-ink-faint);
  font-size: 0.76rem;
  font-weight: 700;
}

.pv-segmented--inline {
  margin-top: 0;
  width: 19rem;
}

.pv-source-jump {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.pv-source-jump label {
  color: var(--pv-ink-faint);
  font-size: 0.76rem;
  font-weight: 700;
}

.pv-source-jump input {
  width: 6rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--pv-line-strong);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface);
  color: var(--pv-ink);
  font: inherit;
  font-size: 0.85rem;
}

.pv-source-jump input.is-invalid {
  border-color: var(--pv-red);
  background: var(--pv-red-soft);
}

/* Contact sheet: as many columns as fit, each cell a full .pv-thumb. */
.pv-source-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
  gap: 0.7rem;
}

/* Detail head */

.pv-detail-head {
  padding-bottom: 1.1rem;
  border-bottom: 1px solid var(--pv-line);
}

.pv-crumbs {
  margin: 0 0 0.35rem;
  color: var(--pv-ink-faint);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.pv-crumbs span + span::before {
  content: " › ";
  color: var(--pv-line-strong);
}

.pv-detail-head h2 {
  margin: 0 0 0.5rem;
  font-size: clamp(1.3rem, 2.2vw, 1.6rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.25;
}

.pv-detail-meta {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: wrap;
  margin: 0 0 0.7rem;
}

.pv-unitkey {
  padding: 0.12rem 0.42rem;
  border: 1px solid var(--pv-line);
  border-radius: 4px;
  background: var(--pv-surface-sunken);
  color: var(--pv-ink-soft);
  font-family: var(--pv-mono);
  font-size: 0.74rem;
}

.pv-unitkey--alt {
  border-style: dashed;
  color: var(--pv-ink-faint);
}

.pv-keynote {
  color: var(--pv-ink-faint);
  font-size: 0.74rem;
}

.pv-keynote code {
  font-family: var(--pv-mono);
}

.pv-detail-desc {
  margin: 0 0 0.85rem;
  max-width: 72ch;
  color: var(--pv-ink-soft);
  font-size: 0.9rem;
}

.pv-detail-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* Provenance body */

.pv-prov {
  margin-top: 1.4rem;
}

.pv-prov-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 0.9rem;
}

.pv-prov-head h3 {
  margin: 0;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--pv-ink-faint);
}

.pv-prov-head span {
  color: var(--pv-ink-faint);
  font-size: 0.78rem;
}

.pv-deck {
  margin-bottom: 1.1rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  overflow: hidden;
}

.pv-deck-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  padding: 0.7rem 0.9rem;
  border-bottom: 1px solid var(--pv-line);
  background: var(--pv-surface-muted);
}

.pv-deck-title {
  margin: 0;
  font-size: 0.97rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.pv-deck-title .pv-chip {
  margin-left: 0.4rem;
}

.pv-deck-slides {
  color: var(--pv-ink-soft);
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
}

.pv-deck-slides strong {
  color: var(--pv-ink);
  font-weight: 700;
}

.pv-deck-notes {
  flex-basis: 100%;
  margin: 0.25rem 0 0;
  color: var(--pv-ink-faint);
  font-size: 0.78rem;
}

.pv-mapping {
  padding: 0.75rem 0.9rem 0.9rem;
}

.pv-mapping + .pv-mapping {
  border-top: 1px dashed var(--pv-line);
}

.pv-evidence {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  margin: 0 0 0.6rem;
  color: var(--pv-ink-faint);
  font-size: 0.76rem;
}

.pv-ev {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius-pill);
  background: var(--pv-surface-muted);
  color: var(--pv-ink-soft);
  cursor: help;
}

.pv-ev-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--pv-gray);
}

.pv-ev--strong .pv-ev-dot {
  background: var(--pv-green);
}

.pv-ev--weak .pv-ev-dot {
  background: var(--pv-amber);
}

/* Style hook for a future 'inferred' evidence type (Phase 2), distinct from
   today's neutral extraction-evidence chip -- so inference-based mappings
   read differently the moment they exist, with no further redesign here.
   Currently only reachable from the lightbox "used by" panel. */
.pv-ev--inferred {
  border-style: dashed;
  background: var(--pv-accent-soft);
  color: var(--pv-accent-ink);
}

.pv-ev--inferred .pv-ev-dot {
  background: var(--pv-accent);
}

/* Thumbnail strip */

.pv-thumbs {
  display: flex;
  gap: 0.55rem;
  padding-bottom: 0.35rem;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
}

.pv-thumb {
  padding: 0;
  border: 1px solid var(--pv-line-strong);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface-sunken);
  cursor: zoom-in;
  overflow: hidden;
  transition:
    border-color 0.15s ease,
    transform 0.15s ease;
}

.pv-thumb:hover {
  border-color: var(--pv-accent);
  transform: translateY(-1px);
}

/* Wraps the spinner/img/fallback stack so the used-by badge (Sources grid
   only -- see app.js renderThumb()) can sit absolutely over the image area
   without disturbing the caption strip below it. */
.pv-thumb-media {
  position: relative;
  display: block;
}

/* "used by N" badge -- Sources grid only, on tiles the reverse index maps to
   at least one chapter. Unbadged tiles are simply unmapped; no negative
   styling marks them, per the honest-absence rule. */
.pv-thumb-usage {
  position: absolute;
  right: 0.35rem;
  bottom: 0.35rem;
  /* Above the curated-slide stripe overlay below (z-index 1) -- a used slide
     that is ALSO curated must keep its "used by N" badge legible, not have
     the stripe painted over it. */
  z-index: 2;
  padding: 0.08rem 0.42rem;
  border-radius: var(--pv-radius-pill);
  background: var(--pv-accent);
  color: #fff;
  font-size: 0.64rem;
  font-weight: 800;
  letter-spacing: 0.01em;
  line-height: 1.5;
  pointer-events: none;
  box-shadow: 0 0 0 2px var(--pv-surface);
}

/* Tile wrapper: the thumb button plus (Sources grid only) its usage-dots
   row underneath -- a sibling, not a child, of the button, since a <button>
   cannot validly nest another <button>. Both browsing surfaces share this
   shape; the dots row itself only ever renders for the Sources grid (see
   app.js renderThumb()). */
.pv-thumb-tile {
  display: flex;
  flex-direction: column;
  /* Own stacking context (position + an integer z-index -- z-index alone on
     a static element does nothing) at a fixed baseline, raised only while
     this exact tile has something open. Without this, a CSS Grid row has no
     inherent paint order between tiles beyond DOM order: a usage dot's
     z-index:5 tooltip (see .pv-dot-tip below) would get silently painted
     OVER by the next grid row's own thumbnail image, which sits later in
     the grid but shares no isolating ancestor to compare z-index against. */
  position: relative;
  z-index: 0;
}

.pv-thumb-tile:hover,
.pv-thumb-tile:focus-within {
  z-index: 10;
}

/* Fixed-width strip (chapter detail) vs. fluid contact-sheet grid (Sources). */
.pv-thumbs .pv-thumb-tile {
  flex: none;
  width: 10.5rem;
}

.pv-source-grid .pv-thumb-tile {
  width: 100%;
}

.pv-thumb-tile .pv-thumb {
  width: 100%;
}

/* The tile a "go to slide N" jump landed on -- cleared automatically on the
   next render() since this class lives on markup that gets replaced. */
.pv-thumb.is-jump-target {
  border-color: var(--pv-accent);
  box-shadow: 0 0 0 2px var(--pv-accent-soft);
}

.pv-thumb-img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  /* contain, never cover: an auditor must see the WHOLE slide, including a
     16:9 deck's top and bottom bands, before deciding to open it. */
  object-fit: contain;
  background: #fff;
}

/* Loading spinner -- shown while the authenticated Storage fetch for this
   slide is in flight. Every thumbnail starts here (see app.js renderThumb). */

.pv-thumb-spinner {
  display: none;
  width: 100%;
  aspect-ratio: 4 / 3;
  align-items: center;
  justify-content: center;
  background: var(--pv-surface-sunken);
}

.pv-thumb.is-loading .pv-thumb-spinner {
  display: flex;
}

.pv-thumb-spinner::after {
  content: "";
  width: 1.2rem;
  height: 1.2rem;
  border: 2px solid var(--pv-line-strong);
  border-top-color: var(--pv-accent);
  border-radius: 50%;
  animation: pv-spin 0.7s linear infinite;
}

@keyframes pv-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Terminal non-image states. One state class lives on .pv-thumb at a time
   (is-loaded / is-missing / is-denied / is-error); each shows exactly one of
   these fallback spans. is-loaded shows the real <img> instead (see above). */

.pv-thumb-fallback {
  display: none;
  align-items: center;
  justify-content: center;
  width: 100%;
  aspect-ratio: 4 / 3;
  padding: 0.5rem;
  color: var(--pv-ink-faint);
  font-size: 0.72rem;
  text-align: center;
  line-height: 1.35;
}

.pv-thumb.is-missing .pv-thumb-fallback--missing {
  display: flex;
}

/* Quiet, not alarming: this is a session/permissions fact, not a data defect. */
.pv-thumb.is-denied .pv-thumb-fallback--denied {
  display: flex;
  color: var(--pv-amber);
  background: var(--pv-amber-soft);
}

/* The one retryable state -- cursor + underlined label signal "click me". */
.pv-thumb.is-error .pv-thumb-fallback--error {
  display: flex;
  color: var(--pv-red);
  background: var(--pv-red-soft);
}

.pv-thumb.is-error {
  cursor: pointer;
}

.pv-thumb-retry {
  display: block;
  margin-top: 0.2rem;
  font-weight: 700;
  text-decoration: underline;
}

.pv-thumb-cap {
  display: block;
  padding: 0.3rem 0.45rem;
  border-top: 1px solid var(--pv-line);
  background: var(--pv-surface);
  color: var(--pv-ink-soft);
  font-size: 0.72rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: left;
}

/* ---- Slide curation ("not useful" EiC marks, Sources grid only) ---------
 * A curated slide is a deliberate, visible editorial call -- dimmed image +
 * diagonal stripe + a small grey "not useful" tag under the tile. Usage dots
 * (below) still render if the slide is ALSO used -- both states are shown
 * together, honestly, rather than one hiding the other (see
 * curationTagState() in app.js for the "both" -> amber-warn decision). An
 * uncurated slide gets none of this: no negative styling on a merely
 * unmapped tile, per the same honest-absence rule as everywhere else here. */

.pv-thumb-tile.is-curated .pv-thumb-img {
  opacity: 0.45;
  filter: grayscale(65%);
}

.pv-thumb-tile.is-curated .pv-thumb-media::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    -45deg,
    rgb(103 112 133 / 16%) 0,
    rgb(103 112 133 / 16%) 6px,
    transparent 6px,
    transparent 15px
  );
}

.pv-curation-tag {
  align-self: flex-start;
  margin: 0.35rem 0.5rem 0;
  padding: 0.08rem 0.44rem;
  border-radius: var(--pv-radius-pill);
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
  font-size: 0.62rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: default;
}

/* Curated AND still used -- a real contradiction, tinted amber (the same
   "quiet, not alarming" caution color .pv-thumb-fallback--denied uses)
   rather than either fact silently winning over the other. */
.pv-curation-tag--warn {
  background: var(--pv-amber-soft);
  color: var(--pv-amber);
}

/* Mini toggle: a SIBLING of the .pv-thumb button (never nested -- same
   reasoning as the dots row), pinned over the tile's top-left corner.
   Hidden until the tile is hovered/focused, EXCEPT once a slide is actually
   marked -- then it stays visible so "how do I undo this" is never a
   hover-only secret. */
.pv-thumb-nu {
  position: absolute;
  top: 0.35rem;
  left: 0.35rem;
  z-index: 4;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  border: 1px solid var(--pv-line-strong);
  border-radius: 50%;
  background: var(--pv-surface);
  color: var(--pv-ink-soft);
  opacity: 0;
  cursor: pointer;
  transition:
    opacity 0.12s ease,
    background-color 0.12s ease,
    color 0.12s ease,
    border-color 0.12s ease;
}

.pv-thumb-nu svg {
  width: 0.8rem;
  height: 0.8rem;
}

.pv-thumb-tile:hover .pv-thumb-nu,
.pv-thumb-tile:focus-within .pv-thumb-nu,
.pv-thumb-nu:focus-visible,
.pv-thumb-nu.is-marked {
  opacity: 1;
}

/* Unmarked: hover suggests the action's effect (marking -- red/caution). */
.pv-thumb-nu:hover,
.pv-thumb-nu:focus-visible {
  background: var(--pv-red-soft);
  color: var(--pv-red);
  border-color: var(--pv-red);
}

/* Marked: quiet grey at rest; hover suggests undo (green/restore), the
   opposite affordance from the unmarked state above. */
.pv-thumb-nu.is-marked {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
  border-color: var(--pv-line-strong);
}

.pv-thumb-nu.is-marked:hover,
.pv-thumb-nu.is-marked:focus-visible {
  background: var(--pv-green-soft);
  color: var(--pv-green);
  border-color: var(--pv-green);
}

/* ---- Usage dots (Sources grid only) --------------------------------------
 * One dot per chapter that used this slide (see app.js renderUsageDots()),
 * colored by that using chapter's own module -- same module, same color,
 * everywhere (buildPartColorIndex() reads course-catalog.json's own
 * parts[].color, the same per-part hex style/part-colors.js keys the site's
 * sidebar coloring off of). A slide with no recorded usage renders no row
 * at all -- honest absence, same rule as the corner badge above it. */

.pv-thumb-dots {
  /* .pv-dot-tip's containing block -- see its comment below. Positioned
     here (the row), not on .pv-thumb-tile, so the tooltip anchors close to
     the dots themselves rather than floating above the whole tile. */
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.3rem;
  flex-wrap: wrap;
  padding: 0.4rem 0.5rem 0.15rem;
}

.pv-dot {
  /* Deliberately NOT position:relative, and never `transform` even on
     hover/focus below -- EITHER one makes this element its own containing
     block for its absolutely-positioned .pv-dot-tip child (a `transform`
     does this regardless of `position`), which would shrink the tooltip
     down to this 0.6rem circle's own size instead of the full row/tile
     width it needs (see .pv-dot-tip). The "grow on hover" affordance is a
     box-shadow ring instead, which has no such effect on containing blocks. */
  width: 0.62rem;
  height: 0.62rem;
  flex: none;
  padding: 0;
  border: 1px solid rgb(0 0 0 / 12%);
  border-radius: 50%;
  background: var(--pv-dot-color, var(--pv-gray));
  cursor: pointer;
  transition: box-shadow 0.12s ease;
}

.pv-dot:hover,
.pv-dot:focus-visible {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--pv-dot-color, var(--pv-gray)) 45%, transparent);
}

.pv-dot:disabled {
  cursor: default;
  opacity: 0.55;
}

.pv-dot:disabled:hover,
.pv-dot:disabled:focus-visible {
  box-shadow: none;
}

.pv-dot--overflow {
  width: auto;
  height: 1.05rem;
  padding: 0 0.32rem;
  border-radius: var(--pv-radius-pill);
  background: var(--pv-gray-soft);
  color: var(--pv-ink-soft);
  font-size: 0.6rem;
  font-weight: 800;
  line-height: 1.05rem;
  cursor: pointer;
  transition: background-color 0.12s ease;
}

.pv-dot--overflow:hover,
.pv-dot--overflow:focus-visible {
  background: var(--pv-line);
}

/* Tooltip: identifies which chapter a dot represents ("<title> · <module> ·
   <unit id>"). Clamped to the DOTS ROW's own width (left:0/right:0 against
   .pv-thumb-dots, i.e. the tile's column) rather than the individual dot's
   -- a dot near either edge of the grid would otherwise push a wide caption
   past the pane and into its own overflow-x clipping. Anchored BELOW the
   row (not above it): the row sits directly under the image with only the
   thin caption strip between them, not enough room for a multi-line tip to
   clear the image without overlapping the corner "used by N" badge -- below
   the tile, it floats over the grid's row gap / the next row down instead,
   which is the normal way a tooltip overlaps adjacent content. Shown on
   hover AND on keyboard focus (aria-label on the dot carries the same text
   for a screen reader regardless of whether this visual box is showing). */
.pv-dot-tip {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 0.3rem);
  z-index: 5;
  padding: 0.4rem 0.55rem;
  border-radius: var(--pv-radius-sm);
  background: var(--pv-ink);
  color: #fff;
  font-size: 0.66rem;
  font-weight: 600;
  line-height: 1.35;
  text-align: center;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-2px);
  transition:
    opacity 0.12s ease,
    transform 0.12s ease;
  pointer-events: none;
}

.pv-dot:hover .pv-dot-tip,
.pv-dot:focus-visible .pv-dot-tip,
.pv-dot:focus .pv-dot-tip {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

/* Empty provenance state */

.pv-empty {
  padding: 1.75rem 1.5rem;
  border: 1px dashed var(--pv-line-strong);
  border-radius: var(--pv-radius);
  background: var(--pv-surface-muted);
  text-align: center;
}

.pv-empty h3 {
  margin: 0 0 0.35rem;
  font-size: 1rem;
  font-weight: 700;
  color: var(--pv-ink-soft);
}

.pv-empty p {
  margin: 0 auto;
  max-width: 52ch;
  color: var(--pv-ink-faint);
  font-size: 0.86rem;
}

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

.pv-lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  /* Horizontally centered (justify-items), but STRETCHED to the full
     available height (align-items) rather than shrink-wrapped to content.
     A shrink-wrapped frame's height depends on what its children currently
     want -- and paintLightbox() briefly hides the slide image on every
     repaint before its fetch resolves, so a shrink-wrapped frame would
     collapse to a tiny height right when app.js measures .pv-lightbox-stage
     to size that same image (see paintLightbox()). Stretching keeps the
     frame's height constant and deterministic regardless of that. */
  justify-items: center;
  align-items: stretch;
  padding: clamp(0.75rem, 3vw, 2rem);
}

.pv-lightbox-backdrop {
  position: absolute;
  inset: 0;
  background: rgb(15 25 34 / 78%);
}

.pv-lightbox-frame {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(1400px, 100%);
  max-height: 100%;
  margin: 0;
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  overflow: hidden;
}

.pv-lightbox-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 0.75rem 0.6rem 1rem;
  border-bottom: 1px solid var(--pv-line);
}

.pv-lightbox-caption {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.92rem;
  font-weight: 700;
}

.pv-lightbox-tools {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex: none;
}

/* Marked state for the lightbox header's curation toggle -- text/aria-pressed
   are set per-slide by app.js paintLightboxCurationButton(); this only
   carries the visual "this is currently on" cue. */
.pv-lightbox-curation-btn.is-marked {
  border-color: var(--pv-gray);
  background: var(--pv-gray-soft);
  color: var(--pv-ink);
}

.pv-lightbox-curation-btn.is-marked:hover {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-lightbox-stage {
  position: relative;
  display: grid;
  place-items: center;
  flex: 1;
  min-height: 0;
  padding: 0.75rem 3.25rem;
  background: var(--pv-surface-sunken);
  /* Safety net: the image must never bleed into the position line or the
     "used by" panel below it (see .pv-lightbox-stage img's max-height). */
  overflow: hidden;
}

.pv-lightbox-stage img {
  max-width: 100%;
  /* Fallback only -- app.js (paintLightbox()) sets an inline max-height in
     pixels, measured directly off .pv-lightbox-stage's real layout box,
     every time the lightbox paints. A CSS percentage/100vh-relative value
     here cannot see the "used by" panel (present only for slides with
     recorded usages, and itself variable height) changing how much room the
     stage actually has, so it would let the image overflow underneath that
     panel. This value only matters before JS has run, or in environments
     with no real layout (e.g. the test harness's jsdom smoke) -- .pv-
     lightbox-stage's overflow:hidden is the belt-and-braces backstop either
     way. */
  max-height: calc(100vh - 12rem);
  object-fit: contain;
  background: #fff;
  border: 1px solid var(--pv-line);
  border-radius: 4px;
}

.pv-lightbox-stage img[hidden] + .pv-lightbox-missing {
  display: block;
}

.pv-lightbox-missing {
  margin: 3rem 0;
  color: var(--pv-ink-faint);
  font-size: 0.9rem;
  text-align: center;
}

/* The one lightbox state that is a retry affordance, not just a status
   message -- a network/server error on this slide (see app.js paintLightbox). */
.pv-lightbox-missing.is-retryable {
  color: var(--pv-red);
  text-decoration: underline;
  cursor: pointer;
}

.pv-navbtn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 2.4rem;
  height: 2.9rem;
  border: 1px solid var(--pv-line-strong);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface);
  color: var(--pv-ink-soft);
  font-size: 1.35rem;
  line-height: 1;
  cursor: pointer;
}

.pv-navbtn:hover:not(:disabled) {
  background: var(--pv-surface-hover);
  color: var(--pv-ink);
  border-color: var(--pv-accent);
}

.pv-navbtn:disabled {
  opacity: 0.35;
  cursor: default;
}

.pv-navbtn--prev {
  left: 0.5rem;
}

.pv-navbtn--next {
  right: 0.5rem;
}

.pv-lightbox-position {
  margin: 0;
  padding: 0.5rem 1rem 0.65rem;
  border-top: 1px solid var(--pv-line);
  color: var(--pv-ink-faint);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* ---- Lightbox "used by" panel --------------------------------------------
 * Reverse provenance for the slide currently open. Quiet by design: a
 * muted band under the position line, own scroll region so a heavily-reused
 * slide (a deck's most-cited diagram can run to half a dozen+ chapters)
 * never shrinks the image itself below a usable size. Absent entirely
 * (container stays [hidden]) when the slide has no recorded usage. */

.pv-lightbox-usedby {
  border-top: 1px solid var(--pv-line);
  background: var(--pv-surface-muted);
  padding: 0.75rem 1rem 0.85rem;
  max-height: 13rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.pv-usedby-head {
  margin: 0 0 0.55rem;
  color: var(--pv-ink-faint);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.pv-usedby-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.pv-usedby-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface);
}

.pv-usedby-info {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.pv-usedby-title {
  margin: 0;
  color: var(--pv-ink);
  font-size: 0.86rem;
  font-weight: 700;
}

.pv-usedby-module {
  margin: 0;
  padding: 0.06rem 0.42rem;
  border-radius: var(--pv-radius-pill);
  background: var(--pv-gray-soft);
  color: var(--pv-ink-faint);
  font-size: 0.66rem;
  font-weight: 700;
}

.pv-usedby-evidence {
  margin: 0;
  display: inline-flex;
  gap: 0.3rem;
}

.pv-usedby-actions {
  display: flex;
  flex: none;
  align-items: center;
  gap: 0.9rem;
}

.pv-usedby-actions [data-usedby-view]:disabled {
  color: var(--pv-ink-faint);
  cursor: default;
}

/* ---- Toast --------------------------------------------------------------
 * Quiet, self-dismissing failure notice for a curation write that didn't
 * save (see app.js showToast()/toggleCuration()). Flat/no-shadow per this
 * file's own discipline note at the top -- solid ink fill against the page
 * carries the contrast instead. */

.pv-toast-region {
  position: fixed;
  right: clamp(1rem, 3vw, 2rem);
  bottom: clamp(1rem, 3vw, 2rem);
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  pointer-events: none;
}

.pv-toast {
  padding: 0.6rem 0.95rem;
  border: 1px solid rgb(255 255 255 / 12%);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-ink);
  color: #fff;
  font-size: 0.82rem;
  font-weight: 600;
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity 0.2s ease,
    transform 0.2s ease;
}

.pv-toast.is-leaving {
  opacity: 0;
  transform: translateY(4px);
}

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

@media (max-width: 1080px) {
  .pv-intro-inner {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 900px) {
  .pv-shell-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .pv-rail {
    position: static;
    max-height: 26rem;
  }

  .pv-detail {
    position: static;
    max-height: none;
    overflow: visible;
  }

  .pv-topbar {
    position: static;
  }

  /* Toolbar (hybrid layout, 2026-08-08): non-sticky below the SAME
   * breakpoint .pv-topbar/.pv-rail/.pv-detail already go static at, rather
   * than a separate one -- .pv-toolbar's sticky top offset is measured off
   * .pv-topbar's sticky height, which stops being real the moment .pv-topbar
   * itself goes static, so the two must flip together. This also covers the
   * brief's own <720px "never covers more than ~40% of the viewport height"
   * requirement for the wrapped mobile toolbar as a strict subset -- the
   * brief's own sanctioned simpler option ("non-sticky below the
   * breakpoint"), just applied one breakpoint earlier for consistency with
   * every other sticky element on the page. */
  .pv-toolbar {
    position: static;
  }
}

@media (max-width: 720px) {
  /* Mode nav narrow-viewport fallback: the three chip groups give way to the
   * single grouped <select> (see .pv-mode-select-field / #pv-mode-select in
   * index.html); app.js keeps them in sync regardless of which one a mode
   * switch came through. */
  .pv-mode-groups {
    display: none;
  }

  .pv-mode-select-field {
    display: block;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    transition: none !important;
  }
}

/* ---- Four-way segmented filter (Images rail: All types/Figures/Photos/Other)
 * Same look as the base 3-way .pv-segmented (Chapters rail); only the column
 * count differs, so this stacks as a modifier rather than a new component.
 * (Gaps/Figures/Images' own decision-status filters moved onto the shared
 * .pv-status-toggle above, 2026-08-08 -- this modifier now serves only the
 * Images-mode TYPE filter, which stays its own fixed 4-option control.) */

.pv-segmented--four {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

/* ---- Checkbox rail control (Interactives mode's "Orphans only") -----------
 * The rail's only checkbox control anywhere in this app -- everything else
 * is a segmented/status-toggle button or a <select>, so this is a small,
 * deliberately understated new component rather than a reuse of any of
 * those. */

.pv-checkbox-field {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin-top: 0.65rem;
  color: var(--pv-ink-soft);
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
}

.pv-checkbox-field input[type='checkbox'] {
  width: 0.95rem;
  height: 0.95rem;
  accent-color: var(--pv-accent);
  cursor: pointer;
}

/* ---- Feature 3: deck-header slide-state breakdown -------------------------
 * used / not-useful / suggested / unused, always shown together (see
 * app.js deckBucketCounts()'s doc comment for why zeroes are not hidden the
 * way the rest of this app's per-slide badges are). */

.pv-bucket-counts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.5rem 0 0;
}

.pv-badge--used {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-badge--notuseful {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
}

.pv-badge--suggested {
  background: var(--pv-accent-soft);
  color: var(--pv-accent-ink);
}

.pv-badge--unused {
  background: transparent;
  color: var(--pv-ink-faint);
  box-shadow: inset 0 0 0 1px var(--pv-line);
}

/* ---- Suggested chip + unused indicator (Sources-grid tiles) --------------
 * Sibling of .pv-curation-tag (see renderThumb()) -- "suggested" reuses that
 * pill SHAPE but deliberately NOT the amber "not useful"/"warn" color family:
 * the brief calls for a chip "distinct from the existing curation styling",
 * so this uses the accent blue instead (the same hue .pv-chip--module and
 * .pv-badge--kind already use for "informational, not yet a verdict" chips).
 * The two states are mutually exclusive per slide anyway (see slideBucket()
 * in app.js -- any curation row at all takes a slide out of "suggested"),
 * but the color keeps them visually unambiguous regardless.
 * "unused" is deliberately NOT a pill: Feature 3 asks for a subtle, neutral
 * mark that never competes with the colored chips or the usage dots below
 * it, so it is plain small-caps text with no background. */

.pv-curation-tag--suggested {
  background: var(--pv-accent-soft);
  color: var(--pv-accent-ink);
}

.pv-unused-tag {
  align-self: flex-start;
  margin: 0.35rem 0.5rem 0;
  color: var(--pv-ink-faint);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  cursor: default;
}

/* ---- Auto-triage banner (Sources rail) ------------------------------------
 * Rendered only when pending suggestions exist AND the count is known (see
 * app.js renderTriageBanner()) -- amber, matching the same caution color
 * used for the "warn" curation-tag contradiction and the denied-thumbnail
 * state elsewhere in this app. */

.pv-triage-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  margin: 0 0 0.75rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid #f0d999;
  border-radius: var(--pv-radius-sm);
  background: var(--pv-amber-soft);
  color: #6b4700;
  font: inherit;
  font-size: 0.82rem;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
}

.pv-triage-banner:hover {
  background: #ffedb8;
}

.pv-triage-banner-arrow {
  flex: none;
  font-weight: 800;
}

/* ---- Triage view (Sources-mode sub-view) ---------------------------------
 * An accordion, deck by deck, one section open at a time -- see app.js
 * renderTriageView()/renderTriageDeckSection(). Reuses .pv-row--part's own
 * caret/label/badge shape for each deck's header row, and .pv-thumb-tile's
 * default (bare, 4-arg renderThumb()) rendering for each pending slide, same
 * as a chapter's own thumbnail strip. */

.pv-triage-head .pv-linkbtn {
  display: inline-block;
  margin-bottom: 0.5rem;
}

.pv-triage-decks {
  margin-top: 1.1rem;
}

.pv-triage-deck {
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  margin-bottom: 0.6rem;
  overflow: hidden;
}

.pv-triage-deck-head {
  width: 100%;
  background: var(--pv-surface-muted);
}

.pv-triage-deck.is-open .pv-triage-deck-head {
  border-bottom: 1px solid var(--pv-line);
}

.pv-triage-deck-body {
  padding: 0.9rem;
}

.pv-triage-bulk {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.9rem;
}

.pv-triage-rows {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.pv-triage-row {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  flex-wrap: wrap;
  padding: 0.6rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface);
}

.pv-triage-row .pv-thumb-tile {
  width: 6.5rem;
  flex: none;
}

.pv-triage-row-info {
  flex: 1;
  min-width: 10rem;
}

.pv-triage-reason {
  margin: 0;
  color: var(--pv-ink-soft);
  font-size: 0.84rem;
}

.pv-triage-row-actions {
  display: flex;
  flex: none;
  gap: 0.5rem;
}

/* ---- Content gaps (Gaps mode) ---------------------------------------------
 * One accordion card per cluster -- see app.js renderGapCard(). Structurally
 * mirrors the triage deck accordion above (same caret/expand idiom) but each
 * row is a decidable cluster rather than a deck group. */

.pv-gap-card {
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  margin-bottom: 0.7rem;
  overflow: hidden;
}

.pv-gap-head {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  width: 100%;
  padding: 0.85rem 0.9rem;
  background: var(--pv-surface);
}

.pv-gap-card.is-open .pv-gap-head {
  border-bottom: 1px solid var(--pv-line);
  background: var(--pv-surface-muted);
}

.pv-gap-head .pv-row-caret {
  margin-top: 0.2rem;
}

.pv-gap-head-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  text-align: left;
}

.pv-gap-title {
  margin: 0;
  color: var(--pv-ink);
  font-size: 0.92rem;
  font-weight: 700;
}

.pv-gap-meta {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: wrap;
  color: var(--pv-ink-faint);
  font-size: 0.76rem;
}

.pv-chip--module,
.pv-chip--figures-jump,
.pv-chip--figures {
  border: 0;
  background: var(--pv-accent-soft);
  color: var(--pv-accent-ink);
  font: inherit;
  cursor: pointer;
}

.pv-chip--module:hover,
.pv-chip--figures-jump:hover,
.pv-chip--figures:hover {
  text-decoration: underline;
}

.pv-chip--muted {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
}

.pv-gap-status {
  flex: none;
  padding: 0.2rem 0.55rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.pv-gap-status--pending {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
}

.pv-gap-status--approved {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-gap-status--dismissed {
  background: var(--pv-red-soft);
  color: var(--pv-red);
}

.pv-gap-body {
  padding: 0.9rem;
}

.pv-gap-summary {
  margin: 0 0 0.9rem;
  color: var(--pv-ink-soft);
  font-size: 0.88rem;
  line-height: 1.55;
}

.pv-gap-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-top: 0.9rem;
}

.pv-gap-note,
.pv-figure-note {
  margin: 0.6rem 0 0;
  padding: 0.5rem 0.7rem;
  border-left: 3px solid var(--pv-line-strong);
  color: var(--pv-ink-faint);
  font-size: 0.8rem;
}

/* ---- Figure-candidate review (Figures mode) --------------------------------
 * One accordion group per unit -- see app.js renderFigureGroup(). Reuses
 * .pv-row/.pv-row--part for the header's look (same idiom the triage deck
 * accordion already uses) and .pv-triage-row's thumb+info+actions row shape
 * for each figure -- structurally identical, kept as its own block for the
 * same reason gap/triage stayed separate rather than sharing one generic
 * "accordion row" component. */

.pv-figure-group {
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  margin-bottom: 0.6rem;
  overflow: hidden;
}

.pv-figure-group-head {
  width: 100%;
  background: var(--pv-surface-muted);
}

.pv-figure-group.is-open .pv-figure-group-head {
  border-bottom: 1px solid var(--pv-line);
}

.pv-figure-group-info {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.pv-figure-group-body {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 0.9rem;
}

.pv-figure-row {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  flex-wrap: wrap;
  padding: 0.6rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface);
}

.pv-figure-row--reference {
  background: var(--pv-surface-muted);
}

.pv-figure-row .pv-thumb-tile {
  width: 6.5rem;
  flex: none;
}

.pv-figure-row-info {
  flex: 1;
  min-width: 12rem;
}

.pv-figure-row-badges {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  margin-bottom: 0.35rem;
}

.pv-figure-desc {
  margin: 0;
  color: var(--pv-ink);
  font-size: 0.85rem;
  line-height: 1.5;
}

.pv-figure-rationale {
  margin: 0.3rem 0 0;
  color: var(--pv-ink-faint);
  font-size: 0.78rem;
  line-height: 1.5;
}

.pv-figure-row-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: none;
  flex-wrap: wrap;
}

.pv-fig-confidence {
  color: var(--pv-ink-faint);
  font-size: 0.72rem;
}

.pv-fig-decision {
  flex: none;
  padding: 0.14rem 0.5rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.68rem;
  font-weight: 700;
}

.pv-fig-decision--needed-diagram {
  background: var(--pv-accent-soft);
  color: var(--pv-accent-ink);
}

.pv-fig-decision--needed-photo {
  background: var(--pv-amber-soft);
  color: var(--pv-amber);
}

.pv-fig-decision--muted {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
}

.pv-fig-status {
  flex: none;
  padding: 0.2rem 0.55rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.pv-fig-status--approved {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-fig-status--dismissed {
  background: var(--pv-red-soft);
  color: var(--pv-red);
}

/* Sources triage queue's own decided-row status pill -- same treatment as
 * Figures mode's .pv-fig-status just above, kept as its own class since
 * "figure" doesn't describe a triage suggestion (see app.js
 * renderTriageRow(), founder directive 2026-08-08). */
.pv-triage-status {
  flex: none;
  padding: 0.2rem 0.55rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.pv-triage-status--approved {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-triage-status--dismissed {
  background: var(--pv-red-soft);
  color: var(--pv-red);
}

/* ---- Image review (Images mode) -------------------------------------------
 * A flat, PAGINATED responsive tile grid -- see app.js renderImagesView()/
 * renderImageTile() -- deliberately denser-per-row than the Sources
 * contact-sheet grid (.pv-source-grid's 8.5rem minimum) but still auto-fill,
 * per the original Photos-mode brief's "like the Sources grid density but
 * larger tiles" (unchanged by the type/stage generalization). No accordion
 * here: every image is its own reviewable unit, unlike Figures mode's
 * per-chapter groups. Pagination reuses .pv-source-pagebar/-pagenav/
 * -pageindicator as-is (see renderImagesView()) -- no separate CSS needed.
 *
 * Minimum retuned 260px -> 240px for the hybrid layout (2026-08-08): this
 * grid now sits at the FULL container width (no more rail eating ~22rem off
 * the left), so the old minimum was leaving an extra column's worth of
 * space unused on a typical laptop width -- denser-but-legible per the
 * brief, tiles still comfortably above the Sources grid's own 8.5rem floor. */

.pv-image-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1rem;
}

.pv-image-batch-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
  padding: 1rem 1.15rem;
  border: 1px solid color-mix(in srgb, var(--pv-accent) 35%, var(--pv-line));
  border-left: 5px solid var(--pv-accent);
  border-radius: var(--pv-radius);
  background: color-mix(in srgb, var(--pv-accent) 7%, var(--pv-surface));
}

.pv-image-batch-banner h2,
.pv-image-batch-banner p {
  margin: 0;
}

.pv-image-batch-banner h2 {
  margin-top: 0.15rem;
  font-size: 1.05rem;
}

.pv-image-batch-kicker {
  color: var(--pv-accent);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.pv-image-batch-progress {
  margin-top: 0.45rem !important;
  color: var(--pv-ink-soft);
  font-size: 0.82rem;
}

.pv-image-batch-progress span {
  margin: 0 0.25rem;
  color: var(--pv-ink-faint);
}

.pv-image-tile {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  overflow: hidden;
}

/* PHOTO-B4: candidate review is intentionally adjacent to, but unmistakably
 * separate from, the licensed photo estate. A picked candidate remains
 * licence-pending until the documented registry promotion gate is completed. */
.pv-photo-group {
  margin-bottom: 1.2rem;
}

.pv-photo-group-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin: 0 0 0.65rem;
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  background: var(--pv-surface-muted);
}

.pv-photo-group-head h2,
.pv-photo-group-head p {
  margin: 0;
}

.pv-photo-group-head h2 {
  color: var(--pv-ink);
  font-size: 1rem;
}

.pv-photo-group-head p {
  color: var(--pv-ink-faint);
  font-size: 0.75rem;
  font-weight: 700;
}

.pv-photo-group--candidates .pv-photo-group-head {
  border-color: color-mix(in srgb, var(--pv-amber) 48%, var(--pv-line));
  background: var(--pv-amber-soft);
}

.pv-photo-group--approved .pv-photo-group-head {
  border-color: color-mix(in srgb, var(--pv-green) 48%, var(--pv-line));
  background: var(--pv-green-soft);
}

.pv-photo-group--approved .pv-image-tile--candidate {
  border-color: color-mix(in srgb, var(--pv-green) 42%, var(--pv-line));
  box-shadow: inset 0 3px 0 color-mix(in srgb, var(--pv-green) 72%, transparent);
}

.pv-image-tile--candidate {
  border-color: color-mix(in srgb, var(--pv-amber) 42%, var(--pv-line));
  box-shadow: inset 0 3px 0 color-mix(in srgb, var(--pv-amber) 72%, transparent);
}

/* Distinct from .is-curated's diagonal-stripe treatment (slide_curation) --
 * a rejected image is a different kind of "set aside" (a licensing/quality
 * call on a real asset, not a not-useful-slide mark), so this dims the whole
 * card via opacity + a faint desaturation instead of reusing that pattern. */
.pv-image-tile--rejected {
  opacity: 0.62;
  filter: grayscale(35%);
}

/* A FLAGGED tile is the opposite intent of a rejected one -- founder
 * clarification: "flagged tiles get an attention style, NOT dimmed/
 * removed-looking" -- so this draws the eye (amber left rule) rather than
 * fading the card. */
.pv-image-tile--flagged {
  border-color: var(--pv-amber);
  box-shadow: inset 3px 0 0 var(--pv-amber);
}

.pv-image-media {
  position: relative;
  aspect-ratio: 4 / 3;
  background: var(--pv-surface-sunken);
  overflow: hidden;
}

.pv-image-media[data-image-open] {
  cursor: pointer;
}

.pv-image-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Small type badge, top-left of the media area -- the grid mixes figures,
 * photos, and raster art, so each tile names its own bucket at a glance. */
.pv-image-type {
  position: absolute;
  top: 0.5rem;
  left: 0.5rem;
  z-index: 1;
  padding: 0.14rem 0.5rem;
  border-radius: var(--pv-radius-pill);
  background: rgb(15 23 33 / 62%);
  color: #fff;
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* Origin chip (founder directive 2026-08-08, image lineage): top-RIGHT of
 * the media area, mirroring .pv-image-type's own top-LEFT badge -- small,
 * color-coded, ALWAYS visible on the tile per the brief, never gated behind
 * hover. Four honest states: the three real origins plus "Not tracked" (no
 * lineage record at all yet -- see app.js lineageOrigin()'s own doc comment
 * on why that is never folded into "Legacy"). */
.pv-image-origin {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  z-index: 1;
  padding: 0.14rem 0.5rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.03em;
}

.pv-image-origin--generated {
  background: rgb(140 82 255 / 85%);
  color: #fff;
}

.pv-image-origin--downloaded {
  background: rgb(24 101 242 / 85%);
  color: #fff;
}

.pv-image-origin--estate-legacy {
  background: rgb(15 23 33 / 55%);
  color: #fff;
}

.pv-image-origin--unknown {
  background: rgb(15 23 33 / 30%);
  color: rgb(255 255 255 / 85%);
  font-style: italic;
}

.pv-image-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.6rem;
  background: rgb(15 23 33 / 68%);
  opacity: 0;
  transition: opacity 0.15s ease;
  pointer-events: none;
}

.pv-image-media:hover .pv-image-overlay,
.pv-image-media:focus-within .pv-image-overlay {
  opacity: 1;
  pointer-events: auto;
}

.pv-image-open {
  padding: 0.32rem 0.7rem;
  border: 1px solid rgb(255 255 255 / 35%);
  border-radius: var(--pv-radius-pill);
  background: rgb(255 255 255 / 12%);
  color: #fff;
  font: inherit;
  font-size: 0.76rem;
  font-weight: 700;
  text-align: center;
  cursor: pointer;
}

.pv-image-open:hover,
.pv-image-open:focus-visible {
  background: rgb(255 255 255 / 24%);
}

.pv-image-info {
  padding: 0.7rem 0.8rem 0;
  flex: 1;
}

/* Informational stage badge -- see app.js imageStageInfo() for the four
 * tones: 'neutral' (still awaiting something, no decision to celebrate or
 * worry about yet), 'positive' (an approval, either stage), 'amber'
 * (flagged), 'red' (rejected). */
.pv-image-badge {
  display: inline-block;
  margin: 0 0 0.4rem;
  padding: 0.2rem 0.55rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.pv-image-badge--neutral {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
}

.pv-image-badge--positive {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-image-badge--amber {
  background: var(--pv-amber-soft);
  color: var(--pv-amber);
}

.pv-image-badge--red {
  background: var(--pv-red-soft);
  color: var(--pv-red);
}

.pv-image-subject {
  margin: 0 0 0.3rem;
  color: var(--pv-ink);
  font-size: 0.88rem;
  font-weight: 700;
}

.pv-image-caption {
  margin: 0 0 0.4rem;
  color: var(--pv-ink-soft);
  font-size: 0.8rem;
  line-height: 1.5;
}

.pv-image-caption--none {
  color: var(--pv-ink-faint);
  font-style: italic;
}

.pv-image-credit {
  margin: 0 0 0.55rem;
  color: var(--pv-ink-faint);
  font-size: 0.72rem;
}

.pv-image-credit a {
  color: inherit;
}

.pv-candidate-meta {
  margin-bottom: 0.55rem;
}

.pv-candidate-licence,
.pv-candidate-recommended,
.pv-candidate-approved {
  display: inline-block;
  margin: 0 0.3rem 0.45rem 0;
  padding: 0.16rem 0.5rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.65rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.pv-candidate-licence {
  background: var(--pv-amber-soft);
  color: var(--pv-amber);
}

.pv-candidate-recommended {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-candidate-approved {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-candidate-pack {
  margin: 0.5rem 0 0;
  font-size: 0.76rem;
  font-weight: 800;
}

.pv-candidate-pack a {
  color: var(--pv-accent);
}

.pv-candidate-terms,
.pv-candidate-relevance,
.pv-candidate-resolution {
  margin: 0 0 0.38rem;
  color: var(--pv-ink-soft);
  font-size: 0.74rem;
  line-height: 1.45;
}

.pv-candidate-terms strong,
.pv-candidate-relevance strong,
.pv-candidate-resolution strong {
  color: var(--pv-ink);
}

.pv-candidate-note {
  display: grid;
  gap: 0.3rem;
  margin: 0 0 0.7rem;
  color: var(--pv-ink-soft);
  font-size: 0.72rem;
  font-weight: 700;
}

.pv-candidate-note textarea {
  box-sizing: border-box;
  width: 100%;
  min-height: 3.7rem;
  resize: vertical;
  padding: 0.45rem 0.55rem;
  border: 1px solid var(--pv-line-strong);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface);
  color: var(--pv-ink);
  font: inherit;
  font-weight: 500;
  line-height: 1.4;
}

.pv-candidate-note textarea:focus-visible {
  outline: 2px solid var(--pv-accent);
  outline-offset: 2px;
}

.pv-image-chip--candidate {
  font-style: normal;
}

.pv-image-live-links {
  display: grid;
  gap: 0.4rem;
  margin: 0 0 0.7rem;
}

.pv-image-live-link {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.55rem 0.65rem;
  border: 1px solid color-mix(in srgb, var(--pv-accent) 35%, var(--pv-line));
  border-radius: calc(var(--pv-radius) * 0.7);
  background: color-mix(in srgb, var(--pv-accent) 7%, var(--pv-surface));
  color: var(--pv-accent);
  font-size: 0.8rem;
  font-weight: 800;
  text-decoration: none;
}

.pv-image-live-link span {
  color: var(--pv-ink-faint);
  font-size: 0.68rem;
  font-weight: 600;
  text-align: right;
}

.pv-image-live-link:hover,
.pv-image-live-link:focus-visible {
  text-decoration: underline;
}

.pv-image-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-bottom: 0.7rem;
}

/* Same --pv-dot-color CSS-variable mechanism the Sources-grid usage dots
 * (.pv-dot) use -- see app.js renderImageChips()/usageDotColor() -- so the
 * SAME module reads as the SAME color on both a slide's usage dots and an
 * image's usage chips. */
.pv-image-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.16rem 0.55rem 0.16rem 0.4rem;
  border-radius: var(--pv-radius-pill);
  background: color-mix(in srgb, var(--pv-dot-color, var(--pv-gray)) 14%, var(--pv-surface));
  color: color-mix(in srgb, var(--pv-dot-color, var(--pv-gray)) 65%, var(--pv-ink));
  font-size: 0.72rem;
  font-weight: 700;
  text-decoration: none;
}

.pv-image-chip:hover {
  text-decoration: underline;
}

.pv-image-chip::before {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  flex: none;
  border-radius: 50%;
  background: var(--pv-dot-color, var(--pv-gray));
}

.pv-image-chip--neutral {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
  cursor: default;
}

.pv-image-chip--neutral::before {
  background: var(--pv-gray);
}

.pv-image-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  padding: 0.7rem 0.8rem 0.8rem;
  border-top: 1px solid var(--pv-line);
}

.pv-btn--batch-rejected {
  border-color: var(--pv-amber);
  background: var(--pv-amber-soft);
  color: var(--pv-amber);
}

@media (max-width: 640px) {
  .pv-image-batch-banner {
    align-items: stretch;
    flex-direction: column;
  }
}

/* ============================================================================
 * Image lineage / History overlay (founder directive 2026-08-08,
 * PANEL-BRIEF-IMAGE-LINEAGE.md) -- "approval and management go together".
 * Same fixed/backdrop/frame overlay shape as .pv-lightbox above (see app.js
 * openHistory()/closeHistory()), a two-column body instead of a single
 * stage: LEFT the inspiration sources, RIGHT our own asset + version chain.
 * ============================================================================ */

.pv-history {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  justify-items: center;
  align-items: stretch;
  padding: clamp(0.75rem, 3vw, 2rem);
}

.pv-history-backdrop {
  position: absolute;
  inset: 0;
  background: rgb(15 25 34 / 78%);
}

.pv-history-frame {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(1200px, 100%);
  max-height: 100%;
  margin: 0;
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  overflow: hidden;
}

.pv-history-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 0.75rem 0.6rem 1rem;
  border-bottom: 1px solid var(--pv-line);
}

.pv-history-title {
  margin: 0;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--pv-ink);
  font-size: 0.95rem;
  font-weight: 700;
}

.pv-history-body {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
  min-height: 0;
  overflow-y: auto;
}

.pv-history-col {
  min-width: 0;
  padding: 1rem 1.1rem 1.5rem;
}

.pv-history-col--sources {
  border-right: 1px solid var(--pv-line);
  background: var(--pv-surface-muted);
}

.pv-history-col-head {
  margin: 0 0 0.75rem;
  color: var(--pv-ink-faint);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Below ~44rem the two columns stack rather than compress unreadably thin --
 * same breakpoint family the rest of this panel already uses for its own
 * narrow-viewport fallbacks. */
@media (max-width: 720px) {
  .pv-history-body {
    grid-template-columns: 1fr;
  }

  .pv-history-col--sources {
    border-right: none;
    border-bottom: 1px solid var(--pv-line);
  }
}

/* ---- LEFT column: inspiration sources ------------------------------------ */

.pv-lineage-chip {
  display: inline-block;
  margin: 0 0 0.75rem;
  padding: 0.2rem 0.6rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.pv-lineage-chip--generated {
  background: rgb(140 82 255 / 16%);
  color: #6b3fd6;
}

.pv-lineage-chip--downloaded {
  background: var(--pv-accent-soft);
  color: var(--pv-accent-ink);
}

.pv-lineage-chip--estate-legacy {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
}

.pv-lineage-chip--unknown {
  background: var(--pv-surface-sunken);
  color: var(--pv-ink-faint);
  font-style: italic;
}

.pv-history-sources-list {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.pv-history-source {
  padding-bottom: 0.85rem;
  border-bottom: 1px solid var(--pv-line);
}

.pv-history-source:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

.pv-history-thumb {
  width: 9rem;
  margin-bottom: 0.4rem;
}

.pv-history-source-label {
  margin: 0.3rem 0 0;
  color: var(--pv-ink);
  font-size: 0.82rem;
  font-weight: 700;
}

.pv-history-source-note,
.pv-history-source-credit {
  margin: 0.2rem 0 0;
  color: var(--pv-ink-soft);
  font-size: 0.76rem;
  line-height: 1.5;
}

.pv-history-source-link {
  display: inline-block;
  color: var(--pv-accent-ink);
  font-size: 0.82rem;
  font-weight: 700;
  word-break: break-all;
}

.pv-history-source-figure-img {
  display: block;
  width: 6rem;
  aspect-ratio: 4 / 3;
  margin-bottom: 0.4rem;
  object-fit: contain;
  background: #fff;
  border: 1px solid var(--pv-line-strong);
  border-radius: var(--pv-radius-sm);
}

.pv-history-empty {
  margin: 0;
  color: var(--pv-ink-faint);
  font-size: 0.82rem;
  font-style: italic;
  line-height: 1.5;
}

.pv-history-note {
  margin: 0.85rem 0 0;
  padding-top: 0.75rem;
  border-top: 1px dashed var(--pv-line);
  color: var(--pv-ink-soft);
  font-size: 0.78rem;
  line-height: 1.5;
}

/* ---- RIGHT column: our asset + version chain ------------------------------ */

.pv-history-version {
  display: block;
  width: 100%;
  margin-bottom: 0.85rem;
  padding: 0.7rem;
  border: 1px solid var(--pv-line-strong);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface);
  font: inherit;
  text-align: left;
  cursor: zoom-in;
  transition: border-color 0.15s ease;
}

.pv-history-version:hover,
.pv-history-version:focus-visible {
  border-color: var(--pv-accent);
}

.pv-history-badge {
  display: inline-block;
  margin: 0 0 0.5rem;
  padding: 0.2rem 0.55rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.pv-history-badge--current {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-history-badge--replaced {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
}

.pv-history-badge--pending {
  background: var(--pv-amber-soft);
  color: var(--pv-amber);
}

.pv-history-badge--unknown {
  background: var(--pv-surface-sunken);
  color: var(--pv-ink-faint);
  font-style: italic;
}

.pv-history-version-img {
  display: block;
  width: 100%;
  max-height: 14rem;
  object-fit: contain;
  background: #fff;
  border: 1px solid var(--pv-line);
  border-radius: 4px;
}

.pv-history-version-date {
  display: block;
  margin-top: 0.5rem;
  color: var(--pv-ink-soft);
  font-size: 0.76rem;
  font-weight: 700;
}

.pv-history-version-src {
  display: block;
  margin-top: 0.15rem;
  color: var(--pv-ink-faint);
  font-family: var(--pv-mono);
  font-size: 0.68rem;
  word-break: break-all;
}

/* ============================================================================
 * Unit review-state system (founder directive 2026-08-06) -- lives INSIDE
 * Chapters mode, not a new tab: a rail summary block (live counts, each
 * clickable to filter the tree), a small glyph per tree row (chapter or
 * platform page), and a review card in the detail pane. Copy throughout
 * reads as an audit ("Reviewed" / "Needs re-review"), never as publish
 * gating -- see app.js renderReviewCard()'s own note.
 * ========================================================================== */

/* ---- Rail summary block (#pv-review-rail) --------------------------------
 * Restyled 2026-08-08 onto the shared .pv-status-toggle/.pv-status-chip
 * control every other mode's decision-status filter uses (plus a new "All"
 * chip) -- see app.js renderReviewRail() -- so the interaction is visually
 * identical everywhere. #pv-review-rail itself is now just a spacing
 * wrapper around the injected .pv-status-toggle child; it carries no look
 * of its own. */

.pv-review-rail {
  margin-top: 0.65rem;
}

.pv-review-rail .pv-status-toggle {
  margin-top: 0;
}

/* ---- Tree-row glyph (chapter rows + platform-page rows) ------------------
 * Deliberately subtle at tree level -- a plain dot, not a pill/badge, so it
 * never competes with the existing mapped/unmapped .pv-badge. 'never' is a
 * quiet hollow ring (nothing to flag yet); 'approved' a solid green dot;
 * 'changed' an amber dot -- "needs another look", not an error. */

.pv-review-glyph {
  flex: none;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--pv-line-strong);
}

.pv-review-glyph--approved {
  background: var(--pv-green);
  box-shadow: none;
}

.pv-review-glyph--changed {
  background: var(--pv-amber);
  box-shadow: none;
}

/* 'flagged' exists ONLY on a KC tile's own glyph (see app.js
 * renderKcGlyph()/kcReviewStateClass()) -- a manual unapprove the founder
 * gave a reason for, distinct from 'changed's automatic content-drift flip,
 * so it earns the same red the rest of this app reserves for a rejected/
 * denied verdict rather than amber's "needs another look". */
.pv-review-glyph--flagged {
  background: var(--pv-red);
  box-shadow: none;
}

/* ---- Platform pages group (Chapters-mode tree, appended after the book
 * list -- these objects are not part of the chapter catalog tree). Reuses
 * .pv-row--chapter's own indent/size; only the row content (title + glyph,
 * no slide-count badge) differs. */

.pv-row--page .pv-row-label {
  flex: 1;
}

/* ---- Review card (chapter detail pane + platform-page detail pane) ------- */

.pv-review-card {
  margin: 1.1rem 0;
  padding: 0.9rem 1rem 1rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  background: var(--pv-surface-muted);
}

.pv-review-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}

.pv-review-card-head h3 {
  margin: 0;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--pv-ink-faint);
}

.pv-review-badge {
  padding: 0.16rem 0.55rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.7rem;
  font-weight: 700;
}

.pv-review-badge--never {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
}

.pv-review-badge--approved {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-review-badge--changed {
  background: var(--pv-amber-soft);
  color: var(--pv-amber);
}

.pv-review-dates {
  margin: 0 0 0.5rem;
  color: var(--pv-ink-soft);
  font-size: 0.8rem;
}

.pv-review-note {
  margin: 0 0 0.6rem;
  padding: 0.5rem 0.65rem;
  border-left: 3px solid var(--pv-line-strong);
  background: var(--pv-surface);
  color: var(--pv-ink-soft);
  font-size: 0.82rem;
  line-height: 1.5;
}

.pv-review-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.8rem;
}

.pv-review-history h4 {
  margin: 0 0 0.4rem;
  color: var(--pv-ink-faint);
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.pv-review-events {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.pv-review-events li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface);
  font-size: 0.78rem;
}

.pv-review-event-label {
  color: var(--pv-ink);
  font-weight: 600;
}

.pv-review-event-date {
  color: var(--pv-ink-faint);
  text-align: right;
  white-space: nowrap;
}

.pv-review-events-loading,
.pv-review-events-empty {
  margin: 0;
  color: var(--pv-ink-faint);
  font-size: 0.8rem;
}

/* ============================================================================
 * Interactives mode (sixth top-level mode, founder directive 2026-08-06) --
 * every animation/simulation/game, reviewed on the SAME content_review_state
 * ledger as units/pages/platform pages (see app.js renderInteractivesView()/
 * renderInteractiveTile()). A paginated tile grid, WIDER per-tile than the
 * Images grid's compact photo cells (.pv-image-grid's 260px minimum) --
 * each tile embeds a full .pv-review-card (approve/unapprove + dates + up to
 * 5 history events), which needs real room to read, not a thumbnail-sized
 * cell. Pagination reuses .pv-source-pagebar/-pagenav/-pageindicator as-is,
 * same as Images mode -- no separate CSS needed for that part.
 * ========================================================================== */

.pv-interactive-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: 1rem;
}

.pv-interactive-tile {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  padding: 0.9rem;
}

.pv-interactive-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.pv-interactive-title {
  margin: 0;
  color: var(--pv-ink);
  font-size: 0.95rem;
  font-weight: 700;
  line-height: 1.35;
}

/* The founder's own "try it" step -- opens the live interactive in a new tab. */
.pv-interactive-launch {
  align-self: flex-start;
}

.pv-interactive-usages {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

/* The review card sits directly inside a tile's own padded flow here, not a
 * standalone detail pane -- .pv-review-card's default margin (meant to space
 * it off a chapter/page detail pane's other content) would just double up
 * with the tile's own `gap`, so it is zeroed for this one context. */
.pv-interactive-review .pv-review-card {
  margin: 0;
}

/* ============================================================================
 * Knowledge-checks mode (seventh top-level mode, founder governance
 * programme) -- every inline <iot-knowledge-check> quiz question, reviewed
 * on the SAME content_review_state ledger as units/pages/interactives (see
 * app.js renderKcView()/renderKcTile()). A paginated card list, wide enough
 * to read a question + its options comfortably (a touch wider than the
 * Interactives grid's 340px minimum, since a card carries prose, not a
 * launch button). Pagination + the page-level bulk-approve button both reuse
 * .pv-source-pagebar/-pagenav/-pageindicator as-is -- no separate CSS needed
 * for that part, the bulk button is just its own flex child.
 * ========================================================================== */

.pv-kc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
  gap: 1rem;
}

.pv-kc-tile {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  padding: 0.9rem;
}

.pv-kc-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.pv-kc-question {
  margin: 0;
  color: var(--pv-ink);
  font-size: 0.95rem;
  font-weight: 700;
  line-height: 1.4;
}

.pv-kc-options {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.pv-kc-option {
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface-muted);
  color: var(--pv-ink-soft);
  font-size: 0.84rem;
  line-height: 1.4;
}

/* The one option scripts/generate-kc-registry.py's own extraction found
 * marked "correct": true in the source JSON -- the panel's whole point is
 * to let the founder read question + options + answer key without a page
 * visit, so this must read as unmistakably distinct at a glance. */
.pv-kc-option--correct {
  border-color: var(--pv-green);
  background: var(--pv-green-soft);
  color: var(--pv-green);
  font-weight: 600;
}

.pv-kc-option--correct::before {
  content: '✓ ';
  font-weight: 800;
}

.pv-kc-no-options,
.pv-kc-missing-answer {
  margin: 0;
  color: var(--pv-amber);
  font-size: 0.78rem;
  font-weight: 600;
}

.pv-kc-explanation {
  margin: 0;
  padding: 0.5rem 0.65rem;
  border-left: 3px solid var(--pv-line-strong);
  background: var(--pv-surface-muted);
  color: var(--pv-ink-soft);
  font-size: 0.82rem;
  line-height: 1.5;
}

/* The review card sits directly inside a tile's own padded flow here, not a
 * standalone detail pane -- same zeroed-margin reasoning as
 * .pv-interactive-review .pv-review-card above. */
.pv-kc-review .pv-review-card {
  margin: 0;
}

/* ============================================================================
 * Badges mode (eighth top-level mode, governance programme) -- every module/
 * milestone/chapter badge the platform awards, reviewed on the SAME
 * content_review_state ledger as units/pages/interactives/KCs (see app.js
 * renderBadgesView()/renderBadgeTile()). A hybrid of the Images grid's own
 * artwork tile (badges ARE images -- lazy-loaded, object-fit contain) and
 * the Interactives/KC tile's own in-flow review card (see .pv-badge-review
 * below). Pagination + the page-level bulk-approve button both reuse
 * .pv-source-pagebar/-pagenav/-pageindicator as-is -- no separate CSS needed
 * for that part, same as the KC grid above.
 *
 * Minimum retuned 220px -> 200px for the hybrid layout (2026-08-08), same
 * reasoning and same proportional trim as .pv-image-grid above -- full
 * container width now, so more columns fit without the tiles going
 * illegibly small.
 * ========================================================================== */

.pv-badge-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1rem;
}

.pv-badge-tile {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  overflow: hidden;
}

.pv-badge-media {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1 / 1;
  background: var(--pv-surface-sunken);
  overflow: hidden;
}

.pv-badge-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Honest-absence fallback -- a real, on-disk-verified coverage gap (see
 * scripts/generate-badge-registry.py's own "missing artwork" run-summary
 * line), never a broken <img>. */
.pv-badge-media--missing {
  padding: 0.5rem;
}

.pv-badge-missing-note {
  color: var(--pv-ink-faint);
  font-size: 0.76rem;
  font-weight: 600;
  text-align: center;
}

.pv-badge-info {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.7rem 0.8rem 0;
  flex: 1;
}

.pv-badge-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.pv-badge-title {
  margin: 0;
  color: var(--pv-ink);
  font-size: 0.9rem;
  font-weight: 700;
  line-height: 1.35;
}

.pv-badge-criteria {
  margin: 0;
  color: var(--pv-ink-soft);
  font-size: 0.8rem;
  line-height: 1.5;
}

.pv-badge-criteria--none {
  color: var(--pv-ink-faint);
  font-style: italic;
}

.pv-badge-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

/* The review card sits directly inside a tile's own padded flow here, not a
 * standalone detail pane -- same zeroed-margin reasoning as
 * .pv-interactive-review/.pv-kc-review .pv-review-card above. */
.pv-badge-review {
  padding: 0 0.8rem 0.8rem;
}

.pv-badge-review .pv-review-card {
  margin: 0;
}

/* ============================================================================
 * Paths mode (ninth top-level mode, §A of the paths/links/trust governance
 * brief) -- the 4 persona learning-path definitions, reviewed on the SAME
 * content_review_state ledger as units/pages/interactives/KCs/badges (see
 * app.js renderPathsView()/renderPathCard()). A small, fixed set (4 cards,
 * never paginated) -- unlike every other registry-backed mode's grid, this
 * is a plain vertical stack, one full-width card per path, since each card
 * carries a whole ordered route (up to several chips wide) that needs room
 * to read, not a thumbnail-sized tile.
 * ========================================================================== */

.pv-path-list {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* The route's own accent color (persona-routes.json's `color`, carried
 * through path-registry.json) tints the left edge -- the SAME visual
 * language the live /paths.html page's own .j-card persona cards use, so
 * this audit view reads as the same system as what it is auditing. */
.pv-path-card {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  border: 1px solid var(--pv-line);
  border-left: 4px solid var(--pv-dot-color, var(--pv-gray));
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  padding: 1rem 1.1rem;
}

.pv-path-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

/* Same --pv-dot-color CSS-variable mechanism .pv-image-chip/.pv-path-card
 * use -- a persona kicker pill tinted with the route's own accent color,
 * honest neutral-gray fallback via usageDotColor()'s own convention when a
 * route carries no valid color. */
.pv-path-kicker {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.6rem;
  border-radius: var(--pv-radius-pill);
  background: color-mix(in srgb, var(--pv-dot-color, var(--pv-gray)) 14%, var(--pv-surface));
  color: color-mix(in srgb, var(--pv-dot-color, var(--pv-gray)) 65%, var(--pv-ink));
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.pv-path-title {
  margin: 0;
  color: var(--pv-ink);
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1.35;
}

.pv-path-persona {
  margin: 0;
  color: var(--pv-ink-soft);
  font-size: 0.85rem;
  line-height: 1.55;
}

/* The ordered route itself -- a plain flex-wrapped chip row (not a numbered
 * <ol> marker layout, which does not combine cleanly with flex-wrap); each
 * step's own 1-based position is baked into the chip's own text instead
 * (see app.js renderPathRouteChips()). */
.pv-path-route {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.pv-path-route-empty {
  margin: 0;
  color: var(--pv-ink-faint);
  font-size: 0.8rem;
  font-style: italic;
}

/* A dead route step (see scripts/generate-path-registry.py's own
 * route_step_resolved()) -- reuses .pv-image-chip's base shape but recolors
 * it into the SAME red this app reserves for a rejected/broken state
 * elsewhere (mirrors .pv-review-glyph--flagged's own reasoning), struck
 * through and never a clickable link (there is nowhere real to send an
 * editor). */
.pv-image-chip--dead {
  background: var(--pv-red-soft);
  color: var(--pv-red);
  text-decoration: line-through;
  cursor: default;
}

.pv-image-chip--dead::before {
  background: var(--pv-red);
}

/* The review card sits directly inside a card's own padded flow here, not a
 * standalone detail pane -- same zeroed-margin reasoning as
 * .pv-interactive-review/.pv-kc-review/.pv-badge-review .pv-review-card
 * above. */
.pv-path-review .pv-review-card {
  margin: 0;
}

/* -----------------------------------------------------------------------
 * Links mode (tenth top-level mode, §B of the paths/links/trust governance
 * brief) -- see app.js renderLinkRow()/renderLinksView(). A flat paginated
 * row list, structurally mirroring .pv-figure-row (flex row, info block +
 * actions block) but with no thumbnail (a link has no slide image) and its
 * own 4-way status pill (ok/broken/exception/resolved) instead of Figures'
 * approved/dismissed pair. Pagination reuses .pv-source-pagebar/-pagenav/
 * -pageindicator as-is -- no separate CSS needed, same as Images/
 * Interactives/KC/Badges above. */

.pv-link-list {
  display: flex;
  flex-direction: column;
}

.pv-link-row {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
  flex-wrap: wrap;
  padding: 0.7rem 0.8rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface);
  margin-bottom: 0.6rem;
}

.pv-link-info {
  flex: 1;
  min-width: 14rem;
}

.pv-link-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.4rem;
}

.pv-link-url {
  overflow-wrap: anywhere;
  color: var(--pv-accent-ink);
  font-family: var(--pv-mono);
  font-size: 0.82rem;
  text-decoration: none;
}

.pv-link-url:hover {
  text-decoration: underline;
}

.pv-link-detail {
  color: var(--pv-ink-faint);
  font-size: 0.72rem;
}

.pv-link-status {
  flex: none;
  padding: 0.2rem 0.55rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.pv-link-status--ok {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-link-status--broken {
  background: var(--pv-red-soft);
  color: var(--pv-red);
}

.pv-link-status--exception {
  background: var(--pv-amber-soft);
  color: var(--pv-amber);
}

.pv-link-status--resolved {
  background: var(--pv-accent-soft);
  color: var(--pv-accent-ink);
}

.pv-link-usages {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.pv-link-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: none;
  flex-wrap: wrap;
}

.pv-link-note {
  flex-basis: 100%;
  margin: 0.6rem 0 0;
  padding: 0.5rem 0.7rem;
  border-left: 3px solid var(--pv-line-strong);
  color: var(--pv-ink-faint);
  font-size: 0.8rem;
}

/* Rail report-age banner -- neutral by default, the SAME amber the triage
 * banner/honesty note use for "needs attention" once the report is older
 * than Core.LINK_REPORT_STALE_DAYS or its own age is unknown (see app.js
 * renderLinkReportBanner()). Non-interactive (plain text), unlike the
 * clickable triage banner it otherwise mirrors the box treatment of. */
.pv-link-report-banner {
  margin: 0 0 0.75rem;
  padding: 0.55rem 0.75rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface-muted);
  color: var(--pv-ink-soft);
  font-size: 0.78rem;
  font-weight: 600;
}

.pv-link-report-banner--stale {
  border-color: #f0d999;
  background: var(--pv-amber-soft);
  color: #6b4700;
}

/* ============================================================================
 * Trust-ledger dashboard (Trust mode, eleventh top-level mode -- §C of the
 * paths/links/trust governance brief, the governance programme's CAPSTONE)
 * -- see app.js buildTrustRows()/renderTrustView()/renderTrustRow(). One
 * dashboard reading EVERYTHING every other mode already loaded: a plain
 * vertical stack of rows (a small, fixed count -- 11, never paginated, same
 * "no pagination" idiom .pv-path-list above already uses), each a progress
 * bar + counts + (for the six perishable classes) the approved/changed/
 * never split + a click-through CTA into that class's own mode.
 *
 * Deliberately NO good/bad color threshold on the bar or the percent figure
 * -- every row uses the SAME neutral accent fill, because a red/green
 * traffic light here would read as a pass/fail release gate, which the
 * brief explicitly forbids ("never release-gating"). The perishable split's
 * own three colors reuse the SAME vocabulary .pv-review-badge--never/
 * --approved/--changed already carry everywhere else in this app for these
 * exact states -- not a new invented scheme.
 * ========================================================================== */

.pv-trust-list {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.pv-trust-row {
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  padding: 1rem 1.1rem;
}

.pv-trust-row-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
}

.pv-trust-label {
  margin: 0;
  color: var(--pv-ink);
  font-size: 1rem;
  font-weight: 700;
}

.pv-trust-percent {
  flex: none;
  color: var(--pv-ink);
  font-size: 1.15rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.pv-trust-bar {
  margin-top: 0.6rem;
  height: 0.5rem;
  border-radius: var(--pv-radius-pill);
  background: var(--pv-surface-sunken);
  overflow: hidden;
}

.pv-trust-bar-fill {
  /* Deliberately NO min-width floor -- an honest 0% must render as a
     genuinely empty bar, never a fake sliver implying some nonzero
     progress that was never actually made (see the brief's own "honest
     zeros included"). */
  height: 100%;
  border-radius: var(--pv-radius-pill);
  background: var(--pv-accent);
}

.pv-trust-counts {
  margin: 0.5rem 0 0;
  color: var(--pv-ink-soft);
  font-size: 0.82rem;
}

.pv-trust-split {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.65rem;
}

.pv-trust-split-item {
  padding: 0.16rem 0.55rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.72rem;
  font-weight: 700;
}

.pv-trust-split-item--approved {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-trust-split-item--changed {
  background: var(--pv-amber-soft);
  color: var(--pv-amber);
}

.pv-trust-split-item--never {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
}

.pv-trust-row-foot {
  margin-top: 0.8rem;
}

/* ---- Workroom to-do (Todo mode) -------------------------------------------
 * The founder<->AI coordination queue (founder directive 2026-08-08). Cards
 * are always fully expanded (no accordion, unlike Gaps/Triage) -- reuses
 * .pv-chip (author), .pv-btn/.pv-btn--ghost/.pv-btn--primary (links + status
 * actions + submit), .pv-placeholder/.pv-empty (empty/no-match states), and
 * the shared .pv-status-toggle/.pv-status-chip control (rail filter) rather
 * than inventing parallel look-alikes for any of them. */

.pv-todo-card {
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  margin-bottom: 0.9rem;
  padding: 1rem 1.1rem 1.1rem;
  background: var(--pv-surface);
}

.pv-todo-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.pv-todo-head-main {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  min-width: 0;
}

.pv-todo-title {
  margin: 0;
  color: var(--pv-ink);
  font-size: 1rem;
  font-weight: 700;
  line-height: 1.4;
}

.pv-todo-meta {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: wrap;
}

.pv-todo-date {
  color: var(--pv-ink-faint);
  font-size: 0.76rem;
}

.pv-todo-priority {
  padding: 0.16rem 0.5rem;
  border-radius: var(--pv-radius-pill);
  background: var(--pv-red-soft);
  color: var(--pv-red);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.pv-todo-actions {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  flex: none;
}

/* Author color-coding -- built on the shared .pv-chip base (see index.html
 * markup: `class="pv-chip pv-todo-author pv-todo-author--*"`), same idiom as
 * .pv-chip--admin: only background/color change per variant. */
.pv-todo-author--claude {
  background: var(--pv-accent-soft);
  color: var(--pv-accent-ink);
}

.pv-todo-author--codex {
  background: var(--pv-amber-soft);
  color: var(--pv-amber);
}

.pv-todo-author--founder {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-todo-author--other {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
}

.pv-todo-status-pill {
  padding: 0.2rem 0.55rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.pv-todo-status-pill--open {
  background: var(--pv-accent-soft);
  color: var(--pv-accent-ink);
}

.pv-todo-status-pill--in-review {
  background: var(--pv-amber-soft);
  color: var(--pv-amber);
}

.pv-todo-status-pill--done {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-todo-status-pill--dismissed {
  background: var(--pv-gray-soft);
  color: var(--pv-gray);
}

.pv-todo-body {
  margin-top: 0.75rem;
  color: var(--pv-ink-soft);
  font-size: 0.9rem;
  line-height: 1.6;
}

.pv-todo-body p {
  margin: 0 0 0.75rem;
}

.pv-todo-body p:last-child {
  margin-bottom: 0;
}

.pv-todo-body-empty {
  color: var(--pv-ink-faint);
  font-style: italic;
}

.pv-todo-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

.pv-todo-link {
  font-size: 0.8rem;
}

/* Feedback thread -- subtle chat-like styling: each entry a quiet filled
 * block, oldest first, author+date+body. */
.pv-todo-thread {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-top: 1rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--pv-line);
}

.pv-todo-thread--empty {
  padding-top: 0.6rem;
}

.pv-todo-thread-empty {
  margin: 0;
  color: var(--pv-ink-faint);
  font-size: 0.82rem;
  font-style: italic;
}

.pv-todo-feedback-item {
  padding: 0.55rem 0.7rem;
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface-muted);
}

.pv-todo-feedback-item-head {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin-bottom: 0.25rem;
}

.pv-todo-feedback-author {
  color: var(--pv-ink);
  font-size: 0.76rem;
  font-weight: 700;
}

.pv-todo-feedback-date {
  color: var(--pv-ink-faint);
  font-size: 0.72rem;
}

.pv-todo-feedback-body {
  margin: 0;
  color: var(--pv-ink-soft);
  font-size: 0.86rem;
  line-height: 1.5;
}

/* Submit box -- textarea full width, the primary button aligned under it. */
.pv-todo-feedback-form {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.5rem;
  margin-top: 1rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--pv-line);
}

.pv-todo-feedback-input {
  width: 100%;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--pv-line-strong);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface);
  color: var(--pv-ink);
  font: inherit;
  font-size: 0.88rem;
  resize: vertical;
}

.pv-todo-feedback-input::placeholder {
  color: var(--pv-ink-faint);
}

.pv-todo-feedback-input:disabled {
  background: var(--pv-surface-sunken);
  color: var(--pv-ink-faint);
}

.pv-todo-feedback-submit:disabled {
  opacity: 0.6;
  cursor: default;
}

/* ---- Todo thumbnail strip (kind:"image" links) ----------------------------
 * Founder upgrade: "show me then and there, click and enlarge and see" --
 * links whose row carries `"kind":"image"` render as a strip of thumbnails
 * ABOVE the plain .pv-todo-links row (see app.js renderTodoThumbStrip(),
 * called before renderTodoLinks() in renderTodoCard()), so an easy call
 * needs no click through to the linked page. Sized/contained like
 * .pv-thumb-img above (object-fit: contain over a neutral backdrop, never
 * cover -- the founder must see the WHOLE image before deciding), but its
 * own class prefix since these are plain public <img> tiles, not the
 * signed-URL-fetched slide thumbnails .pv-thumb-tile hydrates. */

.pv-todo-thumbstrip {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  margin-top: 0.85rem;
}

.pv-todo-thumb {
  display: flex;
  flex-direction: column;
  width: 8rem;
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: zoom-in;
}

.pv-todo-thumb-frame {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  border: 1px solid var(--pv-line-strong);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface-sunken);
  overflow: hidden;
  transition: border-color 0.15s ease, transform 0.15s ease;
}

.pv-todo-thumb:hover .pv-todo-thumb-frame,
.pv-todo-thumb:focus-visible .pv-todo-thumb-frame {
  border-color: var(--pv-accent);
  transform: translateY(-1px);
}

.pv-todo-thumb-img {
  display: block;
  width: 100%;
  height: 100%;
  /* contain, never cover -- see the section note above. */
  object-fit: contain;
  background: #fff;
}

/* Quiet failed-tile fallback: a broken thumbnail URL (404, DNS failure, a
   typo'd path) must never break the card around it -- see app.js
   hydrateTodoThumbs(), which adds .is-broken on the <img>'s native `error`
   event. The class lives on .pv-todo-thumb (not the frame) so a single rule
   pair drives both halves of the swap. */
.pv-todo-thumb-fallback {
  display: none;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  padding: 0.5rem;
  color: var(--pv-ink-faint);
  font-size: 0.72rem;
  text-align: center;
  line-height: 1.35;
}

.pv-todo-thumb.is-broken .pv-todo-thumb-img {
  display: none;
}

.pv-todo-thumb.is-broken .pv-todo-thumb-fallback {
  display: flex;
}

.pv-todo-thumb-caption {
  display: block;
  margin-top: 0.3rem;
  color: var(--pv-ink-soft);
  font-size: 0.74rem;
  font-weight: 600;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- Todo image preview overlay -------------------------------------------
 * Enlarges one thumbnail strip image (app.js openTodoPreview()/
 * closeTodoPreview()). A minimal sibling to .pv-lightbox/.pv-history above --
 * same fixed/backdrop/frame shape and sizing discipline as .pv-lightbox
 * (see that section's own comment for why align-items:stretch beats a
 * shrink-wrapped frame), just without the slide-sequence chrome (no
 * prev/next, no curation, no "used by" panel) a single arbitrary public URL
 * has no data to drive. */

.pv-todo-preview {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  justify-items: center;
  align-items: stretch;
  padding: clamp(0.75rem, 3vw, 2rem);
}

.pv-todo-preview-backdrop {
  position: absolute;
  inset: 0;
  background: rgb(15 25 34 / 78%);
}

.pv-todo-preview-frame {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(1000px, 100%);
  max-height: 100%;
  margin: 0;
  border-radius: var(--pv-radius);
  background: var(--pv-surface);
  overflow: hidden;
}

.pv-todo-preview-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 0.75rem 0.6rem 1rem;
  border-bottom: 1px solid var(--pv-line);
}

.pv-todo-preview-caption {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.92rem;
  font-weight: 700;
}

.pv-todo-preview-stage {
  position: relative;
  display: grid;
  place-items: center;
  flex: 1;
  min-height: 0;
  padding: 0.75rem 1.25rem;
  background: var(--pv-surface-sunken);
  overflow: hidden;
}

.pv-todo-preview-stage img {
  max-width: 100%;
  max-height: calc(100vh - 12rem);
  object-fit: contain;
  background: #fff;
  border: 1px solid var(--pv-line);
  border-radius: 4px;
}

.pv-todo-preview-missing {
  margin: 3rem 0;
  color: var(--pv-ink-faint);
  font-size: 0.9rem;
  text-align: center;
}

/* ============================================================================
 * Learner feedback inbox (Feedback mode, thirteenth top-level mode -- §A of
 * PANEL-BRIEF-TIER1-SIGNALS.md, founder-approved 2026-08-08). One card per
 * chapter/interactive, worst-rate-first. Reuses .pv-chip (type/tag chips),
 * .pv-btn/.pv-btn--ghost/.pv-btn--primary (the live-page link + Create to-do
 * + Load more), .pv-linkbtn (the comments-expand toggle), and
 * .pv-placeholder/.pv-empty (empty/no-match states) -- same discipline as
 * the Workroom to-do cards immediately above. UNLIKE Trust's deliberately
 * neutral progress bar, the rate bar below DOES use green/red: helpful vs.
 * needs-work is a genuine two-outcome split reported by learners, not a
 * review-progress metric, so a traffic-light read is accurate here, not a
 * false pass/fail gate. ========================================================================== */

.pv-feedback-list {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.pv-feedback-card {
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius);
  padding: 1rem 1.1rem 1.1rem;
  background: var(--pv-surface);
}

.pv-feedback-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.pv-feedback-head-main {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  min-width: 0;
}

.pv-feedback-title {
  margin: 0;
  color: var(--pv-ink);
  font-size: 1rem;
  font-weight: 700;
  line-height: 1.4;
}

.pv-feedback-meta {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: wrap;
}

.pv-feedback-type-chip {
  background: var(--pv-surface-muted);
  color: var(--pv-ink-soft);
}

.pv-feedback-target {
  color: var(--pv-ink-faint);
  font-size: 0.76rem;
  font-family: var(--pv-font-mono, monospace);
}

.pv-feedback-response-count {
  color: var(--pv-ink-faint);
  font-size: 0.78rem;
}

.pv-feedback-actions {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  flex: none;
}

.pv-feedback-rate {
  margin-top: 0.85rem;
}

.pv-feedback-rate-bar {
  display: flex;
  height: 0.5rem;
  border-radius: var(--pv-radius-pill);
  background: var(--pv-surface-sunken);
  overflow: hidden;
}

.pv-feedback-rate-fill {
  height: 100%;
}

.pv-feedback-rate-fill--helpful {
  background: var(--pv-green);
}

.pv-feedback-rate-fill--needs {
  background: var(--pv-red);
}

.pv-feedback-rate-label {
  margin: 0.4rem 0 0;
  color: var(--pv-ink-soft);
  font-size: 0.82rem;
}

.pv-feedback-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.75rem;
}

.pv-feedback-tag {
  background: var(--pv-surface-muted);
  color: var(--pv-ink-soft);
  font-size: 0.74rem;
}

.pv-feedback-comments-block {
  margin-top: 0.8rem;
}

.pv-feedback-comments {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-top: 0.6rem;
}

.pv-feedback-comment {
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--pv-line);
  border-radius: var(--pv-radius-sm);
  background: var(--pv-surface-muted);
}

.pv-feedback-comment-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.pv-feedback-comment-author {
  color: var(--pv-ink-soft);
  font-size: 0.78rem;
  font-weight: 700;
}

.pv-feedback-comment-rating {
  padding: 0.1rem 0.45rem;
  border-radius: var(--pv-radius-pill);
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.pv-feedback-comment-rating--up {
  background: var(--pv-green-soft);
  color: var(--pv-green);
}

.pv-feedback-comment-rating--down {
  background: var(--pv-red-soft);
  color: var(--pv-red);
}

.pv-feedback-comment-date {
  color: var(--pv-ink-faint);
  font-size: 0.72rem;
}

.pv-feedback-comment-body {
  margin: 0.35rem 0 0;
  color: var(--pv-ink);
  font-size: 0.85rem;
  line-height: 1.55;
}

.pv-feedback-comments-empty {
  margin: 0;
  color: var(--pv-ink-faint);
  font-size: 0.8rem;
  font-style: italic;
}

.pv-feedback-load-more {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1.1rem;
  text-align: center;
}

.pv-feedback-load-note {
  margin: 0;
  color: var(--pv-ink-faint);
  font-size: 0.78rem;
}

/* Operations owns learner feedback after ADM-D. The mature inbox remains a
   single implementation here because Create-to-do shares Workroom state and
   helpers; its explicit embedded route suppresses all content-management
   chrome and exposes only the feedback toolbar and results. */
body.pv-feedback-operations-surface .pv-homebar,
body.pv-feedback-operations-surface .pv-topbar,
body.pv-feedback-operations-surface .pv-intro,
body.pv-feedback-operations-surface .pv-mode-nav {
  display: none;
}

body.pv-feedback-operations-surface .pv-main {
  padding-top: 1rem;
}
